Add converter layers, redactions app

Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
This commit is contained in:
Roberto Rosario
2019-08-20 00:10:12 -04:00
parent 0917bd57b3
commit ad37228466
43 changed files with 1462 additions and 460 deletions

View File

@@ -45,7 +45,7 @@
{{ field }} {{ field }}
{% endfor %} {% endfor %}
{% for field in form.visible_fields %} {% for field in form.visible_fields %}
<div class="form-group {% if field.errors %}has-error{% endif %}"> <div class="form-group {% if field.errors %}has-error{% endif %} {{ form_field_css_classes }}">
{# We display the label then the field for all except checkboxes #} {# We display the label then the field for all except checkboxes #}
{% if field|widget_type != 'checkboxinput' and not field.field.widget.attrs.hidden %} {% 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 %} {% if not hide_labels %}{{ field.label_tag }}{% if field.field.required and not read_only %} ({% trans 'required' %}){% endif %}{% endif %}

View File

@@ -355,8 +355,8 @@ class DocumentCheckoutViewTestCase(
class NewVersionBlockViewTestCase( class NewVersionBlockViewTestCase(
DocumentCheckoutTestMixin, DocumentCheckoutViewTestMixin, DocumentCheckoutTestMixin, DocumentCheckoutViewTestMixin,
GenericDocumentViewTestCase): GenericDocumentViewTestCase
):
def test_document_check_out_new_version(self): def test_document_check_out_new_version(self):
""" """
Gitlab issue #231 Gitlab issue #231

View File

@@ -430,6 +430,21 @@ class RestrictedQuerysetMixin(object):
object_permission = None object_permission = None
source_queryset = 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): def get_source_queryset(self):
if self.source_queryset is None: if self.source_queryset is None:
if self.model: if self.model:
@@ -445,17 +460,6 @@ class RestrictedQuerysetMixin(object):
return self.source_queryset.all() 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): class ViewPermissionCheckMixin(object):
""" """
@@ -467,11 +471,16 @@ class ViewPermissionCheckMixin(object):
view_permission = None view_permission = None
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
if self.view_permission: view_permission = self.get_view_permission()
if view_permission:
Permission.check_user_permissions( Permission.check_user_permissions(
permissions=(self.view_permission,), user=self.request.user permissions=(view_permission,),
user=self.request.user
) )
return super( return super(
ViewPermissionCheckMixin, self ViewPermissionCheckMixin, self
).dispatch(request, *args, **kwargs) ).dispatch(request, *args, **kwargs)
def get_view_permission(self):
return self.view_permission

View File

@@ -3,14 +3,15 @@ from __future__ import unicode_literals
from django.utils.encoding import force_text from django.utils.encoding import force_text
from django.utils.translation import ugettext_lazy as _ 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.apps import MayanAppConfig
from mayan.apps.common.menus import menu_object, menu_secondary from mayan.apps.common.menus import menu_object, menu_secondary
from mayan.apps.navigation.classes import SourceColumn from mayan.apps.navigation.classes import SourceColumn
from .dependencies import * # NOQA from .dependencies import * # NOQA
from .links import ( from .links import (
link_transformation_create, link_transformation_delete, link_transformation_delete, link_transformation_edit,
link_transformation_edit link_transformation_select
) )
@@ -24,26 +25,31 @@ class ConverterApp(MayanAppConfig):
def ready(self): def ready(self):
super(ConverterApp, self).ready() 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( SourceColumn(
source=Transformation, label=_('Transformation'), source=LayerTransformation, label=_('Transformation'),
func=lambda context: force_text(context['object']) func=lambda context: force_text(context['object'])
) )
SourceColumn( SourceColumn(
attribute='arguments', source=Transformation attribute='arguments', source=LayerTransformation
) )
menu_object.bind_links( menu_object.bind_links(
links=(link_transformation_edit, link_transformation_delete), links=(link_transformation_edit, link_transformation_delete),
sources=(Transformation,) sources=(LayerTransformation,)
) )
menu_secondary.bind_links( menu_secondary.bind_links(
links=(link_transformation_create,), sources=(Transformation,) links=(link_transformation_select,), sources=(LayerTransformation,)
) )
menu_secondary.bind_links( menu_secondary.bind_links(
links=(link_transformation_create,), links=(link_transformation_select,),
sources=( sources=(
'converter:transformation_create', 'converter:transformation_create',
'converter:transformation_list' 'converter:transformation_list'

View File

@@ -1,5 +1,6 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import copy
from io import BytesIO from io import BytesIO
import logging import logging
import os import os
@@ -8,9 +9,16 @@ import shutil
from PIL import Image from PIL import Image
import sh 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 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.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.settings import setting_temporary_directory
from mayan.apps.storage.utils import ( from mayan.apps.storage.utils import (
NamedTemporaryFile, fs_cleanup, mkdtemp NamedTemporaryFile, fs_cleanup, mkdtemp
@@ -202,3 +210,228 @@ class ConverterBase(object):
for transformation in transformations: for transformation in transformations:
self.image = transformation.execute_on(image=self.image) 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

View File

@@ -8,13 +8,49 @@ from django.utils.translation import ugettext_lazy as _
from mayan.apps.common.serialization import yaml_load 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: class Meta:
fields = ('name', 'arguments', 'order') fields = ('arguments', 'order')
model = Transformation 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): def clean(self):
try: try:

View File

@@ -4,10 +4,7 @@ from mayan.apps.appearance.classes import Icon
icon_transformations = Icon(driver_name='fontawesome', symbol='crop') 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_delete = Icon(driver_name='fontawesome', symbol='times')
icon_transformation_edit = Icon(driver_name='fontawesome', symbol='pencil-alt') icon_transformation_edit = Icon(driver_name='fontawesome', symbol='pencil-alt')
icon_transformation_list = icon_transformations icon_transformation_list = icon_transformations
icon_transformation_select = Icon(driver_name='fontawesome', symbol='plus')

View File

@@ -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'
)

View File

@@ -1,55 +1,37 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django.apps import apps
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from mayan.apps.navigation.classes import Link from .classes import LayerLink
from .layers import layer_saved_transformations
from .permissions import (
permission_transformation_create, permission_transformation_delete,
permission_transformation_edit, permission_transformation_view
)
def get_kwargs_factory(variable_name): def conditional_active(context, resolved_link):
def get_kwargs(context): return resolved_link.link.view == resolved_link.current_view_name and context.get('layer_name', None) == resolved_link.link.layer_name
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
link_transformation_create = Link( link_transformation_delete = LayerLink(
icon_class_path='mayan.apps.converter.icons.icon_transformation_create', action='delete',
kwargs=get_kwargs_factory('content_object'), kwargs={'layer_name': 'layer_name', 'pk': 'resolved_object.pk'},
permissions=(permission_transformation_create,),
text=_('Create new transformation'), view='converter:transformation_create'
)
link_transformation_delete = Link(
args='resolved_object.pk',
icon_class_path='mayan.apps.converter.icons.icon_transformation_delete', 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' tags='dangerous', text=_('Delete'), view='converter:transformation_delete'
) )
link_transformation_edit = Link( link_transformation_edit = LayerLink(
args='resolved_object.pk', action='edit',
kwargs={'layer_name': 'layer_name', 'pk': 'resolved_object.pk'},
icon_class_path='mayan.apps.converter.icons.icon_transformation_edit', icon_class_path='mayan.apps.converter.icons.icon_transformation_edit',
permissions=(permission_transformation_edit,), layer=layer_saved_transformations,
text=_('Edit'), view='converter:transformation_edit' text=_('Edit'), view='converter:transformation_edit'
) )
link_transformation_list = Link( link_transformation_list = LayerLink(
icon_class_path='mayan.apps.converter.icons.icon_transformation_list', action='list', conditional_active=conditional_active,
kwargs=get_kwargs_factory('resolved_object'), layer=layer_saved_transformations, text=_('Transformations'),
permissions=(permission_transformation_view,), text=_('Transformations'),
view='converter:transformation_list' 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'
)

View File

@@ -2,75 +2,89 @@ from __future__ import unicode_literals
import logging import logging
from django.apps import apps
from django.contrib.contenttypes.models import ContentType 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 from .transformations import BaseTransformation
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class TransformationManager(models.Manager): class LayerTransformationManager(models.Manager):
def add_to_object(self, obj, transformation, arguments=None): def get_for_object(
content_type = ContentType.objects.get_for_model(model=obj) self, obj, as_classes=False, maximum_layer_order=None,
only_stored_layer=None, user=None
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):
""" """
as_classes == True returns the transformation classes from .classes as_classes == True returns the transformation classes from .classes
ready to be feed to the converter class 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) content_type = ContentType.objects.get_for_model(model=obj)
transformations = self.filter( 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: if as_classes:

View File

@@ -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')]),
),
]

View File

@@ -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
)
]

View File

@@ -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',
),
]

View File

@@ -9,7 +9,8 @@ from django.db.models import Max
from django.utils.encoding import python_2_unicode_compatible from django.utils.encoding import python_2_unicode_compatible
from django.utils.translation import ugettext_lazy as _ 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 .transformations import BaseTransformation
from .validators import YAMLValidator from .validators import YAMLValidator
@@ -17,7 +18,47 @@ logger = logging.getLogger(__name__)
@python_2_unicode_compatible @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 Model that stores the transformation and transformation arguments
for a given object for a given object
@@ -29,9 +70,10 @@ class Transformation(models.Model):
transformation argument. Example: if a page is rotated with the Rotation transformation argument. Example: if a page is rotated with the Rotation
transformation, this field will show by how many degrees it was rotated. transformation, this field will show by how many degrees it was rotated.
""" """
content_type = models.ForeignKey(on_delete=models.CASCADE, to=ContentType) object_layer = models.ForeignKey(
object_id = models.PositiveIntegerField() on_delete=models.CASCADE, related_name='transformations',
content_object = GenericForeignKey('content_type', 'object_id') to=ObjectLayer, verbose_name=_('Object layer')
)
order = models.PositiveIntegerField( order = models.PositiveIntegerField(
blank=True, db_index=True, default=0, help_text=_( blank=True, db_index=True, default=0, help_text=_(
'Order in which the transformations will be executed. If left ' 'Order in which the transformations will be executed. If left '
@@ -48,23 +90,27 @@ class Transformation(models.Model):
'dictionary. ie: {"degrees": 180}' 'dictionary. ie: {"degrees": 180}'
), validators=[YAMLValidator()], verbose_name=_('Arguments') ), validators=[YAMLValidator()], verbose_name=_('Arguments')
) )
enabled = models.BooleanField(default=True, verbose_name=_('Enabled'))
objects = TransformationManager() objects = LayerTransformationManager()
class Meta: class Meta:
ordering = ('order',) ordering = ('object_layer__stored_layer__order', 'order',)
unique_together = ('content_type', 'object_id', 'order') unique_together = ('object_layer', 'order')
verbose_name = _('Transformation') verbose_name = _('Layer transformation')
verbose_name_plural = _('Transformations') verbose_name_plural = _('Layer transformations')
def __str__(self): def __str__(self):
return self.get_name_display() return self.get_name_display()
def get_transformation_class(self):
return BaseTransformation.get(name=self.name)
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
if not self.order: if not self.order:
last_order = Transformation.objects.filter( last_order = LayerTransformation.objects.filter(
content_type=self.content_type, object_id=self.object_id object_layer=self.object_layer
).aggregate(Max('order'))['order__max'] ).aggregate(Max('order'))['order__max']
if last_order is not None: if last_order is not None:
self.order = last_order + 1 self.order = last_order + 1
super(Transformation, self).save(*args, **kwargs) super(LayerTransformation, self).save(*args, **kwargs)

View File

@@ -2,6 +2,7 @@ from __future__ import unicode_literals
TEST_TRANSFORMATION_NAME = 'rotate' TEST_TRANSFORMATION_NAME = 'rotate'
TEST_TRANSFORMATION_ARGUMENT = 'degrees: 180' TEST_TRANSFORMATION_ARGUMENT = 'degrees: 180'
TEST_TRANSFORMATION_ARGUMENT_EDITED = 'degrees: 270'
TEST_TRANSFORMATION_COMBINED_CACHE_HASH = '384bf78014d2aed7255d9e548a0694c70af0b22545653214bcceb1ac6286b5f7' TEST_TRANSFORMATION_COMBINED_CACHE_HASH = '384bf78014d2aed7255d9e548a0694c70af0b22545653214bcceb1ac6286b5f7'
TEST_TRANSFORMATION_RESIZE_CACHE_HASH = b'4aa319f5a6950985a19380a1f279a66769d04138bd1583844270fe8c269260fc' TEST_TRANSFORMATION_RESIZE_CACHE_HASH = b'4aa319f5a6950985a19380a1f279a66769d04138bd1583844270fe8c269260fc'
TEST_TRANSFORMATION_RESIZE_CACHE_HASH_2 = b'cc8d220d40e810b995181c0c69b44b7a61c3bb039c0be96a5465fcaf698ca99a' TEST_TRANSFORMATION_RESIZE_CACHE_HASH_2 = b'cc8d220d40e810b995181c0c69b44b7a61c3bb039c0be96a5465fcaf698ca99a'

View File

@@ -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
}
)

View File

@@ -4,7 +4,6 @@ from django.test import TestCase
from mayan.apps.documents.tests import GenericDocumentTestCase from mayan.apps.documents.tests import GenericDocumentTestCase
from ..models import Transformation
from ..transformations import ( from ..transformations import (
BaseTransformation, TransformationCrop, TransformationLineArt, BaseTransformation, TransformationCrop, TransformationLineArt,
TransformationResize, TransformationRotate, TransformationRotate90, TransformationResize, TransformationRotate, TransformationRotate90,
@@ -24,6 +23,7 @@ from .literals import (
TEST_TRANSFORMATION_ZOOM_CACHE_HASH, TEST_TRANSFORMATION_ZOOM_CACHE_HASH,
TEST_TRANSFORMATION_ZOOM_PERCENT, TEST_TRANSFORMATION_ZOOM_PERCENT,
) )
from .mixins import LayerTestMixin
class TransformationBaseTestCase(TestCase): class TransformationBaseTestCase(TestCase):
@@ -110,14 +110,14 @@ class TransformationBaseTestCase(TestCase):
) )
class TransformationTestCase(GenericDocumentTestCase): class TransformationTestCase(LayerTestMixin, GenericDocumentTestCase):
def test_crop_transformation_optional_arguments(self): def test_crop_transformation_optional_arguments(self):
self._silence_logger(name='mayan.apps.converter.managers') self._silence_logger(name='mayan.apps.converter.managers')
document_page = self.test_document.pages.first() document_page = self.test_document.pages.first()
Transformation.objects.add_to_object( self.test_layer.add_transformation_to(
obj=document_page, transformation=TransformationCrop, obj=document_page, transformation_class=TransformationCrop,
arguments={'top': '10'} arguments={'top': '10'}
) )
@@ -128,8 +128,8 @@ class TransformationTestCase(GenericDocumentTestCase):
document_page = self.test_document.pages.first() document_page = self.test_document.pages.first()
Transformation.objects.add_to_object( self.test_layer.add_transformation_to(
obj=document_page, transformation=TransformationCrop, obj=document_page, transformation_class=TransformationCrop,
arguments={'top': 'x', 'left': '-'} arguments={'top': 'x', 'left': '-'}
) )
self.assertTrue(document_page.generate_image()) self.assertTrue(document_page.generate_image())
@@ -139,8 +139,8 @@ class TransformationTestCase(GenericDocumentTestCase):
document_page = self.test_document.pages.first() document_page = self.test_document.pages.first()
Transformation.objects.add_to_object( self.test_layer.add_transformation_to(
obj=document_page, transformation=TransformationCrop, obj=document_page, transformation_class=TransformationCrop,
arguments={'top': '-1000', 'bottom': '100000000'} arguments={'top': '-1000', 'bottom': '100000000'}
) )
@@ -151,13 +151,13 @@ class TransformationTestCase(GenericDocumentTestCase):
document_page = self.test_document.pages.first() document_page = self.test_document.pages.first()
Transformation.objects.add_to_object( self.test_layer.add_transformation_to(
obj=document_page, transformation=TransformationCrop, obj=document_page, transformation_class=TransformationCrop,
arguments={'top': '1000', 'bottom': '1000'} arguments={'top': '1000', 'bottom': '1000'}
) )
Transformation.objects.add_to_object( self.test_layer.add_transformation_to(
obj=document_page, transformation=TransformationCrop, obj=document_page, transformation_class=TransformationCrop,
arguments={'left': '1000', 'right': '10000'} arguments={'left': '1000', 'right': '10000'}
) )
@@ -166,8 +166,8 @@ class TransformationTestCase(GenericDocumentTestCase):
def test_lineart_transformations(self): def test_lineart_transformations(self):
document_page = self.test_document.pages.first() document_page = self.test_document.pages.first()
Transformation.objects.add_to_object( self.test_layer.add_transformation_to(
obj=document_page, transformation=TransformationLineArt, obj=document_page, transformation_class=TransformationLineArt,
arguments={} arguments={}
) )
@@ -176,22 +176,22 @@ class TransformationTestCase(GenericDocumentTestCase):
def test_rotate_transformations(self): def test_rotate_transformations(self):
document_page = self.test_document.pages.first() document_page = self.test_document.pages.first()
Transformation.objects.add_to_object( self.test_layer.add_transformation_to(
obj=document_page, transformation=TransformationRotate90, obj=document_page, transformation_class=TransformationRotate90,
arguments={} arguments={}
) )
self.assertTrue(document_page.generate_image()) self.assertTrue(document_page.generate_image())
Transformation.objects.add_to_object( self.test_layer.add_transformation_to(
obj=document_page, transformation=TransformationRotate180, obj=document_page, transformation_class=TransformationRotate180,
arguments={} arguments={}
) )
self.assertTrue(document_page.generate_image()) self.assertTrue(document_page.generate_image())
Transformation.objects.add_to_object( self.test_layer.add_transformation_to(
obj=document_page, transformation=TransformationRotate270, obj=document_page, transformation_class=TransformationRotate270,
arguments={} arguments={}
) )

View File

@@ -1,110 +1,123 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django.contrib.contenttypes.models import ContentType
from mayan.apps.documents.tests import GenericDocumentViewTestCase from mayan.apps.documents.tests import GenericDocumentViewTestCase
from ..models import Transformation from ..models import LayerTransformation
from ..permissions import (
permission_transformation_create, permission_transformation_delete,
permission_transformation_view
)
from .literals import TEST_TRANSFORMATION_NAME, TEST_TRANSFORMATION_ARGUMENT from .mixins import TransformationTestMixin, TransformationViewsTestMixin
class TransformationViewsTestCase(GenericDocumentViewTestCase): class TransformationViewsTestCase(
def _transformation_create_view(self): TransformationTestMixin, TransformationViewsTestMixin,
return self.post( GenericDocumentViewTestCase
viewname='converter:transformation_create', kwargs={ ):
'app_label': 'documents', 'model': 'document', def test_transformation_create_view_no_permission(self):
'object_id': self.test_document.pk transformation_count = LayerTransformation.objects.count()
}, data={
'name': TEST_TRANSFORMATION_NAME,
'arguments': TEST_TRANSFORMATION_ARGUMENT
}
)
def test_transformation_create_view_no_permissions(self): response = self._request_transformation_create_view()
transformation_count = Transformation.objects.count() self.assertEqual(response.status_code, 404)
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)
self.assertEqual( self.assertEqual(
Transformation.objects.count(), transformation_count + 1 LayerTransformation.objects.count(), transformation_count
) )
def _request_transformation_delete_view(self): def test_transformation_create_view_with_permission(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):
self.grant_access( 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( self.assertContains(
response=response, text=self.test_document.label, status_code=200 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
)

View File

@@ -5,12 +5,19 @@ import logging
from PIL import Image, ImageColor, ImageDraw, ImageFilter 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.translation import string_concat, ugettext_lazy as _
from django.utils.encoding import force_bytes
from .layers import layer_saved_transformations
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class BaseTransformationType(type):
def __str__(self):
return force_text(self.label)
class BaseTransformation(object): class BaseTransformation(object):
""" """
Transformation can modify the appearance of the document's page preview. Transformation can modify the appearance of the document's page preview.
@@ -18,7 +25,9 @@ class BaseTransformation(object):
""" """
arguments = () arguments = ()
name = 'base_transformation' name = 'base_transformation'
_layer_transformations = {}
_registry = {} _registry = {}
__metaclass__ = BaseTransformationType
@staticmethod @staticmethod
def combine(transformations): def combine(transformations):
@@ -44,16 +53,25 @@ class BaseTransformation(object):
return cls.label return cls.label
@classmethod @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( return sorted(
[ [
(name, klass.get_label()) for name, klass in cls._registry.items() (name, klass.get_label()) for name, klass in transformation_list
] ]
) )
@classmethod @classmethod
def register(cls, transformation): def register(cls, layer, transformation):
cls._registry[transformation.name] = transformation cls._registry[transformation.name] = transformation
cls._layer_transformations.setdefault(layer, [])
cls._layer_transformations[layer].append(transformation)
def __init__(self, **kwargs): def __init__(self, **kwargs):
self.kwargs = {} self.kwargs = {}
@@ -517,19 +535,19 @@ class TransformationZoom(BaseTransformation):
) )
BaseTransformation.register(transformation=TransformationCrop) BaseTransformation.register(layer=layer_saved_transformations, transformation=TransformationCrop)
BaseTransformation.register(transformation=TransformationDrawRectangle) BaseTransformation.register(layer=layer_saved_transformations, transformation=TransformationDrawRectangle)
BaseTransformation.register( BaseTransformation.register(
transformation=TransformationDrawRectanglePercent layer=layer_saved_transformations, transformation=TransformationDrawRectanglePercent
) )
BaseTransformation.register(transformation=TransformationFlip) BaseTransformation.register(layer=layer_saved_transformations, transformation=TransformationFlip)
BaseTransformation.register(transformation=TransformationGaussianBlur) BaseTransformation.register(layer=layer_saved_transformations, transformation=TransformationGaussianBlur)
BaseTransformation.register(transformation=TransformationLineArt) BaseTransformation.register(layer=layer_saved_transformations, transformation=TransformationLineArt)
BaseTransformation.register(transformation=TransformationMirror) BaseTransformation.register(layer=layer_saved_transformations, transformation=TransformationMirror)
BaseTransformation.register(transformation=TransformationResize) BaseTransformation.register(layer=layer_saved_transformations, transformation=TransformationResize)
BaseTransformation.register(transformation=TransformationRotate) BaseTransformation.register(layer=layer_saved_transformations, transformation=TransformationRotate)
BaseTransformation.register(transformation=TransformationRotate90) BaseTransformation.register(layer=layer_saved_transformations, transformation=TransformationRotate90)
BaseTransformation.register(transformation=TransformationRotate180) BaseTransformation.register(layer=layer_saved_transformations, transformation=TransformationRotate180)
BaseTransformation.register(transformation=TransformationRotate270) BaseTransformation.register(layer=layer_saved_transformations, transformation=TransformationRotate270)
BaseTransformation.register(transformation=TransformationUnsharpMask) BaseTransformation.register(layer=layer_saved_transformations, transformation=TransformationUnsharpMask)
BaseTransformation.register(transformation=TransformationZoom) BaseTransformation.register(layer=layer_saved_transformations, transformation=TransformationZoom)

View File

@@ -3,25 +3,29 @@ from __future__ import unicode_literals
from django.conf.urls import url from django.conf.urls import url
from .views import ( from .views import (
TransformationCreateView, TransformationDeleteView, TransformationEditView, TransformationCreateView, TransformationDeleteView,
TransformationListView TransformationEditView, TransformationListView, TransformationSelectView
) )
urlpatterns = [ urlpatterns = [
url( url(
regex=r'^object/(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_id>\d+)/transformations/$', regex=r'^object/(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_id>\d+)/layers/(?P<layer_name>[-_\w]+)/transformations/$',
view=TransformationListView.as_view(), name='transformation_list' view=TransformationListView.as_view(), name='transformation_list'
), ),
url( url(
regex=r'^object/(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_id>\d+)/transformations/create/$', regex=r'^object/(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_id>\d+)/layers/(?P<layer_name>[-_\w]+)/transformations/select/$',
view=TransformationSelectView.as_view(), name='transformation_select'
),
url(
regex=r'^object/(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_id>\d+)/layers/(?P<layer_name>[-_\w]+)/transformations/(?P<transformation_name>[-_\w]+)/create/$',
view=TransformationCreateView.as_view(), name='transformation_create' view=TransformationCreateView.as_view(), name='transformation_create'
), ),
url( url(
regex=r'^transformations/(?P<pk>\d+)/delete/$', view=TransformationDeleteView.as_view(), regex=r'^layers/(?P<layer_name>[-_\w]+)/transformations/(?P<pk>\d+)/delete/$',
name='transformation_delete' view=TransformationDeleteView.as_view(), name='transformation_delete'
), ),
url( url(
regex=r'^transformations/(?P<pk>\d+)/edit/$', view=TransformationEditView.as_view(), regex=r'^layers/(?P<layer_name>[-_\w]+)/transformations/(?P<pk>\d+)/edit/$',
name='transformation_edit' view=TransformationEditView.as_view(), name='transformation_edit'
), ),
] ]

View File

@@ -2,59 +2,56 @@ from __future__ import absolute_import, unicode_literals
import logging import logging
from django.contrib.contenttypes.models import ContentType from django.http import HttpResponseRedirect
from django.http import Http404
from django.shortcuts import get_object_or_404
from django.template import RequestContext from django.template import RequestContext
from django.urls import reverse from django.urls import reverse
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from mayan.apps.acls.models import AccessControlList
from mayan.apps.common.generics import ( from mayan.apps.common.generics import (
SingleObjectCreateView, SingleObjectDeleteView, SingleObjectEditView, FormView, SingleObjectCreateView, SingleObjectDeleteView,
SingleObjectListView SingleObjectEditView, SingleObjectListView
) )
from mayan.apps.common.mixins import ExternalContentTypeObjectMixin
from .forms import TransformationForm from .classes import Layer
from .icons import icon_transformation_list from .forms import LayerTransformationForm, LayerTransformationSelectForm
from .links import link_transformation_create from .links import link_transformation_select
from .models import Transformation from .models import LayerTransformation, ObjectLayer
from .permissions import ( from .transformations import BaseTransformation
permission_transformation_create, permission_transformation_delete,
permission_transformation_edit, permission_transformation_view
)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class TransformationCreateView(SingleObjectCreateView): class LayerViewMixin(object):
form_class = TransformationForm
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
content_type = get_object_or_404( self.layer = self.get_layer()
klass=ContentType, app_label=self.kwargs['app_label'], return super(LayerViewMixin, self).dispatch(
model=self.kwargs['model'] request=request, *args, **kwargs
) )
try: def get_layer(self):
self.content_object = content_type.get_object_for_this_type( return Layer.get(
pk=self.kwargs['object_id'] name=self.kwargs['layer_name']
)
except content_type.model_class().DoesNotExist:
raise Http404
AccessControlList.objects.check_access(
obj=self.content_object,
permissions=(permission_transformation_create,), user=request.user
) )
return super(TransformationCreateView, self).dispatch(
request, *args, **kwargs class TransformationCreateView(
) LayerViewMixin, ExternalContentTypeObjectMixin, SingleObjectCreateView
):
form_class = LayerTransformationForm
def form_valid(self, form): 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 = 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: try:
instance.full_clean() instance.full_clean()
instance.save() instance.save()
@@ -66,91 +63,101 @@ class TransformationCreateView(SingleObjectCreateView):
def get_extra_context(self): def get_extra_context(self):
return { 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',), 'navigation_object_list': ('content_object',),
'title': _( 'title': _(
'Create new transformation for: %s' 'Create layer "%(layer)s" transformation '
) % self.content_object, '"%(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): def get_post_action_redirect(self):
return reverse( return reverse(
viewname='converter:transformation_list', kwargs={ viewname='converter:transformation_list', kwargs={
'app_label': self.kwargs['app_label'], 'app_label': self.kwargs['app_label'],
'model': self.kwargs['model'], '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): def get_queryset(self):
return Transformation.objects.get_for_object(obj=self.content_object) return self.layer.get_transformations_for(
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']
) )
AccessControlList.objects.check_access( def get_template_names(self):
obj=self.transformation.content_object, return [
permissions=(permission_transformation_delete,), user=request.user getattr(
) self.get_transformation_class(), 'template_name',
self.template_name
)
]
return super(TransformationDeleteView, self).dispatch( def get_transformation_class(self):
request, *args, **kwargs return BaseTransformation.get(name=self.kwargs['transformation_name'])
)
def get_post_action_redirect(self):
return reverse( class TransformationDeleteView(LayerViewMixin, SingleObjectDeleteView):
viewname='converter:transformation_list', kwargs={ model = LayerTransformation
'app_label': self.transformation.content_type.app_label,
'model': self.transformation.content_type.model,
'object_id': self.transformation.object_id
}
)
def get_extra_context(self): def get_extra_context(self):
return { 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'), 'navigation_object_list': ('content_object', 'transformation'),
'previous': reverse( 'previous': reverse(
viewname='converter:transformation_list', kwargs={ viewname='converter:transformation_list', kwargs={
'app_label': self.transformation.content_type.app_label, 'app_label': self.object.object_layer.content_type.app_label,
'model': self.transformation.content_type.model, 'model': self.object.object_layer.content_type.model,
'object_id': self.transformation.object_id 'object_id': self.object.object_layer.object_id,
'layer_name': self.object.object_layer.stored_layer.name
} }
), ),
'title': _( 'title': _(
'Delete transformation "%(transformation)s" for: ' 'Delete transformation "%(transformation)s" for: '
'%(content_object)s?' '%(content_object)s?'
) % { ) % {
'transformation': self.transformation, 'transformation': self.object,
'content_object': self.transformation.content_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): def get_post_action_redirect(self):
form_class = TransformationForm return reverse(
model = Transformation viewname='converter:transformation_list', kwargs={
'app_label': self.object.object_layer.content_type.app_label,
def dispatch(self, request, *args, **kwargs): 'model': self.object.object_layer.content_type.model,
self.transformation = get_object_or_404( 'object_id': self.object.object_layer.object_id,
klass=Transformation, pk=self.kwargs['pk'] '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( class TransformationEditView(LayerViewMixin, SingleObjectEditView):
request, *args, **kwargs form_class = LayerTransformationForm
) model = LayerTransformation
def form_valid(self, form): def form_valid(self, form):
instance = form.save(commit=False) instance = form.save(commit=False)
@@ -165,72 +172,121 @@ class TransformationEditView(SingleObjectEditView):
def get_extra_context(self): def get_extra_context(self):
return { 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'), 'navigation_object_list': ('content_object', 'transformation'),
'title': _( 'title': _(
'Edit transformation "%(transformation)s" for: %(content_object)s' 'Edit transformation "%(transformation)s" '
'for: %(content_object)s'
) % { ) % {
'transformation': self.transformation, 'transformation': self.object,
'content_object': self.transformation.content_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): def get_post_action_redirect(self):
return reverse( return reverse(
viewname='converter:transformation_list', kwargs={ viewname='converter:transformation_list', kwargs={
'app_label': self.transformation.content_type.app_label, 'app_label': self.object.object_layer.content_type.app_label,
'model': self.transformation.content_type.model, 'model': self.object.object_layer.content_type.model,
'object_id': self.transformation.object_id 'object_id': self.object.object_layer.object_id,
'layer_name': self.object.object_layer.stored_layer.name
} }
) )
def get_template_names(self):
class TransformationListView(SingleObjectListView): return [
def dispatch(self, request, *args, **kwargs): getattr(
content_type = get_object_or_404( self.object.get_transformation_class(), 'template_name',
klass=ContentType, app_label=self.kwargs['app_label'], self.template_name
model=self.kwargs['model']
)
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_view,), user=request.user
)
return super(TransformationListView, self).dispatch( class TransformationListView(
request, *args, **kwargs 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): def get_extra_context(self):
return { return {
'content_object': self.content_object, 'layer': self.layer,
'hide_link': True, 'layer_name': self.kwargs['layer_name'],
'hide_object': True,
'navigation_object_list': ('content_object',), 'navigation_object_list': ('content_object',),
'no_results_icon': icon_transformation_list, 'content_object': self.external_object,
'no_results_main_link': link_transformation_create.resolve( 'submit_label': _('Select'),
context=RequestContext( 'title': _(
request=self.request, dict_={ 'Select new layer "%(layer)s" transformation '
'content_object': self.content_object 'for: %(object)s'
} ) % {
) 'layer': self.layer,
), 'object': self.external_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,
} }
def get_source_queryset(self): def get_form_extra_kwargs(self):
return Transformation.objects.get_for_object(obj=self.content_object) return {
'layer': self.layer
}

View File

@@ -196,10 +196,16 @@ class APIDocumentPageImageView(generics.RetrieveAPIView):
if rotation: if rotation:
rotation = int(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( task = task_generate_document_page_image.apply_async(
kwargs=dict( kwargs=dict(
document_page_id=self.get_object().pk, width=width, 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
) )
) )

View File

@@ -106,7 +106,6 @@ def is_document_page_enabled(context):
return context['object'].enabled return context['object'].enabled
class DocumentsApp(MayanAppConfig): class DocumentsApp(MayanAppConfig):
app_namespace = 'documents' app_namespace = 'documents'
app_url = 'documents' app_url = 'documents'

View File

@@ -7,7 +7,7 @@ from ..storages import storage_documentimagecache
def operation_clear_old_cache(apps, schema_editor): def operation_clear_old_cache(apps, schema_editor):
DocumentPageCachedImage = apps.get_model( DocumentPageCachedImage = apps.get_model(
'documents', 'DocumentPageCachedImage' app_label='documents', model_name='DocumentPageCachedImage'
) )
for cached_image in DocumentPageCachedImage.objects.using(schema_editor.connection.alias).all(): for cached_image in DocumentPageCachedImage.objects.using(schema_editor.connection.alias).all():

View File

@@ -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.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 ( from mayan.apps.converter.transformations import (
BaseTransformation, TransformationResize, TransformationRotate, BaseTransformation, TransformationResize, TransformationRotate,
TransformationZoom TransformationZoom
@@ -83,8 +83,8 @@ class DocumentPage(models.Model):
def document(self): def document(self):
return self.document_version.document return self.document_version.document
def generate_image(self, *args, **kwargs): def generate_image(self, user=None, **kwargs):
transformation_list = self.get_combined_transformation_list(*args, **kwargs) transformation_list = self.get_combined_transformation_list(user=user, **kwargs)
combined_cache_filename = BaseTransformation.combine(transformation_list) combined_cache_filename = BaseTransformation.combine(transformation_list)
# Check is transformed image is available # Check is transformed image is available
@@ -136,7 +136,7 @@ class DocumentPage(models.Model):
return final_url.tostr() 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 Return a list of transformation containing the server side
document page transformation as well as tranformations created document page transformation as well as tranformations created
@@ -161,8 +161,13 @@ class DocumentPage(models.Model):
# Generate transformation hash # Generate transformation hash
transformation_list = [] transformation_list = []
maximum_layer_order = kwargs.get('maximum_layer_order', None)
# Stored transformations first # 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) transformation_list.append(stored_transformation)
# Interactive transformations second # Interactive transformations second

View File

@@ -15,7 +15,7 @@ from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from mayan.apps.converter.exceptions import InvalidOfficeFormat, PageCountError 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.transformations import TransformationRotate
from mayan.apps.converter.utils import get_converter_class from mayan.apps.converter.utils import get_converter_class
from mayan.apps.mimetype.api import get_mimetype from mayan.apps.mimetype.api import get_mimetype
@@ -156,7 +156,7 @@ class DocumentVersion(models.Model):
for page in self.pages.all(): for page in self.pages.all():
degrees = page.detect_orientation() degrees = page.detect_orientation()
if degrees: if degrees:
Transformation.objects.add_to_object( layer_saved_transformations.add_to_object(
obj=page, transformation=TransformationRotate, obj=page, transformation=TransformationRotate,
arguments='{{"degrees": {}}}'.format(360 - degrees) arguments='{{"degrees": {}}}'.format(360 - degrees)
) )

View File

@@ -65,13 +65,19 @@ def task_delete_stubs():
@app.task() @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( DocumentPage = apps.get_model(
app_label='documents', model_name='DocumentPage' 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) 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) @app.task(ignore_result=True)

View File

@@ -2,8 +2,10 @@ from __future__ import absolute_import, unicode_literals
import logging import logging
from django.conf import settings
from django.contrib import messages from django.contrib import messages
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.db import transaction
from django.http import HttpResponseRedirect from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.urls import reverse from django.urls import reverse
@@ -17,7 +19,7 @@ from mayan.apps.common.generics import (
SingleObjectDetailView, SingleObjectDownloadView, SingleObjectEditView, SingleObjectDetailView, SingleObjectDownloadView, SingleObjectEditView,
SingleObjectListView SingleObjectListView
) )
from mayan.apps.converter.models import Transformation from mayan.apps.converter.layers import layer_saved_transformations
from mayan.apps.converter.permissions import ( from mayan.apps.converter.permissions import (
permission_transformation_delete, permission_transformation_edit permission_transformation_delete, permission_transformation_edit
) )
@@ -522,7 +524,7 @@ class DocumentTransformationsClearView(MultipleObjectConfirmActionView):
def object_action(self, form, instance): def object_action(self, form, instance):
try: try:
for page in instance.pages.all(): 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: except Exception as exception:
messages.error( messages.error(
self.request, _( self.request, _(
@@ -545,24 +547,29 @@ class DocumentTransformationsCloneView(FormView):
pk=form.cleaned_data['page'].pk pk=form.cleaned_data['page'].pk
) )
for page in target_pages: with transaction.atomic():
Transformation.objects.get_for_object(obj=page).delete() for page in target_pages:
layer_saved_transformations.get_transformations_for(obj=page).delete()
Transformation.objects.copy( layer_saved_transformations.copy_transformations(
source=form.cleaned_data['page'], targets=target_pages source=form.cleaned_data['page'], targets=target_pages
) )
except Exception as exception: except Exception as exception:
messages.error( if settings.DEBUG:
self.request, _( raise
'Error deleting the page transformations for ' else:
'document: %(document)s; %(error)s.' messages.error(
) % { message=_(
'document': instance, 'error': exception 'Error cloning the page transformations for '
} 'document: %(document)s; %(error)s.'
) ) % {
'document': instance, 'error': exception
}, request=self.request
)
else: else:
messages.success( messages.success(
self.request, _('Transformations cloned successfully.') message=_('Transformations cloned successfully.'),
request=self.request
) )
return super(DocumentTransformationsCloneView, self).form_valid(form=form) return super(DocumentTransformationsCloneView, self).form_valid(form=form)

View File

@@ -23,5 +23,6 @@ class ScanDuplicatedDocuments(ConfirmView):
def view_action(self): def view_action(self):
task_scan_duplicates_all.apply_async() task_scan_duplicates_all.apply_async()
messages.success( messages.success(
self.request, _('Duplicated document scan queued successfully.') message=_('Duplicated document scan queued successfully.'),
request=self.request
) )

View File

@@ -48,19 +48,21 @@ class Link(object):
def __init__( def __init__(
self, text=None, view=None, args=None, badge_text=None, condition=None, self, text=None, view=None, args=None, badge_text=None, condition=None,
conditional_disable=None, description=None, html_data=None, conditional_active=None, conditional_disable=None, description=None,
html_extra_classes=None, icon_class=None, icon_class_path=None, html_data=None, html_extra_classes=None, icon_class=None,
keep_query=False, kwargs=None, name=None, permissions=None, icon_class_path=None, keep_query=False, kwargs=None, name=None,
remove_from_query=None, tags=None, url=None permissions=None, remove_from_query=None, tags=None, url=None
): ):
self.args = args or [] self.args = args or []
self.badge_text = badge_text self.badge_text = badge_text
self.condition = condition self.condition = condition
self.conditional_active = conditional_active
self.conditional_disable = conditional_disable self.conditional_disable = conditional_disable
self.description = description self.description = description
self.html_data = html_data self.html_data = html_data
self.html_extra_classes = html_extra_classes self.html_extra_classes = html_extra_classes
self.icon_class = icon_class self.icon_class = icon_class
self.icon_class_path = icon_class_path
self.keep_query = keep_query self.keep_query = keep_query
self.kwargs = kwargs or {} self.kwargs = kwargs or {}
self.name = name self.name = name
@@ -71,7 +73,13 @@ class Link(object):
self.view = view self.view = view
self.url = url 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: if self.icon_class:
raise ImproperlyConfigured( raise ImproperlyConfigured(
'Specify the icon_class or the icon_class_path but not ' 'Specify the icon_class or the icon_class_path but not '
@@ -79,17 +87,14 @@ class Link(object):
) )
else: else:
try: 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: except ImportError as exception:
logger.error( logger.error(
'Exception importing icon: %s; %s', icon_class_path, 'Exception importing icon: %s; %s', self.icon_class_path,
exception exception
) )
raise raise
if name:
self.__class__._registry[name] = self
def resolve(self, context=None, request=None, resolved_object=None): def resolve(self, context=None, request=None, resolved_object=None):
if not context and not request: if not context and not request:
raise ImproperlyConfigured( raise ImproperlyConfigured(
@@ -525,7 +530,13 @@ class ResolvedLink(object):
@property @property
def active(self): 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 @property
def badge_text(self): def badge_text(self):

View File

@@ -12,4 +12,3 @@ class SourceColumnLinkWidget(object):
'column': self.column, 'value': value 'column': self.column, 'value': value
} }
) )

View File

@@ -0,0 +1,3 @@
from __future__ import unicode_literals
default_app_config = 'mayan.apps.redactions.apps.RedactionsApp'

View File

@@ -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,)
)

View File

@@ -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'
)

View File

@@ -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'
)

View File

@@ -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'
)

View File

@@ -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 %}
<link href="{% static 'redactions/node_modules/cropperjs/dist/cropper.css' %}" rel="stylesheet">
<style>
.cropper-invisible {
background: #000;
opacity: 1;
}
.cropper-main {
width: 100%;
}
.cropper-main img {
max-width: 100%;
}
.cropper-modal {
opacity: 0;
}
</style>
{% endblock %}
{% block content %}
<div class="cropper-main">
<img id="cropper-img" src="{% get_api_image_url obj=content_object maximum_layer_order=layer.order %}">
</div>
<br>
{% with '' as title %}
{% include 'appearance/generic_form_subtemplate.html' %}
{% endwith %}
{% endblock content %}
{% block javascript %}
<script>
var crop_left, crop_top, crop_right, crop_bottom;
var pic_real_width, pic_real_height;
var canvasData;
var containerData;
var $image = $('#cropper-img');
var image = document.getElementById('cropper-img');
var defaultArguments = {
left: 10,
top: 10,
right: 10,
bottom: 10,
}
var initialArguments = JSON.parse(
$('#id_arguments').text() || JSON.stringify(defaultArguments)
);
var callbackCrop = function (data) {
var crop_left = (data.detail.x / pic_real_width * 100).toFixed(2);
var crop_top = (data.detail.y / pic_real_height * 100).toFixed(2);
var crop_right = (
100.001 - (data.detail.x + data.detail.width) / pic_real_width * 100
).toFixed(2);
var crop_bottom = (
100.001 - (data.detail.y + data.detail.height) / pic_real_height * 100
).toFixed(2);
var arguments = {
'left': parseFloat(crop_left),
'top': parseFloat(crop_top),
'right': parseFloat(crop_right),
'bottom': parseFloat(crop_bottom),
}
$('#id_arguments').text(JSON.stringify(arguments));
}
jQuery(document).ready(function() {
$('.help-block').hide();
$('label').hide();
});
$.getScript("{% static 'redactions/node_modules/cropperjs/dist/cropper.js' %}")
.done(function (script, textStatus) {
$.getScript("{% static 'redactions/node_modules/jquery-cropper/dist/jquery-cropper.js' %}")
.done(function (script, textStatus) {
jQuery(document).ready(function () {
// Create DOM new image to get the real
// (unscaled) image size
$('<img/>')
.attr('src', $image.attr('src'))
.on('load', function () {
pic_real_width = this.width;
pic_real_height = this.height;
});
new Cropper(
image, {
crop: callbackCrop,
highlight: false,
mouseWheelZoom: false,
movable: false,
ready: function () {
canvasData = this.cropper.getCanvasData();
containerData = this.cropper.getContainerData();
this.cropper.setCropBoxData({
left: initialArguments.left / 100.0 * canvasData.width + canvasData.left,
top: initialArguments.top / 100.0 * canvasData.height + canvasData.top,
width: (100.0 - initialArguments.right - initialArguments.left) / 100.0 * canvasData.width,
height: (100.0 - initialArguments.bottom - initialArguments.top) / 100.0 * canvasData.height,
});
},
rotatable: false,
touchDragZoom: false,
viewMode: 1,
zoomable: false,
});
})
})
});
</script>
{% endblock %}

View File

@@ -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
)

View File

@@ -17,8 +17,8 @@ from mayan.apps.navigation.classes import SourceColumn
from .classes import StagingFile from .classes import StagingFile
from .dependencies import * # NOQA from .dependencies import * # NOQA
from .handlers import ( from .handlers import (
handler_copy_transformations_to_version, handler_create_default_document_source, handler_copy_transformations_to_version,
handler_initialize_periodic_tasks handler_create_default_document_source, handler_initialize_periodic_tasks
) )
from .links import ( from .links import (
link_document_create_multiple, link_setup_sources, link_document_create_multiple, link_setup_sources,

View File

@@ -3,19 +3,16 @@ from __future__ import unicode_literals
from django.apps import apps from django.apps import apps
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from mayan.apps.converter.layers import layer_saved_transformations
from .literals import SOURCE_UNCOMPRESS_CHOICE_ASK from .literals import SOURCE_UNCOMPRESS_CHOICE_ASK
def handler_copy_transformations_to_version(sender, **kwargs): def handler_copy_transformations_to_version(sender, instance, **kwargs):
Transformation = apps.get_model(
app_label='converter', model_name='Transformation'
)
instance = kwargs['instance']
# TODO: Fix this, source should be previous version # TODO: Fix this, source should be previous version
# TODO: Fix this, shouldn't this be at the documents app # 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() source=instance.document, targets=instance.pages.all()
) )

View File

@@ -12,7 +12,7 @@ from model_utils.managers import InheritanceManager
from mayan.apps.common.compressed_files import Archive from mayan.apps.common.compressed_files import Archive
from mayan.apps.common.exceptions import NoMIMETypeMatch 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.models import Document, DocumentType
from mayan.apps.documents.settings import setting_language from mayan.apps.documents.settings import setting_language
@@ -131,7 +131,7 @@ class Source(models.Model):
if user: if user:
document.add_as_recent_document_for_user(user=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() source=self, targets=document_version.pages.all()
) )

View File

@@ -123,6 +123,7 @@ INSTALLED_APPS = (
'mayan.apps.metadata', 'mayan.apps.metadata',
'mayan.apps.mirroring', 'mayan.apps.mirroring',
'mayan.apps.ocr', 'mayan.apps.ocr',
'mayan.apps.redactions',
'mayan.apps.sources', 'mayan.apps.sources',
'mayan.apps.storage', 'mayan.apps.storage',
'mayan.apps.tags', 'mayan.apps.tags',