diff --git a/mayan/apps/converter/apps.py b/mayan/apps/converter/apps.py index fb0892a014..f0b2642be3 100644 --- a/mayan/apps/converter/apps.py +++ b/mayan/apps/converter/apps.py @@ -3,7 +3,20 @@ from __future__ import unicode_literals from django import apps from django.utils.translation import ugettext_lazy as _ +from common import menu_object, menu_sidebar + +from .links import ( + link_transformation_create, link_transformation_delete, + link_transformation_edit +) +from .models import Transformation + class ConverterApp(apps.AppConfig): name = 'converter' verbose_name = _('Converter') + + def ready(self): + menu_sidebar.bind_links(links=[link_transformation_create], sources=[Transformation]) + menu_sidebar.bind_links(links=[link_transformation_create], sources=['converter:transformation_create', 'converter:transformation_list']) + menu_object.bind_links(links=[link_transformation_edit, link_transformation_delete], sources=[Transformation]) diff --git a/mayan/apps/converter/backends/__init__.py b/mayan/apps/converter/backends/__init__.py index 8b13789179..e69de29bb2 100644 --- a/mayan/apps/converter/backends/__init__.py +++ b/mayan/apps/converter/backends/__init__.py @@ -1 +0,0 @@ - diff --git a/mayan/apps/converter/classes.py b/mayan/apps/converter/classes.py index bc3424c5e4..4a3cf10717 100644 --- a/mayan/apps/converter/classes.py +++ b/mayan/apps/converter/classes.py @@ -4,7 +4,7 @@ import logging import io import os import subprocess -from tempfile import mkstemp +import tempfile try: from cStringIO import StringIO @@ -13,8 +13,6 @@ except ImportError: from PIL import Image -from django.utils.encoding import smart_str -from django.utils.module_loading import import_string from django.utils.translation import ugettext_lazy as _ from common.settings import TEMPORARY_DIRECTORY @@ -22,12 +20,8 @@ from common.utils import fs_cleanup from mimetype.api import get_mimetype from .exceptions import OfficeConversionError, UnknownFileFormat -from .literals import ( - DEFAULT_PAGE_NUMBER, DEFAULT_ZOOM_LEVEL, DEFAULT_ROTATION, - DEFAULT_FILE_FORMAT, TRANSFORMATION_CHOICES, TRANSFORMATION_RESIZE, - TRANSFORMATION_ROTATE, TRANSFORMATION_ZOOM, DIMENSION_SEPARATOR -) -from .settings import GRAPHICS_BACKEND, LIBREOFFICE_PATH +from .literals import DEFAULT_PAGE_NUMBER, DEFAULT_FILE_FORMAT +from .settings import LIBREOFFICE_PATH CONVERTER_OFFICE_FILE_MIMETYPES = [ 'application/msword', @@ -111,7 +105,7 @@ class ConverterBase(object): readline = proc.stderr.readline() logger.debug('stderr: %s', readline) if return_code != 0: - raise OfficeBackendError(readline) + raise OfficeConversionError(readline) filename, extension = os.path.splitext(os.path.basename(input_filepath)) logger.debug('filename: %s', filename) @@ -158,7 +152,7 @@ class ConverterBase(object): if self.mime_type in CONVERTER_OFFICE_FILE_MIMETYPES: if os.path.exists(LIBREOFFICE_PATH): if not self.soffice_file_object: - converted_output = Converter.soffice(self.file_object) + converted_output = ConverterBase.soffice(self.file_object) self.file_object.seek(0) self.soffice_file_object = open(converted_output) self.mime_type = 'application/pdf' @@ -187,12 +181,16 @@ class BaseTransformation(object): _registry = {} @classmethod - def get_transformations_classes(cls): - return map(lambda name: getattr(cls, name), filter(lambda entry: entry.startswith('Transform'), dir(cls))) + def register(cls, transformation): + cls._registry[transformation.name] = transformation @classmethod - def get_transformations_choices(cls): - return [(transformation.name, transformation.label) for transformation in cls.get_transformations_classes()] + def get_transformation_choices(cls): + return [(name, klass.label) for name, klass in cls._registry.items()] + + @classmethod + def get(cls, name): + return cls._registry[name] def __init__(self, **kwargs): for argument_name in self.arguments: @@ -206,7 +204,7 @@ class BaseTransformation(object): class TransformationResize(BaseTransformation): name = 'resize' arguments = ('width', 'height') - label = _('Resize') + label = _('Resize ') def execute_on(self, *args, **kwargs): super(TransformationResize, self).execute_on(*args, **kwargs) @@ -244,7 +242,7 @@ class TransformationResize(BaseTransformation): class TransformationRotate(BaseTransformation): name = 'rotate' arguments = ('degrees',) - label = _('Rotate') + label = _('Rotate ') def execute_on(self, *args, **kwargs): super(TransformationRotate, self).execute_on(*args, **kwargs) @@ -255,10 +253,15 @@ class TransformationRotate(BaseTransformation): class TransformationZoom(BaseTransformation): name = 'zoom' arguments = ('percent',) - label = _('Zoom') + label = _('Zoom ') def execute_on(self, *args, **kwargs): super(TransformationZoom, self).execute_on(*args, **kwargs) decimal_value = float(self.percent) / 100 return self.image.resize((int(self.image.size[0] * decimal_value), int(self.image.size[1] * decimal_value)), Image.ANTIALIAS) + + +BaseTransformation.register(TransformationResize) +BaseTransformation.register(TransformationRotate) +BaseTransformation.register(TransformationZoom) diff --git a/mayan/apps/converter/forms.py b/mayan/apps/converter/forms.py new file mode 100644 index 0000000000..6c897cf27a --- /dev/null +++ b/mayan/apps/converter/forms.py @@ -0,0 +1,11 @@ +from __future__ import unicode_literals + +from django import forms + +from .models import Transformation + + +class TransformationForm(forms.ModelForm): + class Meta: + fields = ('order', 'name', 'arguments') + model = Transformation diff --git a/mayan/apps/converter/links.py b/mayan/apps/converter/links.py new file mode 100644 index 0000000000..476e180415 --- /dev/null +++ b/mayan/apps/converter/links.py @@ -0,0 +1,25 @@ +from __future__ import unicode_literals + +from django.contrib.contenttypes.models import ContentType +from django.utils.translation import ugettext_lazy as _ + +from navigation import Link + +from .permissions import ( + PERMISSION_TRANSFORMATION_CREATE, PERMISSION_TRANSFORMATION_DELETE, + PERMISSION_TRANSFORMATION_EDIT, PERMISSION_TRANSFORMATION_VIEW +) + + +def get_kwargs_factory(variable_name): + def get_kwargs(context): + 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(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', permissions=[PERMISSION_TRANSFORMATION_DELETE], tags='dangerous', text=_('Delete'), view='converter:transformation_delete') +link_transformation_edit = Link(args='resolved_object.pk', permissions=[PERMISSION_TRANSFORMATION_EDIT], text=_('Edit'), view='converter:transformation_edit') +link_transformation_list = Link(kwargs=get_kwargs_factory('resolved_object'), permissions=[PERMISSION_TRANSFORMATION_VIEW], text=_('Transformations'), view='converter:transformation_list') diff --git a/mayan/apps/converter/managers.py b/mayan/apps/converter/managers.py new file mode 100644 index 0000000000..a7cda9ba97 --- /dev/null +++ b/mayan/apps/converter/managers.py @@ -0,0 +1,42 @@ +from __future__ import unicode_literals + +from ast import literal_eval +import logging + +from django.contrib.contenttypes.models import ContentType +from django.db import models + +from .classes import BaseTransformation + +logger = logging.getLogger(__name__) + + +class TransformationManager(models.Manager): + def get_for_model(self, obj, as_classes=False): + """ + as_classes == True returns the transformation classes from .classes + ready to be feed to the converter class + """ + + content_type = ContentType.objects.get_for_model(obj) + + transformations = self.filter(content_type=content_type, object_id=obj.pk) + + if as_classes: + result = [] + for transformation in transformations: + try: + transformation_class = BaseTransformation.get(transformation.name) + except KeyError: + # Non existant transformation, but we don't raise an error + logger.error('Non existant transformation: %s for %s', transformation.name, obj) + else: + # TODO: what to do if literal_eval fails? + result.append(transformation_class(**literal_eval(transformation.arguments))) + + return result + else: + return transformations + + + diff --git a/mayan/apps/converter/migrations/0001_initial.py b/mayan/apps/converter/migrations/0001_initial.py new file mode 100644 index 0000000000..8332770f5a --- /dev/null +++ b/mayan/apps/converter/migrations/0001_initial.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +import converter.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('contenttypes', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Transformation', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('object_id', models.PositiveIntegerField()), + ('order', models.PositiveIntegerField(default=0, null=True, verbose_name='Order', db_index=True, blank=True)), + ('transformation', models.CharField(max_length=128, verbose_name='Transformation', choices=[('rotate', 'Rotate'), ('zoom', 'Zoom'), ('resize', 'Resize')])), + ('arguments', models.TextField(blank=True, null=True, verbose_name='Arguments', validators=[converter.models.argument_validator])), + ('content_type', models.ForeignKey(to='contenttypes.ContentType')), + ], + options={ + 'ordering': ('order',), + 'verbose_name': 'Transformation', + 'verbose_name_plural': 'Transformations', + }, + bases=(models.Model,), + ), + ] diff --git a/mayan/apps/converter/migrations/0002_auto_20150608_1943.py b/mayan/apps/converter/migrations/0002_auto_20150608_1943.py new file mode 100644 index 0000000000..f809529002 --- /dev/null +++ b/mayan/apps/converter/migrations/0002_auto_20150608_1943.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('converter', '0001_initial'), + ] + + operations = [ + migrations.RenameField( + model_name='transformation', + old_name='transformation', + new_name='name', + ), + ] diff --git a/mayan/apps/converter/migrations/__init__.py b/mayan/apps/converter/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/mayan/apps/converter/models.py b/mayan/apps/converter/models.py new file mode 100644 index 0000000000..a8c32743e1 --- /dev/null +++ b/mayan/apps/converter/models.py @@ -0,0 +1,54 @@ +from __future__ import unicode_literals + +from ast import literal_eval +import logging + +from django.contrib.contenttypes import generic +from django.contrib.contenttypes.models import ContentType +from django.core.exceptions import ValidationError +from django.core.urlresolvers import reverse +from django.db import models +from django.utils.encoding import python_2_unicode_compatible +from django.utils.translation import ugettext_lazy as _ + +from .classes import BaseTransformation +from .managers import TransformationManager + +logger = logging.getLogger(__name__) + + +def argument_validator(value): + """ + Validates that the input evaluates correctly. + """ + value = value.strip() + try: + literal_eval(value) + except (ValueError, SyntaxError): + raise ValidationError(_('Enter a valid value.'), code='invalid') + + +@python_2_unicode_compatible +class Transformation(models.Model): + """ + Model that stores the transformation and transformation arguments + for a given object + """ + + content_type = models.ForeignKey(ContentType) + object_id = models.PositiveIntegerField() + content_object = generic.GenericForeignKey('content_type', 'object_id') + + order = models.PositiveIntegerField(default=0, blank=True, null=True, verbose_name=_('Order'), db_index=True) + name = models.CharField(choices=BaseTransformation.get_transformation_choices(), max_length=128, verbose_name=_('Transformation')) + arguments = models.TextField(blank=True, null=True, verbose_name=_('Arguments'), validators=[argument_validator]) + + objects = TransformationManager() + + def __str__(self): + return self.get_transformation_display() + + class Meta: + ordering = ('order',) + verbose_name = _('Transformation') + verbose_name_plural = _('Transformations') diff --git a/mayan/apps/converter/permissions.py b/mayan/apps/converter/permissions.py new file mode 100644 index 0000000000..023802d30b --- /dev/null +++ b/mayan/apps/converter/permissions.py @@ -0,0 +1,11 @@ +from __future__ import absolute_import, unicode_literals + +from django.utils.translation import ugettext_lazy as _ + +from permissions.models import Permission, PermissionNamespace + +namespace = PermissionNamespace('converter', _('Converter')) +PERMISSION_TRANSFORMATION_CREATE = Permission.objects.register(namespace, 'transformation_create', _('Create new transformations')) +PERMISSION_TRANSFORMATION_DELETE = Permission.objects.register(namespace, 'transformation_delete', _('Delete transformations')) +PERMISSION_TRANSFORMATION_EDIT = Permission.objects.register(namespace, 'transformation_edit', _('Edit transformations')) +PERMISSION_TRANSFORMATION_VIEW = Permission.objects.register(namespace, 'transformation_view', _('View existing transformations')) diff --git a/mayan/apps/converter/urls.py b/mayan/apps/converter/urls.py new file mode 100644 index 0000000000..60af32b529 --- /dev/null +++ b/mayan/apps/converter/urls.py @@ -0,0 +1,11 @@ +from __future__ import unicode_literals + +from django.conf.urls import patterns, url + +urlpatterns = patterns( + 'converter.views', + url(r'^create_for/(?P[-\w]+)/(?P[-\w]+)/(?P\d+)/$', 'transformation_create', (), 'transformation_create'), + url(r'^list_for/(?P[-\w]+)/(?P[-\w]+)/(?P\d+)/$', 'transformation_list', (), 'transformation_list'), + url(r'^delete/(?P\d+)/$', 'transformation_delete', (), 'transformation_delete'), + url(r'^edit/(?P\d+)/$', 'transformation_edit', (), 'transformation_edit'), +) diff --git a/mayan/apps/converter/views.py b/mayan/apps/converter/views.py new file mode 100644 index 0000000000..9e7ab7b9ff --- /dev/null +++ b/mayan/apps/converter/views.py @@ -0,0 +1,142 @@ +from __future__ import absolute_import, unicode_literals + +import logging + +from django.conf import settings +from django.contrib import messages +from django.contrib.contenttypes.models import ContentType +from django.core.exceptions import ObjectDoesNotExist, PermissionDenied +from django.core.urlresolvers import reverse +from django.http import Http404, HttpResponseRedirect +from django.shortcuts import get_object_or_404, render_to_response +from django.template import RequestContext +from django.utils.http import urlencode +from django.utils.translation import ugettext_lazy as _ + +from common.utils import encapsulate +from permissions.models import Permission + +from .forms import TransformationForm +from .models import Transformation +from .permissions import ( + PERMISSION_TRANSFORMATION_CREATE, PERMISSION_TRANSFORMATION_DELETE, + PERMISSION_TRANSFORMATION_EDIT, PERMISSION_TRANSFORMATION_VIEW +) + +logger = logging.getLogger(__name__) + + +def transformation_list(request, app_label, model, object_id): + content_type = get_object_or_404(ContentType, app_label=app_label, model=model) + + try: + content_object = content_type.get_object_for_this_type(pk=object_id) + except content_type.model_class().DoesNotExist: + raise Http404 + + try: + Permission.objects.check_permissions(request.user, [PERMISSION_TRANSFORMATION_VIEW]) + except PermissionDenied: + AccessEntry.objects.check_access(PERMISSION_TRANSFORMATION_VIEW, request.user, content_object) + + context = { + 'object_list': Transformation.objects.get_for_model(content_object), + 'content_object': content_object, + 'navigation_object_list': ['content_object'], + 'title': _('Transformations for: %s') % content_object, + 'extra_columns': [ + {'name': _('Order'), 'attribute': 'order'}, + {'name': _('Transformation'), 'attribute': encapsulate(lambda x: x.get_name_display())}, + {'name': _('Arguments'), 'attribute': 'arguments'} + ], + 'hide_link': True, + 'hide_object': True, + } + return render_to_response( + 'appearance/generic_list.html', context, context_instance=RequestContext(request) + ) + + +def transformation_create(request, app_label, model, object_id): + content_type = get_object_or_404(ContentType, app_label=app_label, model=model) + + try: + content_object = content_type.get_object_for_this_type(pk=object_id) + except content_type.model_class().DoesNotExist: + raise Http404 + + try: + Permission.objects.check_permissions(request.user, [PERMISSION_TRANSFORMATION_CREATE]) + except PermissionDenied: + AccessEntry.objects.check_access(PERMISSION_TRANSFORMATION_CREATE, request.user, content_object) + + if request.method == 'POST': + form = TransformationForm(request.POST, initial={'content_object': content_object}) + if form.is_valid(): + instance = form.save(commit=False) + instance.content_object = content_object + instance.save() + messages.success(request, _('Transformation created successfully.')) + return HttpResponseRedirect(reverse('converter:transformation_list', args=[app_label, model, object_id])) + else: + form = TransformationForm(initial={'content_object': content_object}) + + return render_to_response('appearance/generic_form.html', { + 'form': form, + 'content_object': content_object, + 'navigation_object_list': ['content_object'], + 'title': _('Create new transformation for: %s') % content_object, + }, context_instance=RequestContext(request)) + + +def transformation_delete(request, object_id): + transformation = get_object_or_404(Transformation, pk=object_id) + + try: + Permission.objects.check_permissions(request.user, [PERMISSION_TRANSFORMATION_DELETE]) + except PermissionDenied: + AccessEntry.objects.check_access(PERMISSION_TRANSFORMATION_DELETE, request.user, transformation.content_object) + + if request.method == 'POST': + transformation.delete() + messages.success(request, _('Transformation deleted successfully.')) + return HttpResponseRedirect(reverse('converter:transformation_list', args=[transformation.content_type.app_label, transformation.content_type.model, transformation.object_id])) + + return render_to_response('appearance/generic_confirm.html', { + 'content_object': transformation.content_object, + 'delete_view': True, + 'navigation_object_list': ['content_object', 'transformation'], + 'previous': reverse('converter:transformation_list', args=[transformation.content_type.app_label, transformation.content_type.model, transformation.object_id]), + 'title': _('Are you sure you wish to delete transformation "%(transformation)s" for: %(content_object)s') % { + 'transformation': transformation, + 'content_object': transformation.content_object}, + 'transformation': transformation, + }, context_instance=RequestContext(request)) + + +def transformation_edit(request, object_id): + transformation = get_object_or_404(Transformation, pk=object_id) + + try: + Permission.objects.check_permissions(request.user, [PERMISSION_TRANSFORMATION_EDIT]) + except PermissionDenied: + AccessEntry.objects.check_access(PERMISSION_TRANSFORMATION_EDIT, request.user, transformation.content_object) + + if request.method == 'POST': + form = TransformationForm(request.POST, instance=transformation) + if form.is_valid(): + form.save() + messages.success(request, _('Transformation edited successfully.')) + return HttpResponseRedirect(reverse('converter:transformation_list', args=[transformation.content_type.app_label, transformation.content_type.model, transformation.object_id])) + else: + form = TransformationForm(instance=transformation) + + return render_to_response('appearance/generic_form.html', { + 'content_object': transformation.content_object, + 'form': form, + 'navigation_object_list': ['content_object', 'transformation'], + 'title': _('Edit transformation "%(transformation)s" for: %(content_object)s') % { + 'transformation': transformation, + 'content_object': transformation.content_object}, + 'transformation': transformation, + }, context_instance=RequestContext(request)) diff --git a/mayan/urls.py b/mayan/urls.py index 97f805ae61..a8f444b44e 100644 --- a/mayan/urls.py +++ b/mayan/urls.py @@ -16,6 +16,7 @@ urlpatterns = patterns( url(r'^authentication/', include('authentication.urls', namespace='authentication')), url(r'^checkouts/', include('checkouts.urls', namespace='checkouts')), url(r'^comments/', include('document_comments.urls', namespace='comments')), + url(r'^converter/', include('converter.urls', namespace='converter')), url(r'^document/signatures/', include('document_signatures.urls', namespace='signatures')), url(r'^document/states/', include('document_states.urls', namespace='document_states')), url(r'^documents/', include('documents.urls', namespace='documents')),