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 }}
{% endfor %}
{% 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 #}
{% 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 %}

View File

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

View File

@@ -430,6 +430,21 @@ class RestrictedQuerysetMixin(object):
object_permission = None
source_queryset = None
def get_object_permission(self):
return self.object_permission
def get_queryset(self):
queryset = self.get_source_queryset()
object_permission = self.get_object_permission()
if object_permission:
queryset = AccessControlList.objects.restrict_queryset(
permission=object_permission, queryset=queryset,
user=self.request.user
)
return queryset
def get_source_queryset(self):
if self.source_queryset is None:
if self.model:
@@ -445,17 +460,6 @@ class RestrictedQuerysetMixin(object):
return self.source_queryset.all()
def get_queryset(self):
queryset = self.get_source_queryset()
if self.object_permission:
queryset = AccessControlList.objects.restrict_queryset(
permission=self.object_permission, queryset=queryset,
user=self.request.user
)
return queryset
class ViewPermissionCheckMixin(object):
"""
@@ -467,11 +471,16 @@ class ViewPermissionCheckMixin(object):
view_permission = None
def dispatch(self, request, *args, **kwargs):
if self.view_permission:
view_permission = self.get_view_permission()
if view_permission:
Permission.check_user_permissions(
permissions=(self.view_permission,), user=self.request.user
permissions=(view_permission,),
user=self.request.user
)
return super(
ViewPermissionCheckMixin, self
).dispatch(request, *args, **kwargs)
def get_view_permission(self):
return self.view_permission

View File

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

View File

@@ -1,5 +1,6 @@
from __future__ import unicode_literals
import copy
from io import BytesIO
import logging
import os
@@ -8,9 +9,16 @@ import shutil
from PIL import Image
import sh
from django.apps import apps
from django.core.exceptions import ImproperlyConfigured
from django.db import transaction
from django.utils.encoding import force_text, python_2_unicode_compatible
from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _
from mayan.apps.appearance.classes import Icon
from mayan.apps.mimetype.api import get_mimetype
from mayan.apps.navigation.classes import Link
from mayan.apps.storage.settings import setting_temporary_directory
from mayan.apps.storage.utils import (
NamedTemporaryFile, fs_cleanup, mkdtemp
@@ -202,3 +210,228 @@ class ConverterBase(object):
for transformation in transformations:
self.image = transformation.execute_on(image=self.image)
@python_2_unicode_compatible
class Layer(object):
_registry = {}
@classmethod
def all(cls):
return cls._registry.values()
@classmethod
def get(cls, name):
return cls._registry[name]
@classmethod
def get_by_value(cls, key, value):
for name, layer in cls._registry.items():
if getattr(layer, key) == value:
return layer
@classmethod
def invalidate_cache(cls):
for layer in cls.all():
layer.__dict__.pop('stored_layer', None)
@classmethod
def update(cls):
for layer in cls.all():
layer.stored_layer
def __init__(
self, label, name, order, permissions, default=False,
empty_results_text=None, symbol=None,
):
"""
access_permission is the permission necessary to view the layer.
exclude_permission is the permission necessary to discard the layer.
"""
self.default = default
self.empty_results_text = empty_results_text
self.label = label
self.name = name
self.order = order
self.permissions = permissions
self.symbol = symbol
# Check order
layer = self.__class__.get_by_value(key='order', value=self.order)
if layer:
raise ImproperlyConfigured(
'Layer "{}" already has order "{}" requested by layer "{}"'.format(
layer.name, order, self.name
)
)
# Check default
if default:
layer = self.__class__.get_by_value(key='default', value=True)
if layer:
raise ImproperlyConfigured(
'Layer "{}" is already the default layer; "{}"'.format(
layer.name, self.name
)
)
self.__class__._registry[name] = self
def get_permission(self, name):
return self.permissions.get(name, None)
def __str__(self):
return force_text(self.label)
def add_transformation_to(self, obj, transformation_class, arguments=None):
ContentType = apps.get_model(
app_label='contenttypes', model_name='ContentType'
)
content_type = ContentType.objects.get_for_model(model=obj)
object_layer, created = self.stored_layer.object_layers.get_or_create(
content_type=content_type, object_id=obj.pk
)
object_layer.transformations.create(
name=transformation_class.name, arguments=arguments
)
def copy_transformations(self, source, targets):
"""
Copy transformation from source to all targets
"""
ContentType = apps.get_model(
app_label='contenttypes', model_name='ContentType'
)
transformations = self.get_transformations_for(obj=source)
with transaction.atomic():
for target in targets:
content_type = ContentType.objects.get_for_model(model=target)
object_layer, created = self.stored_layer.object_layers.get_or_create(
content_type=content_type, object_id=target.pk
)
for transformation in transformations:
object_layer.transformations.create(
order=transformation.order,
name=transformation.name,
arguments=transformation.arguments,
)
def get_empty_results_text(self):
if self.empty_results_text:
return self.empty_results_text
else:
return _(
'Transformations allow changing the visual appearance '
'of documents without making permanent changes to the '
'document file themselves.'
)
def get_icon(self):
return Icon(driver_name='fontawesome', symbol=self.symbol)
def get_model_instance(self):
StoredLayer = apps.get_model(
app_label='converter', model_name='StoredLayer'
)
stored_layer, created = StoredLayer.objects.update_or_create(
name=self.name, defaults={'order': self.order}
)
return stored_layer
def get_transformations_for(self, obj, as_classes=False):
"""
as_classes == True returns the transformation classes from .classes
ready to be feed to the converter class
"""
LayerTransformation = apps.get_model(
app_label='converter', model_name='LayerTransformation'
)
return LayerTransformation.objects.get_for_object(
obj=obj, as_classes=as_classes,
only_stored_layer=self.stored_layer
)
@cached_property
def stored_layer(self):
return self.get_model_instance()
class LayerLink(Link):
@staticmethod
def set_icon(instance, layer):
if instance.action == 'list':
if layer.symbol:
instance.icon_class = layer.get_icon()
def __init__(self, action, layer, object_name=None, **kwargs):
super(LayerLink, self).__init__(**kwargs)
self.action = action
self.layer = layer
self.object_name = object_name or _('transformation')
permission = layer.permissions.get(action, None)
if permission:
self.permissions = (permission,)
if action == 'list':
self.kwargs = LayerLinkKwargsFactory(
layer_name=layer.name
).get_kwargs_function()
if action in ('create', 'select'):
self.kwargs = LayerLinkKwargsFactory().get_kwargs_function()
LayerLink.set_icon(instance=self, layer=layer)
def copy(self, layer):
result = copy.copy(self)
result.kwargs = LayerLinkKwargsFactory(
layer_name=layer.name
).get_kwargs_function()
result._layer_name = layer.name
LayerLink.set_icon(instance=result, layer=layer)
return result
@cached_property
def layer_name(self):
return getattr(
self, '_layer_name', Layer.get_by_value(
key='default', value=True
).name
)
class LayerLinkKwargsFactory(object):
def __init__(self, layer_name=None, variable_name='resolved_object'):
self.layer_name = layer_name
self.variable_name = variable_name
def get_kwargs_function(self):
def get_kwargs(context):
ContentType = apps.get_model(
app_label='contenttypes', model_name='ContentType'
)
content_type = ContentType.objects.get_for_model(
context[self.variable_name]
)
default_layer = Layer.get_by_value(key='default', value=True)
return {
'app_label': '"{}"'.format(content_type.app_label),
'model': '"{}"'.format(content_type.model),
'object_id': '{}.pk'.format(self.variable_name),
'layer_name': '"{}"'.format(
self.layer_name or context.get(
'layer_name', default_layer.name
)
)
}
return get_kwargs

View File

@@ -8,13 +8,49 @@ from django.utils.translation import ugettext_lazy as _
from mayan.apps.common.serialization import yaml_load
from .models import Transformation
from .models import LayerTransformation
from .transformations import BaseTransformation
class TransformationForm(forms.ModelForm):
class LayerTransformationSelectForm(forms.Form):
def __init__(self, *args, **kwargs):
layer = kwargs.pop('layer')
super(LayerTransformationSelectForm, self).__init__(*args, **kwargs)
self.fields[
'transformation'
].choices = BaseTransformation.get_transformation_choices(layer=layer)
transformation = forms.ChoiceField(
choices=(), help_text=_('Available transformations for this layer.'),
label=_('Transformation'),
)
class LayerTransformationForm(forms.ModelForm):
class Meta:
fields = ('name', 'arguments', 'order')
model = Transformation
fields = ('arguments', 'order')
model = LayerTransformation
def __init__(self, *args, **kwargs):
transformation_name = kwargs.pop('transformation_name', None)
super(LayerTransformationForm, self).__init__(*args, **kwargs)
if not transformation_name:
# Get the template name when the transformation is being edited.
template_name = getattr(
self.instance.get_transformation_class(), 'template_name',
None
)
else:
# Get the template name when the transformation is being created
template_name = getattr(
BaseTransformation.get(name=transformation_name),
'template_name', None
)
if template_name:
self.fields['arguments'].widget.attrs['class'] = 'hidden'
self.fields['order'].widget.attrs['class'] = 'hidden'
def clean(self):
try:

View File

@@ -4,10 +4,7 @@ from mayan.apps.appearance.classes import Icon
icon_transformations = Icon(driver_name='fontawesome', symbol='crop')
icon_transformation_create = Icon(
driver_name='fontawesome-dual', primary_symbol='crop',
secondary_symbol='plus'
)
icon_transformation_delete = Icon(driver_name='fontawesome', symbol='times')
icon_transformation_edit = Icon(driver_name='fontawesome', symbol='pencil-alt')
icon_transformation_list = icon_transformations
icon_transformation_select = Icon(driver_name='fontawesome', symbol='plus')

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 django.apps import apps
from django.utils.translation import ugettext_lazy as _
from mayan.apps.navigation.classes import Link
from .permissions import (
permission_transformation_create, permission_transformation_delete,
permission_transformation_edit, permission_transformation_view
)
from .classes import LayerLink
from .layers import layer_saved_transformations
def get_kwargs_factory(variable_name):
def get_kwargs(context):
ContentType = apps.get_model(
app_label='contenttypes', model_name='ContentType'
)
content_type = ContentType.objects.get_for_model(
context[variable_name]
)
return {
'app_label': '"{}"'.format(content_type.app_label),
'model': '"{}"'.format(content_type.model),
'object_id': '{}.pk'.format(variable_name)
}
return get_kwargs
def conditional_active(context, resolved_link):
return resolved_link.link.view == resolved_link.current_view_name and context.get('layer_name', None) == resolved_link.link.layer_name
link_transformation_create = Link(
icon_class_path='mayan.apps.converter.icons.icon_transformation_create',
kwargs=get_kwargs_factory('content_object'),
permissions=(permission_transformation_create,),
text=_('Create new transformation'), view='converter:transformation_create'
)
link_transformation_delete = Link(
args='resolved_object.pk',
link_transformation_delete = LayerLink(
action='delete',
kwargs={'layer_name': 'layer_name', 'pk': 'resolved_object.pk'},
icon_class_path='mayan.apps.converter.icons.icon_transformation_delete',
permissions=(permission_transformation_delete,),
layer=layer_saved_transformations,
tags='dangerous', text=_('Delete'), view='converter:transformation_delete'
)
link_transformation_edit = Link(
args='resolved_object.pk',
link_transformation_edit = LayerLink(
action='edit',
kwargs={'layer_name': 'layer_name', 'pk': 'resolved_object.pk'},
icon_class_path='mayan.apps.converter.icons.icon_transformation_edit',
permissions=(permission_transformation_edit,),
layer=layer_saved_transformations,
text=_('Edit'), view='converter:transformation_edit'
)
link_transformation_list = Link(
icon_class_path='mayan.apps.converter.icons.icon_transformation_list',
kwargs=get_kwargs_factory('resolved_object'),
permissions=(permission_transformation_view,), text=_('Transformations'),
link_transformation_list = LayerLink(
action='list', conditional_active=conditional_active,
layer=layer_saved_transformations, text=_('Transformations'),
view='converter:transformation_list'
)
link_transformation_select = LayerLink(
action='select',
icon_class_path='mayan.apps.converter.icons.icon_transformation_select',
layer=layer_saved_transformations, text=_('Select new transformation'),
view='converter:transformation_select'
)

View File

@@ -2,75 +2,89 @@ from __future__ import unicode_literals
import logging
from django.apps import apps
from django.contrib.contenttypes.models import ContentType
from django.db import models, transaction
from django.core.exceptions import PermissionDenied
from django.db import models
from mayan.apps.common.serialization import yaml_dump, yaml_load
from mayan.apps.acls.models import AccessControlList
from mayan.apps.common.serialization import yaml_load
from .classes import Layer
from .transformations import BaseTransformation
logger = logging.getLogger(__name__)
class TransformationManager(models.Manager):
def add_to_object(self, obj, transformation, arguments=None):
content_type = ContentType.objects.get_for_model(model=obj)
self.create(
content_type=content_type, object_id=obj.pk,
name=transformation.name, arguments=yaml_dump(
data=arguments
)
)
def copy(self, source, targets):
"""
Copy transformation from source to all targets
"""
content_type = ContentType.objects.get_for_model(model=source)
# Get transformations
transformations = self.filter(
content_type=content_type, object_id=source.pk
).values('name', 'arguments', 'order')
logger.debug('source transformations: %s', transformations)
# Get all targets from target QS
targets_dict = map(
lambda entry: {
'content_type': entry[0], 'object_id': entry[1]
}, zip(
ContentType.objects.get_for_models(models=targets).values(),
targets.values_list('pk', flat=True)
)
)
logger.debug('targets: %s', targets_dict)
# Combine the two
results = []
for instance in targets_dict:
for transformation in transformations:
result = instance.copy()
result.update(transformation)
results.append(dict(result))
logger.debug('results: %s', results)
# Bulk create for a single DB query
with transaction.atomic():
self.bulk_create(
map(lambda entry: self.model(**entry), results),
)
def get_for_object(self, obj, as_classes=False):
class LayerTransformationManager(models.Manager):
def get_for_object(
self, obj, as_classes=False, maximum_layer_order=None,
only_stored_layer=None, user=None
):
"""
as_classes == True returns the transformation classes from .classes
ready to be feed to the converter class
"""
Layer.update()
StoredLayer = apps.get_model(
app_label='converter', model_name='StoredLayer'
)
content_type = ContentType.objects.get_for_model(model=obj)
transformations = self.filter(
content_type=content_type, object_id=obj.pk
enabled=True, object_layer__content_type=content_type,
object_layer__object_id=obj.pk, object_layer__enabled=True
)
access_layers = StoredLayer.objects.all()
exclude_layers = StoredLayer.objects.none()
if maximum_layer_order:
access_layers = StoredLayer.objects.filter(
order__lte=maximum_layer_order
)
exclude_layers = StoredLayer.objects.filter(
order__gt=maximum_layer_order
)
for stored_layer in access_layers:
access_permission = stored_layer.get_layer().permissions.get(
'access_permission', None
)
if access_permission:
try:
AccessControlList.objects.check_access(
obj=obj, permissions=(access_permission,), user=user
)
except PermissionDenied:
access_layers = access_layers.exclude(pk=stored_layer.pk)
for stored_layer in exclude_layers:
exclude_permission = stored_layer.get_layer().permissions.get(
'exclude_permission', None
)
if exclude_permission:
try:
AccessControlList.objects.check_access(
obj=obj, permissions=(exclude_permission,), user=user
)
except PermissionDenied:
pass
else:
exclude_layers = exclude_layers.exclude(pk=stored_layer.pk)
if only_stored_layer:
transformations = transformations.filter(
object_layer__stored_layer=only_stored_layer
)
transformations = transformations.filter(
object_layer__stored_layer__in=access_layers
)
transformations = transformations.exclude(
object_layer__stored_layer__in=exclude_layers
)
if as_classes:

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

View File

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

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

View File

@@ -1,110 +1,123 @@
from __future__ import unicode_literals
from django.contrib.contenttypes.models import ContentType
from mayan.apps.documents.tests import GenericDocumentViewTestCase
from ..models import Transformation
from ..permissions import (
permission_transformation_create, permission_transformation_delete,
permission_transformation_view
)
from ..models import LayerTransformation
from .literals import TEST_TRANSFORMATION_NAME, TEST_TRANSFORMATION_ARGUMENT
from .mixins import TransformationTestMixin, TransformationViewsTestMixin
class TransformationViewsTestCase(GenericDocumentViewTestCase):
def _transformation_create_view(self):
return self.post(
viewname='converter:transformation_create', kwargs={
'app_label': 'documents', 'model': 'document',
'object_id': self.test_document.pk
}, data={
'name': TEST_TRANSFORMATION_NAME,
'arguments': TEST_TRANSFORMATION_ARGUMENT
}
)
class TransformationViewsTestCase(
TransformationTestMixin, TransformationViewsTestMixin,
GenericDocumentViewTestCase
):
def test_transformation_create_view_no_permission(self):
transformation_count = LayerTransformation.objects.count()
def test_transformation_create_view_no_permissions(self):
transformation_count = Transformation.objects.count()
response = self._transformation_create_view()
self.assertEqual(response.status_code, 403)
self.assertEqual(Transformation.objects.count(), transformation_count)
def test_transformation_create_view_with_permissions(self):
self.grant_permission(permission=permission_transformation_create)
transformation_count = Transformation.objects.count()
response = self._transformation_create_view()
self.assertEqual(response.status_code, 302)
response = self._request_transformation_create_view()
self.assertEqual(response.status_code, 404)
self.assertEqual(
Transformation.objects.count(), transformation_count + 1
LayerTransformation.objects.count(), transformation_count
)
def _request_transformation_delete_view(self):
return self.post(
viewname='converter:transformation_delete', kwargs={
'pk': self.test_transformation.pk
}
)
def _create_test_transformation(self):
content_type = ContentType.objects.get_for_model(model=self.test_document)
self.test_transformation = Transformation.objects.create(
content_type=content_type, object_id=self.test_document.pk,
name=TEST_TRANSFORMATION_NAME,
arguments=TEST_TRANSFORMATION_ARGUMENT
)
def test_transformation_delete_view_no_permissions(self):
self._create_test_transformation()
transformation_count = Transformation.objects.count()
response = self._request_transformation_delete_view()
self.assertEqual(response.status_code, 403)
self.assertEqual(
Transformation.objects.count(), transformation_count
)
def test_transformation_delete_view_with_permissions(self):
self._create_test_transformation()
self.grant_permission(permission=permission_transformation_delete)
transformation_count = Transformation.objects.count()
response = self._request_transformation_delete_view()
self.assertEqual(response.status_code, 302)
self.assertEqual(
Transformation.objects.count(), transformation_count - 1
)
def _transformation_list_view(self):
return self.get(
viewname='converter:transformation_list', kwargs={
'app_label': 'documents', 'model': 'document',
'object_id': self.test_document.pk
}
)
def test_transformation_list_view_no_permissions(self):
response = self._transformation_list_view()
self.assertEqual(response.status_code, 403)
def test_transformation_list_view_with_permissions(self):
def test_transformation_create_view_with_permission(self):
self.grant_access(
obj=self.test_document, permission=permission_transformation_view
obj=self.test_document, permission=self.test_permission
)
response = self._transformation_list_view()
transformation_count = LayerTransformation.objects.count()
response = self._request_transformation_create_view()
self.assertEqual(response.status_code, 302)
self.assertEqual(
LayerTransformation.objects.count(), transformation_count + 1
)
def test_transformation_delete_view_no_permission(self):
self._create_test_transformation()
transformation_count = LayerTransformation.objects.count()
response = self._request_transformation_delete_view()
self.assertEqual(response.status_code, 404)
self.assertEqual(
LayerTransformation.objects.count(), transformation_count
)
def test_transformation_delete_view_with_access(self):
self._create_test_transformation()
self.grant_access(
obj=self.test_document, permission=self.test_layer_permission
)
transformation_count = LayerTransformation.objects.count()
response = self._request_transformation_delete_view()
self.assertEqual(response.status_code, 302)
self.assertEqual(
LayerTransformation.objects.count(), transformation_count - 1
)
def test_transformation_edit_view_no_permission(self):
self._create_test_transformation()
transformation_arguments = self.test_transformation.arguments
response = self._request_transformation_edit_view()
self.assertEqual(response.status_code, 404)
self.test_transformation.refresh_from_db()
self.assertEqual(
transformation_arguments, self.test_transformation.arguments
)
def test_transformation_edit_view_with_access(self):
self._create_test_transformation()
self.grant_access(
obj=self.test_document, permission=self.test_layer_permission
)
transformation_arguments = self.test_transformation.arguments
response = self._request_transformation_edit_view()
self.assertEqual(response.status_code, 302)
self.test_transformation.refresh_from_db()
self.assertNotEqual(
transformation_arguments, self.test_transformation.arguments
)
def test_transformation_list_view_no_permission(self):
self._create_test_transformation()
response = self._request_transformation_list_view()
self.assertNotContains(
response=response, text=self.test_document.label, status_code=404
)
self.assertNotContains(
response=response,
text=self.test_transformation.get_transformation_class().label,
status_code=404
)
def test_transformation_list_view_with_access(self):
self._create_test_transformation()
self.grant_access(
obj=self.test_document, permission=self.test_permission
)
response = self._request_transformation_list_view()
self.assertContains(
response=response, text=self.test_document.label, status_code=200
)
self.assertContains(
response=response,
text=self.test_transformation.get_transformation_class().label,
status_code=200
)

View File

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

View File

@@ -3,25 +3,29 @@ from __future__ import unicode_literals
from django.conf.urls import url
from .views import (
TransformationCreateView, TransformationDeleteView, TransformationEditView,
TransformationListView
TransformationCreateView, TransformationDeleteView,
TransformationEditView, TransformationListView, TransformationSelectView
)
urlpatterns = [
url(
regex=r'^object/(?P<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'
),
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'
),
url(
regex=r'^transformations/(?P<pk>\d+)/delete/$', view=TransformationDeleteView.as_view(),
name='transformation_delete'
regex=r'^layers/(?P<layer_name>[-_\w]+)/transformations/(?P<pk>\d+)/delete/$',
view=TransformationDeleteView.as_view(), name='transformation_delete'
),
url(
regex=r'^transformations/(?P<pk>\d+)/edit/$', view=TransformationEditView.as_view(),
name='transformation_edit'
regex=r'^layers/(?P<layer_name>[-_\w]+)/transformations/(?P<pk>\d+)/edit/$',
view=TransformationEditView.as_view(), name='transformation_edit'
),
]

View File

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

View File

@@ -196,10 +196,16 @@ class APIDocumentPageImageView(generics.RetrieveAPIView):
if rotation:
rotation = int(rotation)
maximum_layer_order = request.GET.get('maximum_layer_order')
if maximum_layer_order:
maximum_layer_order = int(maximum_layer_order)
task = task_generate_document_page_image.apply_async(
kwargs=dict(
document_page_id=self.get_object().pk, width=width,
height=height, zoom=zoom, rotation=rotation
height=height, zoom=zoom, rotation=rotation,
maximum_layer_order=maximum_layer_order,
user_id=request.user.pk
)
)

View File

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

View File

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

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

View File

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

View File

@@ -65,13 +65,19 @@ def task_delete_stubs():
@app.task()
def task_generate_document_page_image(document_page_id, *args, **kwargs):
def task_generate_document_page_image(document_page_id, user_id=None, **kwargs):
DocumentPage = apps.get_model(
app_label='documents', model_name='DocumentPage'
)
User = get_user_model()
if user_id:
user = User.objects.get(pk=user_id)
else:
user = None
document_page = DocumentPage.passthrough.get(pk=document_page_id)
return document_page.generate_image(*args, **kwargs)
return document_page.generate_image(user=user, **kwargs)
@app.task(ignore_result=True)

View File

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

View File

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

View File

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

View File

@@ -12,4 +12,3 @@ class SourceColumnLinkWidget(object):
'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 .dependencies import * # NOQA
from .handlers import (
handler_copy_transformations_to_version, handler_create_default_document_source,
handler_initialize_periodic_tasks
handler_copy_transformations_to_version,
handler_create_default_document_source, handler_initialize_periodic_tasks
)
from .links import (
link_document_create_multiple, link_setup_sources,

View File

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

View File

@@ -12,7 +12,7 @@ from model_utils.managers import InheritanceManager
from mayan.apps.common.compressed_files import Archive
from mayan.apps.common.exceptions import NoMIMETypeMatch
from mayan.apps.converter.models import Transformation
from mayan.apps.converter.layers import layer_saved_transformations
from mayan.apps.documents.models import Document, DocumentType
from mayan.apps.documents.settings import setting_language
@@ -131,7 +131,7 @@ class Source(models.Model):
if user:
document.add_as_recent_document_for_user(user=user)
Transformation.objects.copy(
layer_saved_transformations.copy_transformations(
source=self, targets=document_version.pages.all()
)

View File

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