Get rid of eval in metadata type default and lookup fields. gh-issue #151.

This commit is contained in:
Roberto Rosario
2015-07-23 02:49:29 -04:00
parent 58d919d173
commit 0a0a92116e
9 changed files with 188 additions and 40 deletions

View File

@@ -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. * Document image and intermediate file caching now has it's own storage backend.
* RGB tags * RGB tags
* ``performupgrade`` management command. * ``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 Upgrading from a previous version
================================= =================================

View File

@@ -0,0 +1 @@
from .classes import MetadataLookup # NOQA

View File

@@ -104,7 +104,9 @@ def get_metadata_string(document):
""" """
Return a formated representation of a document's metadata values 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): def convert_dict_to_dict_list(dictionary):

View File

@@ -1,9 +1,8 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django.core.exceptions import ObjectDoesNotExist
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from .models import MetadataType
class DocumentMetadataHelper(object): class DocumentMetadataHelper(object):
@staticmethod @staticmethod
@@ -17,7 +16,35 @@ class DocumentMetadataHelper(object):
def __getattr__(self, name): def __getattr__(self, name):
try: try:
return self.instance.metadata.get(metadata_type__name=name).value return self.instance.metadata.get(metadata_type__name=name).value
except MetadataType.DoesNotExist: except ObjectDoesNotExist:
raise AttributeError( raise AttributeError(
_('\'metadata\' object has no attribute \'%s\'') % name _('\'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)

View File

@@ -1,16 +1,26 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import shlex
from django import forms from django import forms
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.forms.formsets import formset_factory from django.forms.formsets import formset_factory
from django.template import Context, Template from django.template import Context, Template
from django.utils.module_loading import import_string 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 from .models import MetadataType
class MetadataForm(forms.Form): 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): def clean_value(self):
metadata_type = MetadataType.objects.get(pk=self.cleaned_data['id']) 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['value'].required = False
self.fields['name'].initial = '%s%s' % ( 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 required_string
) )
self.fields['id'].initial = self.metadata_type.pk self.fields['id'].initial = self.metadata_type.pk
if self.metadata_type.lookup: if self.metadata_type.lookup:
try: try:
#choices = eval(self.metadata_type.lookup, setting_available_models.value) ##### template = Template(self.metadata_type.lookup)
choices = [] context = Context(MetadataLookup.get_as_context())
self.fields['value'] = forms.ChoiceField(label=self.fields['value'].label) choices = MetadataForm.comma_splitter(
template.render(context=context)
)
self.fields['value'] = forms.ChoiceField(
label=self.fields['value'].label
)
choices = zip(choices, choices) choices = zip(choices, choices)
if not required: if not required:
choices.insert(0, ('', '------')) choices.insert(0, ('', '------'))
self.fields['value'].choices = choices self.fields['value'].choices = choices
self.fields['value'].required = required self.fields['value'].required = required
except Exception as exception: except Exception as exception:
self.fields['value'].initial = exception self.fields['value'].initial = _(
'Lookup value error: %s'
) % exception
self.fields['value'].widget = forms.TextInput( self.fields['value'].widget = forms.TextInput(
attrs={'readonly': 'readonly'} attrs={'readonly': 'readonly'}
) )
@@ -95,8 +114,12 @@ class MetadataForm(forms.Form):
self.fields['value'].initial = result self.fields['value'].initial = result
except Exception as exception: except Exception as exception:
self.fields['value'].initial = _( self.fields['value'].initial = _(
'Error: %s' 'Default value error: %s'
) % exception ) % exception
self.fields['value'].widget = forms.TextInput(
attrs={'readonly': 'readonly'}
)
id = forms.CharField(label=_('ID'), widget=forms.HiddenInput) 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() 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): class MetadataRemoveForm(MetadataForm):
update = forms.BooleanField( update = forms.BooleanField(
initial=False, label=_('Remove'), required=False initial=False, label=_('Remove'), required=False

View File

@@ -14,7 +14,24 @@ class Migration(migrations.Migration):
migrations.AlterField( migrations.AlterField(
model_name='metadatatype', model_name='metadatatype',
name='validation', 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, preserve_default=True,
), ),
] ]

View File

@@ -25,28 +25,36 @@ class MetadataType(models.Model):
""" """
name = models.CharField( name = models.CharField(
max_length=48, 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') unique=True, verbose_name=_('Name')
) )
label = models.CharField(max_length=48, verbose_name=_('Label')) label = models.CharField(max_length=48, verbose_name=_('Label'))
default = models.CharField( default = models.CharField(
blank=True, max_length=128, null=True, 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') 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( lookup = models.TextField(
blank=True, null=True, 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') verbose_name=_('Lookup')
) )
validation = models.CharField( validation = models.CharField(
blank=True, choices=validation_choices(), max_length=64, blank=True, choices=validation_choices(), max_length=64,
verbose_name=_('Validation function name') 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() objects = MetadataTypeManager()
def __str__(self): def __str__(self):

View File

@@ -24,7 +24,9 @@ from documents.views import DocumentListView
from permissions import Permission from permissions import Permission
from .api import save_metadata_list 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 .models import DocumentMetadata, MetadataType
from .permissions import ( from .permissions import (
permission_metadata_document_add, permission_metadata_document_edit, permission_metadata_document_add, permission_metadata_document_edit,
@@ -148,7 +150,8 @@ def metadata_edit(request, document_id=None, document_id_list=None):
else: else:
messages.error( messages.error(
request, _( request, _(
'Error editing metadata for document %(document)s; %(exception)s.' 'Error editing metadata for document: '
'%(document)s; %(exception)s.'
) % { ) % {
'document': document, 'document': document,
'exception': ', '.join(exception.messages) 'exception': ', '.join(exception.messages)
@@ -157,7 +160,9 @@ def metadata_edit(request, document_id=None, document_id_list=None):
else: else:
messages.success( messages.success(
request, request,
_('Metadata for document %s edited successfully.') % document _(
'Metadata for document %s edited successfully.'
) % document
) )
return HttpResponseRedirect(next) 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)] documents = [get_object_or_404(Document, pk=document_id)]
elif document_id_list: elif document_id_list:
documents = [ 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: if len(set([document.document_type.pk for document in documents])) > 1:
messages.error( messages.error(
@@ -253,11 +261,15 @@ def metadata_add(request, document_id=None, document_id_list=None):
messages.error( messages.error(
request, 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, 'metadata_type': metadata_type,
'document': document, 'document': document,
'exception': ', '.join(getattr(exception, 'messages', exception)) 'exception': ', '.join(
getattr(exception, 'messages', exception)
)
} }
) )
else: else:
@@ -265,17 +277,21 @@ def metadata_add(request, document_id=None, document_id_list=None):
messages.success( messages.success(
request, 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: else:
messages.warning( messages.warning(
request, _( 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') 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 = {} metadata = {}
for document in documents: for document in documents:
@@ -397,15 +417,34 @@ def metadata_remove(request, document_id=None, document_id_list=None):
for form in formset.forms: for form in formset.forms:
if form.cleaned_data['update']: 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: 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() document_metadata.delete()
messages.success(request, _('Successfully remove metadata type "%(metadata_type)s" from document: %(document)s.') % { messages.success(
'metadata_type': metadata_type, 'document': document}) request,
_(
'Successfully remove metadata type "%(metadata_type)s" from document: %(document)s.'
) % {
'metadata_type': metadata_type,
'document': document
}
)
except Exception as exception: except Exception as exception:
messages.error(request, _('Error removing metadata type "%(metadata_type)s" from document: %(document)s; %(exception)s') % { messages.error(
'metadata_type': metadata_type, 'document': document, 'exception': ', '.join(exception.messages)}) 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) return HttpResponseRedirect(next)
@@ -429,23 +468,32 @@ def metadata_remove(request, document_id=None, document_id_list=None):
def metadata_multiple_remove(request): 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): def metadata_view(request, document_id):
document = get_object_or_404(Document, pk=document_id) document = get_object_or_404(Document, pk=document_id)
try: try:
Permission.check_permissions(request.user, [permission_metadata_document_view]) Permission.check_permissions(
request.user, [permission_metadata_document_view]
)
except PermissionDenied: 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', { return render_to_response('appearance/generic_list.html', {
'title': _('Metadata for document: %s') % document, 'title': _('Metadata for document: %s') % document,
'object_list': document.metadata.all(), 'object_list': document.metadata.all(),
'extra_columns': [ 'extra_columns': [
{'name': _('Value'), 'attribute': 'value'}, {'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, 'hide_link': True,
'object': document, 'object': document,
@@ -455,6 +503,7 @@ def metadata_view(request, document_id):
# Setup views # Setup views
class MetadataTypeCreateView(SingleObjectCreateView): class MetadataTypeCreateView(SingleObjectCreateView):
extra_context = {'title': _('Create metadata type')} extra_context = {'title': _('Create metadata type')}
form_class = MetadataTypeForm
model = MetadataType model = MetadataType
post_action_redirect = reverse_lazy('metadata:setup_metadata_type_list') post_action_redirect = reverse_lazy('metadata:setup_metadata_type_list')
view_permission = permission_metadata_type_create view_permission = permission_metadata_type_create
@@ -474,6 +523,7 @@ class MetadataTypeDeleteView(SingleObjectDeleteView):
class MetadataTypeEditView(SingleObjectEditView): class MetadataTypeEditView(SingleObjectEditView):
form_class = MetadataTypeForm
model = MetadataType model = MetadataType
post_action_redirect = reverse_lazy('metadata:setup_metadata_type_list') post_action_redirect = reverse_lazy('metadata:setup_metadata_type_list')
view_permission = permission_metadata_type_edit view_permission = permission_metadata_type_edit

View File

@@ -1,5 +1,6 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django.contrib.auth import get_user_model
from django.contrib.auth.models import User, Group from django.contrib.auth.models import User, Group
from django.utils.translation import ugettext_lazy as _ 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 import menu_multi_item, menu_object, menu_secondary, menu_setup
from common.apps import MayanAppConfig from common.apps import MayanAppConfig
from metadata import MetadataLookup
from rest_api.classes import APIEndPoint from rest_api.classes import APIEndPoint
from .links import ( from .links import (
@@ -28,6 +30,9 @@ class UserManagementApp(MayanAppConfig):
APIEndPoint('users', app_name='user_management') 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( menu_multi_item.bind_links(
links=[link_group_multiple_delete], links=[link_group_multiple_delete],
sources=['user_management:group_list'] sources=['user_management:group_list']