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.
* 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
=================================

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 ', '.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):

View File

@@ -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)

View File

@@ -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

View File

@@ -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,
),
]

View File

@@ -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):

View File

@@ -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

View File

@@ -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']