diff --git a/docs/releases/2.0.rst b/docs/releases/2.0.rst index 4c1a9874cc..04b7e5bdb0 100644 --- a/docs/releases/2.0.rst +++ b/docs/releases/2.0.rst @@ -71,6 +71,7 @@ What's new in Mayan EDMS v2.0 * Document image and intermediate file caching now has it's own storage backend. * RGB tags * ``performupgrade`` management command. +* Removal of eval from metadata type defaults and lookup fields. Django's own template language is now used instead. Upgrading from a previous version ================================= diff --git a/mayan/apps/metadata/__init__.py b/mayan/apps/metadata/__init__.py index e69de29bb2..154437d537 100644 --- a/mayan/apps/metadata/__init__.py +++ b/mayan/apps/metadata/__init__.py @@ -0,0 +1 @@ +from .classes import MetadataLookup # NOQA diff --git a/mayan/apps/metadata/api.py b/mayan/apps/metadata/api.py index 50c9f68024..a190f4fd93 100644 --- a/mayan/apps/metadata/api.py +++ b/mayan/apps/metadata/api.py @@ -104,7 +104,9 @@ def get_metadata_string(document): """ Return a formated representation of a document's metadata values """ - return ', '.join(['%s - %s' % (document_metadata.metadata_type, document_metadata.value) for document_metadata in document.metadata.all()]) + return ', '.join( + ['%s - %s' % (document_metadata.metadata_type, document_metadata.value) for document_metadata in document.metadata.all()] + ) def convert_dict_to_dict_list(dictionary): diff --git a/mayan/apps/metadata/classes.py b/mayan/apps/metadata/classes.py index a1b185bc51..3d16e79cdb 100644 --- a/mayan/apps/metadata/classes.py +++ b/mayan/apps/metadata/classes.py @@ -1,9 +1,8 @@ from __future__ import unicode_literals +from django.core.exceptions import ObjectDoesNotExist from django.utils.translation import ugettext_lazy as _ -from .models import MetadataType - class DocumentMetadataHelper(object): @staticmethod @@ -17,7 +16,35 @@ class DocumentMetadataHelper(object): def __getattr__(self, name): try: return self.instance.metadata.get(metadata_type__name=name).value - except MetadataType.DoesNotExist: + except ObjectDoesNotExist: raise AttributeError( _('\'metadata\' object has no attribute \'%s\'') % name ) + + +class MetadataLookup(object): + _registry = [] + + @classmethod + def get_as_context(cls): + result = {} + for entry in cls._registry: + result[entry.name] = entry.value + + return result + + @classmethod + def get_as_help_text(cls): + result = [] + for entry in cls._registry: + result.append( + '{{{{ {0} }}}} = "{1}"'.format(entry.name, entry.description) + ) + + return ' '.join(result) + + def __init__(self, description, name, value): + self.description = description + self.name = name + self.value = value + self.__class__._registry.append(self) diff --git a/mayan/apps/metadata/forms.py b/mayan/apps/metadata/forms.py index c107f0790e..7930ddab00 100644 --- a/mayan/apps/metadata/forms.py +++ b/mayan/apps/metadata/forms.py @@ -1,16 +1,26 @@ from __future__ import unicode_literals +import shlex + from django import forms from django.core.exceptions import ValidationError from django.forms.formsets import formset_factory from django.template import Context, Template from django.utils.module_loading import import_string -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import string_concat, ugettext_lazy as _ +from .classes import MetadataLookup from .models import MetadataType class MetadataForm(forms.Form): + @staticmethod + def comma_splitter(string): + splitter = shlex.shlex(string.encode('utf-8'), posix=True) + splitter.whitespace = ','.encode('utf-8') + splitter.whitespace_split = True + splitter.commenters = ''.encode('utf-8') + return list(splitter) def clean_value(self): metadata_type = MetadataType.objects.get(pk=self.cleaned_data['id']) @@ -66,23 +76,32 @@ class MetadataForm(forms.Form): self.fields['value'].required = False self.fields['name'].initial = '%s%s' % ( - (self.metadata_type.label if self.metadata_type.label else self.metadata_type.name), + ( + self.metadata_type.label if self.metadata_type.label else self.metadata_type.name + ), required_string ) self.fields['id'].initial = self.metadata_type.pk if self.metadata_type.lookup: try: - #choices = eval(self.metadata_type.lookup, setting_available_models.value) ##### - choices = [] - self.fields['value'] = forms.ChoiceField(label=self.fields['value'].label) + template = Template(self.metadata_type.lookup) + context = Context(MetadataLookup.get_as_context()) + choices = MetadataForm.comma_splitter( + template.render(context=context) + ) + self.fields['value'] = forms.ChoiceField( + label=self.fields['value'].label + ) choices = zip(choices, choices) if not required: choices.insert(0, ('', '------')) self.fields['value'].choices = choices self.fields['value'].required = required except Exception as exception: - self.fields['value'].initial = exception + self.fields['value'].initial = _( + 'Lookup value error: %s' + ) % exception self.fields['value'].widget = forms.TextInput( attrs={'readonly': 'readonly'} ) @@ -95,8 +114,12 @@ class MetadataForm(forms.Form): self.fields['value'].initial = result except Exception as exception: self.fields['value'].initial = _( - 'Error: %s' + 'Default value error: %s' ) % exception + self.fields['value'].widget = forms.TextInput( + attrs={'readonly': 'readonly'} + ) + id = forms.CharField(label=_('ID'), widget=forms.HiddenInput) @@ -123,6 +146,20 @@ class AddMetadataForm(forms.Form): self.fields['metadata_type'].queryset = document_type.metadata.all() +class MetadataTypeForm(forms.ModelForm): + class Meta: + fields = ('name', 'label', 'default', 'lookup', 'validation') + model = MetadataType + + def __init__(self, *args, **kwargs): + super(MetadataTypeForm, self).__init__(*args, **kwargs) + self.fields['lookup'].help_text = string_concat( + self.fields['lookup'].help_text, + _(' Available template context variables: '), + MetadataLookup.get_as_help_text() + ) + + class MetadataRemoveForm(MetadataForm): update = forms.BooleanField( initial=False, label=_('Remove'), required=False diff --git a/mayan/apps/metadata/migrations/0002_auto_20150708_0118.py b/mayan/apps/metadata/migrations/0002_auto_20150708_0118.py index b5eecef9ed..6583f24321 100644 --- a/mayan/apps/metadata/migrations/0002_auto_20150708_0118.py +++ b/mayan/apps/metadata/migrations/0002_auto_20150708_0118.py @@ -14,7 +14,24 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='metadatatype', name='validation', - field=models.CharField(blank=True, max_length=64, verbose_name='Validation function name', choices=[(b'metadata.validators.DateAndTimeValidator', b'metadata.validators.DateAndTimeValidator'), (b'metadata.validators.DateValidator', b'metadata.validators.DateValidator'), (b'metadata.validators.TimeValidator', b'metadata.validators.TimeValidator')]), + field=models.CharField( + blank=True, max_length=64, + verbose_name='Validation function name', + choices=[ + ( + b'metadata.validators.DateAndTimeValidator', + b'metadata.validators.DateAndTimeValidator' + ), + ( + b'metadata.validators.DateValidator', + b'metadata.validators.DateValidator' + ), + ( + b'metadata.validators.TimeValidator', + b'metadata.validators.TimeValidator' + ) + ] + ), preserve_default=True, ), ] diff --git a/mayan/apps/metadata/models.py b/mayan/apps/metadata/models.py index ba49731c07..f1eee974f7 100644 --- a/mayan/apps/metadata/models.py +++ b/mayan/apps/metadata/models.py @@ -25,28 +25,36 @@ class MetadataType(models.Model): """ name = models.CharField( max_length=48, - help_text=_('Name used by other apps to reference this value. Do not use python reserved words, or spaces.'), + help_text=_( + 'Name used by other apps to reference this value. ' + 'Do not use python reserved words, or spaces.' + ), unique=True, verbose_name=_('Name') ) label = models.CharField(max_length=48, verbose_name=_('Label')) default = models.CharField( blank=True, max_length=128, null=True, - help_text=_('Enter a template to render. Use Django\'s default templating language (https://docs.djangoproject.com/en/1.7/ref/templates/builtins/)'), + help_text=_( + 'Enter a template to render. ' + 'Use Django\'s default templating language ' + '(https://docs.djangoproject.com/en/1.7/ref/templates/builtins/)' + ), verbose_name=_('Default') ) - # TODO: Add enable_lookup boolean to allow users to switch the lookup on and - # off without losing the lookup expression lookup = models.TextField( blank=True, null=True, - help_text=_('Enter a string to be evaluated that returns an iterable.'), + help_text=_( + 'Enter a template to render. ' + 'Must result in a command delimited string. ' + 'Use Django\'s default templating language ' + '(https://docs.djangoproject.com/en/1.7/ref/templates/builtins/).' + ), verbose_name=_('Lookup') ) validation = models.CharField( blank=True, choices=validation_choices(), max_length=64, verbose_name=_('Validation function name') ) - # TODO: Find a different way to let users know what models and functions are - # available now that we removed these from the help_text objects = MetadataTypeManager() def __str__(self): diff --git a/mayan/apps/metadata/views.py b/mayan/apps/metadata/views.py index 773b74eadf..0c010caa1c 100644 --- a/mayan/apps/metadata/views.py +++ b/mayan/apps/metadata/views.py @@ -24,7 +24,9 @@ from documents.views import DocumentListView from permissions import Permission from .api import save_metadata_list -from .forms import AddMetadataForm, MetadataFormSet, MetadataRemoveFormSet +from .forms import ( + AddMetadataForm, MetadataFormSet, MetadataRemoveFormSet, MetadataTypeForm +) from .models import DocumentMetadata, MetadataType from .permissions import ( permission_metadata_document_add, permission_metadata_document_edit, @@ -148,7 +150,8 @@ def metadata_edit(request, document_id=None, document_id_list=None): else: messages.error( request, _( - 'Error editing metadata for document %(document)s; %(exception)s.' + 'Error editing metadata for document: ' + '%(document)s; %(exception)s.' ) % { 'document': document, 'exception': ', '.join(exception.messages) @@ -157,7 +160,9 @@ def metadata_edit(request, document_id=None, document_id_list=None): else: messages.success( request, - _('Metadata for document %s edited successfully.') % document + _( + 'Metadata for document %s edited successfully.' + ) % document ) return HttpResponseRedirect(next) @@ -192,7 +197,10 @@ def metadata_add(request, document_id=None, document_id_list=None): documents = [get_object_or_404(Document, pk=document_id)] elif document_id_list: documents = [ - get_object_or_404(Document.objects.select_related('document_type'), pk=document_id) for document_id in document_id_list.split(',') + get_object_or_404( + Document.objects.select_related('document_type'), + pk=document_id + ) for document_id in document_id_list.split(',') ] if len(set([document.document_type.pk for document in documents])) > 1: messages.error( @@ -253,11 +261,15 @@ def metadata_add(request, document_id=None, document_id_list=None): messages.error( request, _( - 'Error adding metadata type "%(metadata_type)s" to document: %(document)s; %(exception)s' + 'Error adding metadata type ' + '"%(metadata_type)s" to document: ' + '%(document)s; %(exception)s' ) % { 'metadata_type': metadata_type, 'document': document, - 'exception': ', '.join(getattr(exception, 'messages', exception)) + 'exception': ', '.join( + getattr(exception, 'messages', exception) + ) } ) else: @@ -265,17 +277,21 @@ def metadata_add(request, document_id=None, document_id_list=None): messages.success( request, _( - 'Metadata type: %(metadata_type)s successfully added to document %(document)s.' + 'Metadata type: %(metadata_type)s ' + 'successfully added to document %(document)s.' ) % { - 'metadata_type': metadata_type, 'document': document + 'metadata_type': metadata_type, + 'document': document } ) else: messages.warning( request, _( - 'Metadata type: %(metadata_type)s already present in document %(document)s.' + 'Metadata type: %(metadata_type)s already ' + 'present in document %(document)s.' ) % { - 'metadata_type': metadata_type, 'document': document + 'metadata_type': metadata_type, + 'document': document } ) @@ -368,7 +384,11 @@ def metadata_remove(request, document_id=None, document_id_list=None): post_action_redirect = reverse('documents:document_list_recent') - next = request.POST.get('next', request.GET.get('next', request.META.get('HTTP_REFERER', post_action_redirect))) + next = request.POST.get( + 'next', request.GET.get( + 'next', request.META.get('HTTP_REFERER', post_action_redirect) + ) + ) metadata = {} for document in documents: @@ -397,15 +417,34 @@ def metadata_remove(request, document_id=None, document_id_list=None): for form in formset.forms: if form.cleaned_data['update']: - metadata_type = get_object_or_404(MetadataType, pk=form.cleaned_data['id']) + metadata_type = get_object_or_404( + MetadataType, pk=form.cleaned_data['id'] + ) try: - document_metadata = DocumentMetadata.objects.get(document=document, metadata_type=metadata_type) + document_metadata = DocumentMetadata.objects.get( + document=document, metadata_type=metadata_type + ) document_metadata.delete() - messages.success(request, _('Successfully remove metadata type "%(metadata_type)s" from document: %(document)s.') % { - 'metadata_type': metadata_type, 'document': document}) + messages.success( + request, + _( + 'Successfully remove metadata type "%(metadata_type)s" from document: %(document)s.' + ) % { + 'metadata_type': metadata_type, + 'document': document + } + ) except Exception as exception: - messages.error(request, _('Error removing metadata type "%(metadata_type)s" from document: %(document)s; %(exception)s') % { - 'metadata_type': metadata_type, 'document': document, 'exception': ', '.join(exception.messages)}) + messages.error( + request, + _( + 'Error removing metadata type "%(metadata_type)s" from document: %(document)s; %(exception)s' + ) % { + 'metadata_type': metadata_type, + 'document': document, + 'exception': ', '.join(exception.messages) + } + ) return HttpResponseRedirect(next) @@ -429,23 +468,32 @@ def metadata_remove(request, document_id=None, document_id_list=None): def metadata_multiple_remove(request): - return metadata_remove(request, document_id_list=request.GET.get('id_list', [])) + return metadata_remove( + request, document_id_list=request.GET.get('id_list', []) + ) def metadata_view(request, document_id): document = get_object_or_404(Document, pk=document_id) try: - Permission.check_permissions(request.user, [permission_metadata_document_view]) + Permission.check_permissions( + request.user, [permission_metadata_document_view] + ) except PermissionDenied: - AccessControlList.objects.check_access(permission_metadata_document_view, request.user, document) + AccessControlList.objects.check_access( + permission_metadata_document_view, request.user, document + ) return render_to_response('appearance/generic_list.html', { 'title': _('Metadata for document: %s') % document, 'object_list': document.metadata.all(), 'extra_columns': [ {'name': _('Value'), 'attribute': 'value'}, - {'name': _('Required'), 'attribute': encapsulate(lambda x: x.metadata_type in document.document_type.metadata.filter(required=True))} + { + 'name': _('Required'), + 'attribute': encapsulate(lambda x: x.metadata_type in document.document_type.metadata.filter(required=True)) + } ], 'hide_link': True, 'object': document, @@ -455,6 +503,7 @@ def metadata_view(request, document_id): # Setup views class MetadataTypeCreateView(SingleObjectCreateView): extra_context = {'title': _('Create metadata type')} + form_class = MetadataTypeForm model = MetadataType post_action_redirect = reverse_lazy('metadata:setup_metadata_type_list') view_permission = permission_metadata_type_create @@ -474,6 +523,7 @@ class MetadataTypeDeleteView(SingleObjectDeleteView): class MetadataTypeEditView(SingleObjectEditView): + form_class = MetadataTypeForm model = MetadataType post_action_redirect = reverse_lazy('metadata:setup_metadata_type_list') view_permission = permission_metadata_type_edit diff --git a/mayan/apps/user_management/apps.py b/mayan/apps/user_management/apps.py index 59e1fb262a..7d6c055b7b 100644 --- a/mayan/apps/user_management/apps.py +++ b/mayan/apps/user_management/apps.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals +from django.contrib.auth import get_user_model from django.contrib.auth.models import User, Group from django.utils.translation import ugettext_lazy as _ @@ -7,6 +8,7 @@ from actstream import registry from common import menu_multi_item, menu_object, menu_secondary, menu_setup from common.apps import MayanAppConfig +from metadata import MetadataLookup from rest_api.classes import APIEndPoint from .links import ( @@ -28,6 +30,9 @@ class UserManagementApp(MayanAppConfig): APIEndPoint('users', app_name='user_management') + MetadataLookup(description=_('All the groups.'), name='group', value=Group.objects.all()) + MetadataLookup(description=_('All the users.'), name='users', value=get_user_model().objects.all()) + menu_multi_item.bind_links( links=[link_group_multiple_delete], sources=['user_management:group_list']