diff --git a/mayan/apps/appearance/templates/appearance/generic_form_instance.html b/mayan/apps/appearance/templates/appearance/generic_form_instance.html index a30a8eb1db..d8aa93755b 100644 --- a/mayan/apps/appearance/templates/appearance/generic_form_instance.html +++ b/mayan/apps/appearance/templates/appearance/generic_form_instance.html @@ -45,7 +45,7 @@ {{ field }} {% endfor %} {% for field in form.visible_fields %} -
+
{# We display the label then the field for all except checkboxes #} {% if field|widget_type != 'checkboxinput' and not field.field.widget.attrs.hidden %} {% if not hide_labels %}{{ field.label_tag }}{% if field.field.required and not read_only %} ({% trans 'required' %}){% endif %}{% endif %} diff --git a/mayan/apps/checkouts/tests/test_views.py b/mayan/apps/checkouts/tests/test_views.py index a8b3256816..5a46a70a64 100644 --- a/mayan/apps/checkouts/tests/test_views.py +++ b/mayan/apps/checkouts/tests/test_views.py @@ -355,8 +355,8 @@ class DocumentCheckoutViewTestCase( class NewVersionBlockViewTestCase( DocumentCheckoutTestMixin, DocumentCheckoutViewTestMixin, - GenericDocumentViewTestCase): - + GenericDocumentViewTestCase +): def test_document_check_out_new_version(self): """ Gitlab issue #231 diff --git a/mayan/apps/common/mixins.py b/mayan/apps/common/mixins.py index 2f06415cfd..861b75df93 100644 --- a/mayan/apps/common/mixins.py +++ b/mayan/apps/common/mixins.py @@ -430,6 +430,21 @@ class RestrictedQuerysetMixin(object): object_permission = None source_queryset = None + def get_object_permission(self): + return self.object_permission + + def get_queryset(self): + queryset = self.get_source_queryset() + object_permission = self.get_object_permission() + + if object_permission: + queryset = AccessControlList.objects.restrict_queryset( + permission=object_permission, queryset=queryset, + user=self.request.user + ) + + return queryset + def get_source_queryset(self): if self.source_queryset is None: if self.model: @@ -445,17 +460,6 @@ class RestrictedQuerysetMixin(object): return self.source_queryset.all() - def get_queryset(self): - queryset = self.get_source_queryset() - - if self.object_permission: - queryset = AccessControlList.objects.restrict_queryset( - permission=self.object_permission, queryset=queryset, - user=self.request.user - ) - - return queryset - class ViewPermissionCheckMixin(object): """ @@ -467,11 +471,16 @@ class ViewPermissionCheckMixin(object): view_permission = None def dispatch(self, request, *args, **kwargs): - if self.view_permission: + view_permission = self.get_view_permission() + if view_permission: Permission.check_user_permissions( - permissions=(self.view_permission,), user=self.request.user + permissions=(view_permission,), + user=self.request.user ) return super( ViewPermissionCheckMixin, self ).dispatch(request, *args, **kwargs) + + def get_view_permission(self): + return self.view_permission diff --git a/mayan/apps/converter/apps.py b/mayan/apps/converter/apps.py index af8a668c6f..03e366c9e9 100644 --- a/mayan/apps/converter/apps.py +++ b/mayan/apps/converter/apps.py @@ -3,14 +3,15 @@ from __future__ import unicode_literals from django.utils.encoding import force_text from django.utils.translation import ugettext_lazy as _ +from mayan.apps.acls.classes import ModelPermission from mayan.apps.common.apps import MayanAppConfig from mayan.apps.common.menus import menu_object, menu_secondary from mayan.apps.navigation.classes import SourceColumn from .dependencies import * # NOQA from .links import ( - link_transformation_create, link_transformation_delete, - link_transformation_edit + link_transformation_delete, link_transformation_edit, + link_transformation_select ) @@ -24,26 +25,31 @@ class ConverterApp(MayanAppConfig): def ready(self): super(ConverterApp, self).ready() - Transformation = self.get_model(model_name='Transformation') + LayerTransformation = self.get_model(model_name='LayerTransformation') - SourceColumn(attribute='order', source=Transformation) + ModelPermission.register_inheritance( + model=LayerTransformation, + related='object_layer__content_object', + ) + + SourceColumn(attribute='order', source=LayerTransformation) SourceColumn( - source=Transformation, label=_('Transformation'), + source=LayerTransformation, label=_('Transformation'), func=lambda context: force_text(context['object']) ) SourceColumn( - attribute='arguments', source=Transformation + attribute='arguments', source=LayerTransformation ) menu_object.bind_links( links=(link_transformation_edit, link_transformation_delete), - sources=(Transformation,) + sources=(LayerTransformation,) ) menu_secondary.bind_links( - links=(link_transformation_create,), sources=(Transformation,) + links=(link_transformation_select,), sources=(LayerTransformation,) ) menu_secondary.bind_links( - links=(link_transformation_create,), + links=(link_transformation_select,), sources=( 'converter:transformation_create', 'converter:transformation_list' diff --git a/mayan/apps/converter/classes.py b/mayan/apps/converter/classes.py index 87635260c1..e99b464438 100644 --- a/mayan/apps/converter/classes.py +++ b/mayan/apps/converter/classes.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals +import copy from io import BytesIO import logging import os @@ -8,9 +9,16 @@ import shutil from PIL import Image import sh +from django.apps import apps +from django.core.exceptions import ImproperlyConfigured +from django.db import transaction +from django.utils.encoding import force_text, python_2_unicode_compatible +from django.utils.functional import cached_property from django.utils.translation import ugettext_lazy as _ +from mayan.apps.appearance.classes import Icon from mayan.apps.mimetype.api import get_mimetype +from mayan.apps.navigation.classes import Link from mayan.apps.storage.settings import setting_temporary_directory from mayan.apps.storage.utils import ( NamedTemporaryFile, fs_cleanup, mkdtemp @@ -202,3 +210,228 @@ class ConverterBase(object): for transformation in transformations: self.image = transformation.execute_on(image=self.image) + + +@python_2_unicode_compatible +class Layer(object): + _registry = {} + + @classmethod + def all(cls): + return cls._registry.values() + + @classmethod + def get(cls, name): + return cls._registry[name] + + @classmethod + def get_by_value(cls, key, value): + for name, layer in cls._registry.items(): + if getattr(layer, key) == value: + return layer + + @classmethod + def invalidate_cache(cls): + for layer in cls.all(): + layer.__dict__.pop('stored_layer', None) + + @classmethod + def update(cls): + for layer in cls.all(): + layer.stored_layer + + def __init__( + self, label, name, order, permissions, default=False, + empty_results_text=None, symbol=None, + ): + """ + access_permission is the permission necessary to view the layer. + exclude_permission is the permission necessary to discard the layer. + """ + self.default = default + self.empty_results_text = empty_results_text + self.label = label + self.name = name + self.order = order + self.permissions = permissions + self.symbol = symbol + + # Check order + layer = self.__class__.get_by_value(key='order', value=self.order) + + if layer: + raise ImproperlyConfigured( + 'Layer "{}" already has order "{}" requested by layer "{}"'.format( + layer.name, order, self.name + ) + ) + + # Check default + if default: + layer = self.__class__.get_by_value(key='default', value=True) + if layer: + raise ImproperlyConfigured( + 'Layer "{}" is already the default layer; "{}"'.format( + layer.name, self.name + ) + ) + + self.__class__._registry[name] = self + + def get_permission(self, name): + return self.permissions.get(name, None) + + def __str__(self): + return force_text(self.label) + + def add_transformation_to(self, obj, transformation_class, arguments=None): + ContentType = apps.get_model( + app_label='contenttypes', model_name='ContentType' + ) + content_type = ContentType.objects.get_for_model(model=obj) + object_layer, created = self.stored_layer.object_layers.get_or_create( + content_type=content_type, object_id=obj.pk + ) + object_layer.transformations.create( + name=transformation_class.name, arguments=arguments + ) + + def copy_transformations(self, source, targets): + """ + Copy transformation from source to all targets + """ + ContentType = apps.get_model( + app_label='contenttypes', model_name='ContentType' + ) + + transformations = self.get_transformations_for(obj=source) + + with transaction.atomic(): + for target in targets: + content_type = ContentType.objects.get_for_model(model=target) + object_layer, created = self.stored_layer.object_layers.get_or_create( + content_type=content_type, object_id=target.pk + ) + for transformation in transformations: + object_layer.transformations.create( + order=transformation.order, + name=transformation.name, + arguments=transformation.arguments, + ) + + def get_empty_results_text(self): + if self.empty_results_text: + return self.empty_results_text + else: + return _( + 'Transformations allow changing the visual appearance ' + 'of documents without making permanent changes to the ' + 'document file themselves.' + ) + + def get_icon(self): + return Icon(driver_name='fontawesome', symbol=self.symbol) + + def get_model_instance(self): + StoredLayer = apps.get_model( + app_label='converter', model_name='StoredLayer' + ) + stored_layer, created = StoredLayer.objects.update_or_create( + name=self.name, defaults={'order': self.order} + ) + + return stored_layer + + def get_transformations_for(self, obj, as_classes=False): + """ + as_classes == True returns the transformation classes from .classes + ready to be feed to the converter class + """ + LayerTransformation = apps.get_model( + app_label='converter', model_name='LayerTransformation' + ) + + return LayerTransformation.objects.get_for_object( + obj=obj, as_classes=as_classes, + only_stored_layer=self.stored_layer + ) + + @cached_property + def stored_layer(self): + return self.get_model_instance() + + +class LayerLink(Link): + @staticmethod + def set_icon(instance, layer): + if instance.action == 'list': + if layer.symbol: + instance.icon_class = layer.get_icon() + + def __init__(self, action, layer, object_name=None, **kwargs): + super(LayerLink, self).__init__(**kwargs) + self.action = action + self.layer = layer + self.object_name = object_name or _('transformation') + + permission = layer.permissions.get(action, None) + if permission: + self.permissions = (permission,) + + if action == 'list': + self.kwargs = LayerLinkKwargsFactory( + layer_name=layer.name + ).get_kwargs_function() + + if action in ('create', 'select'): + self.kwargs = LayerLinkKwargsFactory().get_kwargs_function() + + LayerLink.set_icon(instance=self, layer=layer) + + def copy(self, layer): + result = copy.copy(self) + result.kwargs = LayerLinkKwargsFactory( + layer_name=layer.name + ).get_kwargs_function() + result._layer_name = layer.name + + LayerLink.set_icon(instance=result, layer=layer) + + return result + + @cached_property + def layer_name(self): + return getattr( + self, '_layer_name', Layer.get_by_value( + key='default', value=True + ).name + ) + + +class LayerLinkKwargsFactory(object): + def __init__(self, layer_name=None, variable_name='resolved_object'): + self.layer_name = layer_name + self.variable_name = variable_name + + def get_kwargs_function(self): + def get_kwargs(context): + ContentType = apps.get_model( + app_label='contenttypes', model_name='ContentType' + ) + + content_type = ContentType.objects.get_for_model( + context[self.variable_name] + ) + default_layer = Layer.get_by_value(key='default', value=True) + return { + 'app_label': '"{}"'.format(content_type.app_label), + 'model': '"{}"'.format(content_type.model), + 'object_id': '{}.pk'.format(self.variable_name), + 'layer_name': '"{}"'.format( + self.layer_name or context.get( + 'layer_name', default_layer.name + ) + ) + } + + return get_kwargs diff --git a/mayan/apps/converter/forms.py b/mayan/apps/converter/forms.py index 3137f0dc80..72de022c1a 100644 --- a/mayan/apps/converter/forms.py +++ b/mayan/apps/converter/forms.py @@ -8,13 +8,49 @@ from django.utils.translation import ugettext_lazy as _ from mayan.apps.common.serialization import yaml_load -from .models import Transformation +from .models import LayerTransformation +from .transformations import BaseTransformation -class TransformationForm(forms.ModelForm): +class LayerTransformationSelectForm(forms.Form): + def __init__(self, *args, **kwargs): + layer = kwargs.pop('layer') + super(LayerTransformationSelectForm, self).__init__(*args, **kwargs) + self.fields[ + 'transformation' + ].choices = BaseTransformation.get_transformation_choices(layer=layer) + + transformation = forms.ChoiceField( + choices=(), help_text=_('Available transformations for this layer.'), + label=_('Transformation'), + ) + + +class LayerTransformationForm(forms.ModelForm): class Meta: - fields = ('name', 'arguments', 'order') - model = Transformation + fields = ('arguments', 'order') + model = LayerTransformation + + def __init__(self, *args, **kwargs): + transformation_name = kwargs.pop('transformation_name', None) + super(LayerTransformationForm, self).__init__(*args, **kwargs) + + if not transformation_name: + # Get the template name when the transformation is being edited. + template_name = getattr( + self.instance.get_transformation_class(), 'template_name', + None + ) + else: + # Get the template name when the transformation is being created + template_name = getattr( + BaseTransformation.get(name=transformation_name), + 'template_name', None + ) + + if template_name: + self.fields['arguments'].widget.attrs['class'] = 'hidden' + self.fields['order'].widget.attrs['class'] = 'hidden' def clean(self): try: diff --git a/mayan/apps/converter/icons.py b/mayan/apps/converter/icons.py index f91d1a9d13..9eaa5d928e 100644 --- a/mayan/apps/converter/icons.py +++ b/mayan/apps/converter/icons.py @@ -4,10 +4,7 @@ from mayan.apps.appearance.classes import Icon icon_transformations = Icon(driver_name='fontawesome', symbol='crop') -icon_transformation_create = Icon( - driver_name='fontawesome-dual', primary_symbol='crop', - secondary_symbol='plus' -) icon_transformation_delete = Icon(driver_name='fontawesome', symbol='times') icon_transformation_edit = Icon(driver_name='fontawesome', symbol='pencil-alt') icon_transformation_list = icon_transformations +icon_transformation_select = Icon(driver_name='fontawesome', symbol='plus') diff --git a/mayan/apps/converter/layers.py b/mayan/apps/converter/layers.py new file mode 100644 index 0000000000..892b00c15c --- /dev/null +++ b/mayan/apps/converter/layers.py @@ -0,0 +1,20 @@ +from __future__ import unicode_literals + +from django.utils.translation import ugettext_lazy as _ + +from .classes import Layer +from .permissions import ( + permission_transformation_create, permission_transformation_delete, + permission_transformation_edit, permission_transformation_view +) + +layer_saved_transformations = Layer( + default=True, label=_('Saved transformations'), + name='saved_transformations', order=100, permissions={ + 'create': permission_transformation_create, + 'delete': permission_transformation_delete, + 'edit': permission_transformation_edit, + 'select': permission_transformation_create, + 'view': permission_transformation_view, + }, symbol='crop' +) diff --git a/mayan/apps/converter/links.py b/mayan/apps/converter/links.py index b69074fd9f..f325dd269b 100644 --- a/mayan/apps/converter/links.py +++ b/mayan/apps/converter/links.py @@ -1,55 +1,37 @@ from __future__ import unicode_literals -from django.apps import apps from django.utils.translation import ugettext_lazy as _ -from mayan.apps.navigation.classes import Link - -from .permissions import ( - permission_transformation_create, permission_transformation_delete, - permission_transformation_edit, permission_transformation_view -) +from .classes import LayerLink +from .layers import layer_saved_transformations -def get_kwargs_factory(variable_name): - def get_kwargs(context): - ContentType = apps.get_model( - app_label='contenttypes', model_name='ContentType' - ) - - content_type = ContentType.objects.get_for_model( - context[variable_name] - ) - return { - 'app_label': '"{}"'.format(content_type.app_label), - 'model': '"{}"'.format(content_type.model), - 'object_id': '{}.pk'.format(variable_name) - } - - return get_kwargs +def conditional_active(context, resolved_link): + return resolved_link.link.view == resolved_link.current_view_name and context.get('layer_name', None) == resolved_link.link.layer_name -link_transformation_create = Link( - icon_class_path='mayan.apps.converter.icons.icon_transformation_create', - kwargs=get_kwargs_factory('content_object'), - permissions=(permission_transformation_create,), - text=_('Create new transformation'), view='converter:transformation_create' -) -link_transformation_delete = Link( - args='resolved_object.pk', +link_transformation_delete = LayerLink( + action='delete', + kwargs={'layer_name': 'layer_name', 'pk': 'resolved_object.pk'}, icon_class_path='mayan.apps.converter.icons.icon_transformation_delete', - permissions=(permission_transformation_delete,), + layer=layer_saved_transformations, tags='dangerous', text=_('Delete'), view='converter:transformation_delete' ) -link_transformation_edit = Link( - args='resolved_object.pk', +link_transformation_edit = LayerLink( + action='edit', + kwargs={'layer_name': 'layer_name', 'pk': 'resolved_object.pk'}, icon_class_path='mayan.apps.converter.icons.icon_transformation_edit', - permissions=(permission_transformation_edit,), + layer=layer_saved_transformations, text=_('Edit'), view='converter:transformation_edit' ) -link_transformation_list = Link( - icon_class_path='mayan.apps.converter.icons.icon_transformation_list', - kwargs=get_kwargs_factory('resolved_object'), - permissions=(permission_transformation_view,), text=_('Transformations'), +link_transformation_list = LayerLink( + action='list', conditional_active=conditional_active, + layer=layer_saved_transformations, text=_('Transformations'), view='converter:transformation_list' ) +link_transformation_select = LayerLink( + action='select', + icon_class_path='mayan.apps.converter.icons.icon_transformation_select', + layer=layer_saved_transformations, text=_('Select new transformation'), + view='converter:transformation_select' +) diff --git a/mayan/apps/converter/managers.py b/mayan/apps/converter/managers.py index 45c4d02a10..e0fd3fa65c 100644 --- a/mayan/apps/converter/managers.py +++ b/mayan/apps/converter/managers.py @@ -2,75 +2,89 @@ from __future__ import unicode_literals import logging +from django.apps import apps from django.contrib.contenttypes.models import ContentType -from django.db import models, transaction +from django.core.exceptions import PermissionDenied +from django.db import models -from mayan.apps.common.serialization import yaml_dump, yaml_load +from mayan.apps.acls.models import AccessControlList +from mayan.apps.common.serialization import yaml_load +from .classes import Layer from .transformations import BaseTransformation logger = logging.getLogger(__name__) -class TransformationManager(models.Manager): - def add_to_object(self, obj, transformation, arguments=None): - content_type = ContentType.objects.get_for_model(model=obj) - - self.create( - content_type=content_type, object_id=obj.pk, - name=transformation.name, arguments=yaml_dump( - data=arguments - ) - ) - - def copy(self, source, targets): - """ - Copy transformation from source to all targets - """ - content_type = ContentType.objects.get_for_model(model=source) - - # Get transformations - transformations = self.filter( - content_type=content_type, object_id=source.pk - ).values('name', 'arguments', 'order') - logger.debug('source transformations: %s', transformations) - - # Get all targets from target QS - targets_dict = map( - lambda entry: { - 'content_type': entry[0], 'object_id': entry[1] - }, zip( - ContentType.objects.get_for_models(models=targets).values(), - targets.values_list('pk', flat=True) - ) - ) - logger.debug('targets: %s', targets_dict) - - # Combine the two - results = [] - for instance in targets_dict: - for transformation in transformations: - result = instance.copy() - result.update(transformation) - results.append(dict(result)) - - logger.debug('results: %s', results) - - # Bulk create for a single DB query - with transaction.atomic(): - self.bulk_create( - map(lambda entry: self.model(**entry), results), - ) - - def get_for_object(self, obj, as_classes=False): +class LayerTransformationManager(models.Manager): + def get_for_object( + self, obj, as_classes=False, maximum_layer_order=None, + only_stored_layer=None, user=None + ): """ as_classes == True returns the transformation classes from .classes ready to be feed to the converter class """ + Layer.update() + + StoredLayer = apps.get_model( + app_label='converter', model_name='StoredLayer' + ) content_type = ContentType.objects.get_for_model(model=obj) transformations = self.filter( - content_type=content_type, object_id=obj.pk + enabled=True, object_layer__content_type=content_type, + object_layer__object_id=obj.pk, object_layer__enabled=True + ) + + access_layers = StoredLayer.objects.all() + exclude_layers = StoredLayer.objects.none() + + if maximum_layer_order: + access_layers = StoredLayer.objects.filter( + order__lte=maximum_layer_order + ) + exclude_layers = StoredLayer.objects.filter( + order__gt=maximum_layer_order + ) + + for stored_layer in access_layers: + access_permission = stored_layer.get_layer().permissions.get( + 'access_permission', None + ) + if access_permission: + try: + AccessControlList.objects.check_access( + obj=obj, permissions=(access_permission,), user=user + ) + except PermissionDenied: + access_layers = access_layers.exclude(pk=stored_layer.pk) + + for stored_layer in exclude_layers: + exclude_permission = stored_layer.get_layer().permissions.get( + 'exclude_permission', None + ) + if exclude_permission: + try: + AccessControlList.objects.check_access( + obj=obj, permissions=(exclude_permission,), user=user + ) + except PermissionDenied: + pass + else: + exclude_layers = exclude_layers.exclude(pk=stored_layer.pk) + + if only_stored_layer: + transformations = transformations.filter( + object_layer__stored_layer=only_stored_layer + ) + + transformations = transformations.filter( + object_layer__stored_layer__in=access_layers + ) + + transformations = transformations.exclude( + object_layer__stored_layer__in=exclude_layers ) if as_classes: diff --git a/mayan/apps/converter/migrations/0014_auto_20190814_0013.py b/mayan/apps/converter/migrations/0014_auto_20190814_0013.py new file mode 100644 index 0000000000..a1221ebf4e --- /dev/null +++ b/mayan/apps/converter/migrations/0014_auto_20190814_0013.py @@ -0,0 +1,81 @@ +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion +import mayan.apps.converter.validators + + +class Migration(migrations.Migration): + + dependencies = [ + ('contenttypes', '0002_remove_content_type_name'), + ('converter', '0013_auto_20180823_2353'), + ] + + operations = [ + migrations.CreateModel( + name='LayerTransformation', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('order', models.PositiveIntegerField(blank=True, db_index=True, default=0, help_text='Order in which the transformations will be executed. If left unchanged, an automatic order value will be assigned.', verbose_name='Order')), + ('name', models.CharField(choices=[('crop', 'Crop: left, top, right, bottom'), ('draw_rectangle', 'Draw rectangle: left, top, right, bottom, fillcolor, outlinecolor, outlinewidth'), ('draw_rectangle_percent', 'Draw rectangle (percents coordinates): left, top, right, bottom, fillcolor, outlinecolor, outlinewidth'), ('flip', 'Flip'), ('gaussianblur', 'Gaussian blur: radius'), ('lineart', 'Line art'), ('mirror', 'Mirror'), ('redaction_percent', 'Redaction: left, top, right, bottom'), ('resize', 'Resize: width, height'), ('rotate', 'Rotate: degrees, fillcolor'), ('rotate180', 'Rotate 180 degrees'), ('rotate270', 'Rotate 270 degrees'), ('rotate90', 'Rotate 90 degrees'), ('unsharpmask', 'Unsharp masking: radius, percent, threshold'), ('zoom', 'Zoom: percent')], max_length=128, verbose_name='Name')), + ('arguments', models.TextField(blank=True, help_text='Enter the arguments for the transformation as a YAML dictionary. ie: {"degrees": 180}', validators=[mayan.apps.converter.validators.YAMLValidator()], verbose_name='Arguments')), + ('enabled', models.BooleanField(default=True, verbose_name='Enabled')), + ], + options={ + 'ordering': ('object_layer__stored_layer__order', 'order'), + 'verbose_name': 'Layer transformation', + 'verbose_name_plural': 'Layer transformations', + }, + ), + migrations.CreateModel( + name='ObjectLayer', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('object_id', models.PositiveIntegerField()), + ('enabled', models.BooleanField(default=True, verbose_name='Enabled')), + ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')), + ], + options={ + 'ordering': ('stored_layer__order',), + 'verbose_name': 'Object layer', + 'verbose_name_plural': 'Object layers', + }, + ), + migrations.CreateModel( + name='StoredLayer', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=64, unique=True, verbose_name='Name')), + ('order', models.PositiveIntegerField(db_index=True, unique=True, verbose_name='Order')), + ], + options={ + 'ordering': ('order',), + 'verbose_name': 'Stored layer', + 'verbose_name_plural': 'Stored layers', + }, + ), + migrations.AlterField( + model_name='transformation', + name='name', + field=models.CharField(choices=[('crop', 'Crop: left, top, right, bottom'), ('draw_rectangle', 'Draw rectangle: left, top, right, bottom, fillcolor, outlinecolor, outlinewidth'), ('draw_rectangle_percent', 'Draw rectangle (percents coordinates): left, top, right, bottom, fillcolor, outlinecolor, outlinewidth'), ('flip', 'Flip'), ('gaussianblur', 'Gaussian blur: radius'), ('lineart', 'Line art'), ('mirror', 'Mirror'), ('redaction_percent', 'Redaction: left, top, right, bottom'), ('resize', 'Resize: width, height'), ('rotate', 'Rotate: degrees, fillcolor'), ('rotate180', 'Rotate 180 degrees'), ('rotate270', 'Rotate 270 degrees'), ('rotate90', 'Rotate 90 degrees'), ('unsharpmask', 'Unsharp masking: radius, percent, threshold'), ('zoom', 'Zoom: percent')], max_length=128, verbose_name='Name'), + ), + migrations.AddField( + model_name='objectlayer', + name='stored_layer', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='object_layers', to='converter.StoredLayer', verbose_name='Stored layer'), + ), + migrations.AddField( + model_name='layertransformation', + name='object_layer', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='transformations', to='converter.ObjectLayer', verbose_name='Object layer'), + ), + migrations.AlterUniqueTogether( + name='objectlayer', + unique_together=set([('content_type', 'object_id', 'stored_layer')]), + ), + migrations.AlterUniqueTogether( + name='layertransformation', + unique_together=set([('object_layer', 'order')]), + ), + ] diff --git a/mayan/apps/converter/migrations/0015_auto_20190814_0014.py b/mayan/apps/converter/migrations/0015_auto_20190814_0014.py new file mode 100644 index 0000000000..4704120e34 --- /dev/null +++ b/mayan/apps/converter/migrations/0015_auto_20190814_0014.py @@ -0,0 +1,76 @@ +from __future__ import unicode_literals + +from django.db import migrations + +from ..layers import layer_saved_transformations + + +def code_copy_transformations(apps, schema_editor): + ObjectLayer = apps.get_model( + app_label='converter', model_name='ObjectLayer' + ) + StoredLayer = apps.get_model( + app_label='converter', model_name='StoredLayer' + ) + Transformation = apps.get_model( + app_label='converter', model_name='Transformation' + ) + + stored_layer, created = StoredLayer.objects.using(schema_editor.connection.alias).update_or_create( + name=layer_saved_transformations.name, defaults={'order': layer_saved_transformations.order} + ) + + for transformation in Transformation.objects.using(schema_editor.connection.alias).all(): + object_layer, created = ObjectLayer.objects.get_or_create( + content_type=transformation.content_type, + object_id=transformation.object_id, + stored_layer=stored_layer + ) + + object_layer.transformations.create( + order=transformation.order, name=transformation.name, + arguments=transformation.arguments + ) + + +def code_copy_transformations_reverse(apps, schema_editor): + LayerTransformation = apps.get_model( + app_label='converter', model_name='LayerTransformation' + ) + ObjectLayer = apps.get_model( + app_label='converter', model_name='ObjectLayer' + ) + StoredLayer = apps.get_model( + app_label='converter', model_name='StoredLayer' + ) + Transformation = apps.get_model( + app_label='converter', model_name='Transformation' + ) + + stored_layer, created = StoredLayer.objects.using(schema_editor.connection.alias).update_or_create( + name=layer_saved_transformations.name, defaults={'order': layer_saved_transformations.order} + ) + + for object_layer in ObjectLayer.objects.using(schema_editor.connection.alias).filter(stored_layer=stored_layer): + for layer_transformation in LayerTransformation.objects.using(schema_editor.connection.alias).filter(object_layer=object_layer): + Transformation.objects.using(schema_editor.connection.alias).create( + content_type=object_layer.content_type, + object_id=object_layer.object_id, + order=layer_transformation.order, + name=layer_transformation.name, + arguments=layer_transformation.arguments + ) + + +class Migration(migrations.Migration): + + dependencies = [ + ('converter', '0014_auto_20190814_0013'), + ] + + operations = [ + migrations.RunPython( + code=code_copy_transformations, + reverse_code=code_copy_transformations_reverse + ) + ] diff --git a/mayan/apps/converter/migrations/0016_auto_20190814_0510.py b/mayan/apps/converter/migrations/0016_auto_20190814_0510.py new file mode 100644 index 0000000000..aae3540b27 --- /dev/null +++ b/mayan/apps/converter/migrations/0016_auto_20190814_0510.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.22 on 2019-08-14 05:10 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('converter', '0015_auto_20190814_0014'), + ] + + operations = [ + migrations.AlterUniqueTogether( + name='transformation', + unique_together=set([]), + ), + migrations.RemoveField( + model_name='transformation', + name='content_type', + ), + migrations.DeleteModel( + name='Transformation', + ), + ] diff --git a/mayan/apps/converter/models.py b/mayan/apps/converter/models.py index 73df0efade..bb263bc2d4 100644 --- a/mayan/apps/converter/models.py +++ b/mayan/apps/converter/models.py @@ -9,7 +9,8 @@ from django.db.models import Max from django.utils.encoding import python_2_unicode_compatible from django.utils.translation import ugettext_lazy as _ -from .managers import TransformationManager +from .classes import Layer +from .managers import LayerTransformationManager from .transformations import BaseTransformation from .validators import YAMLValidator @@ -17,7 +18,47 @@ logger = logging.getLogger(__name__) @python_2_unicode_compatible -class Transformation(models.Model): +class StoredLayer(models.Model): + name = models.CharField( + max_length=64, unique=True, verbose_name=_('Name') + ) + order = models.PositiveIntegerField( + db_index=True, unique=True, verbose_name=_('Order') + ) + + def __str__(self): + return self.name + + class Meta: + ordering = ('order',) + verbose_name = _('Stored layer') + verbose_name_plural = _('Stored layers') + + def get_layer(self): + return Layer.get(name=self.name) + + +class ObjectLayer(models.Model): + content_type = models.ForeignKey(on_delete=models.CASCADE, to=ContentType) + object_id = models.PositiveIntegerField() + content_object = GenericForeignKey( + ct_field='content_type', fk_field='object_id' + ) + enabled = models.BooleanField(default=True, verbose_name=_('Enabled')) + stored_layer = models.ForeignKey( + on_delete=models.CASCADE, related_name='object_layers', to=StoredLayer, + verbose_name=_('Stored layer') + ) + + class Meta: + ordering = ('stored_layer__order',) + unique_together = ('content_type', 'object_id', 'stored_layer') + verbose_name = _('Object layer') + verbose_name_plural = _('Object layers') + + +@python_2_unicode_compatible +class LayerTransformation(models.Model): """ Model that stores the transformation and transformation arguments for a given object @@ -29,9 +70,10 @@ class Transformation(models.Model): transformation argument. Example: if a page is rotated with the Rotation transformation, this field will show by how many degrees it was rotated. """ - content_type = models.ForeignKey(on_delete=models.CASCADE, to=ContentType) - object_id = models.PositiveIntegerField() - content_object = GenericForeignKey('content_type', 'object_id') + object_layer = models.ForeignKey( + on_delete=models.CASCADE, related_name='transformations', + to=ObjectLayer, verbose_name=_('Object layer') + ) order = models.PositiveIntegerField( blank=True, db_index=True, default=0, help_text=_( 'Order in which the transformations will be executed. If left ' @@ -48,23 +90,27 @@ class Transformation(models.Model): 'dictionary. ie: {"degrees": 180}' ), validators=[YAMLValidator()], verbose_name=_('Arguments') ) + enabled = models.BooleanField(default=True, verbose_name=_('Enabled')) - objects = TransformationManager() + objects = LayerTransformationManager() class Meta: - ordering = ('order',) - unique_together = ('content_type', 'object_id', 'order') - verbose_name = _('Transformation') - verbose_name_plural = _('Transformations') + ordering = ('object_layer__stored_layer__order', 'order',) + unique_together = ('object_layer', 'order') + verbose_name = _('Layer transformation') + verbose_name_plural = _('Layer transformations') def __str__(self): return self.get_name_display() + def get_transformation_class(self): + return BaseTransformation.get(name=self.name) + def save(self, *args, **kwargs): if not self.order: - last_order = Transformation.objects.filter( - content_type=self.content_type, object_id=self.object_id + last_order = LayerTransformation.objects.filter( + object_layer=self.object_layer ).aggregate(Max('order'))['order__max'] if last_order is not None: self.order = last_order + 1 - super(Transformation, self).save(*args, **kwargs) + super(LayerTransformation, self).save(*args, **kwargs) diff --git a/mayan/apps/converter/tests/literals.py b/mayan/apps/converter/tests/literals.py index fbad176ee9..e40d463e40 100644 --- a/mayan/apps/converter/tests/literals.py +++ b/mayan/apps/converter/tests/literals.py @@ -2,6 +2,7 @@ from __future__ import unicode_literals TEST_TRANSFORMATION_NAME = 'rotate' TEST_TRANSFORMATION_ARGUMENT = 'degrees: 180' +TEST_TRANSFORMATION_ARGUMENT_EDITED = 'degrees: 270' TEST_TRANSFORMATION_COMBINED_CACHE_HASH = '384bf78014d2aed7255d9e548a0694c70af0b22545653214bcceb1ac6286b5f7' TEST_TRANSFORMATION_RESIZE_CACHE_HASH = b'4aa319f5a6950985a19380a1f279a66769d04138bd1583844270fe8c269260fc' TEST_TRANSFORMATION_RESIZE_CACHE_HASH_2 = b'cc8d220d40e810b995181c0c69b44b7a61c3bb039c0be96a5465fcaf698ca99a' diff --git a/mayan/apps/converter/tests/mixins.py b/mayan/apps/converter/tests/mixins.py new file mode 100644 index 0000000000..b5a914a232 --- /dev/null +++ b/mayan/apps/converter/tests/mixins.py @@ -0,0 +1,97 @@ +from __future__ import unicode_literals + +from django.contrib.contenttypes.models import ContentType + +from mayan.apps.acls.classes import ModelPermission +from mayan.apps.permissions.tests.mixins import PermissionTestMixin + +from ..classes import Layer +from ..models import ObjectLayer + +from .literals import ( + TEST_TRANSFORMATION_NAME, TEST_TRANSFORMATION_ARGUMENT, + TEST_TRANSFORMATION_ARGUMENT_EDITED +) + + +class LayerTestMixin(PermissionTestMixin): + test_layer = Layer( + label='Test layer', name='test_layer', order=1000, + permissions={} + ) + + def setUp(self): + super(LayerTestMixin, self).setUp() + self._create_test_permission() + + self.test_layer_permission = self.test_permission + ModelPermission.register( + model=self.test_document._meta.model, permissions=( + self.test_permission, + ) + ) + + self.test_layer.permissions = { + 'create': self.test_layer_permission, + 'delete': self.test_layer_permission, + 'edit': self.test_layer_permission, + 'select': self.test_layer_permission, + 'view': self.test_layer_permission, + } + Layer.invalidate_cache() + Layer.update() + + +class TransformationTestMixin(LayerTestMixin): + def _create_test_transformation(self): + content_type = ContentType.objects.get_for_model(model=self.test_document) + object_layer, created = ObjectLayer.objects.get_or_create( + content_type=content_type, object_id=self.test_document.pk, + stored_layer=self.test_layer.stored_layer + ) + + self.test_transformation = object_layer.transformations.create( + name=TEST_TRANSFORMATION_NAME, + arguments=TEST_TRANSFORMATION_ARGUMENT + ) + + +class TransformationViewsTestMixin(object): + def _request_transformation_create_view(self): + return self.post( + viewname='converter:transformation_create', kwargs={ + 'app_label': 'documents', 'model': 'document', + 'object_id': self.test_document.pk, + 'layer_name': self.test_layer.name, + 'transformation_name': TEST_TRANSFORMATION_NAME, + }, data={ + 'arguments': TEST_TRANSFORMATION_ARGUMENT + } + ) + + def _request_transformation_delete_view(self): + return self.post( + viewname='converter:transformation_delete', kwargs={ + 'layer_name': self.test_layer.name, + 'pk': self.test_transformation.pk + } + ) + + def _request_transformation_edit_view(self): + return self.post( + viewname='converter:transformation_edit', kwargs={ + 'layer_name': self.test_layer.name, + 'pk': self.test_transformation.pk + }, data={ + 'arguments': TEST_TRANSFORMATION_ARGUMENT_EDITED + } + ) + + def _request_transformation_list_view(self): + return self.get( + viewname='converter:transformation_list', kwargs={ + 'app_label': 'documents', 'model': 'document', + 'object_id': self.test_document.pk, + 'layer_name': self.test_layer.name + } + ) diff --git a/mayan/apps/converter/tests/test_transformations.py b/mayan/apps/converter/tests/test_transformations.py index 4267c7fd78..97e7e79f73 100644 --- a/mayan/apps/converter/tests/test_transformations.py +++ b/mayan/apps/converter/tests/test_transformations.py @@ -4,7 +4,6 @@ from django.test import TestCase from mayan.apps.documents.tests import GenericDocumentTestCase -from ..models import Transformation from ..transformations import ( BaseTransformation, TransformationCrop, TransformationLineArt, TransformationResize, TransformationRotate, TransformationRotate90, @@ -24,6 +23,7 @@ from .literals import ( TEST_TRANSFORMATION_ZOOM_CACHE_HASH, TEST_TRANSFORMATION_ZOOM_PERCENT, ) +from .mixins import LayerTestMixin class TransformationBaseTestCase(TestCase): @@ -110,14 +110,14 @@ class TransformationBaseTestCase(TestCase): ) -class TransformationTestCase(GenericDocumentTestCase): +class TransformationTestCase(LayerTestMixin, GenericDocumentTestCase): def test_crop_transformation_optional_arguments(self): self._silence_logger(name='mayan.apps.converter.managers') document_page = self.test_document.pages.first() - Transformation.objects.add_to_object( - obj=document_page, transformation=TransformationCrop, + self.test_layer.add_transformation_to( + obj=document_page, transformation_class=TransformationCrop, arguments={'top': '10'} ) @@ -128,8 +128,8 @@ class TransformationTestCase(GenericDocumentTestCase): document_page = self.test_document.pages.first() - Transformation.objects.add_to_object( - obj=document_page, transformation=TransformationCrop, + self.test_layer.add_transformation_to( + obj=document_page, transformation_class=TransformationCrop, arguments={'top': 'x', 'left': '-'} ) self.assertTrue(document_page.generate_image()) @@ -139,8 +139,8 @@ class TransformationTestCase(GenericDocumentTestCase): document_page = self.test_document.pages.first() - Transformation.objects.add_to_object( - obj=document_page, transformation=TransformationCrop, + self.test_layer.add_transformation_to( + obj=document_page, transformation_class=TransformationCrop, arguments={'top': '-1000', 'bottom': '100000000'} ) @@ -151,13 +151,13 @@ class TransformationTestCase(GenericDocumentTestCase): document_page = self.test_document.pages.first() - Transformation.objects.add_to_object( - obj=document_page, transformation=TransformationCrop, + self.test_layer.add_transformation_to( + obj=document_page, transformation_class=TransformationCrop, arguments={'top': '1000', 'bottom': '1000'} ) - Transformation.objects.add_to_object( - obj=document_page, transformation=TransformationCrop, + self.test_layer.add_transformation_to( + obj=document_page, transformation_class=TransformationCrop, arguments={'left': '1000', 'right': '10000'} ) @@ -166,8 +166,8 @@ class TransformationTestCase(GenericDocumentTestCase): def test_lineart_transformations(self): document_page = self.test_document.pages.first() - Transformation.objects.add_to_object( - obj=document_page, transformation=TransformationLineArt, + self.test_layer.add_transformation_to( + obj=document_page, transformation_class=TransformationLineArt, arguments={} ) @@ -176,22 +176,22 @@ class TransformationTestCase(GenericDocumentTestCase): def test_rotate_transformations(self): document_page = self.test_document.pages.first() - Transformation.objects.add_to_object( - obj=document_page, transformation=TransformationRotate90, + self.test_layer.add_transformation_to( + obj=document_page, transformation_class=TransformationRotate90, arguments={} ) self.assertTrue(document_page.generate_image()) - Transformation.objects.add_to_object( - obj=document_page, transformation=TransformationRotate180, + self.test_layer.add_transformation_to( + obj=document_page, transformation_class=TransformationRotate180, arguments={} ) self.assertTrue(document_page.generate_image()) - Transformation.objects.add_to_object( - obj=document_page, transformation=TransformationRotate270, + self.test_layer.add_transformation_to( + obj=document_page, transformation_class=TransformationRotate270, arguments={} ) diff --git a/mayan/apps/converter/tests/test_views.py b/mayan/apps/converter/tests/test_views.py index faf33c8259..745ca13669 100644 --- a/mayan/apps/converter/tests/test_views.py +++ b/mayan/apps/converter/tests/test_views.py @@ -1,110 +1,123 @@ from __future__ import unicode_literals -from django.contrib.contenttypes.models import ContentType - from mayan.apps.documents.tests import GenericDocumentViewTestCase -from ..models import Transformation -from ..permissions import ( - permission_transformation_create, permission_transformation_delete, - permission_transformation_view -) +from ..models import LayerTransformation -from .literals import TEST_TRANSFORMATION_NAME, TEST_TRANSFORMATION_ARGUMENT +from .mixins import TransformationTestMixin, TransformationViewsTestMixin -class TransformationViewsTestCase(GenericDocumentViewTestCase): - def _transformation_create_view(self): - return self.post( - viewname='converter:transformation_create', kwargs={ - 'app_label': 'documents', 'model': 'document', - 'object_id': self.test_document.pk - }, data={ - 'name': TEST_TRANSFORMATION_NAME, - 'arguments': TEST_TRANSFORMATION_ARGUMENT - } - ) +class TransformationViewsTestCase( + TransformationTestMixin, TransformationViewsTestMixin, + GenericDocumentViewTestCase +): + def test_transformation_create_view_no_permission(self): + transformation_count = LayerTransformation.objects.count() - def test_transformation_create_view_no_permissions(self): - transformation_count = Transformation.objects.count() - - response = self._transformation_create_view() - self.assertEqual(response.status_code, 403) - - self.assertEqual(Transformation.objects.count(), transformation_count) - - def test_transformation_create_view_with_permissions(self): - self.grant_permission(permission=permission_transformation_create) - - transformation_count = Transformation.objects.count() - - response = self._transformation_create_view() - self.assertEqual(response.status_code, 302) + response = self._request_transformation_create_view() + self.assertEqual(response.status_code, 404) self.assertEqual( - Transformation.objects.count(), transformation_count + 1 + LayerTransformation.objects.count(), transformation_count ) - def _request_transformation_delete_view(self): - return self.post( - viewname='converter:transformation_delete', kwargs={ - 'pk': self.test_transformation.pk - } - ) - - def _create_test_transformation(self): - content_type = ContentType.objects.get_for_model(model=self.test_document) - - self.test_transformation = Transformation.objects.create( - content_type=content_type, object_id=self.test_document.pk, - name=TEST_TRANSFORMATION_NAME, - arguments=TEST_TRANSFORMATION_ARGUMENT - ) - - def test_transformation_delete_view_no_permissions(self): - self._create_test_transformation() - - transformation_count = Transformation.objects.count() - - response = self._request_transformation_delete_view() - self.assertEqual(response.status_code, 403) - - self.assertEqual( - Transformation.objects.count(), transformation_count - ) - - def test_transformation_delete_view_with_permissions(self): - self._create_test_transformation() - - self.grant_permission(permission=permission_transformation_delete) - - transformation_count = Transformation.objects.count() - - response = self._request_transformation_delete_view() - self.assertEqual(response.status_code, 302) - - self.assertEqual( - Transformation.objects.count(), transformation_count - 1 - ) - - def _transformation_list_view(self): - return self.get( - viewname='converter:transformation_list', kwargs={ - 'app_label': 'documents', 'model': 'document', - 'object_id': self.test_document.pk - } - ) - - def test_transformation_list_view_no_permissions(self): - response = self._transformation_list_view() - self.assertEqual(response.status_code, 403) - - def test_transformation_list_view_with_permissions(self): + def test_transformation_create_view_with_permission(self): self.grant_access( - obj=self.test_document, permission=permission_transformation_view + obj=self.test_document, permission=self.test_permission ) - response = self._transformation_list_view() + transformation_count = LayerTransformation.objects.count() + + response = self._request_transformation_create_view() + self.assertEqual(response.status_code, 302) + + self.assertEqual( + LayerTransformation.objects.count(), transformation_count + 1 + ) + + def test_transformation_delete_view_no_permission(self): + self._create_test_transformation() + + transformation_count = LayerTransformation.objects.count() + + response = self._request_transformation_delete_view() + self.assertEqual(response.status_code, 404) + + self.assertEqual( + LayerTransformation.objects.count(), transformation_count + ) + + def test_transformation_delete_view_with_access(self): + self._create_test_transformation() + + self.grant_access( + obj=self.test_document, permission=self.test_layer_permission + ) + + transformation_count = LayerTransformation.objects.count() + + response = self._request_transformation_delete_view() + self.assertEqual(response.status_code, 302) + + self.assertEqual( + LayerTransformation.objects.count(), transformation_count - 1 + ) + + def test_transformation_edit_view_no_permission(self): + self._create_test_transformation() + + transformation_arguments = self.test_transformation.arguments + + response = self._request_transformation_edit_view() + self.assertEqual(response.status_code, 404) + + self.test_transformation.refresh_from_db() + self.assertEqual( + transformation_arguments, self.test_transformation.arguments + ) + + def test_transformation_edit_view_with_access(self): + self._create_test_transformation() + + self.grant_access( + obj=self.test_document, permission=self.test_layer_permission + ) + + transformation_arguments = self.test_transformation.arguments + response = self._request_transformation_edit_view() + self.assertEqual(response.status_code, 302) + + self.test_transformation.refresh_from_db() + self.assertNotEqual( + transformation_arguments, self.test_transformation.arguments + ) + + def test_transformation_list_view_no_permission(self): + self._create_test_transformation() + + response = self._request_transformation_list_view() + self.assertNotContains( + response=response, text=self.test_document.label, status_code=404 + ) + self.assertNotContains( + response=response, + text=self.test_transformation.get_transformation_class().label, + status_code=404 + ) + + def test_transformation_list_view_with_access(self): + self._create_test_transformation() + + self.grant_access( + obj=self.test_document, permission=self.test_permission + ) + + response = self._request_transformation_list_view() self.assertContains( response=response, text=self.test_document.label, status_code=200 ) + self.assertContains( + response=response, + text=self.test_transformation.get_transformation_class().label, + status_code=200 + ) diff --git a/mayan/apps/converter/transformations.py b/mayan/apps/converter/transformations.py index 54109957b8..4c02599615 100644 --- a/mayan/apps/converter/transformations.py +++ b/mayan/apps/converter/transformations.py @@ -5,12 +5,19 @@ import logging from PIL import Image, ImageColor, ImageDraw, ImageFilter +from django.utils.encoding import force_bytes, force_text from django.utils.translation import string_concat, ugettext_lazy as _ -from django.utils.encoding import force_bytes + +from .layers import layer_saved_transformations logger = logging.getLogger(__name__) +class BaseTransformationType(type): + def __str__(self): + return force_text(self.label) + + class BaseTransformation(object): """ Transformation can modify the appearance of the document's page preview. @@ -18,7 +25,9 @@ class BaseTransformation(object): """ arguments = () name = 'base_transformation' + _layer_transformations = {} _registry = {} + __metaclass__ = BaseTransformationType @staticmethod def combine(transformations): @@ -44,16 +53,25 @@ class BaseTransformation(object): return cls.label @classmethod - def get_transformation_choices(cls): + def get_transformation_choices(cls, layer=None): + if layer: + transformation_list = [ + (transformation.name, transformation) for transformation in cls._layer_transformations[layer] + ] + else: + transformation_list = cls._registry.items() + return sorted( [ - (name, klass.get_label()) for name, klass in cls._registry.items() + (name, klass.get_label()) for name, klass in transformation_list ] ) @classmethod - def register(cls, transformation): + def register(cls, layer, transformation): cls._registry[transformation.name] = transformation + cls._layer_transformations.setdefault(layer, []) + cls._layer_transformations[layer].append(transformation) def __init__(self, **kwargs): self.kwargs = {} @@ -517,19 +535,19 @@ class TransformationZoom(BaseTransformation): ) -BaseTransformation.register(transformation=TransformationCrop) -BaseTransformation.register(transformation=TransformationDrawRectangle) +BaseTransformation.register(layer=layer_saved_transformations, transformation=TransformationCrop) +BaseTransformation.register(layer=layer_saved_transformations, transformation=TransformationDrawRectangle) BaseTransformation.register( - transformation=TransformationDrawRectanglePercent + layer=layer_saved_transformations, transformation=TransformationDrawRectanglePercent ) -BaseTransformation.register(transformation=TransformationFlip) -BaseTransformation.register(transformation=TransformationGaussianBlur) -BaseTransformation.register(transformation=TransformationLineArt) -BaseTransformation.register(transformation=TransformationMirror) -BaseTransformation.register(transformation=TransformationResize) -BaseTransformation.register(transformation=TransformationRotate) -BaseTransformation.register(transformation=TransformationRotate90) -BaseTransformation.register(transformation=TransformationRotate180) -BaseTransformation.register(transformation=TransformationRotate270) -BaseTransformation.register(transformation=TransformationUnsharpMask) -BaseTransformation.register(transformation=TransformationZoom) +BaseTransformation.register(layer=layer_saved_transformations, transformation=TransformationFlip) +BaseTransformation.register(layer=layer_saved_transformations, transformation=TransformationGaussianBlur) +BaseTransformation.register(layer=layer_saved_transformations, transformation=TransformationLineArt) +BaseTransformation.register(layer=layer_saved_transformations, transformation=TransformationMirror) +BaseTransformation.register(layer=layer_saved_transformations, transformation=TransformationResize) +BaseTransformation.register(layer=layer_saved_transformations, transformation=TransformationRotate) +BaseTransformation.register(layer=layer_saved_transformations, transformation=TransformationRotate90) +BaseTransformation.register(layer=layer_saved_transformations, transformation=TransformationRotate180) +BaseTransformation.register(layer=layer_saved_transformations, transformation=TransformationRotate270) +BaseTransformation.register(layer=layer_saved_transformations, transformation=TransformationUnsharpMask) +BaseTransformation.register(layer=layer_saved_transformations, transformation=TransformationZoom) diff --git a/mayan/apps/converter/urls.py b/mayan/apps/converter/urls.py index 0e46ddbbba..8a0cf36167 100644 --- a/mayan/apps/converter/urls.py +++ b/mayan/apps/converter/urls.py @@ -3,25 +3,29 @@ from __future__ import unicode_literals from django.conf.urls import url from .views import ( - TransformationCreateView, TransformationDeleteView, TransformationEditView, - TransformationListView + TransformationCreateView, TransformationDeleteView, + TransformationEditView, TransformationListView, TransformationSelectView ) urlpatterns = [ url( - regex=r'^object/(?P[-\w]+)/(?P[-\w]+)/(?P\d+)/transformations/$', + regex=r'^object/(?P[-\w]+)/(?P[-\w]+)/(?P\d+)/layers/(?P[-_\w]+)/transformations/$', view=TransformationListView.as_view(), name='transformation_list' ), url( - regex=r'^object/(?P[-\w]+)/(?P[-\w]+)/(?P\d+)/transformations/create/$', + regex=r'^object/(?P[-\w]+)/(?P[-\w]+)/(?P\d+)/layers/(?P[-_\w]+)/transformations/select/$', + view=TransformationSelectView.as_view(), name='transformation_select' + ), + url( + regex=r'^object/(?P[-\w]+)/(?P[-\w]+)/(?P\d+)/layers/(?P[-_\w]+)/transformations/(?P[-_\w]+)/create/$', view=TransformationCreateView.as_view(), name='transformation_create' ), url( - regex=r'^transformations/(?P\d+)/delete/$', view=TransformationDeleteView.as_view(), - name='transformation_delete' + regex=r'^layers/(?P[-_\w]+)/transformations/(?P\d+)/delete/$', + view=TransformationDeleteView.as_view(), name='transformation_delete' ), url( - regex=r'^transformations/(?P\d+)/edit/$', view=TransformationEditView.as_view(), - name='transformation_edit' + regex=r'^layers/(?P[-_\w]+)/transformations/(?P\d+)/edit/$', + view=TransformationEditView.as_view(), name='transformation_edit' ), ] diff --git a/mayan/apps/converter/views.py b/mayan/apps/converter/views.py index fe4c042069..3f7178ca5e 100644 --- a/mayan/apps/converter/views.py +++ b/mayan/apps/converter/views.py @@ -2,59 +2,56 @@ from __future__ import absolute_import, unicode_literals import logging -from django.contrib.contenttypes.models import ContentType -from django.http import Http404 -from django.shortcuts import get_object_or_404 +from django.http import HttpResponseRedirect from django.template import RequestContext from django.urls import reverse from django.utils.translation import ugettext_lazy as _ -from mayan.apps.acls.models import AccessControlList from mayan.apps.common.generics import ( - SingleObjectCreateView, SingleObjectDeleteView, SingleObjectEditView, - SingleObjectListView + FormView, SingleObjectCreateView, SingleObjectDeleteView, + SingleObjectEditView, SingleObjectListView ) +from mayan.apps.common.mixins import ExternalContentTypeObjectMixin -from .forms import TransformationForm -from .icons import icon_transformation_list -from .links import link_transformation_create -from .models import Transformation -from .permissions import ( - permission_transformation_create, permission_transformation_delete, - permission_transformation_edit, permission_transformation_view -) +from .classes import Layer +from .forms import LayerTransformationForm, LayerTransformationSelectForm +from .links import link_transformation_select +from .models import LayerTransformation, ObjectLayer +from .transformations import BaseTransformation logger = logging.getLogger(__name__) -class TransformationCreateView(SingleObjectCreateView): - form_class = TransformationForm - +class LayerViewMixin(object): def dispatch(self, request, *args, **kwargs): - content_type = get_object_or_404( - klass=ContentType, app_label=self.kwargs['app_label'], - model=self.kwargs['model'] + self.layer = self.get_layer() + return super(LayerViewMixin, self).dispatch( + request=request, *args, **kwargs ) - try: - self.content_object = content_type.get_object_for_this_type( - pk=self.kwargs['object_id'] - ) - except content_type.model_class().DoesNotExist: - raise Http404 - - AccessControlList.objects.check_access( - obj=self.content_object, - permissions=(permission_transformation_create,), user=request.user + def get_layer(self): + return Layer.get( + name=self.kwargs['layer_name'] ) - return super(TransformationCreateView, self).dispatch( - request, *args, **kwargs - ) + +class TransformationCreateView( + LayerViewMixin, ExternalContentTypeObjectMixin, SingleObjectCreateView +): + form_class = LayerTransformationForm def form_valid(self, form): + layer = self.layer + content_type = self.get_content_type() + object_layer, created = ObjectLayer.objects.get_or_create( + content_type=content_type, object_id=self.external_object.pk, + stored_layer=layer.stored_layer + ) + instance = form.save(commit=False) - instance.content_object = self.content_object + instance.content_object = self.external_object + instance.name = self.kwargs['transformation_name'] + instance.object_layer = object_layer try: instance.full_clean() instance.save() @@ -66,91 +63,101 @@ class TransformationCreateView(SingleObjectCreateView): def get_extra_context(self): return { - 'content_object': self.content_object, + 'content_object': self.external_object, + 'form_field_css_classes': 'hidden' if hasattr( + self.get_transformation_class(), 'template_name' + ) else '', + 'layer': self.layer, + 'layer_name': self.layer.name, 'navigation_object_list': ('content_object',), 'title': _( - 'Create new transformation for: %s' - ) % self.content_object, + 'Create layer "%(layer)s" transformation ' + '"%(transformation)s" for: %(object)s' + ) % { + 'layer': self.layer, + 'transformation': self.get_transformation_class(), + 'object': self.external_object, + } } + def get_form_extra_kwargs(self): + return { + 'transformation_name': self.kwargs['transformation_name'] + } + + def get_external_object_permission(self): + return self.layer.permissions.get('create', None) + def get_post_action_redirect(self): return reverse( viewname='converter:transformation_list', kwargs={ 'app_label': self.kwargs['app_label'], 'model': self.kwargs['model'], - 'object_id': self.kwargs['object_id'] + 'object_id': self.kwargs['object_id'], + 'layer_name': self.kwargs['layer_name'] } ) def get_queryset(self): - return Transformation.objects.get_for_object(obj=self.content_object) - - -class TransformationDeleteView(SingleObjectDeleteView): - model = Transformation - - def dispatch(self, request, *args, **kwargs): - self.transformation = get_object_or_404( - klass=Transformation, pk=self.kwargs['pk'] + return self.layer.get_transformations_for( + obj=self.content_object ) - AccessControlList.objects.check_access( - obj=self.transformation.content_object, - permissions=(permission_transformation_delete,), user=request.user - ) + def get_template_names(self): + return [ + getattr( + self.get_transformation_class(), 'template_name', + self.template_name + ) + ] - return super(TransformationDeleteView, self).dispatch( - request, *args, **kwargs - ) + def get_transformation_class(self): + return BaseTransformation.get(name=self.kwargs['transformation_name']) - def get_post_action_redirect(self): - return reverse( - viewname='converter:transformation_list', kwargs={ - 'app_label': self.transformation.content_type.app_label, - 'model': self.transformation.content_type.model, - 'object_id': self.transformation.object_id - } - ) + +class TransformationDeleteView(LayerViewMixin, SingleObjectDeleteView): + model = LayerTransformation def get_extra_context(self): return { - 'content_object': self.transformation.content_object, + 'content_object': self.object.object_layer.content_object, + 'layer_name': self.layer.name, 'navigation_object_list': ('content_object', 'transformation'), 'previous': reverse( viewname='converter:transformation_list', kwargs={ - 'app_label': self.transformation.content_type.app_label, - 'model': self.transformation.content_type.model, - 'object_id': self.transformation.object_id + 'app_label': self.object.object_layer.content_type.app_label, + 'model': self.object.object_layer.content_type.model, + 'object_id': self.object.object_layer.object_id, + 'layer_name': self.object.object_layer.stored_layer.name } ), 'title': _( 'Delete transformation "%(transformation)s" for: ' '%(content_object)s?' ) % { - 'transformation': self.transformation, - 'content_object': self.transformation.content_object + 'transformation': self.object, + 'content_object': self.object.object_layer.content_object }, - 'transformation': self.transformation, + 'transformation': self.object, } + def get_object_permission(self): + return self.layer.permissions.get('delete', None) -class TransformationEditView(SingleObjectEditView): - form_class = TransformationForm - model = Transformation - - def dispatch(self, request, *args, **kwargs): - self.transformation = get_object_or_404( - klass=Transformation, pk=self.kwargs['pk'] + def get_post_action_redirect(self): + return reverse( + viewname='converter:transformation_list', kwargs={ + 'app_label': self.object.object_layer.content_type.app_label, + 'model': self.object.object_layer.content_type.model, + 'object_id': self.object.object_layer.object_id, + 'layer_name': self.object.object_layer.stored_layer.name + } ) - AccessControlList.objects.check_access( - obj=self.transformation.content_object, - permissions=(permission_transformation_edit,), user=request.user - ) - return super(TransformationEditView, self).dispatch( - request, *args, **kwargs - ) +class TransformationEditView(LayerViewMixin, SingleObjectEditView): + form_class = LayerTransformationForm + model = LayerTransformation def form_valid(self, form): instance = form.save(commit=False) @@ -165,72 +172,121 @@ class TransformationEditView(SingleObjectEditView): def get_extra_context(self): return { - 'content_object': self.transformation.content_object, + 'content_object': self.object.object_layer.content_object, + 'form_field_css_classes': 'hidden' if hasattr( + self.object.get_transformation_class(), 'template_name' + ) else '', + 'layer': self.layer, + 'layer_name': self.layer.name, 'navigation_object_list': ('content_object', 'transformation'), 'title': _( - 'Edit transformation "%(transformation)s" for: %(content_object)s' + 'Edit transformation "%(transformation)s" ' + 'for: %(content_object)s' ) % { - 'transformation': self.transformation, - 'content_object': self.transformation.content_object + 'transformation': self.object, + 'content_object': self.object.object_layer.content_object }, - 'transformation': self.transformation, + 'transformation': self.object, } + def get_object_permission(self): + return self.layer.permissions.get('edit', None) + def get_post_action_redirect(self): return reverse( viewname='converter:transformation_list', kwargs={ - 'app_label': self.transformation.content_type.app_label, - 'model': self.transformation.content_type.model, - 'object_id': self.transformation.object_id + 'app_label': self.object.object_layer.content_type.app_label, + 'model': self.object.object_layer.content_type.model, + 'object_id': self.object.object_layer.object_id, + 'layer_name': self.object.object_layer.stored_layer.name } ) - -class TransformationListView(SingleObjectListView): - def dispatch(self, request, *args, **kwargs): - content_type = get_object_or_404( - klass=ContentType, app_label=self.kwargs['app_label'], - model=self.kwargs['model'] - ) - - try: - self.content_object = content_type.get_object_for_this_type( - pk=self.kwargs['object_id'] + def get_template_names(self): + return [ + getattr( + self.object.get_transformation_class(), 'template_name', + self.template_name ) - except content_type.model_class().DoesNotExist: - raise Http404 + ] - AccessControlList.objects.check_access( - obj=self.content_object, - permissions=(permission_transformation_view,), user=request.user - ) - return super(TransformationListView, self).dispatch( - request, *args, **kwargs +class TransformationListView( + LayerViewMixin, ExternalContentTypeObjectMixin, SingleObjectListView +): + def get_external_object_permission(self): + return self.layer.permissions.get('view', None) + + def get_extra_context(self): + return { + 'object': self.external_object, + 'hide_link': True, + 'hide_object': True, + 'layer_name': self.layer.name, + 'no_results_icon': self.layer.get_icon(), + 'no_results_main_link': link_transformation_select.resolve( + context=RequestContext( + request=self.request, dict_={ + 'resolved_object': self.external_object, + 'layer_name': self.kwargs['layer_name'], + } + ) + ), + 'no_results_text': self.layer.get_empty_results_text(), + 'no_results_title': _( + 'There are no entries for layer "%(layer_name)s"' + ) % {'layer_name': self.layer.label}, + 'title': _( + 'Layer "%(layer)s" transformations for: %(object)s' + ) % { + 'layer': self.layer, + 'object': self.external_object, + } + } + + def get_source_queryset(self): + return self.layer.get_transformations_for(obj=self.external_object) + + +class TransformationSelectView( + ExternalContentTypeObjectMixin, LayerViewMixin, FormView +): + form_class = LayerTransformationSelectForm + template_name = 'appearance/generic_form.html' + + def form_valid(self, form): + return HttpResponseRedirect( + redirect_to=reverse( + viewname='converter:transformation_create', + kwargs={ + 'app_label': self.kwargs['app_label'], + 'model': self.kwargs['model'], + 'object_id': self.kwargs['object_id'], + 'layer_name': self.kwargs['layer_name'], + 'transformation_name': form.cleaned_data[ + 'transformation' + ] + } + ) ) def get_extra_context(self): return { - 'content_object': self.content_object, - 'hide_link': True, - 'hide_object': True, + 'layer': self.layer, + 'layer_name': self.kwargs['layer_name'], 'navigation_object_list': ('content_object',), - 'no_results_icon': icon_transformation_list, - 'no_results_main_link': link_transformation_create.resolve( - context=RequestContext( - request=self.request, dict_={ - 'content_object': self.content_object - } - ) - ), - 'no_results_text': _( - 'Transformations allow changing the visual appearance ' - 'of documents without making permanent changes to the ' - 'document file themselves.' - ), - 'no_results_title': _('No transformations'), - 'title': _('Transformations for: %s') % self.content_object, + 'content_object': self.external_object, + 'submit_label': _('Select'), + 'title': _( + 'Select new layer "%(layer)s" transformation ' + 'for: %(object)s' + ) % { + 'layer': self.layer, + 'object': self.external_object, + } } - def get_source_queryset(self): - return Transformation.objects.get_for_object(obj=self.content_object) + def get_form_extra_kwargs(self): + return { + 'layer': self.layer + } diff --git a/mayan/apps/documents/api_views.py b/mayan/apps/documents/api_views.py index 26878c8acb..4d17038722 100644 --- a/mayan/apps/documents/api_views.py +++ b/mayan/apps/documents/api_views.py @@ -196,10 +196,16 @@ class APIDocumentPageImageView(generics.RetrieveAPIView): if rotation: rotation = int(rotation) + maximum_layer_order = request.GET.get('maximum_layer_order') + if maximum_layer_order: + maximum_layer_order = int(maximum_layer_order) + task = task_generate_document_page_image.apply_async( kwargs=dict( document_page_id=self.get_object().pk, width=width, - height=height, zoom=zoom, rotation=rotation + height=height, zoom=zoom, rotation=rotation, + maximum_layer_order=maximum_layer_order, + user_id=request.user.pk ) ) diff --git a/mayan/apps/documents/apps.py b/mayan/apps/documents/apps.py index ad9e6b2dee..09e85928c7 100644 --- a/mayan/apps/documents/apps.py +++ b/mayan/apps/documents/apps.py @@ -106,7 +106,6 @@ def is_document_page_enabled(context): return context['object'].enabled - class DocumentsApp(MayanAppConfig): app_namespace = 'documents' app_url = 'documents' diff --git a/mayan/apps/documents/migrations/0049_auto_20190715_0454.py b/mayan/apps/documents/migrations/0049_auto_20190715_0454.py index 889d244bfe..ccba51c786 100644 --- a/mayan/apps/documents/migrations/0049_auto_20190715_0454.py +++ b/mayan/apps/documents/migrations/0049_auto_20190715_0454.py @@ -7,7 +7,7 @@ from ..storages import storage_documentimagecache def operation_clear_old_cache(apps, schema_editor): DocumentPageCachedImage = apps.get_model( - 'documents', 'DocumentPageCachedImage' + app_label='documents', model_name='DocumentPageCachedImage' ) for cached_image in DocumentPageCachedImage.objects.using(schema_editor.connection.alias).all(): diff --git a/mayan/apps/documents/models/document_page_models.py b/mayan/apps/documents/models/document_page_models.py index 0cf3c26334..00be66ab4b 100644 --- a/mayan/apps/documents/models/document_page_models.py +++ b/mayan/apps/documents/models/document_page_models.py @@ -12,7 +12,7 @@ from django.utils.translation import ugettext_lazy as _ from mayan.apps.converter.literals import DEFAULT_ZOOM_LEVEL, DEFAULT_ROTATION -from mayan.apps.converter.models import Transformation +from mayan.apps.converter.models import LayerTransformation from mayan.apps.converter.transformations import ( BaseTransformation, TransformationResize, TransformationRotate, TransformationZoom @@ -83,8 +83,8 @@ class DocumentPage(models.Model): def document(self): return self.document_version.document - def generate_image(self, *args, **kwargs): - transformation_list = self.get_combined_transformation_list(*args, **kwargs) + def generate_image(self, user=None, **kwargs): + transformation_list = self.get_combined_transformation_list(user=user, **kwargs) combined_cache_filename = BaseTransformation.combine(transformation_list) # Check is transformed image is available @@ -136,7 +136,7 @@ class DocumentPage(models.Model): return final_url.tostr() - def get_combined_transformation_list(self, *args, **kwargs): + def get_combined_transformation_list(self, user=None, *args, **kwargs): """ Return a list of transformation containing the server side document page transformation as well as tranformations created @@ -161,8 +161,13 @@ class DocumentPage(models.Model): # Generate transformation hash transformation_list = [] + maximum_layer_order = kwargs.get('maximum_layer_order', None) + # Stored transformations first - for stored_transformation in Transformation.objects.get_for_object(self, as_classes=True): + for stored_transformation in LayerTransformation.objects.get_for_object( + self, maximum_layer_order=maximum_layer_order, as_classes=True, + user=user + ): transformation_list.append(stored_transformation) # Interactive transformations second diff --git a/mayan/apps/documents/models/document_version_models.py b/mayan/apps/documents/models/document_version_models.py index e18e474fae..1c24fbfeda 100644 --- a/mayan/apps/documents/models/document_version_models.py +++ b/mayan/apps/documents/models/document_version_models.py @@ -15,7 +15,7 @@ from django.utils.functional import cached_property from django.utils.translation import ugettext_lazy as _ from mayan.apps.converter.exceptions import InvalidOfficeFormat, PageCountError -from mayan.apps.converter.models import Transformation +from mayan.apps.converter.layers import layer_saved_transformations from mayan.apps.converter.transformations import TransformationRotate from mayan.apps.converter.utils import get_converter_class from mayan.apps.mimetype.api import get_mimetype @@ -156,7 +156,7 @@ class DocumentVersion(models.Model): for page in self.pages.all(): degrees = page.detect_orientation() if degrees: - Transformation.objects.add_to_object( + layer_saved_transformations.add_to_object( obj=page, transformation=TransformationRotate, arguments='{{"degrees": {}}}'.format(360 - degrees) ) diff --git a/mayan/apps/documents/tasks.py b/mayan/apps/documents/tasks.py index b77ca1e0b4..34bfad3616 100644 --- a/mayan/apps/documents/tasks.py +++ b/mayan/apps/documents/tasks.py @@ -65,13 +65,19 @@ def task_delete_stubs(): @app.task() -def task_generate_document_page_image(document_page_id, *args, **kwargs): +def task_generate_document_page_image(document_page_id, user_id=None, **kwargs): DocumentPage = apps.get_model( app_label='documents', model_name='DocumentPage' ) + User = get_user_model() + + if user_id: + user = User.objects.get(pk=user_id) + else: + user = None document_page = DocumentPage.passthrough.get(pk=document_page_id) - return document_page.generate_image(*args, **kwargs) + return document_page.generate_image(user=user, **kwargs) @app.task(ignore_result=True) diff --git a/mayan/apps/documents/views/document_views.py b/mayan/apps/documents/views/document_views.py index 4d2bf53ea6..566077c99c 100644 --- a/mayan/apps/documents/views/document_views.py +++ b/mayan/apps/documents/views/document_views.py @@ -2,8 +2,10 @@ from __future__ import absolute_import, unicode_literals import logging +from django.conf import settings from django.contrib import messages from django.core.exceptions import PermissionDenied +from django.db import transaction from django.http import HttpResponseRedirect from django.shortcuts import get_object_or_404 from django.urls import reverse @@ -17,7 +19,7 @@ from mayan.apps.common.generics import ( SingleObjectDetailView, SingleObjectDownloadView, SingleObjectEditView, SingleObjectListView ) -from mayan.apps.converter.models import Transformation +from mayan.apps.converter.layers import layer_saved_transformations from mayan.apps.converter.permissions import ( permission_transformation_delete, permission_transformation_edit ) @@ -522,7 +524,7 @@ class DocumentTransformationsClearView(MultipleObjectConfirmActionView): def object_action(self, form, instance): try: for page in instance.pages.all(): - Transformation.objects.get_for_object(obj=page).delete() + layer_saved_transformations.get_transformations_for(obj=page).delete() except Exception as exception: messages.error( self.request, _( @@ -545,24 +547,29 @@ class DocumentTransformationsCloneView(FormView): pk=form.cleaned_data['page'].pk ) - for page in target_pages: - Transformation.objects.get_for_object(obj=page).delete() + with transaction.atomic(): + for page in target_pages: + layer_saved_transformations.get_transformations_for(obj=page).delete() - Transformation.objects.copy( - source=form.cleaned_data['page'], targets=target_pages - ) + layer_saved_transformations.copy_transformations( + source=form.cleaned_data['page'], targets=target_pages + ) except Exception as exception: - messages.error( - self.request, _( - 'Error deleting the page transformations for ' - 'document: %(document)s; %(error)s.' - ) % { - 'document': instance, 'error': exception - } - ) + if settings.DEBUG: + raise + else: + messages.error( + message=_( + 'Error cloning the page transformations for ' + 'document: %(document)s; %(error)s.' + ) % { + 'document': instance, 'error': exception + }, request=self.request + ) else: messages.success( - self.request, _('Transformations cloned successfully.') + message=_('Transformations cloned successfully.'), + request=self.request ) return super(DocumentTransformationsCloneView, self).form_valid(form=form) diff --git a/mayan/apps/documents/views/misc_views.py b/mayan/apps/documents/views/misc_views.py index 5500f53951..b1f1712187 100644 --- a/mayan/apps/documents/views/misc_views.py +++ b/mayan/apps/documents/views/misc_views.py @@ -23,5 +23,6 @@ class ScanDuplicatedDocuments(ConfirmView): def view_action(self): task_scan_duplicates_all.apply_async() messages.success( - self.request, _('Duplicated document scan queued successfully.') + message=_('Duplicated document scan queued successfully.'), + request=self.request ) diff --git a/mayan/apps/navigation/classes.py b/mayan/apps/navigation/classes.py index d32d405d26..a646f82726 100644 --- a/mayan/apps/navigation/classes.py +++ b/mayan/apps/navigation/classes.py @@ -48,19 +48,21 @@ class Link(object): def __init__( self, text=None, view=None, args=None, badge_text=None, condition=None, - conditional_disable=None, description=None, html_data=None, - html_extra_classes=None, icon_class=None, icon_class_path=None, - keep_query=False, kwargs=None, name=None, permissions=None, - remove_from_query=None, tags=None, url=None + conditional_active=None, conditional_disable=None, description=None, + html_data=None, html_extra_classes=None, icon_class=None, + icon_class_path=None, keep_query=False, kwargs=None, name=None, + permissions=None, remove_from_query=None, tags=None, url=None ): self.args = args or [] self.badge_text = badge_text self.condition = condition + self.conditional_active = conditional_active self.conditional_disable = conditional_disable self.description = description self.html_data = html_data self.html_extra_classes = html_extra_classes self.icon_class = icon_class + self.icon_class_path = icon_class_path self.keep_query = keep_query self.kwargs = kwargs or {} self.name = name @@ -71,7 +73,13 @@ class Link(object): self.view = view self.url = url - if icon_class_path: + self.process_icon() + + if name: + self.__class__._registry[name] = self + + def process_icon(self): + if self.icon_class_path: if self.icon_class: raise ImproperlyConfigured( 'Specify the icon_class or the icon_class_path but not ' @@ -79,17 +87,14 @@ class Link(object): ) else: try: - self.icon_class = import_string(dotted_path=icon_class_path) + self.icon_class = import_string(dotted_path=self.icon_class_path) except ImportError as exception: logger.error( - 'Exception importing icon: %s; %s', icon_class_path, + 'Exception importing icon: %s; %s', self.icon_class_path, exception ) raise - if name: - self.__class__._registry[name] = self - def resolve(self, context=None, request=None, resolved_object=None): if not context and not request: raise ImproperlyConfigured( @@ -525,7 +530,13 @@ class ResolvedLink(object): @property def active(self): - return self.link.view == self.current_view_name + conditional_active = self.link.conditional_active + if conditional_active: + return conditional_active( + context=self.context, resolved_link=self + ) + else: + return self.link.view == self.current_view_name @property def badge_text(self): diff --git a/mayan/apps/navigation/html_widgets.py b/mayan/apps/navigation/html_widgets.py index 164c3e928a..2f37294140 100644 --- a/mayan/apps/navigation/html_widgets.py +++ b/mayan/apps/navigation/html_widgets.py @@ -12,4 +12,3 @@ class SourceColumnLinkWidget(object): 'column': self.column, 'value': value } ) - diff --git a/mayan/apps/redactions/__init__.py b/mayan/apps/redactions/__init__.py new file mode 100644 index 0000000000..2abd401976 --- /dev/null +++ b/mayan/apps/redactions/__init__.py @@ -0,0 +1,3 @@ +from __future__ import unicode_literals + +default_app_config = 'mayan.apps.redactions.apps.RedactionsApp' diff --git a/mayan/apps/redactions/apps.py b/mayan/apps/redactions/apps.py new file mode 100644 index 0000000000..48d9ddaa39 --- /dev/null +++ b/mayan/apps/redactions/apps.py @@ -0,0 +1,41 @@ +from __future__ import unicode_literals + +import logging + +from django.apps import apps +from django.utils.translation import ugettext_lazy as _ + +from mayan.apps.common.apps import MayanAppConfig +from mayan.apps.converter.links import link_transformation_list +from mayan.apps.common.menus import menu_list_facet + +from .dependencies import * # NOQA +from .layers import layer_redactions # NOQA +from .transformations import * # NOQA + +logger = logging.getLogger(__name__) + + +class RedactionsApp(MayanAppConfig): + app_namespace = 'redactions' + app_url = 'redactions' + has_rest_api = False + has_tests = False + name = 'mayan.apps.redactions' + verbose_name = _('Redactions') + + def ready(self): + super(RedactionsApp, self).ready() + + DocumentPage = apps.get_model( + app_label='documents', model_name='DocumentPage' + ) + + link_redaction_list = link_transformation_list.copy( + layer=layer_redactions + ) + link_redaction_list.text = _('Redactions') + + menu_list_facet.bind_links( + links=(link_redaction_list,), sources=(DocumentPage,) + ) diff --git a/mayan/apps/redactions/dependencies.py b/mayan/apps/redactions/dependencies.py new file mode 100644 index 0000000000..77c4dc7879 --- /dev/null +++ b/mayan/apps/redactions/dependencies.py @@ -0,0 +1,13 @@ +from __future__ import unicode_literals + +from django.utils.translation import ugettext_lazy as _ + +from mayan.apps.dependencies.classes import JavaScriptDependency + +JavaScriptDependency( + label=_('JavaScript image cropper'), module=__name__, name='cropperjs', + version_string='=1.4.1' +) +JavaScriptDependency( + module=__name__, name='jquery-cropper', version_string='=1.0.0' +) diff --git a/mayan/apps/redactions/layers.py b/mayan/apps/redactions/layers.py new file mode 100644 index 0000000000..e44fa77856 --- /dev/null +++ b/mayan/apps/redactions/layers.py @@ -0,0 +1,27 @@ +from __future__ import unicode_literals + +from django.utils.translation import ugettext_lazy as _ + +from mayan.apps.converter.classes import Layer +from mayan.apps.converter.layers import layer_saved_transformations + +from .permissions import ( + permission_redaction_create, permission_redaction_delete, + permission_redaction_edit, permission_redaction_exclude, + permission_redaction_view +) + +layer_redactions = Layer( + empty_results_text=_( + 'Redactions allow removing access to confidential and ' + 'sensitive information without having to modify the document.' + ), label=_('Redactions'), name='redactions', + order=layer_saved_transformations.order - 1, permissions={ + 'create': permission_redaction_create, + 'delete': permission_redaction_delete, + 'exclude': permission_redaction_exclude, + 'edit': permission_redaction_edit, + 'select': permission_redaction_create, + 'view': permission_redaction_view, + }, symbol='highlighter' +) diff --git a/mayan/apps/redactions/migrations/__init__.py b/mayan/apps/redactions/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/mayan/apps/redactions/permissions.py b/mayan/apps/redactions/permissions.py new file mode 100644 index 0000000000..1ee2d31799 --- /dev/null +++ b/mayan/apps/redactions/permissions.py @@ -0,0 +1,23 @@ +from __future__ import absolute_import, unicode_literals + +from django.utils.translation import ugettext_lazy as _ + +from mayan.apps.permissions import PermissionNamespace + +namespace = PermissionNamespace(label=_('Redactions'), name='redactions') + +permission_redaction_create = namespace.add_permission( + label=_('Create new redactions'), name='redaction_create' +) +permission_redaction_delete = namespace.add_permission( + label=_('Delete redactions'), name='redaction_delete' +) +permission_redaction_edit = namespace.add_permission( + label=_('Edit redactions'), name='redaction_edit' +) +permission_redaction_exclude = namespace.add_permission( + label=_('Exclude redactions'), name='redaction_exclude' +) +permission_redaction_view = namespace.add_permission( + label=_('View existing redactions'), name='redaction_view' +) diff --git a/mayan/apps/redactions/templates/redactions/cropper.html b/mayan/apps/redactions/templates/redactions/cropper.html new file mode 100644 index 0000000000..6dcbb8bea5 --- /dev/null +++ b/mayan/apps/redactions/templates/redactions/cropper.html @@ -0,0 +1,127 @@ +{% extends 'appearance/base.html' %} + +{% load i18n %} +{% load static %} + +{% load common_tags %} +{% load documents_tags %} + +{% block title %}{{ title }}{% endblock title %} + +{% block stylesheets %} + + + +{% endblock %} + +{% block content %} +
+ +
+ +
+ {% with '' as title %} + {% include 'appearance/generic_form_subtemplate.html' %} + {% endwith %} +{% endblock content %} + +{% block javascript %} + +{% endblock %} diff --git a/mayan/apps/redactions/transformations.py b/mayan/apps/redactions/transformations.py new file mode 100644 index 0000000000..19e594e5c9 --- /dev/null +++ b/mayan/apps/redactions/transformations.py @@ -0,0 +1,21 @@ +from __future__ import unicode_literals + +from django.utils.translation import ugettext_lazy as _ + +from mayan.apps.converter.transformations import ( + BaseTransformation, TransformationDrawRectanglePercent +) + +from .layers import layer_redactions + + +class TransformationRedactionPercent(TransformationDrawRectanglePercent): + arguments = ('left', 'top', 'right', 'bottom') + label = _('Redaction') + name = 'redaction_percent' + template_name = 'redactions/cropper.html' + + +BaseTransformation.register( + layer=layer_redactions, transformation=TransformationRedactionPercent +) diff --git a/mayan/apps/sources/apps.py b/mayan/apps/sources/apps.py index f74954b481..7139e5a846 100644 --- a/mayan/apps/sources/apps.py +++ b/mayan/apps/sources/apps.py @@ -17,8 +17,8 @@ from mayan.apps.navigation.classes import SourceColumn from .classes import StagingFile from .dependencies import * # NOQA from .handlers import ( - handler_copy_transformations_to_version, handler_create_default_document_source, - handler_initialize_periodic_tasks + handler_copy_transformations_to_version, + handler_create_default_document_source, handler_initialize_periodic_tasks ) from .links import ( link_document_create_multiple, link_setup_sources, diff --git a/mayan/apps/sources/handlers.py b/mayan/apps/sources/handlers.py index 522031e405..a83d841c13 100644 --- a/mayan/apps/sources/handlers.py +++ b/mayan/apps/sources/handlers.py @@ -3,19 +3,16 @@ from __future__ import unicode_literals from django.apps import apps from django.utils.translation import ugettext_lazy as _ +from mayan.apps.converter.layers import layer_saved_transformations + from .literals import SOURCE_UNCOMPRESS_CHOICE_ASK -def handler_copy_transformations_to_version(sender, **kwargs): - Transformation = apps.get_model( - app_label='converter', model_name='Transformation' - ) - - instance = kwargs['instance'] - +def handler_copy_transformations_to_version(sender, instance, **kwargs): # TODO: Fix this, source should be previous version # TODO: Fix this, shouldn't this be at the documents app - Transformation.objects.copy( + + layer_saved_transformations.copy_transformations( source=instance.document, targets=instance.pages.all() ) diff --git a/mayan/apps/sources/models/base.py b/mayan/apps/sources/models/base.py index 9c18d9edbc..d7cf22489c 100644 --- a/mayan/apps/sources/models/base.py +++ b/mayan/apps/sources/models/base.py @@ -12,7 +12,7 @@ from model_utils.managers import InheritanceManager from mayan.apps.common.compressed_files import Archive from mayan.apps.common.exceptions import NoMIMETypeMatch -from mayan.apps.converter.models import Transformation +from mayan.apps.converter.layers import layer_saved_transformations from mayan.apps.documents.models import Document, DocumentType from mayan.apps.documents.settings import setting_language @@ -131,7 +131,7 @@ class Source(models.Model): if user: document.add_as_recent_document_for_user(user=user) - Transformation.objects.copy( + layer_saved_transformations.copy_transformations( source=self, targets=document_version.pages.all() ) diff --git a/mayan/settings/base.py b/mayan/settings/base.py index f00d601035..816dc34c77 100644 --- a/mayan/settings/base.py +++ b/mayan/settings/base.py @@ -123,6 +123,7 @@ INSTALLED_APPS = ( 'mayan.apps.metadata', 'mayan.apps.mirroring', 'mayan.apps.ocr', + 'mayan.apps.redactions', 'mayan.apps.sources', 'mayan.apps.storage', 'mayan.apps.tags',