diff --git a/mayan/apps/acls/links.py b/mayan/apps/acls/links.py index f4c4215173..b824662632 100644 --- a/mayan/apps/acls/links.py +++ b/mayan/apps/acls/links.py @@ -17,6 +17,6 @@ def get_kwargs_factory(variable_name): link_acl_delete = Link(permissions=[permission_acl_edit], tags='dangerous', text=_('Delete'), view='acls:acl_delete', args='resolved_object.pk') -link_acl_new = Link(permissions=[permission_acl_edit], text=_('New ACL'), view='acls:acl_new', kwargs=get_kwargs_factory('resolved_object')) link_acl_list = Link(permissions=[permission_acl_view], text=_('ACLs'), view='acls:acl_list', kwargs=get_kwargs_factory('resolved_object')) +link_acl_new = Link(permissions=[permission_acl_edit], text=_('New ACL'), view='acls:acl_new', kwargs=get_kwargs_factory('resolved_object')) link_acl_permissions = Link(permissions=[permission_acl_edit], text=_('Permissions'), view='acls:acl_permissions', args='resolved_object.pk') diff --git a/mayan/apps/acls/models.py b/mayan/apps/acls/models.py index d308bef4ce..8fc4cafcb3 100644 --- a/mayan/apps/acls/models.py +++ b/mayan/apps/acls/models.py @@ -10,7 +10,6 @@ from django.utils.translation import ugettext_lazy as _ from permissions.models import Role, StoredPermission -from .classes import ModelPermission from .managers import AccessControlListManager logger = logging.getLogger(__name__) diff --git a/mayan/apps/acls/test_models.py b/mayan/apps/acls/test_models.py index bbab981630..8977146162 100644 --- a/mayan/apps/acls/test_models.py +++ b/mayan/apps/acls/test_models.py @@ -1,24 +1,14 @@ from __future__ import absolute_import, unicode_literals -from django.conf import settings from django.contrib.auth import get_user_model from django.contrib.auth.models import Group from django.core.exceptions import PermissionDenied from django.core.files import File -from django.core.urlresolvers import reverse -from django.test.client import Client from django.test import TestCase from documents.models import Document, DocumentType from documents.permissions import permission_document_view -from documents.test_models import ( - TEST_ADMIN_PASSWORD, TEST_ADMIN_USERNAME, TEST_ADMIN_EMAIL, - TEST_SMALL_DOCUMENT_FILENAME, TEST_NON_ASCII_DOCUMENT_FILENAME, - TEST_NON_ASCII_COMPRESSED_DOCUMENT_FILENAME, TEST_DOCUMENT_PATH, - TEST_SIGNED_DOCUMENT_PATH, TEST_SMALL_DOCUMENT_PATH, - TEST_NON_ASCII_DOCUMENT_PATH, TEST_NON_ASCII_COMPRESSED_DOCUMENT_PATH, - TEST_DOCUMENT_DESCRIPTION, TEST_DOCUMENT_TYPE -) +from documents.test_models import TEST_SMALL_DOCUMENT_PATH, TEST_DOCUMENT_TYPE from permissions.classes import Permission from permissions.models import Role @@ -128,7 +118,6 @@ class PermissionTestCase(TestCase): self.assertTrue(self.document_2 in result) self.assertTrue(self.document_3 not in result) - def test_filtering_with_inherited_permissions_and_local_acl(self): self.group.user_set.add(self.user) self.role.permissions.add(permission_document_view.stored_permission) diff --git a/mayan/apps/checkouts/apps.py b/mayan/apps/checkouts/apps.py index 6ab7e17fd8..7b03f15492 100644 --- a/mayan/apps/checkouts/apps.py +++ b/mayan/apps/checkouts/apps.py @@ -24,7 +24,6 @@ from .permissions import ( ) - class CheckoutsApp(MayanAppConfig): name = 'checkouts' verbose_name = _('Checkouts') @@ -34,18 +33,10 @@ class CheckoutsApp(MayanAppConfig): APIEndPoint('checkouts') - Document.add_to_class('is_checked_out', lambda document: DocumentCheckout.objects.is_document_checked_out(document)) Document.add_to_class('check_in', lambda document, user=None: DocumentCheckout.objects.check_in_document(document, user)) Document.add_to_class('checkout_info', lambda document: DocumentCheckout.objects.document_checkout_info(document)) Document.add_to_class('checkout_state', lambda document: DocumentCheckout.objects.document_checkout_state(document)) - - app.conf.CELERYBEAT_SCHEDULE.update({ - 'check_expired_check_outs': { - 'task': 'checkouts.tasks.task_check_expired_check_outs', - 'schedule': timedelta(seconds=CHECK_EXPIRED_CHECK_OUTS_INTERVAL), - 'options': {'queue': 'checkouts'} - }, - }) + Document.add_to_class('is_checked_out', lambda document: DocumentCheckout.objects.is_document_checked_out(document)) ModelPermission.register( model=Document, permissions=( @@ -55,6 +46,14 @@ class CheckoutsApp(MayanAppConfig): ) ) + app.conf.CELERYBEAT_SCHEDULE.update({ + 'check_expired_check_outs': { + 'task': 'checkouts.tasks.task_check_expired_check_outs', + 'schedule': timedelta(seconds=CHECK_EXPIRED_CHECK_OUTS_INTERVAL), + 'options': {'queue': 'checkouts'} + }, + }) + menu_facet.bind_links(links=[link_checkout_info], sources=[Document]) menu_main.bind_links(links=[link_checkout_list]) menu_sidebar.bind_links(links=[link_checkout_document, link_checkin_document], sources=['checkouts:checkout_info', 'checkouts:checkout_document', 'checkouts:checkin_document']) diff --git a/mayan/apps/checkouts/events.py b/mayan/apps/checkouts/events.py index 459a015698..28994ba725 100644 --- a/mayan/apps/checkouts/events.py +++ b/mayan/apps/checkouts/events.py @@ -4,7 +4,7 @@ from django.utils.translation import ugettext_lazy as _ from events.classes import Event -event_document_check_out = Event(name='checkouts_document_check_out', label=_('Document checked out')) -event_document_check_in = Event(name='checkouts_document_check_in', label=_('Document checked in')) event_document_auto_check_in = Event(name='checkouts_document_auto_check_in', label=_('Document automatically checked in')) +event_document_check_in = Event(name='checkouts_document_check_in', label=_('Document checked in')) +event_document_check_out = Event(name='checkouts_document_check_out', label=_('Document checked out')) event_document_forceful_check_in = Event(name='checkouts_document_forceful_check_in', label=_('Document forcefully checked in')) diff --git a/mayan/apps/checkouts/literals.py b/mayan/apps/checkouts/literals.py index 1b4d41e4e3..c61fc4175b 100644 --- a/mayan/apps/checkouts/literals.py +++ b/mayan/apps/checkouts/literals.py @@ -12,4 +12,3 @@ STATE_LABELS = { STATE_CHECKED_OUT: _('Checked out'), STATE_CHECKED_IN: _('Checked in/available'), } - diff --git a/mayan/apps/checkouts/migrations/0003_auto_20150617_0325.py b/mayan/apps/checkouts/migrations/0003_auto_20150617_0325.py index 67b7945e0d..10690079c7 100644 --- a/mayan/apps/checkouts/migrations/0003_auto_20150617_0325.py +++ b/mayan/apps/checkouts/migrations/0003_auto_20150617_0325.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations +from django.db import migrations def move_from_content_type_user_to_foreign_key_field_user(apps, schema_editor): diff --git a/mayan/apps/checkouts/models.py b/mayan/apps/checkouts/models.py index cf376aa3e2..2ead7b9834 100644 --- a/mayan/apps/checkouts/models.py +++ b/mayan/apps/checkouts/models.py @@ -35,6 +35,9 @@ class DocumentCheckout(models.Model): def __str__(self): return unicode(self.document) + def get_absolute_url(self): + return reverse('checkout:checkout_info', args=[self.document.pk]) + def clean(self): if self.expiration_datetime < now(): raise ValidationError(_('Check out expiration date and time must be in the future.')) @@ -51,9 +54,6 @@ class DocumentCheckout(models.Model): return result - def get_absolute_url(self): - return reverse('checkout:checkout_info', args=[self.document.pk]) - class Meta: verbose_name = _('Document checkout') verbose_name_plural = _('Document checkouts') diff --git a/mayan/apps/checkouts/permissions.py b/mayan/apps/checkouts/permissions.py index 00d78f2a63..5e20291464 100644 --- a/mayan/apps/checkouts/permissions.py +++ b/mayan/apps/checkouts/permissions.py @@ -6,6 +6,6 @@ from permissions import PermissionNamespace namespace = PermissionNamespace('checkouts', _('Document checkout')) -permission_document_checkout = namespace.add_permission(name='checkout_document', label=_('Check out documents')) permission_document_checkin = namespace.add_permission(name='checkin_document', label=_('Check in documents')) permission_document_checkin_override = namespace.add_permission(name='checkin_document_override', label=_('Forcefully check in documents')) +permission_document_checkout = namespace.add_permission(name='checkout_document', label=_('Check out documents')) diff --git a/mayan/apps/common/generics.py b/mayan/apps/common/generics.py new file mode 100644 index 0000000000..2b203c7706 --- /dev/null +++ b/mayan/apps/common/generics.py @@ -0,0 +1,382 @@ +from __future__ import absolute_import, unicode_literals + +from django.conf import settings +from django.contrib import messages +from django.contrib.auth.models import User +from django.contrib.contenttypes.models import ContentType +from django.core.exceptions import ImproperlyConfigured +from django.http import Http404, HttpResponseRedirect +from django.utils.translation import ugettext_lazy as _ +from django.views.generic import FormView, TemplateView +from django.views.generic.detail import SingleObjectMixin +from django.views.generic.edit import CreateView, DeleteView, UpdateView +from django.views.generic.list import ListView + +from .forms import ChoiceForm +from .mixins import ( + ExtraContextMixin, ObjectListPermissionFilterMixin, + ObjectPermissionCheckMixin, RedirectionMixin, ViewPermissionCheckMixin +) + + +__all__ = ( + 'AssignRemoveView', 'ConfirmView', 'MultiFormView', 'ParentChildListView', + 'SingleObjectCreateView', 'SingleObjectDeleteView', + 'SingleObjectEditView', 'SingleObjectListView', 'SimpleView', +) + + +class AssignRemoveView(ExtraContextMixin, ViewPermissionCheckMixin, ObjectPermissionCheckMixin, TemplateView): + decode_content_type = False + extra_context = None + grouped = False + left_list_title = None + right_list_title = None + template_name = 'appearance/generic_form.html' + + LEFT_LIST_NAME = 'left_list' + RIGHT_LIST_NAME = 'right_list' + + @staticmethod + def generate_choices(choices): + results = [] + for choice in choices: + ct = ContentType.objects.get_for_model(choice) + if isinstance(choice, User): + label = choice.get_full_name() if choice.get_full_name() else choice + else: + label = unicode(choice) + + results.append(('%s,%s' % (ct.model, choice.pk), '%s' % (label))) + + # Sort results by the label not the key value + return sorted(results, key=lambda x: x[1]) + + def left_list(self): + # Subclass must override + raise NotImplementedError + + def right_list(self): + # Subclass must override + raise NotImplementedError + + def add(self, item): + # Subclass must override + raise NotImplementedError + + def remove(self, item): + # Subclass must override + raise NotImplementedError + + def get_disabled_choices(self): + return () + + def get_help_text(self): + return self.help_text + + def get(self, request, *args, **kwargs): + self.unselected_list = ChoiceForm(prefix=self.LEFT_LIST_NAME, choices=self.left_list()) + self.selected_list = ChoiceForm(prefix=self.RIGHT_LIST_NAME, choices=self.right_list(), disabled_choices=self.get_disabled_choices(), help_text=self.get_help_text()) + return self.render_to_response(self.get_context_data()) + + def process_form(self, prefix, items_function, action_function): + if '%s-submit' % prefix in self.request.POST.keys(): + form = ChoiceForm( + self.request.POST, prefix=prefix, + choices=items_function() + ) + + if form.is_valid(): + for selection in form.cleaned_data['selection']: + if self.grouped: + flat_list = [] + for group in items_function(): + flat_list.extend(group[1]) + else: + flat_list = items_function() + + label = dict(flat_list)[selection] + if self.decode_content_type: + model, pk = selection.split(',') + selection_obj = ContentType.objects.get(model=model).get_object_for_this_type(pk=pk) + else: + selection_obj = selection + + try: + action_function(selection_obj) + except: + if settings.DEBUG: + raise + else: + messages.error(self.request, _('Unable to transfer selection: %s.') % label) + + def post(self, request, *args, **kwargs): + self.process_form(prefix=self.LEFT_LIST_NAME, items_function=self.left_list, action_function=self.add) + self.process_form(prefix=self.RIGHT_LIST_NAME, items_function=self.right_list, action_function=self.remove) + return self.get(request, *args, **kwargs) + + def get_context_data(self, **kwargs): + data = super(AssignRemoveView, self).get_context_data(**kwargs) + data.update({ + 'subtemplates_list': [ + { + 'name': 'appearance/generic_form_subtemplate.html', + 'column_class': 'col-xs-12 col-sm-6 col-md-6 col-lg-6', + 'context': { + 'form': self.unselected_list, + 'title': self.left_list_title or ' ', + 'submit_label': _('Add'), + 'submit_icon': 'fa fa-plus', + 'hide_labels': True, + } + }, + { + 'name': 'appearance/generic_form_subtemplate.html', + 'column_class': 'col-xs-12 col-sm-6 col-md-6 col-lg-6', + 'context': { + 'form': self.selected_list, + 'title': self.right_list_title or ' ', + 'submit_label': _('Remove'), + 'submit_icon': 'fa fa-minus', + 'hide_labels': True, + } + }, + + ], + }) + return data + + +class ConfirmView(ObjectListPermissionFilterMixin, ViewPermissionCheckMixin, ExtraContextMixin, RedirectionMixin, TemplateView): + template_name = 'appearance/generic_confirm.html' + + +class MultiFormView(FormView): + prefixes = {} + + prefix = None + + def get_form_kwargs(self, form_name): + kwargs = {} + kwargs.update({'initial': self.get_initial(form_name)}) + kwargs.update({'prefix': self.get_prefix(form_name)}) + + if self.request.method in ('POST', 'PUT'): + kwargs.update({ + 'data': self.request.POST, + 'files': self.request.FILES, + }) + + return kwargs + + def _create_form(self, form_name, klass): + form_kwargs = self.get_form_kwargs(form_name) + form_create_method = 'create_%s_form' % form_name + if hasattr(self, form_create_method): + form = getattr(self, form_create_method)(**form_kwargs) + else: + form = klass(**form_kwargs) + return form + + def get_forms(self, form_classes): + return dict([(key, self._create_form(key, klass)) for key, klass in form_classes.items()]) + + def get_initial(self, form_name): + initial_method = 'get_%s_initial' % form_name + if hasattr(self, initial_method): + return getattr(self, initial_method)() + else: + return self.initial.copy() + + def get_prefix(self, form_name): + return self.prefixes.get(form_name, self.prefix) + + def get(self, request, *args, **kwargs): + form_classes = self.get_form_classes() + forms = self.get_forms(form_classes) + return self.render_to_response(self.get_context_data(forms=forms)) + + def forms_valid(self, forms): + for form_name, form in forms.items(): + form_valid_method = '%s_form_valid' % form_name + + if hasattr(self, form_valid_method): + return getattr(self, form_valid_method)(form) + + self.all_forms_valid(forms) + + return HttpResponseRedirect(self.get_success_url()) + + def forms_invalid(self, forms): + return self.render_to_response(self.get_context_data(forms=forms)) + + def post(self, request, *args, **kwargs): + form_classes = self.get_form_classes() + forms = self.get_forms(form_classes) + + if all([form.is_valid() for form in forms.values()]): + return self.forms_valid(forms) + else: + return self.forms_invalid(forms) + + +class ParentChildListView(ViewPermissionCheckMixin, ObjectPermissionCheckMixin, ExtraContextMixin, ListView, SingleObjectMixin): + parent_model = None + parent_queryset = None + template_name = 'appearance/generic_list.html' + + def get(self, request, *args, **kwargs): + # Parent + self.object = self.get_object() + + # Children + self.object_list = self.get_queryset() + allow_empty = self.get_allow_empty() + if not allow_empty: + # When pagination is enabled and object_list is a queryset, + # it's better to do a cheap query than to load the unpaginated + # queryset in memory. + if (self.get_paginate_by(self.object_list) is not None + and hasattr(self.object_list, 'exists')): + is_empty = not self.object_list.exists() + else: + is_empty = len(self.object_list) == 0 + if is_empty: + raise Http404(_("Empty list and '%(class_name)s.allow_empty' is False.") + % {'class_name': self.__class__.__name__}) + + context = self.get_context_data(object=self.object) + return self.render_to_response(context) + + def get_object(self, queryset=None): + # Use a custom queryset if provided; this is required for subclasses + # like DateDetailView + if queryset is None: + queryset = self.get_parent_queryset() + # Next, try looking up by primary key. + pk = self.kwargs.get(self.pk_url_kwarg, None) + slug = self.kwargs.get(self.slug_url_kwarg, None) + if pk is not None: + queryset = queryset.filter(pk=pk) + # Next, try looking up by slug. + if slug is not None and (pk is None or self.query_pk_and_slug): + slug_field = self.get_slug_field() + queryset = queryset.filter(**{slug_field: slug}) + # If none of those are defined, it's an error. + if pk is None and slug is None: + raise AttributeError("Generic detail view %s must be called with " + "either an object pk or a slug." + % self.__class__.__name__) + try: + # Get the single item from the filtered queryset + obj = queryset.get() + except queryset.model.DoesNotExist: + raise Http404(_("No %(verbose_name)s found matching the query") % + {'verbose_name': queryset.model._meta.verbose_name}) + return obj + + def get_parent_queryset(self): + """ + Return the `QuerySet` that will be used to look up the object. + Note that this method is called by the default implementation of + `get_object` and may not be called if `get_object` is overridden. + """ + if self.parent_queryset is None: + if self.parent_model: + return self.model._default_manager.all() + else: + raise ImproperlyConfigured( + "%(cls)s is missing a QuerySet. Define " + "%(cls)s.parent_model, %(cls)s.parent_queryset, or override " + "%(cls)s.get_parent_queryset()." % { + 'cls': self.__class__.__name__ + } + ) + return self.parent_queryset.all() + + def get_queryset(self): + raise NotImplementedError + + +class SimpleView(ViewPermissionCheckMixin, ExtraContextMixin, TemplateView): + pass + + +class SingleObjectCreateView(ViewPermissionCheckMixin, ExtraContextMixin, RedirectionMixin, CreateView): + template_name = 'appearance/generic_form.html' + + def form_invalid(self, form): + result = super(SingleObjectCreateView, self).form_invalid(form) + + try: + messages.error(self.request, _('Error creating new %s.') % self.extra_context['object_name']) + except KeyError: + messages.error(self.request, _('Error creating object.')) + + return result + + def form_valid(self, form): + result = super(SingleObjectCreateView, self).form_valid(form) + try: + messages.success(self.request, _('%s created successfully.') % self.extra_context['object_name'].capitalize()) + except KeyError: + messages.success(self.request, _('New object created successfully.')) + + return result + + +class SingleObjectDeleteView(ViewPermissionCheckMixin, ObjectPermissionCheckMixin, ExtraContextMixin, RedirectionMixin, DeleteView): + template_name = 'appearance/generic_confirm.html' + + def get_context_data(self, **kwargs): + context = super(SingleObjectDeleteView, self).get_context_data(**kwargs) + context.update({'delete_view': True}) + return context + + def delete(self, request, *args, **kwargs): + try: + result = super(SingleObjectDeleteView, self).delete(request, *args, **kwargs) + except Exception as exception: + try: + messages.error(self.request, _('Error deleting %s.') % self.extra_context['object_name']) + except KeyError: + messages.error(self.request, _('Error deleting object.')) + + raise exception + else: + try: + messages.success(self.request, _('%s deleted successfully.') % self.extra_context['object_name'].capitalize()) + except KeyError: + messages.success(self.request, _('Object deleted successfully.')) + + return result + + +# TODO: check/test if ViewPermissionCheckMixin, ObjectPermissionCheckMixin are +# in the right MRO +class SingleObjectEditView(ViewPermissionCheckMixin, ObjectPermissionCheckMixin, ExtraContextMixin, RedirectionMixin, UpdateView): + template_name = 'appearance/generic_form.html' + + def form_invalid(self, form): + result = super(SingleObjectEditView, self).form_invalid(form) + + try: + messages.error(self.request, _('Error saving %s details.') % self.extra_context['object_name']) + except KeyError: + messages.error(self.request, _('Error saving details.')) + + return result + + def form_valid(self, form): + result = super(SingleObjectEditView, self).form_valid(form) + + try: + messages.success(self.request, _('%s details saved successfully.') % self.extra_context['object_name'].capitalize()) + except KeyError: + messages.success(self.request, _('Details saved successfully.')) + + return result + + +class SingleObjectListView(ViewPermissionCheckMixin, ObjectListPermissionFilterMixin, ExtraContextMixin, RedirectionMixin, ListView): + template_name = 'appearance/generic_list.html' diff --git a/mayan/apps/common/middleware/timezone.py b/mayan/apps/common/middleware/timezone.py index e3e7fcf1bc..491abed3e2 100644 --- a/mayan/apps/common/middleware/timezone.py +++ b/mayan/apps/common/middleware/timezone.py @@ -11,7 +11,7 @@ class TimezoneMiddleware(object): if hasattr(request, 'session'): tzname = request.session.get(settings.TIMEZONE_SESSION_KEY) else: - tzname = HttpRequest.COOKIES.get(settings.TIMEZONE_COOKIE_NAME) + tzname = request.COOKIES.get(settings.TIMEZONE_COOKIE_NAME) if tzname: timezone.activate(pytz.timezone(tzname)) diff --git a/mayan/apps/common/migrations/0003_auto_20150614_0723.py b/mayan/apps/common/migrations/0003_auto_20150614_0723.py index 438d705a9f..6c0271eb7f 100644 --- a/mayan/apps/common/migrations/0003_auto_20150614_0723.py +++ b/mayan/apps/common/migrations/0003_auto_20150614_0723.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations +from django.db import migrations class Migration(migrations.Migration): diff --git a/mayan/apps/common/migrations/0004_delete_anonymoususersingleton.py b/mayan/apps/common/migrations/0004_delete_anonymoususersingleton.py index 2e251ea11a..30339b9a87 100644 --- a/mayan/apps/common/migrations/0004_delete_anonymoususersingleton.py +++ b/mayan/apps/common/migrations/0004_delete_anonymoususersingleton.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations +from django.db import migrations class Migration(migrations.Migration): diff --git a/mayan/apps/common/migrations/0005_auto_20150706_1832.py b/mayan/apps/common/migrations/0005_auto_20150706_1832.py index ea1466f039..aefc315a9f 100644 --- a/mayan/apps/common/migrations/0005_auto_20150706_1832.py +++ b/mayan/apps/common/migrations/0005_auto_20150706_1832.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations +from django.db import migrations class Migration(migrations.Migration): diff --git a/mayan/apps/common/utils.py b/mayan/apps/common/utils.py index 9b7609da5e..5224503281 100644 --- a/mayan/apps/common/utils.py +++ b/mayan/apps/common/utils.py @@ -6,7 +6,6 @@ import tempfile import types from django.conf import settings -from django.contrib.auth.models import User from django.utils.datastructures import MultiValueDict from django.utils.http import urlquote as django_urlquote from django.utils.http import urlencode as django_urlencode diff --git a/mayan/apps/common/views.py b/mayan/apps/common/views.py index f1d4b82d77..06e53a9eff 100644 --- a/mayan/apps/common/views.py +++ b/mayan/apps/common/views.py @@ -4,164 +4,29 @@ from json import dumps, loads from django.conf import settings from django.contrib import messages -from django.contrib.auth.models import User -from django.contrib.contenttypes.models import ContentType -from django.core.exceptions import ImproperlyConfigured from django.core.urlresolvers import reverse, reverse_lazy -from django.http import Http404, HttpResponseRedirect -from django.shortcuts import render_to_response +from django.http import HttpResponseRedirect from django.template import RequestContext from django.utils import timezone, translation from django.utils.http import urlencode from django.utils.translation import ugettext_lazy as _ -from django.views.generic import FormView, TemplateView -from django.views.generic.detail import SingleObjectMixin -from django.views.generic.edit import CreateView, DeleteView, UpdateView -from django.views.generic.list import ListView +from django.views.generic import TemplateView from documents.search import document_search from .classes import MissingItem from .forms import ( - ChoiceForm, LicenseForm, LocaleProfileForm, LocaleProfileForm_view, + LicenseForm, LocaleProfileForm, LocaleProfileForm_view, UserForm, UserForm_view ) +from .generics import * # NOQA from .menus import menu_tools, menu_setup -from .mixins import ( - ExtraContextMixin, ObjectListPermissionFilterMixin, - ObjectPermissionCheckMixin, RedirectionMixin, ViewPermissionCheckMixin -) - - -class AssignRemoveView(ExtraContextMixin, ViewPermissionCheckMixin, ObjectPermissionCheckMixin, TemplateView): - decode_content_type = False - extra_context = None - grouped = False - left_list_title = None - right_list_title = None - template_name = 'appearance/generic_form.html' - - LEFT_LIST_NAME = 'left_list' - RIGHT_LIST_NAME = 'right_list' - - @staticmethod - def generate_choices(choices): - results = [] - for choice in choices: - ct = ContentType.objects.get_for_model(choice) - if isinstance(choice, User): - label = choice.get_full_name() if choice.get_full_name() else choice - else: - label = unicode(choice) - - results.append(('%s,%s' % (ct.model, choice.pk), '%s' % (label))) - - # Sort results by the label not the key value - return sorted(results, key=lambda x: x[1]) - - def left_list(self): - # Subclass must override - raise NotImplementedError - - def right_list(self): - # Subclass must override - raise NotImplementedError - - def add(self, item): - # Subclass must override - raise NotImplementedError - - def remove(self, item): - # Subclass must override - raise NotImplementedError - - def get_disabled_choices(self): - return () - - def get_help_text(self): - return self.help_text - - def get(self, request, *args, **kwargs): - self.unselected_list = ChoiceForm(prefix=self.LEFT_LIST_NAME, choices=self.left_list()) - self.selected_list = ChoiceForm(prefix=self.RIGHT_LIST_NAME, choices=self.right_list(), disabled_choices=self.get_disabled_choices(), help_text=self.get_help_text()) - return self.render_to_response(self.get_context_data()) - - def process_form(self, prefix, items_function, action_function): - if '%s-submit' % prefix in self.request.POST.keys(): - form = ChoiceForm( - self.request.POST, prefix=prefix, - choices=items_function() - ) - - if form.is_valid(): - for selection in form.cleaned_data['selection']: - if self.grouped: - flat_list = [] - for group in items_function(): - flat_list.extend(group[1]) - else: - flat_list = items_function() - - label = dict(flat_list)[selection] - if self.decode_content_type: - model, pk = selection.split(',') - selection_obj = ContentType.objects.get(model=model).get_object_for_this_type(pk=pk) - else: - selection_obj = selection - - try: - action_function(selection_obj) - except: - if settings.DEBUG: - raise - else: - messages.error(self.request, _('Unable to transfer selection: %s.') % label) - - def post(self, request, *args, **kwargs): - self.process_form(prefix=self.LEFT_LIST_NAME, items_function=self.left_list, action_function=self.add) - self.process_form(prefix=self.RIGHT_LIST_NAME, items_function=self.right_list, action_function=self.remove) - return self.get(request, *args, **kwargs) - - def get_context_data(self, **kwargs): - data = super(AssignRemoveView, self).get_context_data(**kwargs) - data.update({ - 'subtemplates_list': [ - { - 'name': 'appearance/generic_form_subtemplate.html', - 'column_class': 'col-xs-12 col-sm-6 col-md-6 col-lg-6', - 'context': { - 'form': self.unselected_list, - 'title': self.left_list_title or ' ', - 'submit_label': _('Add'), - 'submit_icon': 'fa fa-plus', - 'hide_labels': True, - } - }, - { - 'name': 'appearance/generic_form_subtemplate.html', - 'column_class': 'col-xs-12 col-sm-6 col-md-6 col-lg-6', - 'context': { - 'form': self.selected_list, - 'title': self.right_list_title or ' ', - 'submit_label': _('Remove'), - 'submit_icon': 'fa fa-minus', - 'hide_labels': True, - } - }, - - ], - }) - return data class AboutView(TemplateView): template_name = 'appearance/about.html' -class ConfirmView(ObjectListPermissionFilterMixin, ViewPermissionCheckMixin, ExtraContextMixin, RedirectionMixin, TemplateView): - template_name = 'appearance/generic_confirm.html' - - class CurrentUserDetailsView(TemplateView): template_name = 'appearance/generic_form.html' @@ -175,6 +40,15 @@ class CurrentUserDetailsView(TemplateView): return data +class CurrentUserEditView(SingleObjectEditView): + extra_context = {'title': _('Edit current user details')} + form_class = UserForm + post_action_redirect = reverse_lazy('common:current_user_details') + + def get_object(self): + return self.request.user + + class CurrentUserLocaleProfileDetailsView(TemplateView): template_name = 'appearance/generic_form.html' @@ -188,6 +62,30 @@ class CurrentUserLocaleProfileDetailsView(TemplateView): return data +class CurrentUserLocaleProfileEditView(SingleObjectEditView): + extra_context = {'title': _('Edit current user locale profile details')} + form_class = LocaleProfileForm + post_action_redirect = reverse_lazy('common:current_user_locale_profile_details') + + def form_valid(self, form): + form.save() + + timezone.activate(form.cleaned_data['timezone']) + translation.activate(form.cleaned_data['language']) + + if hasattr(self.request, 'session'): + self.request.session[translation.LANGUAGE_SESSION_KEY] = form.cleaned_data['language'] + self.request.session[settings.TIMEZONE_SESSION_KEY] = form.cleaned_data['timezone'] + else: + self.request.set_cookie(settings.LANGUAGE_COOKIE_NAME, form.cleaned_data['language']) + self.request.set_cookie(settings.TIMEZONE_COOKIE_NAME, form.cleaned_data['timezone']) + + return super(CurrentUserLocaleProfileEditView, self).form_valid(form) + + def get_object(self): + return self.request.user.locale_profile + + class HomeView(TemplateView): template_name = 'appearance/home.html' @@ -216,7 +114,7 @@ class HomeView(TemplateView): return self.render_to_response(context) -class LicenseView(ExtraContextMixin, TemplateView): +class LicenseView(SimpleView): extra_context = { 'form': LicenseForm(), 'read_only': True, @@ -225,233 +123,6 @@ class LicenseView(ExtraContextMixin, TemplateView): template_name = 'appearance/generic_form.html' -class MultiFormView(FormView): - prefixes = {} - - prefix = None - - def get_form_kwargs(self, form_name): - kwargs = {} - kwargs.update({'initial': self.get_initial(form_name)}) - kwargs.update({'prefix': self.get_prefix(form_name)}) - - if self.request.method in ('POST', 'PUT'): - kwargs.update({ - 'data': self.request.POST, - 'files': self.request.FILES, - }) - - return kwargs - - def _create_form(self, form_name, klass): - form_kwargs = self.get_form_kwargs(form_name) - form_create_method = 'create_%s_form' % form_name - if hasattr(self, form_create_method): - form = getattr(self, form_create_method)(**form_kwargs) - else: - form = klass(**form_kwargs) - return form - - def get_forms(self, form_classes): - return dict([(key, self._create_form(key, klass)) for key, klass in form_classes.items()]) - - def get_initial(self, form_name): - initial_method = 'get_%s_initial' % form_name - if hasattr(self, initial_method): - return getattr(self, initial_method)() - else: - return self.initial.copy() - - def get_prefix(self, form_name): - return self.prefixes.get(form_name, self.prefix) - - def get(self, request, *args, **kwargs): - form_classes = self.get_form_classes() - forms = self.get_forms(form_classes) - return self.render_to_response(self.get_context_data(forms=forms)) - - def forms_valid(self, forms): - for form_name, form in forms.items(): - form_valid_method = '%s_form_valid' % form_name - - if hasattr(self, form_valid_method): - return getattr(self, form_valid_method)(form) - - self.all_forms_valid(forms) - - return HttpResponseRedirect(self.get_success_url()) - - def forms_invalid(self, forms): - return self.render_to_response(self.get_context_data(forms=forms)) - - def post(self, request, *args, **kwargs): - form_classes = self.get_form_classes() - forms = self.get_forms(form_classes) - - if all([form.is_valid() for form in forms.values()]): - return self.forms_valid(forms) - else: - return self.forms_invalid(forms) - - -# TODO: check/test if ViewPermissionCheckMixin, ObjectPermissionCheckMixin are -# in the right MRO -class SingleObjectEditView(ViewPermissionCheckMixin, ObjectPermissionCheckMixin, ExtraContextMixin, RedirectionMixin, UpdateView): - template_name = 'appearance/generic_form.html' - - def form_invalid(self, form): - result = super(SingleObjectEditView, self).form_invalid(form) - - try: - messages.error(self.request, _('Error saving %s details.') % self.extra_context['object_name']) - except KeyError: - messages.error(self.request, _('Error saving details.')) - - return result - - def form_valid(self, form): - result = super(SingleObjectEditView, self).form_valid(form) - - try: - messages.success(self.request, _('%s details saved successfully.') % self.extra_context['object_name'].capitalize()) - except KeyError: - messages.success(self.request, _('Details saved successfully.')) - - return result - - -class SingleObjectCreateView(ViewPermissionCheckMixin, ExtraContextMixin, RedirectionMixin, CreateView): - template_name = 'appearance/generic_form.html' - - def form_invalid(self, form): - result = super(SingleObjectCreateView, self).form_invalid(form) - - try: - messages.error(self.request, _('Error creating new %s.') % self.extra_context['object_name']) - except KeyError: - messages.error(self.request, _('Error creating object.')) - - return result - - def form_valid(self, form): - result = super(SingleObjectCreateView, self).form_valid(form) - try: - messages.success(self.request, _('%s created successfully.') % self.extra_context['object_name'].capitalize()) - except KeyError: - messages.success(self.request, _('New object created successfully.')) - - return result - - -class SingleObjectDeleteView(ViewPermissionCheckMixin, ObjectPermissionCheckMixin, ExtraContextMixin, RedirectionMixin, DeleteView): - template_name = 'appearance/generic_confirm.html' - - def get_context_data(self, **kwargs): - context = super(SingleObjectDeleteView, self).get_context_data(**kwargs) - context.update({'delete_view': True}) - return context - - def delete(self, request, *args, **kwargs): - try: - result = super(SingleObjectDeleteView, self).delete(request, *args, **kwargs) - except Exception as exception: - try: - messages.error(self.request, _('Error deleting %s.') % self.extra_context['object_name']) - except KeyError: - messages.error(self.request, _('Error deleting object.')) - - raise exception - else: - try: - messages.success(self.request, _('%s deleted successfully.') % self.extra_context['object_name'].capitalize()) - except KeyError: - messages.success(self.request, _('Object deleted successfully.')) - - return result - - -class SingleObjectListView(ViewPermissionCheckMixin, ObjectListPermissionFilterMixin, ExtraContextMixin, RedirectionMixin, ListView): - template_name = 'appearance/generic_list.html' - - -class ParentChildListView(ViewPermissionCheckMixin, ObjectPermissionCheckMixin, ExtraContextMixin, ListView, SingleObjectMixin): - parent_model = None - parent_queryset = None - template_name = 'appearance/generic_list.html' - - def get(self, request, *args, **kwargs): - # Parent - self.object = self.get_object() - - # Children - self.object_list = self.get_queryset() - allow_empty = self.get_allow_empty() - if not allow_empty: - # When pagination is enabled and object_list is a queryset, - # it's better to do a cheap query than to load the unpaginated - # queryset in memory. - if (self.get_paginate_by(self.object_list) is not None - and hasattr(self.object_list, 'exists')): - is_empty = not self.object_list.exists() - else: - is_empty = len(self.object_list) == 0 - if is_empty: - raise Http404(_("Empty list and '%(class_name)s.allow_empty' is False.") - % {'class_name': self.__class__.__name__}) - - context = self.get_context_data(object=self.object) - return self.render_to_response(context) - - def get_object(self, queryset=None): - # Use a custom queryset if provided; this is required for subclasses - # like DateDetailView - if queryset is None: - queryset = self.get_parent_queryset() - # Next, try looking up by primary key. - pk = self.kwargs.get(self.pk_url_kwarg, None) - slug = self.kwargs.get(self.slug_url_kwarg, None) - if pk is not None: - queryset = queryset.filter(pk=pk) - # Next, try looking up by slug. - if slug is not None and (pk is None or self.query_pk_and_slug): - slug_field = self.get_slug_field() - queryset = queryset.filter(**{slug_field: slug}) - # If none of those are defined, it's an error. - if pk is None and slug is None: - raise AttributeError("Generic detail view %s must be called with " - "either an object pk or a slug." - % self.__class__.__name__) - try: - # Get the single item from the filtered queryset - obj = queryset.get() - except queryset.model.DoesNotExist: - raise Http404(_("No %(verbose_name)s found matching the query") % - {'verbose_name': queryset.model._meta.verbose_name}) - return obj - - def get_parent_queryset(self): - """ - Return the `QuerySet` that will be used to look up the object. - Note that this method is called by the default implementation of - `get_object` and may not be called if `get_object` is overridden. - """ - if self.parent_queryset is None: - if self.parent_model: - return self.model._default_manager.all() - else: - raise ImproperlyConfigured( - "%(cls)s is missing a QuerySet. Define " - "%(cls)s.parent_model, %(cls)s.parent_queryset, or override " - "%(cls)s.get_parent_queryset()." % { - 'cls': self.__class__.__name__ - } - ) - return self.parent_queryset.all() - - def get_queryset(self): - raise NotImplementedError - - class SetupListView(TemplateView): template_name = 'appearance/generic_list_horizontal.html' @@ -464,10 +135,6 @@ class SetupListView(TemplateView): return data -class SimpleView(ViewPermissionCheckMixin, ExtraContextMixin, TemplateView): - pass - - class ToolsListView(SimpleView): template_name = 'appearance/generic_list_horizontal.html' @@ -513,36 +180,3 @@ def multi_object_action_view(request): action, urlencode({'id_list': id_list, 'next': next})) ) - - -class CurrentUserEditView(SingleObjectEditView): - extra_context = {'title': _('Edit current user details')} - form_class = UserForm - post_action_redirect = reverse_lazy('common:current_user_details') - - def get_object(self): - return self.request.user - - -class CurrentUserLocaleProfileEditView(SingleObjectEditView): - extra_context = {'title': _('Edit current user locale profile details')} - form_class = LocaleProfileForm - post_action_redirect = reverse_lazy('common:current_user_locale_profile_details') - - def form_valid(self, form): - form.save() - - timezone.activate(form.cleaned_data['timezone']) - translation.activate(form.cleaned_data['language']) - - if hasattr(self.request, 'session'): - self.request.session[translation.LANGUAGE_SESSION_KEY] = form.cleaned_data['language'] - self.request.session[settings.TIMEZONE_SESSION_KEY] = form.cleaned_data['timezone'] - else: - self.request.set_cookie(settings.LANGUAGE_COOKIE_NAME, form.cleaned_data['language']) - self.request.set_cookie(settings.TIMEZONE_COOKIE_NAME, form.cleaned_data['timezone']) - - return super(CurrentUserLocaleProfileEditView, self).form_valid(form) - - def get_object(self): - return self.request.user.locale_profile diff --git a/mayan/apps/common/widgets.py b/mayan/apps/common/widgets.py index 9d0434eb88..df155cc8b8 100644 --- a/mayan/apps/common/widgets.py +++ b/mayan/apps/common/widgets.py @@ -5,7 +5,6 @@ import os from django import forms from django.forms.util import flatatt -from django.utils.datastructures import MultiValueDict, MergeDict from django.utils.encoding import force_unicode, force_text from django.utils.html import conditional_escape, format_html from django.utils.safestring import mark_safe diff --git a/mayan/apps/converter/migrations/0002_auto_20150608_1943.py b/mayan/apps/converter/migrations/0002_auto_20150608_1943.py index f809529002..a10af53919 100644 --- a/mayan/apps/converter/migrations/0002_auto_20150608_1943.py +++ b/mayan/apps/converter/migrations/0002_auto_20150608_1943.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations +from django.db import migrations class Migration(migrations.Migration): diff --git a/mayan/apps/converter/models.py b/mayan/apps/converter/models.py index 1738298e8d..100ce478e3 100644 --- a/mayan/apps/converter/models.py +++ b/mayan/apps/converter/models.py @@ -4,7 +4,6 @@ import logging from django.contrib.contenttypes import generic from django.contrib.contenttypes.models import ContentType -from django.core.exceptions import ValidationError from django.db import models from django.utils.encoding import python_2_unicode_compatible from django.utils.translation import ugettext_lazy as _ diff --git a/mayan/apps/document_indexing/forms.py b/mayan/apps/document_indexing/forms.py index c3f1262afd..0a3f85e6af 100644 --- a/mayan/apps/document_indexing/forms.py +++ b/mayan/apps/document_indexing/forms.py @@ -5,7 +5,7 @@ from django import forms from common.classes import ModelAttribute from documents.models import Document -from .models import Index, IndexTemplateNode +from .models import IndexTemplateNode class IndexTemplateNodeForm(forms.ModelForm): diff --git a/mayan/apps/document_indexing/managers.py b/mayan/apps/document_indexing/managers.py index 1f0a4af198..d9fd163598 100644 --- a/mayan/apps/document_indexing/managers.py +++ b/mayan/apps/document_indexing/managers.py @@ -89,9 +89,9 @@ class IndexInstanceNodeManager(models.Manager): self.delete_empty_index_nodes() - def rebuild_all_indexes(): + def rebuild_all_indexes(self): for instance_node in self.all(): instance_node.delete() for document in Document.objects.all(): - index_document(document) + self.index_document(document) diff --git a/mayan/apps/document_indexing/migrations/0002_remove_index_name.py b/mayan/apps/document_indexing/migrations/0002_remove_index_name.py index 3feef24cad..41ad3eb87e 100644 --- a/mayan/apps/document_indexing/migrations/0002_remove_index_name.py +++ b/mayan/apps/document_indexing/migrations/0002_remove_index_name.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations +from django.db import migrations class Migration(migrations.Migration): diff --git a/mayan/apps/document_indexing/migrations/0003_auto_20150708_0101.py b/mayan/apps/document_indexing/migrations/0003_auto_20150708_0101.py index 8d1c5d60f1..def52f6856 100644 --- a/mayan/apps/document_indexing/migrations/0003_auto_20150708_0101.py +++ b/mayan/apps/document_indexing/migrations/0003_auto_20150708_0101.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations +from django.db import migrations class Migration(migrations.Migration): diff --git a/mayan/apps/document_indexing/test_models.py b/mayan/apps/document_indexing/test_models.py index 0c040155a4..b4d8448c50 100644 --- a/mayan/apps/document_indexing/test_models.py +++ b/mayan/apps/document_indexing/test_models.py @@ -3,7 +3,7 @@ from __future__ import unicode_literals from django.core.files.base import File from django.test import TestCase -from documents.models import Document, DocumentType +from documents.models import DocumentType from documents.test_models import TEST_SMALL_DOCUMENT_PATH, TEST_DOCUMENT_TYPE from metadata.models import MetadataType, DocumentTypeMetadataType diff --git a/mayan/apps/document_signatures/apps.py b/mayan/apps/document_signatures/apps.py index ceb2505c5f..5e2cb23b4f 100644 --- a/mayan/apps/document_signatures/apps.py +++ b/mayan/apps/document_signatures/apps.py @@ -3,7 +3,6 @@ from __future__ import unicode_literals import io import logging -from django.core.files import File from django.utils.translation import ugettext_lazy as _ from acls import ModelPermission diff --git a/mayan/apps/document_signatures/models.py b/mayan/apps/document_signatures/models.py index a68cef5c18..cdb46bfb19 100644 --- a/mayan/apps/document_signatures/models.py +++ b/mayan/apps/document_signatures/models.py @@ -23,15 +23,12 @@ class DocumentVersionSignature(models.Model): """ Model that describes a document version signature properties """ - document_version = models.ForeignKey(DocumentVersion, verbose_name=_('Document version'), editable=False) - signature_file = models.FileField(blank=True, null=True, upload_to=upload_to, storage=storage_backend, verbose_name=_('Signature file')) + document_version = models.ForeignKey(DocumentVersion, editable=False, verbose_name=_('Document version')) + signature_file = models.FileField(blank=True, null=True, storage=storage_backend, upload_to=upload_to, verbose_name=_('Signature file')) has_embedded_signature = models.BooleanField(default=False, verbose_name=_('Has embedded signature')) objects = DocumentVersionSignatureManager() - def delete_detached_signature_file(self): - self.signature_file.storage.delete(self.signature_file.path) - def check_for_embedded_signature(self): logger.debug('checking for embedded signature') @@ -39,6 +36,9 @@ class DocumentVersionSignature(models.Model): self.has_embedded_signature = gpg.has_embedded_signature(file_object) self.save() + def delete_detached_signature_file(self): + self.signature_file.storage.delete(self.signature_file.path) + class Meta: verbose_name = _('Document version signature') verbose_name_plural = _('Document version signatures') diff --git a/mayan/apps/document_signatures/test_models.py b/mayan/apps/document_signatures/test_models.py index 8c9f7c7d84..ae2e3ebb59 100644 --- a/mayan/apps/document_signatures/test_models.py +++ b/mayan/apps/document_signatures/test_models.py @@ -6,14 +6,13 @@ from django.conf import settings from django.core.files.base import File from django.test import TestCase -from documents.models import Document, DocumentType -from documents.test_models import TEST_DOCUMENT_TYPE +from documents.models import DocumentType +from documents.test_models import TEST_DOCUMENT_PATH, TEST_DOCUMENT_TYPE from django_gpg.literals import SIGNATURE_STATE_VALID from django_gpg.runtime import gpg from .models import DocumentVersionSignature -TEST_DOCUMENT_PATH = os.path.join(settings.BASE_DIR, 'contrib', 'sample_documents', 'mayan_11_1.pdf') TEST_SIGNED_DOCUMENT_PATH = os.path.join(settings.BASE_DIR, 'contrib', 'sample_documents', 'mayan_11_1.pdf.gpg') TEST_SIGNATURE_FILE_PATH = os.path.join(settings.BASE_DIR, 'contrib', 'sample_documents', 'mayan_11_1.pdf.sig') TEST_KEY_FILE = os.path.join(settings.BASE_DIR, 'contrib', 'sample_documents', 'key0x5F3F7F75D210724D.asc') diff --git a/mayan/apps/document_states/models.py b/mayan/apps/document_states/models.py index 4df0701340..9bea5af12d 100644 --- a/mayan/apps/document_states/models.py +++ b/mayan/apps/document_states/models.py @@ -91,6 +91,9 @@ class WorkflowInstance(models.Model): workflow = models.ForeignKey(Workflow, related_name='instances', verbose_name=_('Workflow')) document = models.ForeignKey(Document, related_name='workflows', verbose_name=_('Document')) + def __str__(self): + return unicode(self.workflow) + def get_absolute_url(self): return reverse('document_states:workflow_instance_detail', args=[str(self.pk)]) @@ -108,24 +111,21 @@ class WorkflowInstance(models.Model): except AttributeError: return self.workflow.get_initial_state() - def get_last_transition(self): - try: - return self.get_last_log_entry().transition - except AttributeError: - return None - def get_last_log_entry(self): try: return self.log_entries.order_by('datetime').last() except AttributeError: return None + def get_last_transition(self): + try: + return self.get_last_log_entry().transition + except AttributeError: + return None + def get_transition_choices(self): return self.get_current_state().origin_transitions.all() - def __str__(self): - return unicode(self.workflow) - class Meta: unique_together = ('document', 'workflow') verbose_name = _('Workflow instance') diff --git a/mayan/apps/document_states/views.py b/mayan/apps/document_states/views.py index dea2b4095a..600cb68fc2 100644 --- a/mayan/apps/document_states/views.py +++ b/mayan/apps/document_states/views.py @@ -77,7 +77,7 @@ class WorkflowDocumentListView(DocumentListView): 'hide_links': True, 'object': self.workflow, 'title': _('Documents with the workflow: %s') % self.workflow - } + } class WorkflowInstanceDetailView(SingleObjectListView): diff --git a/mayan/apps/documents/api_views.py b/mayan/apps/documents/api_views.py index 9a48e865a6..38c152a536 100644 --- a/mayan/apps/documents/api_views.py +++ b/mayan/apps/documents/api_views.py @@ -7,10 +7,8 @@ from django.shortcuts import get_object_or_404 from rest_framework import generics, status from rest_framework.response import Response -from rest_framework.settings import api_settings from acls.models import AccessControlList -from common.models import SharedUploadedFile from converter.exceptions import UnkownConvertError, UnknownFileFormat from converter.literals import ( DEFAULT_PAGE_NUMBER, DEFAULT_ROTATION, DEFAULT_ZOOM_LEVEL diff --git a/mayan/apps/documents/migrations/0001_initial.py b/mayan/apps/documents/migrations/0001_initial.py index 789b521d73..1249235071 100644 --- a/mayan/apps/documents/migrations/0001_initial.py +++ b/mayan/apps/documents/migrations/0001_initial.py @@ -2,8 +2,8 @@ from __future__ import unicode_literals from django.db import models, migrations -import documents.models from django.conf import settings + import storage.backends.filebasedstorage diff --git a/mayan/apps/documents/migrations/0002_auto_20150608_1902.py b/mayan/apps/documents/migrations/0002_auto_20150608_1902.py index dc9a9e98c6..41ba3e29d6 100644 --- a/mayan/apps/documents/migrations/0002_auto_20150608_1902.py +++ b/mayan/apps/documents/migrations/0002_auto_20150608_1902.py @@ -1,8 +1,7 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations -import storage.backends.filebasedstorage +from django.db import migrations class Migration(migrations.Migration): diff --git a/mayan/apps/documents/migrations/0004_auto_20150616_1930.py b/mayan/apps/documents/migrations/0004_auto_20150616_1930.py index 879a9b1ca7..18812b28c4 100644 --- a/mayan/apps/documents/migrations/0004_auto_20150616_1930.py +++ b/mayan/apps/documents/migrations/0004_auto_20150616_1930.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations +from django.db import migrations class Migration(migrations.Migration): diff --git a/mayan/apps/documents/migrations/0005_auto_20150617_0358.py b/mayan/apps/documents/migrations/0005_auto_20150617_0358.py index 93a635f480..ae74d6ac56 100644 --- a/mayan/apps/documents/migrations/0005_auto_20150617_0358.py +++ b/mayan/apps/documents/migrations/0005_auto_20150617_0358.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations +from django.db import migrations class Migration(migrations.Migration): diff --git a/mayan/apps/documents/migrations/0006_remove_documentpage_content_old.py b/mayan/apps/documents/migrations/0006_remove_documentpage_content_old.py index ad0ddd3b83..18f3241e27 100644 --- a/mayan/apps/documents/migrations/0006_remove_documentpage_content_old.py +++ b/mayan/apps/documents/migrations/0006_remove_documentpage_content_old.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations +from django.db import migrations class Migration(migrations.Migration): diff --git a/mayan/apps/documents/migrations/0007_remove_documentpage_page_label.py b/mayan/apps/documents/migrations/0007_remove_documentpage_page_label.py index b116ca8c12..8b641cb35a 100644 --- a/mayan/apps/documents/migrations/0007_remove_documentpage_page_label.py +++ b/mayan/apps/documents/migrations/0007_remove_documentpage_page_label.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations +from django.db import migrations class Migration(migrations.Migration): diff --git a/mayan/apps/documents/migrations/0014_auto_20150708_0107.py b/mayan/apps/documents/migrations/0014_auto_20150708_0107.py index 755710f2b1..ffb4f10ba1 100644 --- a/mayan/apps/documents/migrations/0014_auto_20150708_0107.py +++ b/mayan/apps/documents/migrations/0014_auto_20150708_0107.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations +from django.db import migrations class Migration(migrations.Migration): diff --git a/mayan/apps/documents/models.py b/mayan/apps/documents/models.py index 4a66446bd7..3a965df93a 100644 --- a/mayan/apps/documents/models.py +++ b/mayan/apps/documents/models.py @@ -102,7 +102,7 @@ class Document(models.Model): """ uuid = models.CharField(default=UUID_FUNCTION, editable=False, max_length=48) - document_type = models.ForeignKey(DocumentType, related_name='documents', verbose_name=_('Document type')) + document_type = models.ForeignKey(DocumentType, related_name='documents', verbose_name=_('Document type')) label = models.CharField(db_index=True, default=_('Uninitialized document'), max_length=255, help_text=_('The name of the document'), verbose_name=_('Label')) description = models.TextField(blank=True, verbose_name=_('Description')) date_added = models.DateTimeField(auto_now_add=True, verbose_name=_('Added')) diff --git a/mayan/apps/documents/tasks.py b/mayan/apps/documents/tasks.py index df7726795f..6dfdc725e3 100644 --- a/mayan/apps/documents/tasks.py +++ b/mayan/apps/documents/tasks.py @@ -4,7 +4,6 @@ from datetime import timedelta import logging from django.contrib.auth.models import User -from django.core.files import File from django.utils.timezone import now from mayan.celery import app diff --git a/mayan/apps/documents/test_api.py b/mayan/apps/documents/test_api.py index 1cf2f12fbe..01dd5101b1 100644 --- a/mayan/apps/documents/test_api.py +++ b/mayan/apps/documents/test_api.py @@ -3,13 +3,9 @@ from __future__ import unicode_literals from json import loads -import os -from django.conf import settings from django.contrib.auth.models import User -from django.core.files import File from django.core.urlresolvers import reverse -from django.test.client import Client from django.test import TestCase from rest_framework import status @@ -18,11 +14,9 @@ from rest_framework.test import APIClient from .models import Document, DocumentType from .test_models import ( TEST_ADMIN_PASSWORD, TEST_ADMIN_USERNAME, TEST_ADMIN_EMAIL, - TEST_SMALL_DOCUMENT_FILENAME, TEST_NON_ASCII_DOCUMENT_FILENAME, - TEST_NON_ASCII_COMPRESSED_DOCUMENT_FILENAME, TEST_DOCUMENT_PATH, - TEST_SIGNED_DOCUMENT_PATH, TEST_SMALL_DOCUMENT_PATH, - TEST_NON_ASCII_DOCUMENT_PATH, TEST_NON_ASCII_COMPRESSED_DOCUMENT_PATH, - TEST_DOCUMENT_DESCRIPTION, TEST_DOCUMENT_TYPE + TEST_SMALL_DOCUMENT_FILENAME, TEST_DOCUMENT_PATH, + TEST_SMALL_DOCUMENT_PATH, + TEST_DOCUMENT_TYPE ) diff --git a/mayan/apps/documents/test_models.py b/mayan/apps/documents/test_models.py index 9bd668ac91..04c8d6a432 100644 --- a/mayan/apps/documents/test_models.py +++ b/mayan/apps/documents/test_models.py @@ -1,20 +1,12 @@ # -*- coding: utf-8 -*- - from __future__ import unicode_literals -from json import loads import os from django.conf import settings -from django.contrib.auth.models import User from django.core.files import File -from django.core.urlresolvers import reverse -from django.test.client import Client from django.test import TestCase -from rest_framework import status -from rest_framework.test import APIClient - from .models import DeletedDocument, Document, DocumentType TEST_ADMIN_PASSWORD = 'test_admin_password' @@ -24,7 +16,6 @@ TEST_SMALL_DOCUMENT_FILENAME = 'title_page.png' TEST_NON_ASCII_DOCUMENT_FILENAME = 'I18N_title_áéíóúüñÑ.png' TEST_NON_ASCII_COMPRESSED_DOCUMENT_FILENAME = 'I18N_title_áéíóúüñÑ.png.zip' TEST_DOCUMENT_PATH = os.path.join(settings.BASE_DIR, 'contrib', 'sample_documents', 'mayan_11_1.pdf') -TEST_SIGNED_DOCUMENT_PATH = os.path.join(settings.BASE_DIR, 'contrib', 'sample_documents', 'mayan_11_1.pdf.gpg') TEST_SMALL_DOCUMENT_PATH = os.path.join(settings.BASE_DIR, 'contrib', 'sample_documents', TEST_SMALL_DOCUMENT_FILENAME) TEST_NON_ASCII_DOCUMENT_PATH = os.path.join(settings.BASE_DIR, 'contrib', 'sample_documents', TEST_NON_ASCII_DOCUMENT_FILENAME) TEST_NON_ASCII_COMPRESSED_DOCUMENT_PATH = os.path.join(settings.BASE_DIR, 'contrib', 'sample_documents', TEST_NON_ASCII_COMPRESSED_DOCUMENT_FILENAME) diff --git a/mayan/apps/documents/test_views.py b/mayan/apps/documents/test_views.py index 51d2efb71f..c8d8e3a49f 100644 --- a/mayan/apps/documents/test_views.py +++ b/mayan/apps/documents/test_views.py @@ -2,27 +2,16 @@ from __future__ import unicode_literals -from json import loads -import os - -from django.conf import settings from django.contrib.auth.models import User from django.core.files import File from django.core.urlresolvers import reverse from django.test.client import Client from django.test import TestCase -from rest_framework import status -from rest_framework.test import APIClient - from .models import DeletedDocument, Document, DocumentType from .test_models import ( TEST_ADMIN_PASSWORD, TEST_ADMIN_USERNAME, TEST_ADMIN_EMAIL, - TEST_SMALL_DOCUMENT_FILENAME, TEST_NON_ASCII_DOCUMENT_FILENAME, - TEST_NON_ASCII_COMPRESSED_DOCUMENT_FILENAME, TEST_DOCUMENT_PATH, - TEST_SIGNED_DOCUMENT_PATH, TEST_SMALL_DOCUMENT_PATH, - TEST_NON_ASCII_DOCUMENT_PATH, TEST_NON_ASCII_COMPRESSED_DOCUMENT_PATH, - TEST_DOCUMENT_DESCRIPTION, TEST_DOCUMENT_TYPE + TEST_SMALL_DOCUMENT_PATH, TEST_DOCUMENT_TYPE ) @@ -45,7 +34,7 @@ class DocumentsViewsFunctionalTestCase(TestCase): self.assertTrue(logged_in) self.assertTrue(self.admin_user.is_authenticated()) - with open(TEST_DOCUMENT_PATH) as file_object: + with open(TEST_SMALL_DOCUMENT_PATH) as file_object: self.document = self.document_type.new_document(file_object=File(file_object), label='mayan_11_1.pdf') def tearDown(self): diff --git a/mayan/apps/dynamic_search/models.py b/mayan/apps/dynamic_search/models.py index 7fbd6d76b2..a5b13810dd 100644 --- a/mayan/apps/dynamic_search/models.py +++ b/mayan/apps/dynamic_search/models.py @@ -23,9 +23,9 @@ class RecentSearch(models.Model): # Should be fixed by DRF v2.4.4 # TODO: Fix after upgrade to DRF v2.4.4 - query = models.TextField(verbose_name=_('Query'), editable=False) - datetime_created = models.DateTimeField(verbose_name=_('Datetime created'), auto_now=True, db_index=True) - hits = models.IntegerField(verbose_name=_('Hits'), editable=False) + query = models.TextField(editable=False, verbose_name=_('Query')) + datetime_created = models.DateTimeField(auto_now=True, db_index=True, verbose_name=_('Datetime created')) + hits = models.IntegerField(editable=False, verbose_name=_('Hits')) objects = RecentSearchManager() diff --git a/mayan/apps/dynamic_search/test_models.py b/mayan/apps/dynamic_search/test_models.py index 149476a4b0..3f06c18b3f 100644 --- a/mayan/apps/dynamic_search/test_models.py +++ b/mayan/apps/dynamic_search/test_models.py @@ -2,15 +2,13 @@ from __future__ import unicode_literals from django.contrib.auth.models import User from django.core.files.base import File -from django.core.urlresolvers import reverse -from django.test.client import Client from django.test import TestCase -from documents.models import Document, DocumentType +from documents.models import DocumentType from documents.search import document_search from documents.test_models import ( TEST_ADMIN_PASSWORD, TEST_ADMIN_USERNAME, TEST_ADMIN_EMAIL, - TEST_DOCUMENT_PATH, TEST_DOCUMENT_TYPE, TEST_SMALL_DOCUMENT_PATH + TEST_DOCUMENT_TYPE, TEST_SMALL_DOCUMENT_PATH ) diff --git a/mayan/apps/dynamic_search/test_views.py b/mayan/apps/dynamic_search/test_views.py index 1aea9a5818..336e7f5c32 100644 --- a/mayan/apps/dynamic_search/test_views.py +++ b/mayan/apps/dynamic_search/test_views.py @@ -6,11 +6,11 @@ from django.core.urlresolvers import reverse from django.test.client import Client from django.test import TestCase -from documents.models import Document, DocumentType +from documents.models import DocumentType from documents.search import document_search from documents.test_models import ( TEST_ADMIN_PASSWORD, TEST_ADMIN_USERNAME, TEST_ADMIN_EMAIL, - TEST_DOCUMENT_PATH, TEST_DOCUMENT_TYPE, TEST_SMALL_DOCUMENT_PATH + TEST_DOCUMENT_TYPE, TEST_SMALL_DOCUMENT_PATH ) diff --git a/mayan/apps/events/views.py b/mayan/apps/events/views.py index b7e4641a0c..fd25cb1074 100644 --- a/mayan/apps/events/views.py +++ b/mayan/apps/events/views.py @@ -2,10 +2,8 @@ from __future__ import absolute_import, unicode_literals from django.contrib.contenttypes.models import ContentType from django.core.exceptions import PermissionDenied -from django.db.models.loading import get_model from django.http import Http404 -from django.shortcuts import get_object_or_404, render_to_response -from django.template import RequestContext +from django.shortcuts import get_object_or_404 from django.utils.translation import ugettext_lazy as _ from actstream.models import Action, any_stream diff --git a/mayan/apps/folders/apps.py b/mayan/apps/folders/apps.py index 4fd62941a9..01d5b2d34e 100644 --- a/mayan/apps/folders/apps.py +++ b/mayan/apps/folders/apps.py @@ -9,7 +9,6 @@ from common import ( MayanAppConfig, menu_facet, menu_main, menu_object, menu_secondary, menu_sidebar, menu_multi_item ) -from common.utils import encapsulate from documents.models import Document from navigation import CombinedSource, SourceColumn from rest_api.classes import APIEndPoint diff --git a/mayan/apps/folders/forms.py b/mayan/apps/folders/forms.py index 5d070f8203..9b9a3fedb3 100644 --- a/mayan/apps/folders/forms.py +++ b/mayan/apps/folders/forms.py @@ -15,12 +15,6 @@ from .permissions import permission_folder_view logger = logging.getLogger(__name__) -class FolderForm(forms.ModelForm): - class Meta: - model = Folder - fields = ('label',) - - class FolderListForm(forms.Form): def __init__(self, *args, **kwargs): user = kwargs.pop('user', None) diff --git a/mayan/apps/folders/migrations/0002_auto_20150708_0333.py b/mayan/apps/folders/migrations/0002_auto_20150708_0333.py index 540eea89c2..03cfbb5551 100644 --- a/mayan/apps/folders/migrations/0002_auto_20150708_0333.py +++ b/mayan/apps/folders/migrations/0002_auto_20150708_0333.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations +from django.db import migrations class Migration(migrations.Migration): diff --git a/mayan/apps/folders/models.py b/mayan/apps/folders/models.py index c451586d27..d0f98663cd 100644 --- a/mayan/apps/folders/models.py +++ b/mayan/apps/folders/models.py @@ -11,9 +11,9 @@ from documents.models import Document @python_2_unicode_compatible class Folder(models.Model): - label = models.CharField(max_length=128, verbose_name=_('Label'), db_index=True) + label = models.CharField(db_index=True, max_length=128, verbose_name=_('Label')) user = models.ForeignKey(User, verbose_name=_('User')) - datetime_created = models.DateTimeField(verbose_name=_('Datetime created'), auto_now_add=True) + datetime_created = models.DateTimeField(auto_now_add=True, verbose_name=_('Datetime created')) documents = models.ManyToManyField(Document, related_name='folders', verbose_name=_('Documents')) def __str__(self): @@ -23,7 +23,7 @@ class Folder(models.Model): return reverse('folders:folder_view', args=[self.pk]) class Meta: - unique_together = ('label', 'user') ordering = ('label',) + unique_together = ('label', 'user') verbose_name = _('Folder') verbose_name_plural = _('Folders') diff --git a/mayan/apps/folders/test_models.py b/mayan/apps/folders/test_models.py index 320315a9e7..5f89a933bb 100644 --- a/mayan/apps/folders/test_models.py +++ b/mayan/apps/folders/test_models.py @@ -10,7 +10,7 @@ from django.test import TestCase from authentication.test_views import ( TEST_ADMIN_EMAIL, TEST_ADMIN_PASSWORD, TEST_ADMIN_USERNAME ) -from documents.models import Document, DocumentType +from documents.models import DocumentType from documents.test_models import TEST_DOCUMENT_TYPE from .models import Folder @@ -35,7 +35,6 @@ class FolderTestCase(TestCase): folder.delete() def test_addition_of_documents(self): - user = User.objects.all()[0] folder = Folder.objects.create(label='test', user=self.user) folder.documents.add(self.document) @@ -44,7 +43,6 @@ class FolderTestCase(TestCase): folder.delete() def test_addition_and_deletion_of_documents(self): - user = User.objects.all()[0] folder = Folder.objects.create(label='test', user=self.user) folder.documents.add(self.document) diff --git a/mayan/apps/folders/views.py b/mayan/apps/folders/views.py index 5dbe708c13..d11c6dc3c1 100644 --- a/mayan/apps/folders/views.py +++ b/mayan/apps/folders/views.py @@ -4,7 +4,7 @@ import logging from django.conf import settings from django.contrib import messages -from django.core.exceptions import PermissionDenied, ValidationError +from django.core.exceptions import PermissionDenied from django.core.urlresolvers import reverse, reverse_lazy from django.http import HttpResponseRedirect from django.shortcuts import get_object_or_404, render_to_response @@ -21,7 +21,7 @@ from documents.models import Document from documents.views import DocumentListView from permissions import Permission -from .forms import FolderForm, FolderListForm +from .forms import FolderListForm from .models import Folder from .permissions import ( permission_folder_add_document, permission_folder_create, diff --git a/mayan/apps/linking/migrations/0002_resolvedsmartlink.py b/mayan/apps/linking/migrations/0002_resolvedsmartlink.py index 658a41ab9e..4be3b71bd8 100644 --- a/mayan/apps/linking/migrations/0002_resolvedsmartlink.py +++ b/mayan/apps/linking/migrations/0002_resolvedsmartlink.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations +from django.db import migrations class Migration(migrations.Migration): diff --git a/mayan/apps/linking/migrations/0003_auto_20150708_0318.py b/mayan/apps/linking/migrations/0003_auto_20150708_0318.py index 49e34f6b84..a330b53136 100644 --- a/mayan/apps/linking/migrations/0003_auto_20150708_0318.py +++ b/mayan/apps/linking/migrations/0003_auto_20150708_0318.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations +from django.db import migrations class Migration(migrations.Migration): diff --git a/mayan/apps/linking/models.py b/mayan/apps/linking/models.py index f5313caa50..4d1a8b9e04 100644 --- a/mayan/apps/linking/models.py +++ b/mayan/apps/linking/models.py @@ -15,7 +15,7 @@ from .literals import ( @python_2_unicode_compatible class SmartLink(models.Model): label = models.CharField(max_length=96, verbose_name=_('Label')) - dynamic_label = models.CharField(blank=True, max_length=96, verbose_name=_('Dynamic label'), help_text=_('This expression will be evaluated against the current selected document.')) + dynamic_label = models.CharField(blank=True, max_length=96, help_text=_('This expression will be evaluated against the current selected document.'), verbose_name=_('Dynamic label')) enabled = models.BooleanField(default=True, verbose_name=_('Enabled')) document_types = models.ManyToManyField(DocumentType, verbose_name=_('Document types')) @@ -31,9 +31,6 @@ class SmartLink(models.Model): else: return self.label - def resolve_for(self, document): - return ResolvedSmartLink(smart_link=self, queryset=self.get_linked_document_for(document)) - def get_linked_document_for(self, document): if document.document_type.pk not in self.document_types.values_list('pk', flat=True): raise Exception(_('This smart link is not allowed for the selected document\'s type.')) @@ -57,6 +54,9 @@ class SmartLink(models.Model): else: return Document.objects.none() + def resolve_for(self, document): + return ResolvedSmartLink(smart_link=self, queryset=self.get_linked_document_for(document)) + class Meta: verbose_name = _('Smart link') verbose_name_plural = _('Smart links') @@ -70,11 +70,11 @@ class ResolvedSmartLink(SmartLink): @python_2_unicode_compatible class SmartLinkCondition(models.Model): smart_link = models.ForeignKey(SmartLink, related_name='conditions', verbose_name=_('Smart link')) - inclusion = models.CharField(default=INCLUSION_AND, max_length=16, choices=INCLUSION_CHOICES, help_text=_('The inclusion is ignored for the first item.')) - foreign_document_data = models.CharField(max_length=128, verbose_name=_('Foreign document attribute'), help_text=_('This represents the metadata of all other documents.')) - operator = models.CharField(max_length=16, choices=OPERATOR_CHOICES) - expression = models.TextField(verbose_name=_('Expression'), help_text=_('This expression will be evaluated against the current document.')) - negated = models.BooleanField(default=False, verbose_name=_('Negated'), help_text=_('Inverts the logic of the operator.')) + inclusion = models.CharField(choices=INCLUSION_CHOICES, default=INCLUSION_AND, help_text=_('The inclusion is ignored for the first item.'), max_length=16) + foreign_document_data = models.CharField(help_text=_('This represents the metadata of all other documents.'), max_length=128, verbose_name=_('Foreign document attribute')) + operator = models.CharField(choices=OPERATOR_CHOICES, max_length=16) + expression = models.TextField(help_text=_('This expression will be evaluated against the current document.'), verbose_name=_('Expression')) + negated = models.BooleanField(default=False, help_text=_('Inverts the logic of the operator.'), verbose_name=_('Negated')) enabled = models.BooleanField(default=True, verbose_name=_('Enabled')) def __str__(self): diff --git a/mayan/apps/linking/views.py b/mayan/apps/linking/views.py index b39559f19c..0048b38ff3 100644 --- a/mayan/apps/linking/views.py +++ b/mayan/apps/linking/views.py @@ -85,7 +85,7 @@ class ResolvedSmartLinkView(DocumentListView): queryset = Document.objects.none() if self.request.user.is_staff or self.request.user.is_superuser: - messages.error(request, _('Smart link query error: %s' % exception)) + messages.error(self.request, _('Smart link query error: %s' % exception)) return queryset diff --git a/mayan/apps/lock_manager/models.py b/mayan/apps/lock_manager/models.py index 553c7fb6b7..e112a1ee04 100644 --- a/mayan/apps/lock_manager/models.py +++ b/mayan/apps/lock_manager/models.py @@ -10,9 +10,9 @@ from .settings import DEFAULT_LOCK_TIMEOUT @python_2_unicode_compatible class Lock(models.Model): - creation_datetime = models.DateTimeField(verbose_name=_('Creation datetime'), auto_now_add=True) + creation_datetime = models.DateTimeField(auto_now_add=True, verbose_name=_('Creation datetime')) timeout = models.IntegerField(default=DEFAULT_LOCK_TIMEOUT, verbose_name=_('Timeout')) - name = models.CharField(max_length=64, verbose_name=_('Name'), unique=True) + name = models.CharField(max_length=64, unique=True, verbose_name=_('Name')) objects = LockManager() diff --git a/mayan/apps/metadata/apps.py b/mayan/apps/metadata/apps.py index 69d7fd60af..53572b841b 100644 --- a/mayan/apps/metadata/apps.py +++ b/mayan/apps/metadata/apps.py @@ -58,8 +58,6 @@ class MetadataApp(MayanAppConfig): ModelAttribute(Document, 'metadata__value', label=_('Metadata type value'), type_name='query') ModelAttribute(Document, 'metadata_value_of', label=_('Value of a metadata'), description=_('Return the value of a specific document metadata'), type_name=['property', 'indexing']) - SourceColumn(source=Document, label=_('Metadata'), attribute=encapsulate(lambda document: get_metadata_string(document))) - ModelPermission.register( model=Document, permissions=( permission_metadata_document_add, permission_metadata_document_edit, @@ -67,6 +65,8 @@ class MetadataApp(MayanAppConfig): ) ) + SourceColumn(source=Document, label=_('Metadata'), attribute=encapsulate(lambda document: get_metadata_string(document))) + document_search.add_model_field(field='metadata__metadata_type__name', label=_('Metadata type')) document_search.add_model_field(field='metadata__value', label=_('Metadata value')) @@ -79,6 +79,6 @@ class MetadataApp(MayanAppConfig): menu_sidebar.bind_links(links=[link_metadata_add, link_metadata_edit, link_metadata_remove], sources=['metadata:metadata_add', 'metadata:metadata_edit', 'metadata:metadata_remove', 'metadata:metadata_view']) menu_tools.bind_links(links=[link_documents_missing_required_metadata]) - post_save.connect(post_document_type_metadata_type_add, dispatch_uid='post_document_type_metadata_type_add', sender=DocumentTypeMetadataType) post_delete.connect(post_document_type_metadata_type_delete, dispatch_uid='post_document_type_metadata_type_delete', sender=DocumentTypeMetadataType) post_document_type_change.connect(post_post_document_type_change_metadata, dispatch_uid='post_post_document_type_change_metadata', sender=Document) + post_save.connect(post_document_type_metadata_type_add, dispatch_uid='post_document_type_metadata_type_add', sender=DocumentTypeMetadataType) diff --git a/mayan/apps/metadata/forms.py b/mayan/apps/metadata/forms.py index 3415a7c143..2dce44d130 100644 --- a/mayan/apps/metadata/forms.py +++ b/mayan/apps/metadata/forms.py @@ -7,10 +7,7 @@ from django.utils.module_loading import import_string from django.utils.translation import ugettext_lazy as _ from .models import MetadataType -from .settings import ( - setting_available_functions, setting_available_models, - setting_available_validators -) +from .settings import setting_available_functions, setting_available_models class MetadataForm(forms.Form): diff --git a/mayan/apps/metadata/migrations/0003_auto_20150708_0323.py b/mayan/apps/metadata/migrations/0003_auto_20150708_0323.py index 97140dff76..3e9aaa6674 100644 --- a/mayan/apps/metadata/migrations/0003_auto_20150708_0323.py +++ b/mayan/apps/metadata/migrations/0003_auto_20150708_0323.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations +from django.db import migrations class Migration(migrations.Migration): diff --git a/mayan/apps/metadata/models.py b/mayan/apps/metadata/models.py index 0ceba77f54..c226d80fe6 100644 --- a/mayan/apps/metadata/models.py +++ b/mayan/apps/metadata/models.py @@ -20,16 +20,12 @@ class MetadataType(models.Model): """ Define a type of metadata """ - name = models.CharField(unique=True, max_length=48, verbose_name=_('Name'), help_text=_('Do not use python reserved words, or spaces.')) + name = models.CharField(max_length=48, help_text=_('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(max_length=128, blank=True, null=True, - verbose_name=_('Default'), - help_text=_('Enter a string to be evaluated.')) + default = models.CharField(blank=True, max_length=128, null=True, help_text=_('Enter a string to be evaluated.'), 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, - verbose_name=_('Lookup'), - help_text=_('Enter a string to be evaluated that returns an iterable.')) + lookup = models.TextField(blank=True, null=True, help_text=_('Enter a string to be evaluated that returns an iterable.'), 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 @@ -55,7 +51,7 @@ class DocumentMetadata(models.Model): """ document = models.ForeignKey(Document, related_name='metadata', verbose_name=_('Document')) metadata_type = models.ForeignKey(MetadataType, verbose_name=_('Type')) - value = models.CharField(max_length=255, blank=True, null=True, verbose_name=_('Value'), db_index=True) + value = models.CharField(blank=True, db_index=True, max_length=255, null=True, verbose_name=_('Value')) def __str__(self): return unicode(self.metadata_type) diff --git a/mayan/apps/ocr/migrations/0003_auto_20150617_0401.py b/mayan/apps/ocr/migrations/0003_auto_20150617_0401.py index e6c1755858..c54acfd1a6 100644 --- a/mayan/apps/ocr/migrations/0003_auto_20150617_0401.py +++ b/mayan/apps/ocr/migrations/0003_auto_20150617_0401.py @@ -1,7 +1,8 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations +from django.db import migrations + def move_content_from_documents_to_ocr_app(apps, schema_editor): DocumentPage = apps.get_model('documents', 'DocumentPage') diff --git a/mayan/apps/ocr/models.py b/mayan/apps/ocr/models.py index d35a1de907..772e801634 100644 --- a/mayan/apps/ocr/models.py +++ b/mayan/apps/ocr/models.py @@ -22,7 +22,7 @@ class DocumentTypeSettings(models.Model): @python_2_unicode_compatible class DocumentVersionOCRError(models.Model): document_version = models.ForeignKey(DocumentVersion, verbose_name=_('Document version')) - datetime_submitted = models.DateTimeField(verbose_name=_('Date time submitted'), auto_now=True, db_index=True) + datetime_submitted = models.DateTimeField(auto_now=True, db_index=True, verbose_name=_('Date time submitted')) result = models.TextField(blank=True, null=True, verbose_name=_('Result')) def __str__(self): diff --git a/mayan/apps/ocr/test_models.py b/mayan/apps/ocr/test_models.py index 9780d42cd5..dbc425ff08 100644 --- a/mayan/apps/ocr/test_models.py +++ b/mayan/apps/ocr/test_models.py @@ -3,7 +3,7 @@ from __future__ import unicode_literals from django.core.files.base import File from django.test import TransactionTestCase -from documents.models import Document, DocumentType +from documents.models import DocumentType from documents.test_models import TEST_SMALL_DOCUMENT_PATH, TEST_DOCUMENT_TYPE diff --git a/mayan/apps/ocr/views.py b/mayan/apps/ocr/views.py index c866761f1b..10a7694f43 100644 --- a/mayan/apps/ocr/views.py +++ b/mayan/apps/ocr/views.py @@ -15,7 +15,7 @@ from documents.models import Document, DocumentType, DocumentVersion from permissions import Permission from .forms import DocumentContentForm -from .models import DocumentTypeSettings, DocumentVersionOCRError +from .models import DocumentVersionOCRError from .permissions import ( permission_ocr_content_view, permission_ocr_document, permission_ocr_document_delete, permission_document_type_ocr_setup diff --git a/mayan/apps/permissions/apps.py b/mayan/apps/permissions/apps.py index 25ec68903d..9b2806bc20 100644 --- a/mayan/apps/permissions/apps.py +++ b/mayan/apps/permissions/apps.py @@ -1,7 +1,5 @@ from __future__ import unicode_literals -from django.contrib.auth.models import User -from django.db.models.signals import post_save from django.utils.translation import ugettext_lazy as _ from common import ( diff --git a/mayan/apps/permissions/migrations/0003_remove_role_name.py b/mayan/apps/permissions/migrations/0003_remove_role_name.py index 204fa1a11b..cc4c670be5 100644 --- a/mayan/apps/permissions/migrations/0003_remove_role_name.py +++ b/mayan/apps/permissions/migrations/0003_remove_role_name.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations +from django.db import migrations class Migration(migrations.Migration): diff --git a/mayan/apps/permissions/test_models.py b/mayan/apps/permissions/test_models.py index 7df2a6537f..c3853c6fc6 100644 --- a/mayan/apps/permissions/test_models.py +++ b/mayan/apps/permissions/test_models.py @@ -1,16 +1,12 @@ from __future__ import unicode_literals -from django.conf import settings from django.contrib.auth import get_user_model from django.contrib.auth.models import Group from django.core.exceptions import PermissionDenied -from django.core.files import File -from django.core.urlresolvers import reverse -from django.test.client import Client from django.test import TestCase from .classes import Permission -from .models import Role, StoredPermission +from .models import Role from .permissions import permission_role_view diff --git a/mayan/apps/sources/migrations/0001_initial.py b/mayan/apps/sources/migrations/0001_initial.py index 4094e579cc..abbf2f7e4e 100644 --- a/mayan/apps/sources/migrations/0001_initial.py +++ b/mayan/apps/sources/migrations/0001_initial.py @@ -2,7 +2,6 @@ from __future__ import unicode_literals from django.db import models, migrations -import sources.models class Migration(migrations.Migration): diff --git a/mayan/apps/sources/migrations/0002_auto_20150608_1902.py b/mayan/apps/sources/migrations/0002_auto_20150608_1902.py index b5a91b30ab..1ee366c90f 100644 --- a/mayan/apps/sources/migrations/0002_auto_20150608_1902.py +++ b/mayan/apps/sources/migrations/0002_auto_20150608_1902.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations +from django.db import migrations class Migration(migrations.Migration): diff --git a/mayan/apps/sources/migrations/0004_auto_20150616_1931.py b/mayan/apps/sources/migrations/0004_auto_20150616_1931.py index e7afb13745..e6d4484d84 100644 --- a/mayan/apps/sources/migrations/0004_auto_20150616_1931.py +++ b/mayan/apps/sources/migrations/0004_auto_20150616_1931.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations +from django.db import migrations class Migration(migrations.Migration): diff --git a/mayan/apps/sources/migrations/0005_auto_20150708_0327.py b/mayan/apps/sources/migrations/0005_auto_20150708_0327.py index ae39e961af..f3fc70ddd8 100644 --- a/mayan/apps/sources/migrations/0005_auto_20150708_0327.py +++ b/mayan/apps/sources/migrations/0005_auto_20150708_0327.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations +from django.db import migrations class Migration(migrations.Migration): diff --git a/mayan/apps/sources/models.py b/mayan/apps/sources/models.py index 6f6bf627e2..db94366984 100644 --- a/mayan/apps/sources/models.py +++ b/mayan/apps/sources/models.py @@ -108,11 +108,11 @@ class StagingFolderSource(InteractiveSource): is_interactive = True source_type = SOURCE_CHOICE_STAGING - folder_path = models.CharField(max_length=255, verbose_name=_('Folder path'), help_text=_('Server side filesystem path.')) - preview_width = models.IntegerField(verbose_name=_('Preview width'), help_text=_('Width value to be passed to the converter backend.')) - preview_height = models.IntegerField(blank=True, null=True, verbose_name=_('Preview height'), help_text=_('Height value to be passed to the converter backend.')) - uncompress = models.CharField(max_length=1, choices=SOURCE_INTERACTIVE_UNCOMPRESS_CHOICES, verbose_name=_('Uncompress'), help_text=_('Whether to expand or not compressed archives.')) - delete_after_upload = models.BooleanField(default=True, verbose_name=_('Delete after upload'), help_text=_('Delete the file after is has been successfully uploaded.')) + folder_path = models.CharField(max_length=255, help_text=_('Server side filesystem path.'), verbose_name=_('Folder path')) + preview_width = models.IntegerField(help_text=_('Width value to be passed to the converter backend.'), verbose_name=_('Preview width')) + preview_height = models.IntegerField(blank=True, null=True, help_text=_('Height value to be passed to the converter backend.'), verbose_name=_('Preview height')) + uncompress = models.CharField(choices=SOURCE_INTERACTIVE_UNCOMPRESS_CHOICES, max_length=1, help_text=_('Whether to expand or not compressed archives.'), verbose_name=_('Uncompress')) + delete_after_upload = models.BooleanField(default=True, help_text=_('Delete the file after is has been successfully uploaded.'), verbose_name=_('Delete after upload')) def get_preview_size(self): dimensions = [] @@ -155,7 +155,7 @@ class WebFormSource(InteractiveSource): source_type = SOURCE_CHOICE_WEB_FORM # TODO: unify uncompress as an InteractiveSource field - uncompress = models.CharField(max_length=1, choices=SOURCE_INTERACTIVE_UNCOMPRESS_CHOICES, verbose_name=_('Uncompress'), help_text=_('Whether to expand or not compressed archives.')) + uncompress = models.CharField(choices=SOURCE_INTERACTIVE_UNCOMPRESS_CHOICES, help_text=_('Whether to expand or not compressed archives.'), max_length=1, verbose_name=_('Uncompress')) # Default path def get_upload_file_object(self, form_data): @@ -175,9 +175,9 @@ class OutOfProcessSource(Source): class IntervalBaseModel(OutOfProcessSource): - interval = models.PositiveIntegerField(default=DEFAULT_INTERVAL, verbose_name=_('Interval'), help_text=_('Interval in seconds between checks for new documents.')) - document_type = models.ForeignKey(DocumentType, verbose_name=_('Document type'), help_text=_('Assign a document type to documents uploaded from this source.')) - uncompress = models.CharField(max_length=1, choices=SOURCE_UNCOMPRESS_CHOICES, verbose_name=_('Uncompress'), help_text=_('Whether to expand or not, compressed archives.')) + interval = models.PositiveIntegerField(default=DEFAULT_INTERVAL, help_text=_('Interval in seconds between checks for new documents.'), verbose_name=_('Interval')) + document_type = models.ForeignKey(DocumentType, help_text=_('Assign a document type to documents uploaded from this source.'), verbose_name=_('Document type')) + uncompress = models.CharField(choices=SOURCE_UNCOMPRESS_CHOICES, help_text=_('Whether to expand or not, compressed archives.'), max_length=1, verbose_name=_('Uncompress')) def _get_periodic_task_name(self, pk=None): return 'check_interval_source-%i' % (pk or self.pk) @@ -223,7 +223,7 @@ class IntervalBaseModel(OutOfProcessSource): class EmailBaseModel(IntervalBaseModel): host = models.CharField(max_length=128, verbose_name=_('Host')) ssl = models.BooleanField(default=True, verbose_name=_('SSL')) - port = models.PositiveIntegerField(blank=True, null=True, verbose_name=_('Port'), help_text=_('Typical choices are 110 for POP3, 995 for POP3 over SSL, 143 for IMAP, 993 for IMAP over SSL.')) + port = models.PositiveIntegerField(blank=True, null=True, help_text=_('Typical choices are 110 for POP3, 995 for POP3 over SSL, 143 for IMAP, 993 for IMAP over SSL.'), verbose_name=_('Port')) username = models.CharField(max_length=96, verbose_name=_('Username')) password = models.CharField(max_length=96, verbose_name=_('Password')) @@ -302,7 +302,7 @@ class POP3Email(EmailBaseModel): class IMAPEmail(EmailBaseModel): source_type = SOURCE_CHOICE_EMAIL_IMAP - mailbox = models.CharField(max_length=64, default=DEFAULT_IMAP_MAILBOX, verbose_name=_('Mailbox'), help_text=_('Mail from which to check for messages with attached documents.')) + mailbox = models.CharField(default=DEFAULT_IMAP_MAILBOX, help_text=_('Mail from which to check for messages with attached documents.'), max_length=64, verbose_name=_('Mailbox')) # http://www.doughellmann.com/PyMOTW/imaplib/ def check_source(self): @@ -341,7 +341,7 @@ class IMAPEmail(EmailBaseModel): class WatchFolderSource(IntervalBaseModel): source_type = SOURCE_CHOICE_WATCH - folder_path = models.CharField(max_length=255, verbose_name=_('Folder path'), help_text=_('Server side filesystem path.')) + folder_path = models.CharField(help_text=_('Server side filesystem path.'), max_length=255, verbose_name=_('Folder path')) def check_source(self): # Force self.folder_path to unicode to avoid os.listdir returning diff --git a/mayan/apps/sources/tasks.py b/mayan/apps/sources/tasks.py index 75d65acf60..57b69bb82a 100644 --- a/mayan/apps/sources/tasks.py +++ b/mayan/apps/sources/tasks.py @@ -1,7 +1,6 @@ import logging from django.contrib.auth.models import User -from django.core.files import File from django.utils.translation import ugettext_lazy as _ from mayan.celery import app diff --git a/mayan/apps/sources/test_models.py b/mayan/apps/sources/test_models.py index aee6e57f63..4ca5eed24b 100644 --- a/mayan/apps/sources/test_models.py +++ b/mayan/apps/sources/test_models.py @@ -4,23 +4,18 @@ import shutil import tempfile from django.contrib.auth.models import User -from django.core.urlresolvers import reverse from django.test.client import Client from django.test import TestCase from documents.models import Document, DocumentType -from sources.literals import SOURCE_CHOICE_WEB_FORM -from sources.models import WebFormSource from documents.test_models import ( TEST_ADMIN_PASSWORD, TEST_ADMIN_USERNAME, TEST_ADMIN_EMAIL, - TEST_DOCUMENT_PATH, TEST_SMALL_DOCUMENT_PATH, - TEST_DOCUMENT_DESCRIPTION, TEST_DOCUMENT_TYPE, - TEST_NON_ASCII_DOCUMENT_FILENAME, TEST_NON_ASCII_DOCUMENT_PATH, - TEST_NON_ASCII_COMPRESSED_DOCUMENT_PATH + TEST_DOCUMENT_TYPE, TEST_NON_ASCII_DOCUMENT_FILENAME, + TEST_NON_ASCII_DOCUMENT_PATH, TEST_NON_ASCII_COMPRESSED_DOCUMENT_PATH ) -from .literals import SOURCE_UNCOMPRESS_CHOICE_N, SOURCE_UNCOMPRESS_CHOICE_Y +from .literals import SOURCE_UNCOMPRESS_CHOICE_Y from .models import WatchFolderSource diff --git a/mayan/apps/sources/test_views.py b/mayan/apps/sources/test_views.py index 65a065334f..cd0064cc97 100644 --- a/mayan/apps/sources/test_views.py +++ b/mayan/apps/sources/test_views.py @@ -1,8 +1,5 @@ from __future__ import unicode_literals -import shutil -import tempfile - from django.contrib.auth.models import User from django.core.urlresolvers import reverse from django.test.client import Client @@ -16,13 +13,8 @@ from documents.test_models import ( TEST_ADMIN_PASSWORD, TEST_ADMIN_USERNAME, TEST_ADMIN_EMAIL, TEST_DOCUMENT_PATH, TEST_SMALL_DOCUMENT_PATH, TEST_DOCUMENT_DESCRIPTION, TEST_DOCUMENT_TYPE, - TEST_NON_ASCII_DOCUMENT_FILENAME, TEST_NON_ASCII_DOCUMENT_PATH, - TEST_NON_ASCII_COMPRESSED_DOCUMENT_PATH ) -from .literals import SOURCE_UNCOMPRESS_CHOICE_N, SOURCE_UNCOMPRESS_CHOICE_Y -from .models import WatchFolderSource - class UploadDocumentTestCase(TestCase): """ diff --git a/mayan/apps/tags/models.py b/mayan/apps/tags/models.py index 23081bf1ea..18e5cff998 100644 --- a/mayan/apps/tags/models.py +++ b/mayan/apps/tags/models.py @@ -11,8 +11,8 @@ from .literals import COLOR_CHOICES, COLOR_CODES @python_2_unicode_compatible class Tag(models.Model): - label = models.CharField(max_length=128, verbose_name=_('Label'), unique=True, db_index=True) - color = models.CharField(max_length=3, choices=COLOR_CHOICES, verbose_name=_('Color')) + label = models.CharField(db_index=True, max_length=128, unique=True, verbose_name=_('Label')) + color = models.CharField(choices=COLOR_CHOICES, max_length=3, verbose_name=_('Color')) documents = models.ManyToManyField(Document, related_name='tags', verbose_name=_('Documents')) class Meta: diff --git a/mayan/apps/user_management/forms.py b/mayan/apps/user_management/forms.py index a297806894..c4dd673a9e 100644 --- a/mayan/apps/user_management/forms.py +++ b/mayan/apps/user_management/forms.py @@ -1,7 +1,7 @@ from __future__ import unicode_literals from django import forms -from django.contrib.auth.models import User, Group +from django.contrib.auth.models import User from django.utils.translation import ugettext_lazy as _ diff --git a/mayan/apps/user_management/permissions.py b/mayan/apps/user_management/permissions.py index 78a2440b66..9a6fc61616 100644 --- a/mayan/apps/user_management/permissions.py +++ b/mayan/apps/user_management/permissions.py @@ -6,11 +6,11 @@ from permissions import PermissionNamespace namespace = PermissionNamespace('user_management', _('User management')) -permission_user_create = namespace.add_permission(name='user_create', label=_('Create new users')) -permission_user_edit = namespace.add_permission(name='user_edit', label=_('Edit existing users')) -permission_user_view = namespace.add_permission(name='user_view', label=_('View existing users')) -permission_user_delete = namespace.add_permission(name='user_delete', label=_('Delete existing users')) permission_group_create = namespace.add_permission(name='group_create', label=_('Create new groups')) +permission_group_delete = namespace.add_permission(name='group_delete', label=_('Delete existing groups')) permission_group_edit = namespace.add_permission(name='group_edit', label=_('Edit existing groups')) permission_group_view = namespace.add_permission(name='group_view', label=_('View existing groups')) -permission_group_delete = namespace.add_permission(name='group_delete', label=_('Delete existing groups')) +permission_user_create = namespace.add_permission(name='user_create', label=_('Create new users')) +permission_user_delete = namespace.add_permission(name='user_delete', label=_('Delete existing users')) +permission_user_edit = namespace.add_permission(name='user_edit', label=_('Edit existing users')) +permission_user_view = namespace.add_permission(name='user_view', label=_('View existing users')) diff --git a/mayan/apps/user_management/urls.py b/mayan/apps/user_management/urls.py index 9eb46412b5..52454bd3a4 100644 --- a/mayan/apps/user_management/urls.py +++ b/mayan/apps/user_management/urls.py @@ -13,6 +13,13 @@ from .views import ( urlpatterns = patterns( 'user_management.views', + url(r'^group/list/$', GroupListView.as_view(), name='group_list'), + url(r'^group/add/$', GroupCreateView.as_view(), name='group_add'), + url(r'^group/(?P\d+)/edit/$', GroupEditView.as_view(), name='group_edit'), + url(r'^group/(?P\d+)/delete/$', 'group_delete', name='group_delete'), + url(r'^group/multiple/delete/$', 'group_multiple_delete', name='group_multiple_delete'), + url(r'^group/(?P\d+)/members/$', GroupMembersView.as_view(), name='group_members'), + url(r'^user/list/$', UserListView.as_view(), name='user_list'), url(r'^user/add/$', 'user_add', name='user_add'), url(r'^user/(?P\d+)/edit/$', 'user_edit', name='user_edit'), @@ -21,13 +28,6 @@ urlpatterns = patterns( url(r'^user/(?P\d+)/set_password/$', 'user_set_password', name='user_set_password'), url(r'^user/multiple/set_password/$', 'user_multiple_set_password', name='user_multiple_set_password'), url(r'^user/(?P\d+)/groups/$', UserGroupsView.as_view(), name='user_groups'), - - url(r'^group/list/$', GroupListView.as_view(), name='group_list'), - url(r'^group/add/$', GroupCreateView.as_view(), name='group_add'), - url(r'^group/(?P\d+)/edit/$', GroupEditView.as_view(), name='group_edit'), - url(r'^group/(?P\d+)/delete/$', 'group_delete', name='group_delete'), - url(r'^group/multiple/delete/$', 'group_multiple_delete', name='group_multiple_delete'), - url(r'^group/(?P\d+)/members/$', GroupMembersView.as_view(), name='group_members'), ) api_urls = patterns(