diff --git a/mayan/apps/control_codes/admin.py b/mayan/apps/control_codes/admin.py new file mode 100644 index 0000000000..fbaeb3112d --- /dev/null +++ b/mayan/apps/control_codes/admin.py @@ -0,0 +1,23 @@ +from __future__ import unicode_literals + +from django.contrib import admin +from django.utils.translation import ugettext_lazy as _ + +from .models import ControlSheet, ControlSheetCode + + +class ControlSheetCodeInline(admin.StackedInline): + allow_add = True + classes = ('collapse-open',) + extra = 1 + model = ControlSheetCode + + +@admin.register(ControlSheet) +class ControlSheetAdmin(admin.ModelAdmin): + inlines = (ControlSheetCodeInline,) + list_display = ('label', 'get_codes_count') + + def get_codes_count(self, instance): + return instance.codes.count() + get_codes_count.short_description = _('Codes') diff --git a/mayan/apps/control_codes/api_views.py b/mayan/apps/control_codes/api_views.py new file mode 100644 index 0000000000..540509e08c --- /dev/null +++ b/mayan/apps/control_codes/api_views.py @@ -0,0 +1,231 @@ +from __future__ import absolute_import, unicode_literals + +from django.http import HttpResponse +from django.shortcuts import get_object_or_404 +from django.views.decorators.cache import cache_control, patch_cache_control + +from rest_framework import generics + +from mayan.apps.acls.models import AccessControlList +from mayan.apps.documents.models import Document, DocumentType +from mayan.apps.documents.permissions import permission_document_type_view +from mayan.apps.rest_api.filters import MayanObjectPermissionsFilter +from mayan.apps.rest_api.permissions import MayanPermission + +from .literals import CONTROL_SHEET_CODE_IMAGE_TASK_TIMEOUT +from .models import ControlSheet +#from .permissions import ( +# permission_workflow_create, permission_workflow_delete, +# permission_workflow_edit, permission_workflow_view +#) +from .serializers import ( + ControlSheetSerializer, ControlSheetCodeSerializer +) + +from .settings import settings_control_sheet_code_image_cache_time +from .tasks import task_generate_control_sheet_code_image + + +class APIControlSheetListView(generics.ListCreateAPIView): + """ + get: Returns a list of all the control sheets. + post: Create a new control sheet. + """ + filter_backends = (MayanObjectPermissionsFilter,) + #mayan_object_permissions = {'GET': (permission_control_sheet_view,)} + #mayan_view_permissions = {'POST': (permission_control_sheet_create,)} + permission_classes = (MayanPermission,) + queryset = ControlSheet.objects.all() + serializer_class = ControlSheetSerializer + + def get_serializer(self, *args, **kwargs): + if not self.request: + return None + + return super(APIControlSheetListView, self).get_serializer( + *args, **kwargs + ) + + #def get_serializer_class(self): + # if self.request.method == 'GET': + # return ControlSheetSerializer + # else: + # return WritableControlSheetSerializer + + +class APIControlSheetView(generics.RetrieveUpdateDestroyAPIView): + """ + delete: Delete the selected control sheet. + get: Return the details of the selected control sheet. + patch: Edit the selected control sheet. + put: Edit the selected control sheet. + """ + filter_backends = (MayanObjectPermissionsFilter,) + #mayan_object_permissions = { + # 'DELETE': (permission_control sheet_delete,), + # 'GET': (permission_control sheet_view,), + # 'PATCH': (permission_control sheet_edit,), + # 'PUT': (permission_control sheet_edit,) + #} + lookup_url_kwarg = 'control_sheet_id' + queryset = ControlSheet.objects.all() + serializer_class = ControlSheetSerializer + + def get_serializer(self, *args, **kwargs): + if not self.request: + return None + + return super(APIControlSheetView, self).get_serializer( + *args, **kwargs + ) + + #def get_serializer_class(self): + # if self.request.method == 'GET': + # return ControlSheetSerializer + # else: + # return WritableControlSheetSerializer + + + +class APIControlSheetCodeListView(generics.ListCreateAPIView): + """ + get: Returns a list of all the control sheet codes. + post: Create a new control sheet code. + """ + serializer_class = ControlSheetCodeSerializer + + def get_queryset(self): + return self.get_control_sheet().codes.all() + + def get_serializer_context(self): + """ + Extra context provided to the serializer class. + """ + context = super(APIControlSheetCodeListView, self).get_serializer_context() + if self.kwargs: + context.update( + { + 'control_sheet': self.get_control_sheet(), + } + ) + + return context + + def get_control_sheet(self): + #if self.request.method == 'GET': + # permission_required = permission_control_sheet_view + #else: + # permission_required = permission_control_sheet_edit + + control_sheet = get_object_or_404( + klass=ControlSheet, pk=self.kwargs['control_sheet_id'] + ) + + #AccessControlList.objects.check_access( + # obj=control_sheet, permissions=(permission_required,), + # user=self.request.user + #) + + return control_sheet + + +class APIControlSheetCodeView(generics.RetrieveUpdateDestroyAPIView): + """ + delete: Delete the selected control sheet code. + get: Return the details of the selected control sheet code. + patch: Edit the selected control sheet code. + put: Edit the selected control sheet code. + """ + lookup_url_kwarg = 'control_sheet_code_id' + serializer_class = ControlSheetCodeSerializer + + def get_queryset(self): + return self.get_control_sheet().codes.all() + + def get_serializer_context(self): + """ + Extra context provided to the serializer class. + """ + context = super(APIControlSheetCodeView, self).get_serializer_context() + if self.kwargs: + context.update( + { + 'control_sheet': self.get_control_sheet(), + } + ) + + return context + + def get_control_sheet(self): + #if self.request.method == 'GET': + # permission_required = permission_control_sheet_view + #else: + # permission_required = permission_control_sheet_edit + + control_sheet = get_object_or_404( + klass=ControlSheet, pk=self.kwargs['control_sheet_id'] + ) + + #AccessControlList.objects.check_access( + # obj=control_sheet, permissions=(permission_required,), + # user=self.request.user + #) + + return control_sheet + + +class APIControlSheetCodeImageView(generics.RetrieveAPIView): + """ + get: Returns an image representation of the selected control_sheet. + """ + filter_backends = (MayanObjectPermissionsFilter,) + #mayan_object_permissions = { + # 'GET': (permission_control_sheet_view,), + #} + lookup_url_kwarg = 'control_sheet_code_id' + #queryset = ControlSheetCode.objects.all() + + def get_queryset(self): + return self.get_control_sheet().codes.all() + + def get_control_sheet(self): + #if self.request.method == 'GET': + # permission_required = permission_control_sheet_view + #else: + # permission_required = permission_control_sheet_edit + + control_sheet = get_object_or_404( + klass=ControlSheet, pk=self.kwargs['control_sheet_id'] + ) + + #AccessControlList.objects.check_access( + # obj=control_sheet, permissions=(permission_required,), + # user=self.request.user + #) + + return control_sheet + + def get_serializer(self, *args, **kwargs): + return None + + def get_serializer_class(self): + return None + + @cache_control(private=True) + def retrieve(self, request, *args, **kwargs): + task = task_generate_control_sheet_code_image.apply_async( + kwargs=dict( + control_sheet_code_id=self.get_object().pk, + ) + ) + + cache_filename = task.get(timeout=CONTROL_SHEET_CODE_IMAGE_TASK_TIMEOUT) + cache_file = self.get_object().cache_partition.get_file(filename=cache_filename) + with cache_file.open() as file_object: + response = HttpResponse(file_object.read(), content_type='image') + if '_hash' in request.GET: + patch_cache_control( + response, + max_age=settings_control_sheet_image_cache_time.value + ) + return response diff --git a/mayan/apps/control_codes/apps.py b/mayan/apps/control_codes/apps.py index 2cad57f417..90156a7101 100644 --- a/mayan/apps/control_codes/apps.py +++ b/mayan/apps/control_codes/apps.py @@ -19,6 +19,7 @@ from mayan.apps.events.links import ( ) from mayan.apps.navigation.classes import SourceColumn +from .control_codes import * from .handlers import handler_process_document_version from .methods import method_document_submit, method_document_version_submit @@ -26,7 +27,7 @@ from .methods import method_document_submit, method_document_version_submit class ControlCodesApp(MayanAppConfig): app_namespace = 'control_codes' app_url = 'control_codes' - has_rest_api = False + has_rest_api = True has_tests = False name = 'mayan.apps.control_codes' verbose_name = _('Control codes') diff --git a/mayan/apps/control_codes/classes.py b/mayan/apps/control_codes/classes.py index c91837a241..041260eb71 100644 --- a/mayan/apps/control_codes/classes.py +++ b/mayan/apps/control_codes/classes.py @@ -8,14 +8,16 @@ import qrcode from django.apps import apps from django.db import transaction +from django.utils.translation import string_concat, ugettext_lazy as _ from mayan.apps.common.serialization import yaml_dump, yaml_load from mayan.apps.documents.literals import DOCUMENT_IMAGE_TASK_TIMEOUT from mayan.apps.documents.tasks import task_generate_document_page_image -CONTROL_CODE_MAGIC_NUMBER = 'MCTRL' -CONTROL_CODE_SEPARATOR = ':' -CONTROL_CODE_VERSION = '1' +from .literals import ( + CONTROL_CODE_MAGIC_NUMBER, CONTROL_CODE_SEPARATOR, CONTROL_CODE_VERSION +) + logger = logging.getLogger(__name__) @@ -27,6 +29,28 @@ class ControlCode(object): def get(cls, name): return cls._registry[name] + @classmethod + def get_label(cls): + if cls.arguments: + return string_concat(cls.label, ': ', ', '.join(cls.arguments)) + else: + return cls.label + + @classmethod + def get_choices(cls): + #if layer: + # transformation_list = [ + # (transformation.name, transformation) for transformation in cls._layer_transformations[layer] + # ] + #else: + #control_code_list = cls._registry.items() + + return sorted( + [ + (name, klass.get_label()) for name, klass in cls._registry.items()#transformation_list + ] + ) + @classmethod def process_document_version(cls, document_version): logger.info( diff --git a/mayan/apps/control_codes/control_codes.py b/mayan/apps/control_codes/control_codes.py new file mode 100644 index 0000000000..eaaef3b746 --- /dev/null +++ b/mayan/apps/control_codes/control_codes.py @@ -0,0 +1,15 @@ +from __future__ import unicode_literals + +from .classes import ControlCode + + +class ControlCodeTest(ControlCode): + arguments = ('argument_1',) + label = 'Test' + name = 'test' + + def execute(self): + pass + + +ControlCode.register(control_code=ControlCodeTest) diff --git a/mayan/apps/control_codes/forms.py b/mayan/apps/control_codes/forms.py new file mode 100644 index 0000000000..1e36c9b64e --- /dev/null +++ b/mayan/apps/control_codes/forms.py @@ -0,0 +1,65 @@ +from __future__ import unicode_literals + +import yaml + +from django import forms +from django.core.exceptions import ValidationError +from django.utils.translation import ugettext_lazy as _ + +from mayan.apps.common.serialization import yaml_load + +from .classes import ControlCode +from .models import ControlSheetCode + + +class ControlCodeClassSelectionForm(forms.Form): + control_code = forms.ChoiceField( + choices=(), help_text=_('Available control codes.'), + label=_('Control code'), + ) + + def __init__(self, *args, **kwargs): + super(ControlCodeClassSelectionForm, self).__init__(*args, **kwargs) + + self.fields[ + 'control_code' + ].choices = ControlCode.get_choices() + + +""" +class ControlSheetCodeForm(forms.ModelForm): + class Meta: + 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: + yaml_load(stream=self.cleaned_data['arguments']) + except yaml.YAMLError: + raise ValidationError( + _( + '"%s" not a valid entry.' + ) % self.cleaned_data['arguments'] + ) +""" diff --git a/mayan/apps/control_codes/handlers.py b/mayan/apps/control_codes/handlers.py index 5e61ac7ff5..6939be4cc6 100644 --- a/mayan/apps/control_codes/handlers.py +++ b/mayan/apps/control_codes/handlers.py @@ -17,5 +17,5 @@ def handler_initialize_new_document_type_settings(sender, instance, **kwargs): def handler_process_document_version(sender, instance, **kwargs): - #if instance.document.document_type.file_metadata_settings.auto_process: + #if instance.document.document_type.control_codes_settings.auto_process: instance.submit_for_control_codes_processing() diff --git a/mayan/apps/control_codes/literals.py b/mayan/apps/control_codes/literals.py new file mode 100644 index 0000000000..a7e3f7cba7 --- /dev/null +++ b/mayan/apps/control_codes/literals.py @@ -0,0 +1,11 @@ +from __future__ import unicode_literals + +CONTROL_CODE_MAGIC_NUMBER = 'MCTRL' +CONTROL_CODE_SEPARATOR = ':' +CONTROL_CODE_VERSION = '1' + +CONTROL_SHEET_CODE_IMAGE_CACHE_NAME = 'workflow_images' +CONTROL_SHEET_CODE_IMAGE_CACHE_STORAGE_INSTANCE_PATH = 'mayan.apps.control_codes.storages.storage_controlsheetcodeimagecache' +CONTROL_SHEET_CODE_IMAGE_TASK_TIMEOUT = 60 + +DEFAULT_CONTROL_SHEET_CODE_IMAGE_CACHE_MAXIMUM_SIZE = 50 * 2 ** 20 # 50 Megabytes diff --git a/mayan/apps/control_codes/migrations/0001_initial.py b/mayan/apps/control_codes/migrations/0001_initial.py new file mode 100644 index 0000000000..6e9a65d1dc --- /dev/null +++ b/mayan/apps/control_codes/migrations/0001_initial.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.22 on 2019-09-01 08:20 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion +import mayan.apps.common.validators + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='ControlSheet', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('label', models.CharField(max_length=196, unique=True, verbose_name='Label')), + ], + options={ + 'ordering': ('label',), + 'verbose_name': 'Control sheet', + 'verbose_name_plural': 'Control sheets', + }, + ), + migrations.CreateModel( + name='ControlSheetCode', + 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=[('test', 'Test: argument_1')], max_length=128, verbose_name='Name')), + ('arguments', models.TextField(blank=True, help_text='Enter the arguments for the control code as a YAML dictionary.', validators=[mayan.apps.common.validators.YAMLValidator()], verbose_name='Arguments')), + ('enabled', models.BooleanField(default=True, verbose_name='Enabled')), + ('control_sheet', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='codes', to='control_codes.ControlSheet', verbose_name='Control sheet')), + ], + options={ + 'verbose_name': 'Control sheet code', + 'verbose_name_plural': 'Control sheet codes', + }, + ), + ] diff --git a/mayan/apps/control_codes/migrations/__init__.py b/mayan/apps/control_codes/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/mayan/apps/control_codes/models.py b/mayan/apps/control_codes/models.py new file mode 100644 index 0000000000..353f8c97e6 --- /dev/null +++ b/mayan/apps/control_codes/models.py @@ -0,0 +1,154 @@ +from __future__ import unicode_literals + +import hashlib +import json +import logging + +from furl import furl + +from django.apps import apps +from django.conf import settings +from django.contrib.contenttypes.fields import GenericForeignKey +from django.contrib.contenttypes.models import ContentType +from django.core import serializers +from django.db import models +from django.db.models import Max +from django.urls import reverse +from django.utils.encoding import ( + force_bytes, force_text, python_2_unicode_compatible +) +from django.utils.functional import cached_property +from django.utils.translation import ugettext_lazy as _ + +from mayan.apps.common.serialization import yaml_load +from mayan.apps.common.validators import YAMLValidator + +from .classes import ControlCode +from .literals import CONTROL_SHEET_CODE_IMAGE_CACHE_NAME + +logger = logging.getLogger(__name__) + + +@python_2_unicode_compatible +class ControlSheet(models.Model): + label = models.CharField( + max_length=196, unique=True, verbose_name=_('Label') + ) + + def __str__(self): + return self.label + + def get_absolute_url(self): + return reverse( + viewname='control_codes:control_sheet_detail', kwargs={ + 'control_sheet_id': self.pk + } + ) + + class Meta: + ordering = ('label',) + verbose_name = _('Control sheet') + verbose_name_plural = _('Control sheets') + + +class ControlSheetCode(models.Model): + control_sheet = models.ForeignKey( + on_delete=models.CASCADE, related_name='codes', to=ControlSheet, + verbose_name=_('Control sheet') + ) + 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=ControlCode.get_choices(), + max_length=128, verbose_name=_('Name') + ) + arguments = models.TextField( + blank=True, help_text=_( + 'Enter the arguments for the control code as a YAML ' + 'dictionary.' + ), validators=[YAMLValidator()], verbose_name=_('Arguments') + ) + enabled = models.BooleanField(default=True, verbose_name=_('Enabled')) + + class Meta: + verbose_name = _('Control sheet code') + verbose_name_plural = _('Control sheet codes') + + @cached_property + def cache(self): + Cache = apps.get_model(app_label='file_caching', model_name='Cache') + return Cache.objects.get(name=CONTROL_SHEET_CODE_IMAGE_CACHE_NAME) + + @cached_property + def cache_partition(self): + partition, created = self.cache.partitions.get_or_create( + name='{}'.format(self.pk) + ) + return partition + + def delete(self, *args, **kwargs): + self.cache_partition.delete() + return super(Workflow, self).delete(*args, **kwargs) + + def generate_image(self): + cache_filename = '{}'.format(self.get_hash()) + + if self.cache_partition.get_file(filename=cache_filename): + logger.debug( + 'workflow cache file "%s" found', cache_filename + ) + else: + logger.debug( + 'workflow cache file "%s" not found', cache_filename + ) + + image = self.render() + with self.cache_partition.create_file(filename=cache_filename) as file_object: + #file_object.write(image) + image.save(file_object) + + return cache_filename + + def get_api_image_url(self, *args, **kwargs): + final_url = furl() + final_url.args = kwargs + final_url.path = reverse( + viewname='rest_api:workflow-image', + kwargs={'pk': self.pk} + ) + final_url.args['_hash'] = self.get_hash() + + return final_url.tostr() + + def get_arguments(self): + return yaml_load(self.arguments or '{}') + + def get_control_code_class(self): + return ControlCode.get(name=self.name) + + def get_hash(self): + objects_lists = list( + ControlSheetCode.objects.filter(pk=self.pk) + ) + + return hashlib.sha256( + force_bytes( + serializers.serialize('json', objects_lists) + ) + ).hexdigest() + + def render(self): + return self.get_control_code_class()(**self.get_arguments()).image + + def save(self, *args, **kwargs): + if not self.order: + last_order = ControlSheetCode.objects.filter( + control_sheet=self.control_sheet + ).aggregate(Max('order'))['order__max'] + if last_order is not None: + self.order = last_order + 1 + super(ControlSheetCode, self).save(*args, **kwargs) diff --git a/mayan/apps/control_codes/serializers.py b/mayan/apps/control_codes/serializers.py new file mode 100644 index 0000000000..b91c9e845b --- /dev/null +++ b/mayan/apps/control_codes/serializers.py @@ -0,0 +1,79 @@ +from __future__ import unicode_literals + +from django.core.exceptions import ValidationError as DjangoValidationError +from django.utils.translation import ugettext_lazy as _ + +from rest_framework import serializers +from rest_framework.exceptions import ValidationError +from rest_framework.reverse import reverse + +from mayan.apps.documents.models import DocumentType +from mayan.apps.documents.serializers import DocumentTypeSerializer +from mayan.apps.rest_api.relations import ( + FilteredPrimaryKeyRelatedField, MultiKwargHyperlinkedIdentityField +) +from mayan.apps.user_management.serializers import UserSerializer + +from .models import ControlSheet, ControlSheetCode + + +class ControlSheetSerializer(serializers.HyperlinkedModelSerializer): + #states = ControlSheetStateSerializer(many=True, required=False) + #transitions = ControlSheetTransitionSerializer(many=True, required=False) + + code_list_url = serializers.HyperlinkedIdentityField( + lookup_url_kwarg='control_sheet_id', + view_name='rest_api:controlsheet-code-list' + ) + + class Meta: + extra_kwargs = { + 'url': { + 'lookup_url_kwarg': 'control_sheet_id', + 'view_name': 'rest_api:controlsheet-detail' + }, + } + fields = ('code_list_url', 'id', 'label', 'url') + model = ControlSheet + + +class ControlSheetCodeSerializer(serializers.HyperlinkedModelSerializer): + control_sheet = ControlSheetSerializer(read_only=True) + image_url = MultiKwargHyperlinkedIdentityField( + view_kwargs=( + { + 'lookup_field': 'control_sheet_id', + 'lookup_url_kwarg': 'control_sheet_id', + }, + { + 'lookup_field': 'pk', + 'lookup_url_kwarg': 'control_sheet_code_id', + } + ), + view_name='rest_api:controlsheet-code-image' + ) + url = MultiKwargHyperlinkedIdentityField( + view_kwargs=( + { + 'lookup_field': 'control_sheet_id', + 'lookup_url_kwarg': 'control_sheet_id', + }, + { + 'lookup_field': 'pk', + 'lookup_url_kwarg': 'control_sheet_code_id', + } + ), + view_name='rest_api:controlsheet-code-detail' + ) + + class Meta: + fields = ( + 'arguments', 'control_sheet', 'id', 'image_url', 'name', + 'order', 'url' + ) + model = ControlSheetCode + + def create(self, validated_data): + validated_data['control_sheet'] = self.context['control_sheet'] + return super(ControlSheetCodeSerializer, self).create(validated_data) + diff --git a/mayan/apps/control_codes/settings.py b/mayan/apps/control_codes/settings.py new file mode 100644 index 0000000000..6bad205ba8 --- /dev/null +++ b/mayan/apps/control_codes/settings.py @@ -0,0 +1,44 @@ +from __future__ import unicode_literals + +import os + +from django.conf import settings +from django.utils.translation import ugettext_lazy as _ + +from mayan.apps.smart_settings.classes import Namespace + +from .literals import DEFAULT_CONTROL_SHEET_CODE_IMAGE_CACHE_MAXIMUM_SIZE +from .utils import callback_update_control_sheet_image_cache_size + +namespace = Namespace(label=_('Control codes'), name='control_codes') + +setting_control_sheet_code_image_cache_maximum_size = namespace.add_setting( + global_name='CONTROL_SHEET_CODE_IMAGE_CACHE_MAXIMUM_SIZE', + default=DEFAULT_CONTROL_SHEET_CODE_IMAGE_CACHE_MAXIMUM_SIZE, + help_text=_( + 'The threshold at which the CONTROL_SHEET_CODE_IMAGE_CACHE_STORAGE_BACKEND ' + 'will start deleting the oldest control sheet code image cache files. ' + 'Specify the size in bytes.' + ), post_edit_function=callback_update_control_sheet_image_cache_size +) +settings_control_sheet_code_image_cache_time = namespace.add_setting( + global_name='CONTROL_SHEETS_CODE_IMAGE_CACHE_TIME', default='31556926', + help_text=_( + 'Time in seconds that the browser should cache the supplied control sheet ' + 'code images. The default of 31559626 seconds corresponde to 1 year.' + ) +) +setting_control_sheet_code_image_cache_storage = namespace.add_setting( + global_name='CONTROL_SHEETS_CODE_IMAGE_CACHE_STORAGE_BACKEND', + default='django.core.files.storage.FileSystemStorage', help_text=_( + 'Path to the Storage subclass to use when storing the cached ' + 'control sheet code image files.' + ) +) +setting_control_sheet_code_image_cache_storage_arguments = namespace.add_setting( + global_name='CONTROL_SHEETS_CODE_IMAGE_CACHE_STORAGE_BACKEND_ARGUMENTS', + default={'location': os.path.join(settings.MEDIA_ROOT, 'control_sheets')}, + help_text=_( + 'Arguments to pass to the CONTROL_SHEETS_CODE_IMAGE_CACHE_STORAGE_BACKEND.' + ) +) diff --git a/mayan/apps/control_codes/storages.py b/mayan/apps/control_codes/storages.py new file mode 100644 index 0000000000..ab8c7a7c4f --- /dev/null +++ b/mayan/apps/control_codes/storages.py @@ -0,0 +1,12 @@ +from __future__ import unicode_literals + +from mayan.apps.storage.utils import get_storage_subclass + +from .settings import ( + setting_control_sheet_code_image_cache_storage, + setting_control_sheet_code_image_storage_arguments +) + +storage_controlsheetcodeimagecache = get_storage_subclass( + dotted_path=setting_control_sheet_code_image_cache_storage.value +)(**setting_control_sheet_code_image_storage_arguments.value) diff --git a/mayan/apps/control_codes/tasks.py b/mayan/apps/control_codes/tasks.py index b945e686a5..78864995f8 100644 --- a/mayan/apps/control_codes/tasks.py +++ b/mayan/apps/control_codes/tasks.py @@ -11,6 +11,17 @@ from .classes import ControlCode logger = logging.getLogger(__name__) +@app.task() +def task_generate_control_sheet_code_image(control_sheet_code_id): + ControlSheetCode = apps.get_model( + app_label='control_codes', model_name='ControlSheetCode' + ) + + control_sheet_code = ControlSheetCode.objects.get(pk=control_sheet_code_id) + + return control_sheet_code.generate_image() + + @app.task(ignore_result=True) def task_process_document_version(document_version_id): DocumentVersion = apps.get_model( diff --git a/mayan/apps/control_codes/urls.py b/mayan/apps/control_codes/urls.py index dd0f21c3d4..a78473ee9d 100644 --- a/mayan/apps/control_codes/urls.py +++ b/mayan/apps/control_codes/urls.py @@ -2,4 +2,40 @@ from __future__ import unicode_literals from django.conf.urls import url -urlpatterns = [] +from .api_views import ( + APIControlSheetCodeListView, APIControlSheetCodeView, + APIControlSheetListView, APIControlSheetView, + APIControlSheetCodeImageView +) +from .views import ControlSheetDetailView + +urlpatterns = [ + url( + regex=r'^control_sheets/(?P\d+)/$', + view=ControlSheetDetailView.as_view(), name='control_sheet_detail' + ), +] + +api_urls = [ + url( + regex=r'^control_sheets/$', view=APIControlSheetListView.as_view(), + name='controlsheet-list' + ), + url( + regex=r'^control_sheets/(?P[0-9]+)/$', + view=APIControlSheetView.as_view(), + name='controlsheet-detail' + ), + url( + regex=r'^control_sheets/(?P[0-9]+)/codes/$', + view=APIControlSheetCodeListView.as_view(), name='controlsheet-code-list' + ), + url( + regex=r'^control_sheets/(?P[0-9]+)/codes/(?P[0-9]+)/$', + view=APIControlSheetCodeView.as_view(), name='controlsheet-code-detail' + ), + url( + regex=r'^control_sheets/(?P[0-9]+)/codes/(?P[0-9]+)/image/$', + name='controlsheet-code-image', view=APIControlSheetCodeImageView.as_view() + ), +] diff --git a/mayan/apps/control_codes/utils.py b/mayan/apps/control_codes/utils.py new file mode 100644 index 0000000000..94866a7404 --- /dev/null +++ b/mayan/apps/control_codes/utils.py @@ -0,0 +1,12 @@ +from __future__ import unicode_literals + +from django.apps import apps + +from .literals import CONTROL_SHEET_CODE_IMAGE_CACHE_NAME + + +def callback_update_control_sheet_image_cache_size(setting): + Cache = apps.get_model(app_label='file_caching', model_name='Cache') + cache = Cache.objects.get(name=CONTROL_SHEET_CODE_IMAGE_CACHE_NAME) + cache.maximum_size = setting.value + cache.save() diff --git a/mayan/apps/control_codes/views.py b/mayan/apps/control_codes/views.py new file mode 100644 index 0000000000..5446b1799c --- /dev/null +++ b/mayan/apps/control_codes/views.py @@ -0,0 +1,294 @@ +from __future__ import absolute_import, unicode_literals + +import logging + +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.common.generics import ( + FormView, SimpleView, SingleObjectCreateView, SingleObjectDeleteView, + SingleObjectDetailView, SingleObjectEditView, SingleObjectListView +) +from mayan.apps.common.mixins import ExternalContentTypeObjectMixin + +from .forms import ControlCodeClassSelectionForm +#from .links import link_transformation_select +from .models import ControlSheet +#from .transformations import BaseTransformation + +logger = logging.getLogger(__name__) + + +class ControlSheetDetailView(SingleObjectDetailView): + fields = ('label',) + pk_url_kwarg = 'control_sheet_id' + model = ControlSheet + + def get_extra_context(self): + return { + 'object': self.object, + 'title': _( + 'Details for control sheet: %s' + ) % self.object + } + + +""" +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.external_object + instance.name = self.kwargs['transformation_name'] + instance.object_layer = object_layer + try: + instance.full_clean() + instance.save() + except Exception as exception: + logger.debug('Invalid form, exception: %s', exception) + return super(TransformationCreateView, self).form_invalid(form) + else: + return super(TransformationCreateView, self).form_valid(form) + + def get_extra_context(self): + return { + '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 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'], + 'layer_name': self.kwargs['layer_name'] + } + ) + + def get_queryset(self): + return self.layer.get_transformations_for( + obj=self.content_object + ) + + def get_template_names(self): + return [ + getattr( + self.get_transformation_class(), 'template_name', + self.template_name + ) + ] + + def get_transformation_class(self): + return BaseTransformation.get(name=self.kwargs['transformation_name']) + + +class TransformationDeleteView(LayerViewMixin, SingleObjectDeleteView): + model = LayerTransformation + + def get_extra_context(self): + return { + '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.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.object, + 'content_object': self.object.object_layer.content_object + }, + 'transformation': self.object, + } + + def get_object_permission(self): + return self.layer.permissions.get('delete', None) + + 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 + } + ) + + +class TransformationEditView(LayerViewMixin, SingleObjectEditView): + form_class = LayerTransformationForm + model = LayerTransformation + + def form_valid(self, form): + instance = form.save(commit=False) + try: + instance.full_clean() + instance.save() + except Exception as exception: + logger.debug('Invalid form, exception: %s', exception) + return super(TransformationEditView, self).form_invalid(form=form) + else: + return super(TransformationEditView, self).form_valid(form=form) + + def get_extra_context(self): + return { + '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' + ) % { + 'transformation': self.object, + 'content_object': self.object.object_layer.content_object + }, + '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.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 + } + ) + + def get_template_names(self): + return [ + getattr( + self.object.get_transformation_class(), 'template_name', + self.template_name + ) + ] + + +class TransformationListView( + LayerViewMixin, ExternalContentTypeObjectMixin, SingleObjectListView +): + def get_external_object_permission(self): + return self.layer.permissions.get('view', None) + + def get_extra_context(self): + return { + 'object': self.external_object, + 'hide_link': True, + 'hide_object': True, + 'layer_name': self.layer.name, + 'no_results_icon': self.layer.get_icon(), + 'no_results_main_link': link_transformation_select.resolve( + context=RequestContext( + request=self.request, dict_={ + 'resolved_object': self.external_object, + 'layer_name': self.kwargs['layer_name'], + } + ) + ), + 'no_results_text': self.layer.get_empty_results_text(), + 'no_results_title': _( + 'There are no entries for layer "%(layer_name)s"' + ) % {'layer_name': self.layer.label}, + 'title': _( + 'Layer "%(layer)s" transformations for: %(object)s' + ) % { + 'layer': self.layer, + 'object': self.external_object, + } + } + + def get_source_queryset(self): + return self.layer.get_transformations_for(obj=self.external_object) + + +class TransformationSelectView( + ExternalContentTypeObjectMixin, LayerViewMixin, FormView +): + form_class = LayerTransformationSelectForm + template_name = 'appearance/generic_form.html' + + def form_valid(self, form): + return HttpResponseRedirect( + redirect_to=reverse( + viewname='converter:transformation_create', + kwargs={ + 'app_label': self.kwargs['app_label'], + 'model': self.kwargs['model'], + 'object_id': self.kwargs['object_id'], + 'layer_name': self.kwargs['layer_name'], + 'transformation_name': form.cleaned_data[ + 'transformation' + ] + } + ) + ) + + def get_extra_context(self): + return { + 'layer': self.layer, + 'layer_name': self.kwargs['layer_name'], + 'navigation_object_list': ('content_object',), + 'content_object': self.external_object, + 'submit_label': _('Select'), + 'title': _( + 'Select new layer "%(layer)s" transformation ' + 'for: %(object)s' + ) % { + 'layer': self.layer, + 'object': self.external_object, + } + } + + def get_form_extra_kwargs(self): + return { + 'layer': self.layer + } +""" diff --git a/mayan/apps/rest_api/relations.py b/mayan/apps/rest_api/relations.py new file mode 100644 index 0000000000..c1371638c4 --- /dev/null +++ b/mayan/apps/rest_api/relations.py @@ -0,0 +1,91 @@ +from __future__ import unicode_literals + +from django.apps import apps +from django.core.exceptions import ImproperlyConfigured +from django.db.models import Manager +from django.db.models.query import QuerySet + +from rest_framework import serializers +from rest_framework.relations import HyperlinkedIdentityField + +from mayan.apps.common.utils import resolve_attribute + + +class FilteredPrimaryKeyRelatedField(serializers.PrimaryKeyRelatedField): + def __init__(self, **kwargs): + self.source_model = kwargs.pop('source_model', None) + self.source_permission = kwargs.pop('source_permission', None) + self.source_queryset = kwargs.pop('source_queryset', None) + self.source_queryset_method = kwargs.pop('source_queryset_method', None) + super(FilteredPrimaryKeyRelatedField, self).__init__(**kwargs) + + def get_queryset(self): + AccessControlList = apps.get_model( + app_label='acls', model_name='AccessControlList' + ) + + if self.source_model: + queryset = self.source_model._meta.default_manager.all() + elif self.source_queryset: + queryset = self.source_queryset + if isinstance(queryset, (QuerySet, Manager)): + # Ensure queryset is re-evaluated whenever used. + queryset = queryset.all() + else: + method_name = self.source_queryset_method or 'get_{}_queryset'.format( + self.field_name + ) + try: + queryset = getattr(self.parent, method_name)() + except AttributeError: + raise ImproperlyConfigured( + 'Need to provide a source_model, a ' + 'source_queryset, a source_queryset_method, or ' + 'a method named "%s".' % method_name + ) + + assert 'request' in self.context, ( + "`%s` requires the request in the serializer" + " context. Add `context={'request': request}` when instantiating " + "the serializer." % self.__class__.__name__ + ) + + request = self.context['request'] + + if self.source_permission: + return AccessControlList.objects.restrict_queryset( + permission=self.source_permission, queryset=queryset, + user=request.user + ) + else: + return queryset + + +class MultiKwargHyperlinkedIdentityField(HyperlinkedIdentityField): + def __init__(self, *args, **kwargs): + self.view_kwargs = kwargs.pop('view_kwargs', []) + super(MultiKwargHyperlinkedIdentityField, self).__init__(*args, **kwargs) + + def get_url(self, obj, view_name, request, format): + """ + Extends HyperlinkedRelatedField to allow passing more than one view + keyword argument. + ---- + Given an object, return the URL that hyperlinks to the object. + + May raise a `NoReverseMatch` if the `view_name` and `lookup_field` + attributes are not configured to correctly match the URL conf. + """ + # Unsaved objects will not yet have a valid URL. + if hasattr(obj, 'pk') and obj.pk in (None, ''): + return None + + kwargs = {} + for entry in self.view_kwargs: + kwargs[entry['lookup_url_kwarg']] = resolve_attribute( + obj=obj, attribute=entry['lookup_field'] + ) + + return self.reverse( + viewname=view_name, kwargs=kwargs, request=request, format=format + )