From 92e615ce4ce5733a267ad2bfba4591d9e4d08173 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Wed, 2 Jan 2019 14:19:32 -0400 Subject: [PATCH 001/209] Add keyword arguments to checkouts app Add keyword arguments to calls and view parameters. Add missing icons. Signed-off-by: Roberto Rosario --- mayan/apps/checkouts/dashboard_widgets.py | 12 +++-- mayan/apps/checkouts/events.py | 14 ++--- mayan/apps/checkouts/handlers.py | 2 +- mayan/apps/checkouts/icons.py | 8 +++ mayan/apps/checkouts/links.py | 12 +++-- mayan/apps/checkouts/managers.py | 10 ++-- mayan/apps/checkouts/models.py | 4 +- mayan/apps/checkouts/permissions.py | 8 +-- mayan/apps/checkouts/queues.py | 6 +-- mayan/apps/checkouts/serializers.py | 2 +- mayan/apps/checkouts/tests/test_api.py | 30 +++++++---- mayan/apps/checkouts/tests/test_views.py | 20 ++++--- mayan/apps/checkouts/urls.py | 26 ++++++---- mayan/apps/checkouts/views.py | 63 ++++++++++++++--------- 14 files changed, 134 insertions(+), 83 deletions(-) diff --git a/mayan/apps/checkouts/dashboard_widgets.py b/mayan/apps/checkouts/dashboard_widgets.py index 53bcd6eb08..27a85d752d 100644 --- a/mayan/apps/checkouts/dashboard_widgets.py +++ b/mayan/apps/checkouts/dashboard_widgets.py @@ -14,7 +14,7 @@ from .permissions import permission_document_checkout_detail_view class DashboardWidgetTotalCheckouts(DashboardWidgetNumeric): icon_class = icon_dashboard_checkouts label = _('Checkedout documents') - link = reverse_lazy('checkouts:checkout_list') + link = reverse_lazy(viewname='checkouts:checkout_list') def render(self, request): AccessControlList = apps.get_model( @@ -25,12 +25,14 @@ class DashboardWidgetTotalCheckouts(DashboardWidgetNumeric): ) queryset = AccessControlList.objects.filter_by_access( permission=permission_document_checkout_detail_view, + queryset=DocumentCheckout.objects.checked_out_documents(), user=request.user, - queryset=DocumentCheckout.objects.checked_out_documents() ) queryset = AccessControlList.objects.filter_by_access( - permission=permission_document_view, user=request.user, - queryset=queryset + permission=permission_document_view, queryset=queryset, + user=request.user ) self.count = queryset.count() - return super(DashboardWidgetTotalCheckouts, self).render(request) + return super(DashboardWidgetTotalCheckouts, self).render( + request=request + ) diff --git a/mayan/apps/checkouts/events.py b/mayan/apps/checkouts/events.py index 69036d2a29..aad73d1fbb 100644 --- a/mayan/apps/checkouts/events.py +++ b/mayan/apps/checkouts/events.py @@ -4,19 +4,19 @@ from django.utils.translation import ugettext_lazy as _ from mayan.apps.events import EventTypeNamespace -namespace = EventTypeNamespace(name='checkouts', label=_('Checkouts')) +namespace = EventTypeNamespace(label=_('Checkouts'), name='checkouts') event_document_auto_check_in = namespace.add_event_type( - name='document_auto_check_in', - label=_('Document automatically checked in') + label=_('Document automatically checked in'), + name='document_auto_check_in' ) event_document_check_in = namespace.add_event_type( - name='document_check_in', label=_('Document checked in') + label=_('Document checked in'), name='document_check_in' ) event_document_check_out = namespace.add_event_type( - name='document_check_out', label=_('Document checked out') + label=_('Document checked out'), name='document_check_out' ) event_document_forceful_check_in = namespace.add_event_type( - name='document_forceful_check_in', - label=_('Document forcefully checked in') + label=_('Document forcefully checked in'), + name='document_forceful_check_in' ) diff --git a/mayan/apps/checkouts/handlers.py b/mayan/apps/checkouts/handlers.py index b3f6b9d712..ca8a1e95e2 100644 --- a/mayan/apps/checkouts/handlers.py +++ b/mayan/apps/checkouts/handlers.py @@ -13,6 +13,6 @@ def handler_check_new_version_creation(sender, instance, **kwargs): app_label='checkouts', model_name='NewVersionBlock' ) - if NewVersionBlock.objects.is_blocked(instance.document) and not instance.pk: + if NewVersionBlock.objects.is_blocked(document=instance.document) and not instance.pk: # Block only new versions (no pk), not existing version being updated. raise NewDocumentVersionNotAllowed diff --git a/mayan/apps/checkouts/icons.py b/mayan/apps/checkouts/icons.py index 8fedf84d7e..a0553346f8 100644 --- a/mayan/apps/checkouts/icons.py +++ b/mayan/apps/checkouts/icons.py @@ -2,6 +2,14 @@ from __future__ import absolute_import, unicode_literals from mayan.apps.appearance.classes import Icon +icon_checkin_document = Icon( + driver_name='fontawesome-dual', primary_symbol='shopping-cart', + secondary_symbol='minus' +) +icon_checkout_document = Icon( + driver_name='fontawesome-dual', primary_symbol='shopping-cart', + secondary_symbol='plus' +) icon_checkout_info = Icon(driver_name='fontawesome', symbol='shopping-cart') icon_dashboard_checkouts = Icon( driver_name='fontawesome', symbol='shopping-cart' diff --git a/mayan/apps/checkouts/links.py b/mayan/apps/checkouts/links.py index 61bfc99f1d..1ed63fc666 100644 --- a/mayan/apps/checkouts/links.py +++ b/mayan/apps/checkouts/links.py @@ -4,7 +4,9 @@ from django.utils.translation import ugettext_lazy as _ from mayan.apps.navigation import Link -from .icons import icon_checkout_info +from .icons import ( + icon_checkin_document, icon_checkout_document, icon_checkout_info +) from .permissions import ( permission_document_checkin, permission_document_checkin_override, permission_document_checkout @@ -33,11 +35,13 @@ link_checkout_list = Link( ) link_checkout_document = Link( args='object.pk', condition=is_not_checked_out, - permissions=(permission_document_checkout,), - text=_('Check out document'), view='checkouts:checkout_document', + icon_class=icon_checkout_document, + permissions=(permission_document_checkout,), text=_('Check out document'), + view='checkouts:checkout_document', ) link_checkin_document = Link( - args='object.pk', condition=is_checked_out, permissions=( + args='object.pk', condition=is_checked_out, + icon_class=icon_checkin_document, permissions=( permission_document_checkin, permission_document_checkin_override ), text=_('Check in document'), view='checkouts:checkin_document', diff --git a/mayan/apps/checkouts/managers.py b/mayan/apps/checkouts/managers.py index db858fff5d..6fd071e959 100644 --- a/mayan/apps/checkouts/managers.py +++ b/mayan/apps/checkouts/managers.py @@ -21,7 +21,7 @@ logger = logging.getLogger(__name__) class DocumentCheckoutManager(models.Manager): def are_document_new_versions_allowed(self, document, user=None): try: - checkout_info = self.document_checkout_info(document) + checkout_info = self.document_checkout_info(document=document) except DocumentNotCheckedOut: return True else: @@ -34,7 +34,7 @@ class DocumentCheckoutManager(models.Manager): raise DocumentNotCheckedOut else: if user: - if self.get_document_checkout_info(document).user != user: + if self.get_document_checkout_info(document=document).user != user: event_document_forceful_check_in.commit( actor=user, target=document ) @@ -51,8 +51,8 @@ class DocumentCheckoutManager(models.Manager): def checkout_document(self, document, expiration_datetime, user, block_new_version=True): return self.create( - document=document, expiration_datetime=expiration_datetime, - user=user, block_new_version=block_new_version + block_new_version=block_new_version, document=document, + expiration_datetime=expiration_datetime, user=user ) def checked_out_documents(self): @@ -80,7 +80,7 @@ class DocumentCheckoutManager(models.Manager): raise DocumentNotCheckedOut def get_document_checkout_state(self, document): - if self.is_document_checked_out(document): + if self.is_document_checked_out(document=document): return STATE_CHECKED_OUT else: return STATE_CHECKED_IN diff --git a/mayan/apps/checkouts/models.py b/mayan/apps/checkouts/models.py index aa43338d01..fa9e621855 100644 --- a/mayan/apps/checkouts/models.py +++ b/mayan/apps/checkouts/models.py @@ -73,7 +73,9 @@ class DocumentCheckout(models.Model): super(DocumentCheckout, self).delete(*args, **kwargs) def get_absolute_url(self): - return reverse('checkout:checkout_info', args=(self.document.pk,)) + return reverse( + viewname='checkout:checkout_info', kwargs={'pk': self.document.pk} + ) def natural_key(self): return self.document.natural_key() diff --git a/mayan/apps/checkouts/permissions.py b/mayan/apps/checkouts/permissions.py index 6122f54e9c..882993d6d0 100644 --- a/mayan/apps/checkouts/permissions.py +++ b/mayan/apps/checkouts/permissions.py @@ -7,14 +7,14 @@ from mayan.apps.permissions import PermissionNamespace namespace = PermissionNamespace(label=_('Document checkout'), name='checkouts') permission_document_checkin = namespace.add_permission( - name='checkin_document', label=_('Check in documents') + label=_('Check in documents'), name='checkin_document' ) permission_document_checkin_override = namespace.add_permission( - name='checkin_document_override', label=_('Forcefully check in documents') + label=_('Forcefully check in documents'), name='checkin_document_override' ) permission_document_checkout = namespace.add_permission( - name='checkout_document', label=_('Check out documents') + label=_('Check out documents'), name='checkout_document' ) permission_document_checkout_detail_view = namespace.add_permission( - name='checkout_detail_view', label=_('Check out details view') + label=_('Check out details view'), name='checkout_detail_view' ) diff --git a/mayan/apps/checkouts/queues.py b/mayan/apps/checkouts/queues.py index 3792211019..9ef477bdc8 100644 --- a/mayan/apps/checkouts/queues.py +++ b/mayan/apps/checkouts/queues.py @@ -5,9 +5,9 @@ from django.utils.translation import ugettext_lazy as _ from mayan.apps.task_manager.classes import CeleryQueue queue_checkouts_periodic = CeleryQueue( - name='checkouts_periodic', label=_('Checkouts periodic'), transient=True + label=_('Checkouts periodic'), name='checkouts_periodic', transient=True ) queue_checkouts_periodic.add_task_type( - name='mayan.apps.task_check_expired_check_outs', - label=_('Check expired checkouts') + label=_('Check expired checkouts'), + name='mayan.apps.task_check_expired_check_outs' ) diff --git a/mayan/apps/checkouts/serializers.py b/mayan/apps/checkouts/serializers.py index 4a5c1c4478..5e51e1f582 100644 --- a/mayan/apps/checkouts/serializers.py +++ b/mayan/apps/checkouts/serializers.py @@ -43,7 +43,7 @@ class NewDocumentCheckoutSerializer(serializers.ModelSerializer): AccessControlList.objects.check_access( permissions=permission_document_checkout, - user=self.context['request'].user, obj=document + obj=document, user=self.context['request'].user ) validated_data['document'] = document diff --git a/mayan/apps/checkouts/tests/test_api.py b/mayan/apps/checkouts/tests/test_api.py index b4b8ace27e..df0fabbcc2 100644 --- a/mayan/apps/checkouts/tests/test_api.py +++ b/mayan/apps/checkouts/tests/test_api.py @@ -25,7 +25,7 @@ class CheckoutsAPITestCase(DocumentTestMixin, BaseAPITestCase): def _request_checkedout_document_view(self): return self.get( viewname='rest_api:checkedout-document-view', - args=(self.checkout.pk,) + kwargs={'document_pk': self.checkout.pk} ) def _checkout_document(self): @@ -44,7 +44,8 @@ class CheckoutsAPITestCase(DocumentTestMixin, BaseAPITestCase): def test_checkedout_document_view_with_checkout_access(self): self._checkout_document() self.grant_access( - permission=permission_document_checkout_detail_view, obj=self.document + obj=self.document, + permission=permission_document_checkout_detail_view ) response = self._request_checkedout_document_view() self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) @@ -52,7 +53,7 @@ class CheckoutsAPITestCase(DocumentTestMixin, BaseAPITestCase): def test_checkedout_document_view_with_document_access(self): self._checkout_document() self.grant_access( - permission=permission_document_view, obj=self.document + obj=self.document, permission=permission_document_view ) response = self._request_checkedout_document_view() self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) @@ -60,14 +61,17 @@ class CheckoutsAPITestCase(DocumentTestMixin, BaseAPITestCase): def test_checkedout_document_view_with_access(self): self._checkout_document() self.grant_access( - permission=permission_document_view, obj=self.document + obj=self.document, permission=permission_document_view ) self.grant_access( - permission=permission_document_checkout_detail_view, obj=self.document + obj=self.document, + permission=permission_document_checkout_detail_view ) response = self._request_checkedout_document_view() self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data['document']['uuid'], force_text(self.document.uuid)) + self.assertEqual( + response.data['document']['uuid'], force_text(self.document.uuid) + ) def _request_document_checkout_view(self): return self.post( @@ -83,7 +87,9 @@ class CheckoutsAPITestCase(DocumentTestMixin, BaseAPITestCase): self.assertEqual(DocumentCheckout.objects.count(), 0) def test_document_checkout_with_access(self): - self.grant_access(permission=permission_document_checkout, obj=self.document) + self.grant_access( + obj=self.document, permission=permission_document_checkout + ) response = self._request_document_checkout_view() self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertEqual( @@ -102,7 +108,7 @@ class CheckoutsAPITestCase(DocumentTestMixin, BaseAPITestCase): def test_checkout_list_view_with_document_access(self): self._checkout_document() self.grant_access( - permission=permission_document_view, obj=self.document + obj=self.document, permission=permission_document_view ) response = self._request_checkout_list_view() self.assertEqual(response.status_code, status.HTTP_200_OK) @@ -111,7 +117,8 @@ class CheckoutsAPITestCase(DocumentTestMixin, BaseAPITestCase): def test_checkout_list_view_with_checkout_access(self): self._checkout_document() self.grant_access( - permission=permission_document_checkout_detail_view, obj=self.document + obj=self.document, + permission=permission_document_checkout_detail_view ) response = self._request_checkout_list_view() self.assertEqual(response.status_code, status.HTTP_200_OK) @@ -120,10 +127,11 @@ class CheckoutsAPITestCase(DocumentTestMixin, BaseAPITestCase): def test_checkout_list_view_with_access(self): self._checkout_document() self.grant_access( - permission=permission_document_view, obj=self.document + obj=self.document, permission=permission_document_view ) self.grant_access( - permission=permission_document_checkout_detail_view, obj=self.document + obj=self.document, + permission=permission_document_checkout_detail_view ) response = self._request_checkout_list_view() self.assertEqual(response.status_code, status.HTTP_200_OK) diff --git a/mayan/apps/checkouts/tests/test_views.py b/mayan/apps/checkouts/tests/test_views.py index 9293baa644..9e49b0cfc1 100644 --- a/mayan/apps/checkouts/tests/test_views.py +++ b/mayan/apps/checkouts/tests/test_views.py @@ -23,7 +23,8 @@ from ..permissions import ( class DocumentCheckoutViewTestCase(GenericDocumentViewTestCase): def _request_document_check_in_view(self): return self.post( - viewname='checkouts:checkin_document', args=(self.document.pk,), + viewname='checkouts:checkin_document', + kwargs={'document_pk': self.document.pk} ) def test_checkin_document_view_no_permission(self): @@ -72,7 +73,8 @@ class DocumentCheckoutViewTestCase(GenericDocumentViewTestCase): def _request_document_checkout_view(self): return self.post( - viewname='checkouts:checkout_document', args=(self.document.pk,), + viewname='checkouts:checkout_document', + kwargs={'document_pk': self.document.pk}, data={ 'expiration_datetime_0': 2, 'expiration_datetime_1': TIME_DELTA_UNIT_DAYS, @@ -125,7 +127,8 @@ class DocumentCheckoutViewTestCase(GenericDocumentViewTestCase): self.assertTrue(self.document.is_checked_out()) response = self.post( - 'sources:upload_version', args=(self.document.pk,), + viewname='sources:upload_version', + kwargs={'document_pk': self.document.pk}, follow=True ) @@ -135,7 +138,8 @@ class DocumentCheckoutViewTestCase(GenericDocumentViewTestCase): ) response = self.get( - 'documents:document_version_list', args=(self.document.pk,), + viewname='documents:document_version_list', + kwargs={'document_pk': self.document.pk}, follow=True ) @@ -176,7 +180,9 @@ class DocumentCheckoutViewTestCase(GenericDocumentViewTestCase): ) response = self.post( - 'checkouts:checkin_document', args=(self.document.pk,), follow=True + viewname='checkouts:checkin_document', + kwargs={'document_pk': self.document.pk}, + follow=True ) self.assertContains( @@ -212,7 +218,9 @@ class DocumentCheckoutViewTestCase(GenericDocumentViewTestCase): permission_document_checkout_detail_view.stored_permission ) response = self.post( - 'checkouts:checkin_document', args=(self.document.pk,), follow=True + viewname='checkouts:checkin_document', + kwargs={'document_pk': self.document.pk}, + follow=True ) self.assertContains( diff --git a/mayan/apps/checkouts/urls.py b/mayan/apps/checkouts/urls.py index 0e2dba3d32..e3360ce8b3 100644 --- a/mayan/apps/checkouts/urls.py +++ b/mayan/apps/checkouts/urls.py @@ -9,28 +9,32 @@ from .views import ( ) urlpatterns = [ - url(r'^list/$', CheckoutListView.as_view(), name='checkout_list'), url( - r'^(?P\d+)/check/out/$', CheckoutDocumentView.as_view(), - name='checkout_document' + regex=r'^documents/$', name='checkout_list', + view=CheckoutListView.as_view() ), url( - r'^(?P\d+)/check/in/$', DocumentCheckinView.as_view(), - name='checkin_document' + regex=r'^documents/(?P\d+)/check/out/$', + name='checkout_document', view=CheckoutDocumentView.as_view() ), url( - r'^(?P\d+)/check/info/$', CheckoutDetailView.as_view(), - name='checkout_info' + regex=r'^documents/(?P\d+)/check/in/$', + name='checkin_document', view=DocumentCheckinView.as_view() + ), + url( + regex=r'^documents/(?P\d+)/check/info/$', + name='checkout_info', view=CheckoutDetailView.as_view() ), ] api_urls = [ url( - r'^checkouts/$', APICheckedoutDocumentListView.as_view(), - name='checkout-document-list' + regex=r'^checkouts/$', name='checkout-document-list', + view=APICheckedoutDocumentListView.as_view() ), url( - r'^checkouts/(?P[0-9]+)/checkout_info/$', APICheckedoutDocumentView.as_view(), - name='checkedout-document-view' + regex=r'^checkouts/(?P[0-9]+)/checkout_info/$', + name='checkedout-document-view', + view=APICheckedoutDocumentView.as_view() ), ] diff --git a/mayan/apps/checkouts/views.py b/mayan/apps/checkouts/views.py index 2f7c7a9579..a9e00d5e6d 100644 --- a/mayan/apps/checkouts/views.py +++ b/mayan/apps/checkouts/views.py @@ -28,11 +28,13 @@ class CheckoutDocumentView(SingleObjectCreateView): form_class = DocumentCheckoutForm def dispatch(self, request, *args, **kwargs): - self.document = get_object_or_404(klass=Document, pk=self.kwargs['pk']) + self.document = get_object_or_404( + klass=Document, pk=self.kwargs['document_pk'] + ) AccessControlList.objects.check_access( - permissions=permission_document_checkout, user=request.user, - obj=self.document + obj=self.document, permissions=permission_document_checkout, + user=request.user ) return super( @@ -46,19 +48,24 @@ class CheckoutDocumentView(SingleObjectCreateView): instance.document = self.document instance.save() except DocumentAlreadyCheckedOut: - messages.error(self.request, _('Document already checked out.')) + messages.error( + request=self.request, + message=_('Document already checked out.') + ) except Exception as exception: messages.error( - self.request, - _('Error trying to check out document; %s') % exception + request=self.request, + message=_('Error trying to check out document; %s') % exception ) else: messages.success( - self.request, - _('Document "%s" checked out successfully.') % self.document + request=self.request, + message=_( + 'Document "%s" checked out successfully.' + ) % self.document ) - return HttpResponseRedirect(self.get_success_url()) + return HttpResponseRedirect(redirect_to=self.get_success_url()) def get_extra_context(self): return { @@ -67,15 +74,18 @@ class CheckoutDocumentView(SingleObjectCreateView): } def get_post_action_redirect(self): - return reverse('checkouts:checkout_info', args=(self.document.pk,)) + return reverse( + viewname='checkouts:checkout_info', + kwargs={'document_pk': self.document.pk} + ) class CheckoutListView(DocumentListView): def get_document_queryset(self): return AccessControlList.objects.filter_by_access( permission=permission_document_checkout_detail_view, - user=self.request.user, - queryset=DocumentCheckout.objects.checked_out_documents() + queryset=DocumentCheckout.objects.checked_out_documents(), + user=self.request.user ) def get_extra_context(self): @@ -129,7 +139,7 @@ class CheckoutDetailView(SingleObjectDetailView): } def get_object(self): - return get_object_or_404(klass=Document, pk=self.kwargs['pk']) + return get_object_or_404(klass=Document, pk=self.kwargs['document_pk']) class DocumentCheckinView(ConfirmView): @@ -151,38 +161,43 @@ class DocumentCheckinView(ConfirmView): return context def get_object(self): - return get_object_or_404(klass=Document, pk=self.kwargs['pk']) + return get_object_or_404(klass=Document, pk=self.kwargs['document_pk']) def get_post_action_redirect(self): - return reverse('checkouts:checkout_info', args=(self.get_object().pk,)) + return reverse( + viewname='checkouts:checkout_info', + kwargs={'document_pk': self.get_object().pk} + ) def view_action(self): document = self.get_object() if document.get_checkout_info().user == self.request.user: AccessControlList.objects.check_access( - permissions=permission_document_checkin, - user=self.request.user, obj=document + obj=document, permissions=permission_document_checkin, + user=self.request.user ) else: AccessControlList.objects.check_access( - permissions=permission_document_checkin_override, - user=self.request.user, obj=document + obj=document, permissions=permission_document_checkin_override, + user=self.request.user ) try: document.check_in(user=self.request.user) except DocumentNotCheckedOut: messages.error( - self.request, _('Document has not been checked out.') + request=self.request, message=_( + 'Document has not been checked out.' + ) ) except Exception as exception: messages.error( - self.request, - _('Error trying to check in document; %s') % exception + request=self.request, + message=_('Error trying to check in document; %s') % exception ) else: messages.success( - self.request, - _('Document "%s" checked in successfully.') % document + request=self.request, + message=_('Document "%s" checked in successfully.') % document ) From 125c133334af223eb41b90e80feb01fbd3004a99 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Wed, 2 Jan 2019 14:32:22 -0400 Subject: [PATCH 002/209] Audit common app Add support to override settings of the FilteredSelectionForm via subclass attributes. Add keyword arguments to calls. Signed-off-by: Roberto Rosario --- mayan/apps/common/classes.py | 4 +- mayan/apps/common/forms.py | 41 +++++++++++---- mayan/apps/common/mixins.py | 47 +++++++++-------- mayan/apps/common/models.py | 1 + mayan/apps/common/paginator.py | 2 +- mayan/apps/common/permissions_runtime.py | 2 +- mayan/apps/common/queues.py | 10 ++-- mayan/apps/common/settings.py | 6 +-- mayan/apps/common/tests/base.py | 13 ++--- mayan/apps/common/tests/test_api.py | 2 +- mayan/apps/common/tests/test_models.py | 2 +- mayan/apps/common/tests/test_views.py | 26 +++++----- mayan/apps/common/urls.py | 64 ++++++++++++------------ mayan/apps/common/views.py | 13 +++-- 14 files changed, 134 insertions(+), 99 deletions(-) diff --git a/mayan/apps/common/classes.py b/mayan/apps/common/classes.py index ffd4b9e626..f8958a304e 100644 --- a/mayan/apps/common/classes.py +++ b/mayan/apps/common/classes.py @@ -301,7 +301,9 @@ class Template(object): self.__class__._registry[name] = self def get_absolute_url(self): - return reverse('rest_api:template-detail', args=(self.name,)) + return reverse( + viewname='rest_api:template-detail', kwargs={'template_pk': self.name} + ) def render(self, request): context = { diff --git a/mayan/apps/common/forms.py b/mayan/apps/common/forms.py index d7cba8309e..fc8146ebac 100644 --- a/mayan/apps/common/forms.py +++ b/mayan/apps/common/forms.py @@ -9,6 +9,8 @@ from django.db import models from django.utils.module_loading import import_string from django.utils.translation import ugettext_lazy as _ +from mayan.apps.acls.models import AccessControlList + from .classes import Package from .models import UserLocaleProfile from .utils import resolve_attribute @@ -134,12 +136,20 @@ class FilteredSelectionForm(forms.Form): Form to select the from a list of choice filtered by access. Can be configure to allow single or multiple selection. """ + _field_name = None + _label = None + _help_text = None + _permission = None + _queryset = None + _widget_class = None + _widget_attributes = None + def __init__(self, *args, **kwargs): - field_name = kwargs.pop('field_name', None) - label = kwargs.pop('label', None) - help_text = kwargs.pop('help_text', None) - permission = kwargs.pop('permission', None) - queryset = kwargs.pop('queryset', None) + field_name = self._field_name or kwargs.pop('field_name', None) + label = self._label or kwargs.pop('label', None) + help_text = self._help_text or kwargs.pop('help_text', None) + permission = self._permission or kwargs.pop('permission', None) + queryset = self.get_queryset() or kwargs.pop('queryset', None) if queryset is None: model = kwargs.pop('model', None) @@ -150,12 +160,19 @@ class FilteredSelectionForm(forms.Form): queryset = model.objects.all() - user = kwargs.pop('user', None) - widget_class = kwargs.pop('widget_class', None) - widget_attributes = kwargs.pop('widget_attributes', {'size': '10'}) + user = self.get_user() or kwargs.pop('user', None) + widget_class = self._widget_class or kwargs.pop('widget_class', None) + widget_attributes = self._widget_attributes or kwargs.pop( + 'widget_attributes', {'size': '10'} + ) if not widget_class: - if kwargs.pop('allow_multiple', False): + if self._allow_multiple is not None: + allow_multiple = self._allow_multiple + else: + allow_multiple = self.kwargs.pop('allow_multiple', False) + + if allow_multiple: extra_kwargs = {} field_class = forms.ModelMultipleChoiceField widget_class = forms.widgets.SelectMultiple @@ -177,6 +194,12 @@ class FilteredSelectionForm(forms.Form): widget=widget_class(attrs=widget_attributes), **extra_kwargs ) + def get_queryset(self): + return self._queryset + + def get_user(self): + return None + class LicenseForm(FileDisplayForm): DIRECTORY = () diff --git a/mayan/apps/common/mixins.py b/mayan/apps/common/mixins.py index b0f258840b..103dc36dbe 100644 --- a/mayan/apps/common/mixins.py +++ b/mayan/apps/common/mixins.py @@ -46,7 +46,7 @@ class DeleteExtraDataMixin(object): else: self.object.delete() - return HttpResponseRedirect(success_url) + return HttpResponseRedirect(redirect_to=success_url) class DynamicFormViewMixin(object): @@ -179,9 +179,9 @@ class MultipleInstanceActionMixin(object): def get_success_message(self, count): return ungettext( - self.success_message, - self.success_message_plural, - count + singular=self.success_message, + plural=self.success_message_plural, + number=count ) % { 'count': count, } @@ -197,11 +197,11 @@ class MultipleInstanceActionMixin(object): count += 1 messages.success( - self.request, - self.get_success_message(count=count) + request=self.request, + message=self.get_success_message(count=count) ) - return HttpResponseRedirect(self.get_success_url()) + return HttpResponseRedirect(redirect_to=self.get_success_url()) class MultipleObjectMixin(object): @@ -278,9 +278,9 @@ class ObjectActionMixin(object): def get_success_message(self, count): return ungettext( - self.success_message, - self.success_message_plural, - count + singular=self.success_message, + plural=self.success_message_plural, + number=count ) % { 'count': count, } @@ -299,14 +299,15 @@ class ObjectActionMixin(object): pass except ActionError: messages.error( - self.request, self.error_message % {'instance': instance} + request=self.request, + message=self.error_message % {'instance': instance} ) else: self.action_count += 1 messages.success( - self.request, - self.get_success_message(count=self.action_count) + request=self.request, + message=self.get_success_message(count=self.action_count) ) @@ -321,17 +322,20 @@ class ObjectListPermissionFilterMixin(object): def dispatch(self, request, *args, **kwargs): if self.access_object_retrieve_method and self.object_permission: AccessControlList.objects.check_access( - permissions=(self.object_permission,), user=request.user, - obj=getattr(self, self.access_object_retrieve_method)() + obj=getattr(self, self.access_object_retrieve_method)(), + permissions=(self.object_permission,), user=request.user ) - return super(ObjectListPermissionFilterMixin, self).dispatch(request, *args, **kwargs) + return super(ObjectListPermissionFilterMixin, self).dispatch( + request, *args, **kwargs + ) def get_queryset(self): queryset = super(ObjectListPermissionFilterMixin, self).get_queryset() if not self.access_object_retrieve_method and self.object_permission: return AccessControlList.objects.filter_by_access( - self.object_permission, self.request.user, queryset=queryset + obj=self.object_permission, queryset=queryset, + user=self.request.user ) else: return queryset @@ -368,9 +372,10 @@ class ObjectPermissionCheckMixin(object): if self.object_permission: try: AccessControlList.objects.check_access( - permissions=self.object_permission, user=request.user, obj=self.get_permission_object(), - related=getattr(self, 'object_permission_related', None) + permissions=self.object_permission, + related=getattr(self, 'object_permission_related', None), + user=request.user ) except PermissionDenied: if self.object_permission_raise_404: @@ -437,8 +442,8 @@ class ViewPermissionCheckMixin(object): def dispatch(self, request, *args, **kwargs): if self.view_permission: Permission.check_permissions( - requester=self.request.user, - permissions=(self.view_permission,) + permissions=(self.view_permission,), + requester=self.request.user ) return super( diff --git a/mayan/apps/common/models.py b/mayan/apps/common/models.py index 7d8fc62a18..148c3b1c98 100644 --- a/mayan/apps/common/models.py +++ b/mayan/apps/common/models.py @@ -18,6 +18,7 @@ from .storages import storage_sharedupload logger = logging.getLogger(__name__) +#TODO: move outside of models.py def upload_to(instance, filename): return 'shared-file-{}'.format(uuid.uuid4().hex) diff --git a/mayan/apps/common/paginator.py b/mayan/apps/common/paginator.py index e5545fcf91..7a7adcd931 100644 --- a/mayan/apps/common/paginator.py +++ b/mayan/apps/common/paginator.py @@ -48,8 +48,8 @@ class PurePaginator(Paginator): self.allow_empty_first_page = allow_empty_first_page self.object_list = object_list self.orphans = orphans - self.per_page = per_page self.page_kwarg = page_kwarg + self.per_page = per_page self.request = request def page(self, number): diff --git a/mayan/apps/common/permissions_runtime.py b/mayan/apps/common/permissions_runtime.py index 9dc4e4797e..20df80407a 100644 --- a/mayan/apps/common/permissions_runtime.py +++ b/mayan/apps/common/permissions_runtime.py @@ -7,5 +7,5 @@ from mayan.apps.permissions import PermissionNamespace namespace = PermissionNamespace(label=_('Common'), name='common') permission_error_log_view = namespace.add_permission( - name='error_log_view', label=_('View error log') + label=_('View error log'), name='error_log_view' ) diff --git a/mayan/apps/common/queues.py b/mayan/apps/common/queues.py index bf649abb48..01569efe9e 100644 --- a/mayan/apps/common/queues.py +++ b/mayan/apps/common/queues.py @@ -5,13 +5,13 @@ from django.utils.translation import ugettext_lazy as _ from mayan.apps.task_manager.classes import CeleryQueue queue_default = CeleryQueue( - name='default', label=_('Default'), is_default_queue=True + is_default_queue=True, label=_('Default'), name='default' ) -queue_tools = CeleryQueue(name='tools', label=_('Tools')) +queue_tools = CeleryQueue(label=_('Tools'), name='tools') queue_common_periodic = CeleryQueue( - name='common_periodic', label=_('Common periodic'), transient=True + label=_('Common periodic'), name='common_periodic', transient=True ) queue_common_periodic.add_task_type( - name='mayan.apps.common.tasks.task_delete_stale_uploads', - label=_('Delete stale uploads') + label=_('Delete stale uploads'), + name='mayan.apps.common.tasks.task_delete_stale_uploads' ) diff --git a/mayan/apps/common/settings.py b/mayan/apps/common/settings.py index 2206192210..c97dbd07fa 100644 --- a/mayan/apps/common/settings.py +++ b/mayan/apps/common/settings.py @@ -11,7 +11,7 @@ from mayan.apps.smart_settings import Namespace from .literals import DEFAULT_COMMON_HOME_VIEW -namespace = Namespace(name='common', label=_('Common')) +namespace = Namespace(label=_('Common'), name='common') setting_auto_logging = namespace.add_setting( global_name='COMMON_AUTO_LOGGING', @@ -86,7 +86,7 @@ setting_temporary_directory = namespace.add_setting( is_path=True ) -namespace = Namespace(name='django', label=_('Django')) +namespace = Namespace(label=_('Django'), name='django') setting_django_allowed_hosts = namespace.add_setting( global_name='ALLOWED_HOSTS', default=settings.ALLOWED_HOSTS, @@ -402,7 +402,7 @@ setting_django_wsgi_application = namespace.add_setting( ), ) -namespace = Namespace(name='celery', label=_('Celery')) +namespace = Namespace(label=_('Celery'), name='celery') setting_celery_always_eager = namespace.add_setting( global_name='CELERY_TASK_ALWAYS_EAGER', diff --git a/mayan/apps/common/tests/base.py b/mayan/apps/common/tests/base.py index f87649500b..81c97bed83 100644 --- a/mayan/apps/common/tests/base.py +++ b/mayan/apps/common/tests/base.py @@ -37,9 +37,7 @@ class BaseTestCase(DatabaseConversionMixin, ACLBaseTestMixin, ContentTypeCheckMi class GenericViewTestCase(BaseTestCase): - def setUp(self): - super(GenericViewTestCase, self).setUp() - self.has_test_view = False + has_test_view = False def tearDown(self): from mayan.urls import urlpatterns @@ -87,13 +85,10 @@ class GenericViewTestCase(BaseTestCase): path=path, data=data, follow=follow ) - def login(self, username, password): - logged_in = self.client.login(username=username, password=password) + def login(self, *args, **kwargs): + logged_in = self.client.login(*args, **kwargs) - user = get_user_model().objects.get(username=username) - - self.assertTrue(logged_in) - self.assertTrue(user.is_authenticated) + return logged_in def login_user(self): self.login(username=TEST_USER_USERNAME, password=TEST_USER_PASSWORD) diff --git a/mayan/apps/common/tests/test_api.py b/mayan/apps/common/tests/test_api.py index 2302c62a64..a0111db300 100644 --- a/mayan/apps/common/tests/test_api.py +++ b/mayan/apps/common/tests/test_api.py @@ -12,7 +12,7 @@ TEST_TEMPLATE_RESULT = '[-\w]+)/(?P[-\w]+)/(?P\d+)/errors/$', - ObjectErrorLogEntryListView.as_view(), name='object_error_list' + regex=r'^objects/(?P[-\w]+)/(?P[-\w]+)/(?P\d+)/errors/$', + name='object_error_list', view=ObjectErrorLogEntryListView.as_view() ), url( - r'^object/(?P[-\w]+)/(?P[-\w]+)/(?P\d+)/errors/clear/$', - ObjectErrorLogEntryListClearView.as_view(), - name='object_error_list_clear' + regex=r'^objects/(?P[-\w]+)/(?P[-\w]+)/(?P\d+)/errors/clear/$', + name='object_error_list_clear', + view=ObjectErrorLogEntryListClearView.as_view() ), ] urlpatterns += [ url( - r'^favicon\.ico$', FaviconRedirectView.as_view() + regex=r'^favicon\.ico$', view=FaviconRedirectView.as_view() ), url( - r'^jsi18n/(?P\S+?)/$', javascript_catalog, - name='javascript_catalog' + regex=r'^jsi18n/(?P\S+?)/$', name='javascript_catalog', + view=javascript_catalog ), url( - r'^set_language/$', set_language, name='set_language' + regex=r'^set_language/$', name='set_language', view=set_language ), ] api_urls = [ url( - r'^content_types/$', APIContentTypeList.as_view(), - name='content-type-list' + regex=r'^content_types/$', name='content-type-list', + view=APIContentTypeList.as_view() ), url( - r'^templates/$', APITemplateListView.as_view(), - name='template-list' + regex=r'^templates/$', name='template-list', + view=APITemplateListView.as_view() ), url( - r'^templates/(?P[-\w]+)/$', APITemplateView.as_view(), - name='template-detail' + regex=r'^templates/(?P[-\w]+)/$', name='template-detail', + view=APITemplateView.as_view() ), ] diff --git a/mayan/apps/common/views.py b/mayan/apps/common/views.py index db0319c234..8bdc694e66 100644 --- a/mayan/apps/common/views.py +++ b/mayan/apps/common/views.py @@ -15,6 +15,9 @@ from django.utils.translation import ugettext_lazy as _ from django.views.generic import RedirectView, TemplateView from mayan.apps.acls.models import AccessControlList +from mayan.apps.common.mixins import ( + ContentTypeViewMixin, ExternalObjectViewMixin +) from .exceptions import NotLatestVersion, UnknownLatestVersion from .forms import ( @@ -171,7 +174,9 @@ class ObjectErrorLogEntryListClearView(ConfirmView): ) -class ObjectErrorLogEntryListView(SingleObjectListView): +class ObjectErrorLogEntryListView(ContentTypeViewMixin, ExternalObjectViewMixin, SingleObjectListView): + #TODO: Update for MERC 6. Return 404. + """ def dispatch(self, request, *args, **kwargs): AccessControlList.objects.check_access( obj=self.get_object(), permissions=permission_error_log_view, @@ -181,6 +186,7 @@ class ObjectErrorLogEntryListView(SingleObjectListView): return super(ObjectErrorLogEntryListView, self).dispatch( request, *args, **kwargs ) + """ def get_extra_context(self): return { @@ -202,6 +208,7 @@ class ObjectErrorLogEntryListView(SingleObjectListView): 'title': _('Error log entries for: %s' % self.get_object()), } + """ def get_object(self): content_type = get_object_or_404( klass=ContentType, app_label=self.kwargs['app_label'], @@ -211,9 +218,9 @@ class ObjectErrorLogEntryListView(SingleObjectListView): return get_object_or_404( klass=content_type.model_class(), pk=self.kwargs['object_id'] ) - + """ def get_object_list(self): - return self.get_object().error_logs.all() + return self.get_external_object().error_logs.all() class PackagesLicensesView(SimpleView): From 924538fe48fc33fbe1a573dbcc6f1c38745b4b49 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Wed, 2 Jan 2019 14:44:08 -0400 Subject: [PATCH 003/209] Initial audit of the convert app Add keyword arguments to call. Sort methods and arguments. Signed-off-by: Roberto Rosario --- mayan/apps/converter/apps.py | 8 +-- mayan/apps/converter/forms.py | 4 +- mayan/apps/converter/managers.py | 12 ++-- mayan/apps/converter/models.py | 4 +- mayan/apps/converter/permissions.py | 8 +-- mayan/apps/converter/settings.py | 2 +- mayan/apps/converter/tests/test_views.py | 4 +- mayan/apps/converter/transformations.py | 25 ++++---- mayan/apps/converter/urls.py | 16 ++--- mayan/apps/converter/views.py | 80 +++++++++++++----------- 10 files changed, 86 insertions(+), 77 deletions(-) diff --git a/mayan/apps/converter/apps.py b/mayan/apps/converter/apps.py index 54d448c76c..0d545877a8 100644 --- a/mayan/apps/converter/apps.py +++ b/mayan/apps/converter/apps.py @@ -23,16 +23,14 @@ class ConverterApp(MayanAppConfig): def ready(self): super(ConverterApp, self).ready() - Transformation = self.get_model('Transformation') + Transformation = self.get_model(model_name='Transformation') - SourceColumn(source=Transformation, label=_('Order'), attribute='order') + SourceColumn(attribute='order', source=Transformation) SourceColumn( source=Transformation, label=_('Transformation'), func=lambda context: force_text(context['object']) ) - SourceColumn( - source=Transformation, label=_('Arguments'), attribute='arguments' - ) + SourceColumn(attribute='arguments', source=Transformation) menu_object.bind_links( links=(link_transformation_edit, link_transformation_delete), diff --git a/mayan/apps/converter/forms.py b/mayan/apps/converter/forms.py index 41dd34b4f4..9a24dea9bd 100644 --- a/mayan/apps/converter/forms.py +++ b/mayan/apps/converter/forms.py @@ -16,10 +16,10 @@ class TransformationForm(forms.ModelForm): def clean(self): try: - yaml.safe_load(self.cleaned_data['arguments']) + yaml.safe_load(stream=self.cleaned_data['arguments']) except yaml.YAMLError: raise ValidationError( - _( + message=_( '"%s" not a valid entry.' ) % self.cleaned_data['arguments'] ) diff --git a/mayan/apps/converter/managers.py b/mayan/apps/converter/managers.py index 484fe45b24..86cccddb59 100644 --- a/mayan/apps/converter/managers.py +++ b/mayan/apps/converter/managers.py @@ -17,15 +17,15 @@ class TransformationManager(models.Manager): content_type = ContentType.objects.get_for_model(obj) self.create( - content_type=content_type, object_id=obj.pk, - name=transformation.name, arguments=yaml.safe_dump(arguments) + arguments=yaml.safe_dump(arguments), content_type=content_type, + name=transformation.name, object_id=obj.pk ) def copy(self, source, targets): """ Copy transformation from source to all targets """ - content_type = ContentType.objects.get_for_model(source) + content_type = ContentType.objects.get_for_model(obj=source) # Get transformations transformations = self.filter( @@ -76,7 +76,7 @@ class TransformationManager(models.Manager): for transformation in transformations: try: transformation_class = BaseTransformation.get( - transformation.name + name=transformation.name ) except KeyError: # Non existant transformation, but we don't raise an error @@ -89,7 +89,9 @@ class TransformationManager(models.Manager): # Some transformations don't require arguments # return an empty dictionary as ** doesn't allow None if transformation.arguments: - kwargs = yaml.safe_load(transformation.arguments) + kwargs = yaml.safe_load( + stream=transformation.arguments + ) else: kwargs = {} diff --git a/mayan/apps/converter/models.py b/mayan/apps/converter/models.py index 73df0efade..e241049bcd 100644 --- a/mayan/apps/converter/models.py +++ b/mayan/apps/converter/models.py @@ -31,7 +31,9 @@ class Transformation(models.Model): """ content_type = models.ForeignKey(on_delete=models.CASCADE, to=ContentType) object_id = models.PositiveIntegerField() - content_object = GenericForeignKey('content_type', 'object_id') + content_object = GenericForeignKey( + ct_field='content_type', fk_field='object_id' + ) order = models.PositiveIntegerField( blank=True, db_index=True, default=0, help_text=_( 'Order in which the transformations will be executed. If left ' diff --git a/mayan/apps/converter/permissions.py b/mayan/apps/converter/permissions.py index 5868f0c070..a111b9de1b 100644 --- a/mayan/apps/converter/permissions.py +++ b/mayan/apps/converter/permissions.py @@ -7,14 +7,14 @@ from mayan.apps.permissions import PermissionNamespace namespace = PermissionNamespace(label=_('Converter'), name='converter') permission_transformation_create = namespace.add_permission( - name='transformation_create', label=_('Create new transformations') + label=_('Create new transformations'), name='transformation_create' ) permission_transformation_delete = namespace.add_permission( - name='transformation_delete', label=_('Delete transformations') + label=_('Delete transformations'), name='transformation_delete' ) permission_transformation_edit = namespace.add_permission( - name='transformation_edit', label=_('Edit transformations') + label=_('Edit transformations'), name='transformation_edit' ) permission_transformation_view = namespace.add_permission( - name='transformation_view', label=_('View existing transformations') + label=_('View existing transformations'), name='transformation_view' ) diff --git a/mayan/apps/converter/settings.py b/mayan/apps/converter/settings.py index e16535d48a..1241191f4f 100644 --- a/mayan/apps/converter/settings.py +++ b/mayan/apps/converter/settings.py @@ -9,7 +9,7 @@ from .literals import ( DEFAULT_PDFTOPPM_PATH, DEFAULT_PDFINFO_PATH, DEFAULT_PILLOW_FORMAT ) -namespace = Namespace(name='converter', label=_('Converter')) +namespace = Namespace(label=_('Converter'), name='converter') setting_graphics_backend = namespace.add_setting( default='mayan.apps.converter.backends.python.Python', help_text=_('Graphics conversion backend to use.'), diff --git a/mayan/apps/converter/tests/test_views.py b/mayan/apps/converter/tests/test_views.py index 65f7bc6589..06de5fc9da 100644 --- a/mayan/apps/converter/tests/test_views.py +++ b/mayan/apps/converter/tests/test_views.py @@ -80,7 +80,7 @@ class TransformationViewsTestCase(GenericDocumentViewTestCase): def _transformation_delete_view(self): return self.post( viewname='converter:transformation_delete', kwargs={ - 'pk': self.transformation.pk + 'transformation_pk': self.transformation.pk } ) @@ -104,7 +104,7 @@ class TransformationViewsTestCase(GenericDocumentViewTestCase): def _transformation_edit_view(self): return self.post( viewname='converter:transformation_edit', kwargs={ - 'pk': self.transformation.pk + 'transformation_pk': self.transformation.pk }, data={ 'arguments': TEST_TRANSFORMATION_ARGUMENT_EDITED, 'name': self.transformation.name diff --git a/mayan/apps/converter/transformations.py b/mayan/apps/converter/transformations.py index 2bf4371f62..1caecc9117 100644 --- a/mayan/apps/converter/transformations.py +++ b/mayan/apps/converter/transformations.py @@ -33,18 +33,6 @@ class BaseTransformation(object): return result.hexdigest() - @classmethod - def register(cls, transformation): - cls._registry[transformation.name] = transformation - - @classmethod - def get_transformation_choices(cls): - return sorted( - [ - (name, klass.get_label()) for name, klass in cls._registry.items() - ] - ) - @classmethod def get(cls, name): return cls._registry[name] @@ -58,6 +46,19 @@ class BaseTransformation(object): else: return cls.label + @classmethod + def get_transformation_choices(cls): + return sorted( + [ + (name, klass.get_label()) for name, klass in cls._registry.items() + ] + ) + + @classmethod + def register(cls, transformation): + cls._registry[transformation.name] = transformation + + def __init__(self, **kwargs): self.kwargs = {} for argument_name in self.arguments: diff --git a/mayan/apps/converter/urls.py b/mayan/apps/converter/urls.py index 56f7abc955..5334d62b33 100644 --- a/mayan/apps/converter/urls.py +++ b/mayan/apps/converter/urls.py @@ -9,19 +9,19 @@ from .views import ( urlpatterns = [ url( - r'^create_for/(?P[-\w]+)/(?P[-\w]+)/(?P\d+)/$', - TransformationCreateView.as_view(), name='transformation_create' + regex=r'^create_for/(?P[-\w]+)/(?P[-\w]+)/(?P\d+)/$', + name='transformation_create', view=TransformationCreateView.as_view() ), url( - r'^list_for/(?P[-\w]+)/(?P[-\w]+)/(?P\d+)/$', - TransformationListView.as_view(), name='transformation_list' + regex=r'^list_for/(?P[-\w]+)/(?P[-\w]+)/(?P\d+)/$', + name='transformation_list', view=TransformationListView.as_view() ), url( - r'^delete/(?P\d+)/$', TransformationDeleteView.as_view(), - name='transformation_delete' + regex=r'^delete/(?P\d+)/$', + name='transformation_delete', view=TransformationDeleteView.as_view() ), url( - r'^edit/(?P\d+)/$', TransformationEditView.as_view(), - name='transformation_edit' + regex=r'^edit/(?P\d+)/$', + name='transformation_edit', view=TransformationEditView.as_view() ), ] diff --git a/mayan/apps/converter/views.py b/mayan/apps/converter/views.py index 1ba13ecd9a..b55789d0b1 100644 --- a/mayan/apps/converter/views.py +++ b/mayan/apps/converter/views.py @@ -29,15 +29,16 @@ logger = logging.getLogger(__name__) class TransformationDeleteView(SingleObjectDeleteView): model = Transformation + pk_url_kwarg = 'transformation_pk' def dispatch(self, request, *args, **kwargs): self.transformation = get_object_or_404( - klass=Transformation, pk=self.kwargs['pk'] + klass=Transformation, pk=self.kwargs['transformation_pk'] ) AccessControlList.objects.check_access( - permissions=permission_transformation_delete, user=request.user, - obj=self.transformation.content_object + permissions=permission_transformation_delete, + obj=self.transformation.content_object, user=request.user ) return super(TransformationDeleteView, self).dispatch( @@ -46,11 +47,11 @@ class TransformationDeleteView(SingleObjectDeleteView): def get_post_action_redirect(self): return reverse( - 'converter:transformation_list', args=( - self.transformation.content_type.app_label, - self.transformation.content_type.model, - self.transformation.object_id - ) + viewname='converter:transformation_list', kwargs={ + 'app_label': self.transformation.content_type.app_label, + 'model': self.transformation.content_type.model, + 'object_id': self.transformation.object_id + } ) def get_extra_context(self): @@ -58,11 +59,11 @@ class TransformationDeleteView(SingleObjectDeleteView): 'content_object': self.transformation.content_object, 'navigation_object_list': ('content_object', 'transformation'), 'previous': reverse( - 'converter:transformation_list', args=( - self.transformation.content_type.app_label, - self.transformation.content_type.model, - self.transformation.object_id - ) + viewname='converter:transformation_list', kwargs={ + 'app_label': self.transformation.content_type.app_label, + 'model': self.transformation.content_type.model, + 'object_id': self.transformation.object_id + } ), 'title': _( 'Delete transformation "%(transformation)s" for: ' @@ -92,8 +93,8 @@ class TransformationCreateView(SingleObjectCreateView): raise Http404 AccessControlList.objects.check_access( - permissions=permission_transformation_create, user=request.user, - obj=self.content_object + permissions=permission_transformation_create, + obj=self.content_object, user=request.user ) return super(TransformationCreateView, self).dispatch( @@ -108,9 +109,11 @@ class TransformationCreateView(SingleObjectCreateView): instance.save() except Exception as exception: logger.debug('Invalid form, exception: %s', exception) - return super(TransformationCreateView, self).form_invalid(form) + return super(TransformationCreateView, self).form_invalid( + form=form + ) else: - return super(TransformationCreateView, self).form_valid(form) + return super(TransformationCreateView, self).form_valid(form=form) def get_extra_context(self): return { @@ -123,32 +126,34 @@ class TransformationCreateView(SingleObjectCreateView): def get_post_action_redirect(self): return reverse( - 'converter:transformation_list', args=( - self.kwargs['app_label'], self.kwargs['model'], - self.kwargs['object_id'] - ) + viewname='converter:transformation_list', kwargs={ + 'app_label': self.kwargs['app_label'], + 'model': self.kwargs['model'], + 'object_id': self.kwargs['object_id'] + } ) def get_queryset(self): - return Transformation.objects.get_for_model(self.content_object) + return Transformation.objects.get_for_model(obj=self.content_object) class TransformationEditView(SingleObjectEditView): form_class = TransformationForm model = Transformation + pk_url_kwarg = 'transformation_pk' def dispatch(self, request, *args, **kwargs): self.transformation = get_object_or_404( - klass=Transformation, pk=self.kwargs['pk'] + klass=Transformation, pk=self.kwargs['transformation_pk'] ) AccessControlList.objects.check_access( - permissions=permission_transformation_edit, user=request.user, - obj=self.transformation.content_object + obj=self.transformation.content_object, + permissions=permission_transformation_edit, user=request.user ) return super(TransformationEditView, self).dispatch( - request, *args, **kwargs + request=request, *args, **kwargs ) def form_valid(self, form): @@ -158,9 +163,9 @@ class TransformationEditView(SingleObjectEditView): instance.save() except Exception as exception: logger.debug('Invalid form, exception: %s', exception) - return super(TransformationEditView, self).form_invalid(form) + return super(TransformationEditView, self).form_invalid(form=form) else: - return super(TransformationEditView, self).form_valid(form) + return super(TransformationEditView, self).form_valid(form=form) def get_extra_context(self): return { @@ -177,11 +182,11 @@ class TransformationEditView(SingleObjectEditView): def get_post_action_redirect(self): return reverse( - 'converter:transformation_list', args=( - self.transformation.content_type.app_label, - self.transformation.content_type.model, - self.transformation.object_id - ) + viewname='converter:transformation_list', kwargs={ + 'app_label': self.transformation.content_type.app_label, + 'model': self.transformation.content_type.model, + 'object_id': self.transformation.object_id + } ) @@ -200,12 +205,13 @@ class TransformationListView(SingleObjectListView): raise Http404 AccessControlList.objects.check_access( - permissions=permission_transformation_view, user=request.user, - obj=self.content_object + obj=self.content_object, + permissions=permission_transformation_view, + user=request.user ) return super(TransformationListView, self).dispatch( - request, *args, **kwargs + request=request, *args, **kwargs ) def get_extra_context(self): @@ -230,4 +236,4 @@ class TransformationListView(SingleObjectListView): } def get_object_list(self): - return Transformation.objects.get_for_model(self.content_object) + return Transformation.objects.get_for_model(obj=self.content_object) From cdb29b11f920dafb8a69a0e4e1de28aa16ae71fd Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Wed, 2 Jan 2019 14:46:41 -0400 Subject: [PATCH 004/209] Add keyword argument Signed-off-by: Roberto Rosario --- mayan/apps/dependencies/javascript.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mayan/apps/dependencies/javascript.py b/mayan/apps/dependencies/javascript.py index 6f6806d7b8..3a879c9ae4 100644 --- a/mayan/apps/dependencies/javascript.py +++ b/mayan/apps/dependencies/javascript.py @@ -154,7 +154,7 @@ class NPMRegistry(object): def _read_package(self): with self.package_file.open(mode='rb') as file_object: - self._package_data = json.loads(file_object.read()) + self._package_data = json.loads(s=file_object.read()) def install(self, package=None): if package: From c6aab93f985cdf468758da31655b673e39af0953 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Wed, 2 Jan 2019 19:16:32 -0400 Subject: [PATCH 005/209] Initial audit of the document index app Add keyword arguments to calls. Sort methods and parameters. Signed-off-by: Roberto Rosario --- mayan/apps/document_indexing/api_views.py | 4 +- mayan/apps/document_indexing/apps.py | 12 +-- mayan/apps/document_indexing/forms.py | 5 +- mayan/apps/document_indexing/managers.py | 2 +- mayan/apps/document_indexing/models.py | 47 +++++------ mayan/apps/document_indexing/permissions.py | 14 ++-- mayan/apps/document_indexing/queues.py | 18 ++-- .../document_indexing/tests/test_models.py | 6 +- .../document_indexing/tests/test_views.py | 16 +++- mayan/apps/document_indexing/urls.py | 84 ++++++++++--------- mayan/apps/document_indexing/views.py | 69 ++++++++------- mayan/apps/document_indexing/widgets.py | 11 ++- 12 files changed, 160 insertions(+), 128 deletions(-) diff --git a/mayan/apps/document_indexing/api_views.py b/mayan/apps/document_indexing/api_views.py index f54a71b5d2..c1be3bbf63 100644 --- a/mayan/apps/document_indexing/api_views.py +++ b/mayan/apps/document_indexing/api_views.py @@ -62,7 +62,7 @@ class APIIndexNodeInstanceDocumentListView(generics.ListAPIView): def get_queryset(self): index_node_instance = get_object_or_404( - klass=IndexInstanceNode, pk=self.kwargs['pk'] + klass=IndexInstanceNode, pk=self.kwargs['index_instance_node_pk'] ) AccessControlList.objects.check_access( permissions=permission_document_indexing_view, @@ -109,7 +109,7 @@ class APIDocumentIndexListView(generics.ListAPIView): serializer_class = IndexInstanceNodeSerializer def get_queryset(self): - document = get_object_or_404(klass=Document, pk=self.kwargs['pk']) + document = get_object_or_404(klass=Document, pk=self.kwargs['document_pk']) AccessControlList.objects.check_access( permissions=permission_document_view, user=self.request.user, obj=document diff --git a/mayan/apps/document_indexing/apps.py b/mayan/apps/document_indexing/apps.py index e17e254d3b..b6dc7a35b8 100644 --- a/mayan/apps/document_indexing/apps.py +++ b/mayan/apps/document_indexing/apps.py @@ -65,12 +65,14 @@ class DocumentIndexingApp(MayanAppConfig): app_label='documents', model_name='DocumentType' ) - DocumentIndexInstanceNode = self.get_model('DocumentIndexInstanceNode') + DocumentIndexInstanceNode = self.get_model( + model_name='DocumentIndexInstanceNode' + ) - Index = self.get_model('Index') - IndexInstance = self.get_model('IndexInstance') - IndexInstanceNode = self.get_model('IndexInstanceNode') - IndexTemplateNode = self.get_model('IndexTemplateNode') + Index = self.get_model(model_name='Index') + IndexInstance = self.get_model(model_name='IndexInstance') + IndexInstanceNode = self.get_model(model_name='IndexInstanceNode') + IndexTemplateNode = self.get_model(model_name='IndexTemplateNode') ModelPermission.register( model=Index, permissions=( diff --git a/mayan/apps/document_indexing/forms.py b/mayan/apps/document_indexing/forms.py index 26ddd8f434..acf6953c69 100644 --- a/mayan/apps/document_indexing/forms.py +++ b/mayan/apps/document_indexing/forms.py @@ -23,8 +23,9 @@ class IndexListForm(forms.Form): user = kwargs.pop('user') super(IndexListForm, self).__init__(*args, **kwargs) queryset = AccessControlList.objects.filter_by_access( - permission=permission_document_indexing_rebuild, user=user, - queryset=Index.objects.filter(enabled=True) + permission=permission_document_indexing_rebuild, + queryset=Index.objects.filter(enabled=True), + user=user ) self.fields['indexes'].queryset = queryset diff --git a/mayan/apps/document_indexing/managers.py b/mayan/apps/document_indexing/managers.py index b40c519ea8..4158e22ffb 100644 --- a/mayan/apps/document_indexing/managers.py +++ b/mayan/apps/document_indexing/managers.py @@ -15,7 +15,7 @@ class IndexManager(models.Manager): return self.get(slug=slug) def index_document(self, document): - for index in self.filter(enabled=True, document_types=document.document_type): + for index in self.filter(document_types=document.document_type, enabled=True): index.index_document(document=document) def rebuild(self): diff --git a/mayan/apps/document_indexing/models.py b/mayan/apps/document_indexing/models.py index 5b28c8652f..ba85ce8107 100644 --- a/mayan/apps/document_indexing/models.py +++ b/mayan/apps/document_indexing/models.py @@ -65,8 +65,8 @@ class Index(models.Model): def get_absolute_url(self): try: return reverse( - 'indexing:index_instance_node_view', - args=(self.instance_root.pk,) + viewname='indexing:index_instance_node_view', + kwargs={'index_instance_node_pk': self.instance_root.pk} ) except IndexInstanceNode.DoesNotExist: return '#' @@ -190,24 +190,19 @@ class IndexTemplateNode(MPTTModel): 'Enter a template to render. ' 'Use Django\'s default templating language ' '(https://docs.djangoproject.com/en/1.11/ref/templates/builtins/)' - ), - verbose_name=_('Indexing expression') + ), verbose_name=_('Indexing expression') ) enabled = models.BooleanField( - default=True, - help_text=_( + default=True, help_text=_( 'Causes this node to be visible and updated when document data ' 'changes.' - ), - verbose_name=_('Enabled') + ), verbose_name=_('Enabled') ) link_documents = models.BooleanField( - default=False, - help_text=_( + default=False, help_text=_( 'Check this option to have this node act as a container for ' 'documents and not as a parent for further nodes.' - ), - verbose_name=_('Link documents') + ), verbose_name=_('Link documents') ) class Meta: @@ -216,7 +211,7 @@ class IndexTemplateNode(MPTTModel): def __str__(self): if self.is_root_node(): - return ugettext('Root') + return ugettext(message='Root') else: return self.expression @@ -250,7 +245,7 @@ class IndexTemplateNode(MPTTModel): for child in self.get_children(): child.index_document( - document=document, acquire_lock=False, + acquire_lock=False, document=document, index_instance_node_parent=index_instance_root_node ) elif self.enabled: @@ -267,7 +262,7 @@ class IndexTemplateNode(MPTTModel): try: context = {'document': document} - template = Template(self.expression) + template = Template(source=self.expression) result = template.render(**context) except Exception as exception: logger.debug('Evaluating error: %s', exception) @@ -294,7 +289,7 @@ class IndexTemplateNode(MPTTModel): for child in self.get_children(): child.index_document( - document=document, acquire_lock=False, + acquire_lock=False, document=document, index_instance_node_parent=index_instance_node ) finally: @@ -341,7 +336,7 @@ class IndexInstanceNode(MPTTModel): # Prevent another process to delete this node. try: lock = locking_backend.acquire_lock( - self.index_template_node.get_lock_string() + name=self.index_template_node.get_lock_string() ) except LockError: raise @@ -359,7 +354,10 @@ class IndexInstanceNode(MPTTModel): lock.release() def get_absolute_url(self): - return reverse('indexing:index_instance_node_view', args=(self.pk,)) + return reverse( + viewname='indexing:index_instance_node_view', + kwargs={'index_instance_node_pk': self.pk} + ) def get_children_count(self): return self.get_children().count() @@ -369,28 +367,29 @@ class IndexInstanceNode(MPTTModel): def get_descendants_document_count(self, user): return AccessControlList.objects.filter_by_access( - permission=permission_document_view, user=user, + permission=permission_document_view, queryset=Document.objects.filter( index_instance_nodes__in=self.get_descendants( include_self=True ) - ) + ), user=user ).count() def get_full_path(self): result = [] for node in self.get_ancestors(include_self=True): if node.is_root_node(): - result.append(force_text(self.index())) + result.append(force_text(s=self.index())) else: - result.append(force_text(node)) + result.append(force_text(s=node)) return ' / '.join(result) def get_item_count(self, user): if self.index_template_node.link_documents: queryset = AccessControlList.objects.filter_by_access( - permission_document_view, user, queryset=self.documents + permission=permission_document_view, queryset=self.documents, + user=user ) return queryset.count() @@ -417,7 +416,7 @@ class IndexInstanceNode(MPTTModel): # parent template node for the lock try: lock = locking_backend.acquire_lock( - self.index_template_node.get_lock_string() + name=self.index_template_node.get_lock_string() ) except LockError: raise diff --git a/mayan/apps/document_indexing/permissions.py b/mayan/apps/document_indexing/permissions.py index 33a44f5787..39fddadf1a 100644 --- a/mayan/apps/document_indexing/permissions.py +++ b/mayan/apps/document_indexing/permissions.py @@ -7,21 +7,21 @@ from mayan.apps.permissions import PermissionNamespace namespace = PermissionNamespace(label=_('Indexing'), name='document_indexing') permission_document_indexing_create = namespace.add_permission( - name='document_index_create', label=_('Create new document indexes') + label=_('Create new document indexes'), name='document_index_create' ) permission_document_indexing_edit = namespace.add_permission( - name='document_index_edit', label=_('Edit document indexes') + label=_('Edit document indexes'), name='document_index_edit' ) permission_document_indexing_delete = namespace.add_permission( - name='document_index_delete', label=_('Delete document indexes') + label=_('Delete document indexes'), name='document_index_delete' ) permission_document_indexing_instance_view = namespace.add_permission( - name='document_index_instance_view', - label=_('View document index instances') + label=_('View document index instances'), + name='document_index_instance_view' ) permission_document_indexing_view = namespace.add_permission( - name='document_index_view', label=_('View document indexes') + label=_('View document indexes'), name='document_index_view' ) permission_document_indexing_rebuild = namespace.add_permission( - name='document_rebuild_indexes', label=_('Rebuild document indexes') + label=_('Rebuild document indexes'), name='document_rebuild_indexes' ) diff --git a/mayan/apps/document_indexing/queues.py b/mayan/apps/document_indexing/queues.py index 96347ba0ef..f8749d9235 100644 --- a/mayan/apps/document_indexing/queues.py +++ b/mayan/apps/document_indexing/queues.py @@ -5,21 +5,21 @@ from django.utils.translation import ugettext_lazy as _ from mayan.apps.common.queues import queue_tools from mayan.apps.task_manager.classes import CeleryQueue -queue_indexing = CeleryQueue(name='indexing', label=_('Indexing')) +queue_indexing = CeleryQueue(label=_('Indexing'), name='indexing') queue_indexing.add_task_type( - name='mayan.apps.document_indexing.tasks.task_delete_empty', - label=_('Delete empty index nodes') + label=_('Delete empty index nodes'), + name='mayan.apps.document_indexing.tasks.task_delete_empty' ) queue_indexing.add_task_type( - name='mayan.apps.document_indexing.tasks.task_remove_document', - label=_('Remove document') + label=_('Remove document'), + name='mayan.apps.document_indexing.tasks.task_remove_document' ) queue_indexing.add_task_type( - name='mayan.apps.document_indexing.tasks.task_index_document', - label=_('Index document') + label=_('Index document'), + name='mayan.apps.document_indexing.tasks.task_index_document' ) queue_tools.add_task_type( - name='mayan.apps.document_indexing.tasks.task_rebuild_index', - label=_('Rebuild index') + label=_('Rebuild index'), + name='mayan.apps.document_indexing.tasks.task_rebuild_index' ) diff --git a/mayan/apps/document_indexing/tests/test_models.py b/mayan/apps/document_indexing/tests/test_models.py index 67fe6a339b..3077e0dce3 100644 --- a/mayan/apps/document_indexing/tests/test_models.py +++ b/mayan/apps/document_indexing/tests/test_models.py @@ -92,7 +92,7 @@ class IndexTestCase(DocumentIndexingTestMixin, DocumentTestMixin, BaseTestCase): document = self.upload_document() self.assertEqual( - [instance.value for instance in IndexInstanceNode.objects.all().order_by('pk')], + [instance.value for instance in IndexInstanceNode.objects.all().order_by('index_instance_node_pk')], [ '', force_text(document.date_added.year), force_text(document.date_added.month).zfill(2) @@ -100,7 +100,7 @@ class IndexTestCase(DocumentIndexingTestMixin, DocumentTestMixin, BaseTestCase): ) self.assertTrue( - document in list(IndexInstanceNode.objects.order_by('pk').last().documents.all()) + document in list(IndexInstanceNode.objects.order_by('index_instance_node_pk').last().documents.all()) ) def test_dual_level_dual_document_index(self): @@ -131,7 +131,7 @@ class IndexTestCase(DocumentIndexingTestMixin, DocumentTestMixin, BaseTestCase): Index.objects.rebuild() self.assertEqual( - [instance.value for instance in IndexInstanceNode.objects.all().order_by('pk')], + [instance.value for instance in IndexInstanceNode.objects.all().order_by('index_instance_node_pk')], [ '', force_text(self.document_2.uuid), self.document_2.label, force_text(self.document.uuid), self.document.label diff --git a/mayan/apps/document_indexing/tests/test_views.py b/mayan/apps/document_indexing/tests/test_views.py index 4efdeb86e8..a38fb13e0e 100644 --- a/mayan/apps/document_indexing/tests/test_views.py +++ b/mayan/apps/document_indexing/tests/test_views.py @@ -45,7 +45,10 @@ class IndexViewTestCase(GenericDocumentViewTestCase): self.assertEqual(Index.objects.first().label, TEST_INDEX_LABEL) def _request_index_delete_view(self, index): - return self.post('indexing:index_setup_delete', args=(index.pk,)) + return self.post( + viewname='indexing:index_setup_delete', + kwargs={'index_pk': index.pk} + ) def test_index_delete_view_no_permission(self): index = Index.objects.create( @@ -72,7 +75,9 @@ class IndexViewTestCase(GenericDocumentViewTestCase): def _request_index_edit_view(self, index): return self.post( - 'indexing:index_setup_edit', args=(index.pk,), data={ + viewname='indexing:index_setup_edit', kwargs={ + 'index_pk': index.pk + }, data={ 'label': TEST_INDEX_LABEL_EDITED, 'slug': TEST_INDEX_SLUG } ) @@ -122,7 +127,8 @@ class IndexViewTestCase(GenericDocumentViewTestCase): def _request_index_instance_node_view(self, index_instance_node): return self.get( - 'indexing:index_instance_node_view', args=(index_instance_node.pk,) + viewname='indexing:index_instance_node_view', + kwargs={'index_instance_node_pk': index_instance_node.pk} ) def test_index_instance_node_view_no_permission(self): @@ -146,7 +152,9 @@ class IndexViewTestCase(GenericDocumentViewTestCase): index_instance_node=self.index.instance_root ) - self.assertContains(response, text=TEST_INDEX_LABEL, status_code=200) + self.assertContains( + response=response, text=TEST_INDEX_LABEL, status_code=200 + ) def _request_index_rebuild_get_view(self): return self.get( diff --git a/mayan/apps/document_indexing/urls.py b/mayan/apps/document_indexing/urls.py index 1a28b7f4d4..5aa96bed20 100644 --- a/mayan/apps/document_indexing/urls.py +++ b/mayan/apps/document_indexing/urls.py @@ -17,80 +17,86 @@ from .views import ( urlpatterns = [ url( - r'^setup/index/list/$', SetupIndexListView.as_view(), - name='index_setup_list' + regex=r'^indexes/$', name='index_setup_list', + view=SetupIndexListView.as_view() ), url( - r'^setup/index/create/$', SetupIndexCreateView.as_view(), - name='index_setup_create' + regex=r'^indexes/create/$', name='index_setup_create', + view=SetupIndexCreateView.as_view() ), url( - r'^setup/index/(?P\d+)/edit/$', SetupIndexEditView.as_view(), - name='index_setup_edit' + regex=r'^indexes/(?P\d+)/delete/$', + name='index_setup_delete', view=SetupIndexDeleteView.as_view() ), url( - r'^setup/index/(?P\d+)/delete/$', SetupIndexDeleteView.as_view(), - name='index_setup_delete' + regex=r'^indexes/(?P\d+)/edit/$', + name='index_setup_edit', view=SetupIndexEditView.as_view() ), url( - r'^setup/index/(?P\d+)/template/$', - SetupIndexTreeTemplateListView.as_view(), name='index_setup_view' + regex=r'^indexes/(?P\d+)/templates/$', + name='index_setup_view', view=SetupIndexTreeTemplateListView.as_view() ), url( - r'^setup/index/(?P\d+)/document_types/$', - SetupIndexDocumentTypesView.as_view(), - name='index_setup_document_types' + regex=r'^indexes/(?P\d+)/document_types/$', + name='index_setup_document_types', + view=SetupIndexDocumentTypesView.as_view() ), url( - r'^setup/template/node/(?P\d+)/create/child/$', - TemplateNodeCreateView.as_view(), name='template_node_create' + regex=r'^indexes/templates/nodes/(?P\d+)/create/child/$', + name='template_node_create', view=TemplateNodeCreateView.as_view() ), url( - r'^setup/template/node/(?P\d+)/edit/$', - TemplateNodeEditView.as_view(), name='template_node_edit' + regex=r'^indexes/templates/nodes/(?P\d+)/edit/$', + name='template_node_edit', view=TemplateNodeEditView.as_view() ), url( - r'^setup/template/node/(?P\d+)/delete/$', - TemplateNodeDeleteView.as_view(), name='template_node_delete' + regex=r'^indexes/templates/nodes/(?P\d+)/delete/$', + name='template_node_delete', view=TemplateNodeDeleteView.as_view() ), - - url(r'^index/list/$', IndexListView.as_view(), name='index_list'), url( - r'^instance/node/(?P\d+)/$', IndexInstanceNodeView.as_view(), - name='index_instance_node_view' + regex=r'^indexes/instances/list/$', name='index_list', + view=IndexListView.as_view() + ), + url( + regex=r'^indexes/instances/node/(?P\d+)/$', + name='index_instance_node_view', view=IndexInstanceNodeView.as_view() ), url( - r'^rebuild/all/$', RebuildIndexesView.as_view(), - name='rebuild_index_instances' + regex=r'^indexes/rebuild/$', name='rebuild_index_instances', + view=RebuildIndexesView.as_view() ), url( - r'^list/for/document/(?P\d+)/$', - DocumentIndexNodeListView.as_view(), name='document_index_list' + regex=r'^documents/(?P\d+)/indexes/$', + name='document_index_list', view=DocumentIndexNodeListView.as_view() ), ] api_urls = [ url( - r'^indexes/node/(?P[0-9]+)/documents/$', - APIIndexNodeInstanceDocumentListView.as_view(), - name='index-node-documents' + regex=r'^indexes/nodes/(?P[0-9]+)/documents/$', + name='index-node-documents', + view=APIIndexNodeInstanceDocumentListView.as_view(), ), url( - r'^indexes/template/(?P[0-9]+)/$', APIIndexTemplateView.as_view(), - name='index-template-detail' + regex=r'^indexes/templates/(?P[0-9]+)/$', + name='index-template-detail', view=APIIndexTemplateView.as_view() ), url( - r'^indexes/(?P[0-9]+)/$', APIIndexView.as_view(), - name='index-detail' + regex=r'^indexes/(?P\d+)/$', name='index-detail', + view=APIIndexView.as_view() ), url( - r'^indexes/(?P[0-9]+)/template/$', - APIIndexTemplateListView.as_view(), name='index-template-detail' + regex=r'^indexes/(?P\d+)/templates/$', + name='index-template-detail', view=APIIndexTemplateListView.as_view() ), - url(r'^indexes/$', APIIndexListView.as_view(), name='index-list'), url( - r'^documents/(?P[0-9]+)/indexes/$', - APIDocumentIndexListView.as_view(), name='document-index-list' + regex=r'^indexes/$', name='index-list', + view=APIIndexListView.as_view() + ), + url( + regex=r'^documents/(?P\d+)/indexes/$', + name='document-index-list', + view=APIDocumentIndexListView.as_view() ), ] diff --git a/mayan/apps/document_indexing/views.py b/mayan/apps/document_indexing/views.py index aa3bdd60ff..96a58983e0 100644 --- a/mayan/apps/document_indexing/views.py +++ b/mayan/apps/document_indexing/views.py @@ -39,14 +39,15 @@ class SetupIndexCreateView(SingleObjectCreateView): extra_context = {'title': _('Create index')} fields = ('label', 'slug', 'enabled') model = Index - post_action_redirect = reverse_lazy('indexing:index_setup_list') + post_action_redirect = reverse_lazy(viewname='indexing:index_setup_list') view_permission = permission_document_indexing_create class SetupIndexDeleteView(SingleObjectDeleteView): model = Index - post_action_redirect = reverse_lazy('indexing:index_setup_list') object_permission = permission_document_indexing_delete + pk_url_kwarg = 'index_pk' + post_action_redirect = reverse_lazy(viewname='indexing:index_setup_list') def get_extra_context(self): return { @@ -58,8 +59,9 @@ class SetupIndexDeleteView(SingleObjectDeleteView): class SetupIndexEditView(SingleObjectEditView): fields = ('label', 'slug', 'enabled') model = Index - post_action_redirect = reverse_lazy('indexing:index_setup_list') object_permission = permission_document_indexing_edit + pk_url_kwarg = 'index_pk' + post_action_redirect = reverse_lazy(viewname='indexing:index_setup_list') def get_extra_context(self): return { @@ -101,25 +103,25 @@ class SetupIndexDocumentTypesView(AssignRemoveView): def get_document_queryset(self): return AccessControlList.objects.filter_by_access( - permission_document_view, self.request.user, - queryset=DocumentType.objects.all() + permission_document_view, queryset=DocumentType.objects.all(), + user=self.request.user ) def get_extra_context(self): return { 'object': self.get_object(), - 'title': _( - 'Document types linked to index: %s' - ) % self.get_object(), 'subtitle': _( 'Only the documents of the types selected will be shown ' 'in the index when built. Only the events of the documents ' 'of the types select will trigger updates in the index.' ), + 'title': _( + 'Document types linked to index: %s' + ) % self.get_object() } def get_object(self): - return get_object_or_404(klass=Index, pk=self.kwargs['pk']) + return get_object_or_404(klass=Index, pk=self.kwargs['index_pk']) def left_list(self): return AssignRemoveView.generate_choices( @@ -133,7 +135,7 @@ class SetupIndexDocumentTypesView(AssignRemoveView): def right_list(self): return AssignRemoveView.generate_choices( - self.get_document_queryset() & self.get_object().document_types.all() + choices=self.get_document_queryset() & self.get_object().document_types.all() ) @@ -149,7 +151,7 @@ class SetupIndexTreeTemplateListView(SingleObjectListView): } def get_index(self): - return get_object_or_404(klass=Index, pk=self.kwargs['pk']) + return get_object_or_404(klass=Index, pk=self.kwargs['index_pk']) def get_object_list(self): return self.get_index().template_root.get_descendants( @@ -163,13 +165,13 @@ class TemplateNodeCreateView(SingleObjectCreateView): def dispatch(self, request, *args, **kwargs): AccessControlList.objects.check_access( - permissions=permission_document_indexing_edit, user=request.user, - obj=self.get_parent_node().index + obj=self.get_parent_node().index, + permissions=permission_document_indexing_edit, user=request.user ) return super( TemplateNodeCreateView, self - ).dispatch(request, *args, **kwargs) + ).dispatch(request=request, *args, **kwargs) def get_extra_context(self): return { @@ -185,7 +187,9 @@ class TemplateNodeCreateView(SingleObjectCreateView): } def get_parent_node(self): - return get_object_or_404(klass=IndexTemplateNode, pk=self.kwargs['pk']) + return get_object_or_404( + klass=IndexTemplateNode, pk=self.kwargs['index_template_node_pk'] + ) class TemplateNodeDeleteView(SingleObjectDeleteView): @@ -205,7 +209,8 @@ class TemplateNodeDeleteView(SingleObjectDeleteView): def get_post_action_redirect(self): return reverse( - 'indexing:index_setup_view', args=(self.get_object().index.pk,) + viewname='indexing:index_setup_view', + kwargs={'index_pk': self.get_object().index.pk} ) @@ -227,7 +232,8 @@ class TemplateNodeEditView(SingleObjectEditView): def get_post_action_redirect(self): return reverse( - 'indexing:index_setup_view', args=(self.get_object().index.pk,) + viewname='indexing:index_setup_view', + kwargs={'index_pk': self.get_object().index.pk} ) @@ -252,7 +258,9 @@ class IndexListView(SingleObjectListView): def get_object_list(self): queryset = IndexInstance.objects.filter(enabled=True) - return queryset.filter(node_templates__index_instance_nodes__isnull=False).distinct() + return queryset.filter( + node_templates__index_instance_nodes__isnull=False + ).distinct() class IndexInstanceNodeView(DocumentListView): @@ -260,21 +268,24 @@ class IndexInstanceNodeView(DocumentListView): def dispatch(self, request, *args, **kwargs): self.index_instance_node = get_object_or_404( - klass=IndexInstanceNode, pk=self.kwargs['pk'] + klass=IndexInstanceNode, pk=self.kwargs['index_instance_node_pk'] ) AccessControlList.objects.check_access( + obj=self.index_instance_node.index(), permissions=permission_document_indexing_instance_view, - user=request.user, obj=self.index_instance_node.index() + user=request.user ) if self.index_instance_node: if self.index_instance_node.index_template_node.link_documents: return super(IndexInstanceNodeView, self).dispatch( - request, *args, **kwargs + request=request, *args, **kwargs ) - return SingleObjectListView.dispatch(self, request, *args, **kwargs) + return SingleObjectListView.dispatch( + self, request=request, *args, **kwargs + ) def get_document_queryset(self): if self.index_instance_node: @@ -331,16 +342,18 @@ class DocumentIndexNodeListView(SingleObjectListView): def dispatch(self, request, *args, **kwargs): AccessControlList.objects.check_access( - permissions=permission_document_view, user=request.user, - obj=self.get_document() + obj=self.get_document(), permissions=permission_document_view, + user=request.user ) return super( DocumentIndexNodeListView, self - ).dispatch(request, *args, **kwargs) + ).dispatch(request=request, *args, **kwargs) def get_document(self): - return get_object_or_404(klass=Document, pk=self.kwargs['pk']) + return get_object_or_404( + klass=Document, pk=self.kwargs['document_pk'] + ) def get_extra_context(self): return { @@ -381,7 +394,7 @@ class RebuildIndexesView(FormView): count += 1 messages.success( - self.request, ungettext( + request=self.request, message=ungettext( singular='%(count)d index queued for rebuild.', plural='%(count)d indexes queued for rebuild.', number=count @@ -398,4 +411,4 @@ class RebuildIndexesView(FormView): } def get_post_action_redirect(self): - return reverse('common:tools_list') + return reverse(viewname='common:tools_list') diff --git a/mayan/apps/document_indexing/widgets.py b/mayan/apps/document_indexing/widgets.py index d5517ebd56..d55f834c27 100644 --- a/mayan/apps/document_indexing/widgets.py +++ b/mayan/apps/document_indexing/widgets.py @@ -12,7 +12,7 @@ def get_instance_link(index_instance_node): Return an HTML anchor to an index node instance """ return mark_safe( - '{text}'.format( + s='{text}'.format( url=index_instance_node.get_absolute_url(), text=escape(index_instance_node.get_full_path()) ) @@ -20,6 +20,7 @@ def get_instance_link(index_instance_node): def index_instance_item_link(index_instance_item): + #TODO: Replace with a file template IndexInstanceNode = apps.get_model( app_label='document_indexing', model_name='IndexInstanceNode' ) @@ -33,7 +34,7 @@ def index_instance_item_link(index_instance_item): icon = '' return mark_safe( - '%(icon)s %(text)s' % { + s='%(icon)s %(text)s' % { 'url': index_instance_item.get_absolute_url(), 'icon': icon, 'text': index_instance_item @@ -45,8 +46,9 @@ def node_level(node): """ Render an indented tree like output for a specific node """ + #TODO: Replace with a file template return mark_safe( - ''.join( + s=''.join( [ '     ' * node.get_level(), '' if node.is_root_node() else icon_index_level_up.render(), @@ -57,6 +59,7 @@ def node_level(node): def node_tree(node, user): + #TODO: Replace with a file template result = [] result.append('
') @@ -84,4 +87,4 @@ def node_tree(node, user): result.append('
') - return mark_safe(''.join(result)) + return mark_safe(s=''.join(result)) From 65ccbd3b7b4e2675ab05727afd4e9e54ee9a1697 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Thu, 3 Jan 2019 14:04:16 -0400 Subject: [PATCH 006/209] Reorganize reusable test code Extract test views and user code into their own separate test case mixins. Append TestCase to test case mixins with base test code to differentiate them from test mixins with reusable view calls. Signed-off-by: Roberto Rosario --- mayan/apps/acls/tests/mixins.py | 48 +++----- mayan/apps/common/tests/base.py | 100 ++------------- mayan/apps/common/tests/mixins.py | 122 ++++++++++++++++--- mayan/apps/permissions/tests/literals.py | 4 +- mayan/apps/permissions/tests/mixins.py | 23 ++++ mayan/apps/rest_api/permissions.py | 10 +- mayan/apps/rest_api/tests/base.py | 87 +------------ mayan/apps/user_management/tests/literals.py | 14 ++- mayan/apps/user_management/tests/mixins.py | 92 +++++++++++++- 9 files changed, 268 insertions(+), 232 deletions(-) create mode 100644 mayan/apps/permissions/tests/mixins.py diff --git a/mayan/apps/acls/tests/mixins.py b/mayan/apps/acls/tests/mixins.py index 353d734b03..7c32cb7287 100644 --- a/mayan/apps/acls/tests/mixins.py +++ b/mayan/apps/acls/tests/mixins.py @@ -1,47 +1,27 @@ from __future__ import unicode_literals -from django.contrib.auth import get_user_model -from django.contrib.auth.models import Group +from django.core.exceptions import ImproperlyConfigured -from mayan.apps.permissions.models import Role -from mayan.apps.permissions.tests.literals import TEST_ROLE_LABEL -from mayan.apps.user_management.tests import ( - TEST_ADMIN_EMAIL, TEST_ADMIN_PASSWORD, TEST_ADMIN_USERNAME, - TEST_GROUP_NAME, TEST_USER_EMAIL, TEST_USER_PASSWORD, TEST_USER_USERNAME -) +from mayan.apps.permissions.tests.mixins import RoleTestCaseMixin +from mayan.apps.user_management.tests.mixins import UserTestCaseMixin from ..models import AccessControlList -class ACLBaseTestMixin(object): - auto_create_group = True - auto_create_users = True +class ACLTestCaseMixin(RoleTestCaseMixin, UserTestCaseMixin): def setUp(self): - super(ACLBaseTestMixin, self).setUp() - if self.auto_create_users: - self.admin_user = get_user_model().objects.create_superuser( - username=TEST_ADMIN_USERNAME, email=TEST_ADMIN_EMAIL, - password=TEST_ADMIN_PASSWORD - ) - - self.user = get_user_model().objects.create_user( - username=TEST_USER_USERNAME, email=TEST_USER_EMAIL, - password=TEST_USER_PASSWORD - ) - - if self.auto_create_group: - self.group = Group.objects.create(name=TEST_GROUP_NAME) - self.role = Role.objects.create(label=TEST_ROLE_LABEL) - self.group.user_set.add(self.user) - self.role.groups.add(self.group) + super(ACLTestCaseMixin, self).setUp() + if hasattr(self, '_test_case_user'): + self._test_case_role.groups.add(self._test_case_group) def grant_access(self, obj, permission): - return AccessControlList.objects.grant( - obj=obj, permission=permission, role=self.role - ) + if not hasattr(self, '_test_case_role'): + raise ImproperlyConfigured( + 'Enable the creation of the test case user, group, and role ' + 'in order to enable the usage of ACLs in tests.' + ) - def grant_permission(self, permission): - self.role.permissions.add( - permission.stored_permission + return AccessControlList.objects.grant( + obj=obj, permission=permission, role=self._test_case_role ) diff --git a/mayan/apps/common/tests/base.py b/mayan/apps/common/tests/base.py index 81c97bed83..82f9f19b21 100644 --- a/mayan/apps/common/tests/base.py +++ b/mayan/apps/common/tests/base.py @@ -1,30 +1,21 @@ from __future__ import absolute_import, unicode_literals -from django.conf.urls import url -from django.contrib.auth import get_user_model -from django.http import HttpResponse -from django.template import Context, Template from django.test import TestCase -from django.test.utils import ContextList -from django.urls import clear_url_caches, reverse +from django.urls import reverse from django_downloadview import assert_download_response -from mayan.apps.acls.tests.mixins import ACLBaseTestMixin +from mayan.apps.acls.tests.mixins import ACLTestCaseMixin from mayan.apps.permissions.classes import Permission from mayan.apps.smart_settings.classes import Namespace -from mayan.apps.user_management.tests import ( - TEST_ADMIN_PASSWORD, TEST_ADMIN_USERNAME, TEST_USER_PASSWORD, - TEST_USER_USERNAME -) -from .literals import TEST_VIEW_NAME, TEST_VIEW_URL from .mixins import ( - ContentTypeCheckMixin, DatabaseConversionMixin, OpenFileCheckMixin, - TempfileCheckMixin + ClientMethodsTestCaseMixin, ContentTypeCheckMixin, DatabaseConversionMixin, + OpenFileCheckTestCaseMixin, TempfileCheckTestCaseMixin, + TestViewTestCaseMixin ) -class BaseTestCase(DatabaseConversionMixin, ACLBaseTestMixin, ContentTypeCheckMixin, OpenFileCheckMixin, TempfileCheckMixin, TestCase): +class BaseTestCase(DatabaseConversionMixin, ACLTestCaseMixin, ContentTypeCheckMixin, OpenFileCheckTestCaseMixin, TempfileCheckTestCaseMixin, TestCase): """ This is the most basic test case class any test in the project should use. """ @@ -36,76 +27,9 @@ class BaseTestCase(DatabaseConversionMixin, ACLBaseTestMixin, ContentTypeCheckMi Permission.invalidate_cache() -class GenericViewTestCase(BaseTestCase): - has_test_view = False - - def tearDown(self): - from mayan.urls import urlpatterns - - self.client.logout() - if self.has_test_view: - urlpatterns.pop(0) - super(GenericViewTestCase, self).tearDown() - - def add_test_view(self, test_object): - from mayan.urls import urlpatterns - - def test_view(request): - template = Template('{{ object }}') - context = Context( - {'object': test_object, 'resolved_object': test_object} - ) - return HttpResponse(template.render(context=context)) - - urlpatterns.insert(0, url(TEST_VIEW_URL, test_view, name=TEST_VIEW_NAME)) - clear_url_caches() - self.has_test_view = True - - def get_test_view(self): - response = self.get(TEST_VIEW_NAME) - if isinstance(response.context, ContextList): - # template widget rendering causes test client response to be - # ContextList rather than RequestContext. Typecast to dictionary - # before updating. - result = dict(response.context).copy() - result.update({'request': response.wsgi_request}) - return Context(result) - else: - response.context.update({'request': response.wsgi_request}) - return Context(response.context) - - def get(self, viewname=None, path=None, *args, **kwargs): - data = kwargs.pop('data', {}) - follow = kwargs.pop('follow', False) - - if viewname: - path = reverse(viewname=viewname, *args, **kwargs) - - return self.client.get( - path=path, data=data, follow=follow - ) - - def login(self, *args, **kwargs): - logged_in = self.client.login(*args, **kwargs) - - return logged_in - - def login_user(self): - self.login(username=TEST_USER_USERNAME, password=TEST_USER_PASSWORD) - - def login_admin_user(self): - self.login(username=TEST_ADMIN_USERNAME, password=TEST_ADMIN_PASSWORD) - - def logout(self): - self.client.logout() - - def post(self, viewname=None, path=None, *args, **kwargs): - data = kwargs.pop('data', {}) - follow = kwargs.pop('follow', False) - - if viewname: - path = reverse(viewname=viewname, *args, **kwargs) - - return self.client.post( - path=path, data=data, follow=follow - ) +class GenericViewTestCase(ClientMethodsTestCaseMixin, TestViewTestCaseMixin, BaseTestCase): + """ + A generic view test case built on top of the base test case providing + single user test view to test object resolution and shorthand HTTP + method functions. + """ diff --git a/mayan/apps/common/tests/mixins.py b/mayan/apps/common/tests/mixins.py index f44bd7f4e9..621353bd68 100644 --- a/mayan/apps/common/tests/mixins.py +++ b/mayan/apps/common/tests/mixins.py @@ -4,18 +4,16 @@ import glob import os from django.conf import settings -from django.contrib.auth import get_user_model -from django.contrib.auth.models import Group +from django.conf.urls import url from django.core import management - -from mayan.apps.user_management.tests import ( - TEST_ADMIN_EMAIL, TEST_ADMIN_PASSWORD, TEST_ADMIN_USERNAME, - TEST_GROUP_NAME, TEST_USER_EMAIL, TEST_USER_PASSWORD, - TEST_USER_USERNAME -) +from django.http import HttpResponse +from django.template import Context, Template +from django.test.utils import ContextList +from django.urls import clear_url_caches, reverse from ..settings import setting_temporary_directory +from .literals import TEST_VIEW_NAME, TEST_VIEW_URL from .utils import mute_stdout @@ -23,6 +21,63 @@ if getattr(settings, 'COMMON_TEST_FILE_HANDLES', False): import psutil +class ClientMethodsTestCaseMixin(object): + def delete(self, viewname=None, path=None, *args, **kwargs): + data = kwargs.pop('data', {}) + follow = kwargs.pop('follow', False) + + if viewname: + path = reverse(viewname=viewname, *args, **kwargs) + + return self.client.delete( + path=path, data=data, follow=follow + ) + + def get(self, viewname=None, path=None, *args, **kwargs): + data = kwargs.pop('data', {}) + follow = kwargs.pop('follow', False) + + if viewname: + path = reverse(viewname=viewname, *args, **kwargs) + + return self.client.get( + path=path, data=data, follow=follow + ) + + def patch(self, viewname=None, path=None, *args, **kwargs): + data = kwargs.pop('data', {}) + follow = kwargs.pop('follow', False) + + if viewname: + path = reverse(viewname=viewname, *args, **kwargs) + + return self.client.patch( + path=path, data=data, follow=follow + ) + + def post(self, viewname=None, path=None, *args, **kwargs): + data = kwargs.pop('data', {}) + follow = kwargs.pop('follow', False) + + if viewname: + path = reverse(viewname=viewname, *args, **kwargs) + + return self.client.post( + path=path, data=data, follow=follow + ) + + def put(self, viewname=None, path=None, *args, **kwargs): + data = kwargs.pop('data', {}) + follow = kwargs.pop('follow', False) + + if viewname: + path = reverse(viewname=viewname, *args, **kwargs) + + return self.client.put( + path=path, data=data, follow=follow + ) + + class ContentTypeCheckMixin(object): expected_content_type = 'text/html; charset=utf-8' @@ -55,7 +110,7 @@ class DatabaseConversionMixin(object): ) -class OpenFileCheckMixin(object): +class OpenFileCheckTestCaseMixin(object): def _get_descriptor_count(self): process = psutil.Process() return process.num_fds() @@ -65,7 +120,7 @@ class OpenFileCheckMixin(object): return process.open_files() def setUp(self): - super(OpenFileCheckMixin, self).setUp() + super(OpenFileCheckTestCaseMixin, self).setUp() if getattr(settings, 'COMMON_TEST_FILE_HANDLES', False): self._open_files = self._get_open_files() @@ -80,10 +135,10 @@ class OpenFileCheckMixin(object): self._skip_file_descriptor_test = False - super(OpenFileCheckMixin, self).tearDown() + super(OpenFileCheckTestCaseMixin, self).tearDown() -class TempfileCheckMixin(object): +class TempfileCheckTestCaseMixin(object): # Ignore the jvmstat instrumentation and GitLab's CI .config files # Ignore LibreOffice fontconfig cache dir ignore_globs = ('hsperfdata_*', '.config', '.cache') @@ -108,7 +163,7 @@ class TempfileCheckMixin(object): ) - set(ignored_result) def setUp(self): - super(TempfileCheckMixin, self).setUp() + super(TempfileCheckTestCaseMixin, self).setUp() if getattr(settings, 'COMMON_TEST_TEMP_FILES', False): self._temporary_items = self._get_temporary_entries() @@ -123,4 +178,43 @@ class TempfileCheckMixin(object): ','.join(final_temporary_items - self._temporary_items) ) ) - super(TempfileCheckMixin, self).tearDown() + super(TempfileCheckTestCaseMixin, self).tearDown() + + +class TestViewTestCaseMixin(object): + has_test_view = False + + def tearDown(self): + from mayan.urls import urlpatterns + + self.client.logout() + if self.has_test_view: + urlpatterns.pop(0) + super(TestViewTestCaseMixin, self).tearDown() + + def add_test_view(self, test_object): + from mayan.urls import urlpatterns + + def test_view(request): + template = Template('{{ object }}') + context = Context( + {'object': test_object, 'resolved_object': test_object} + ) + return HttpResponse(template.render(context=context)) + + urlpatterns.insert(0, url(TEST_VIEW_URL, test_view, name=TEST_VIEW_NAME)) + clear_url_caches() + self.has_test_view = True + + def get_test_view(self): + response = self.get(TEST_VIEW_NAME) + if isinstance(response.context, ContextList): + # template widget rendering causes test client response to be + # ContextList rather than RequestContext. Typecast to dictionary + # before updating. + result = dict(response.context).copy() + result.update({'request': response.wsgi_request}) + return Context(result) + else: + response.context.update({'request': response.wsgi_request}) + return Context(response.context) diff --git a/mayan/apps/permissions/tests/literals.py b/mayan/apps/permissions/tests/literals.py index 52d75bdea2..42815f1718 100644 --- a/mayan/apps/permissions/tests/literals.py +++ b/mayan/apps/permissions/tests/literals.py @@ -1,5 +1,5 @@ from __future__ import unicode_literals -TEST_ROLE_2_LABEL = 'test role 2' -TEST_ROLE_LABEL = 'test role' +TEST_CASE_ROLE_LABEL = 'test case role' +TEST_ROLE_LABEL = 'test role 2' TEST_ROLE_LABEL_EDITED = 'test role label edited' diff --git a/mayan/apps/permissions/tests/mixins.py b/mayan/apps/permissions/tests/mixins.py new file mode 100644 index 0000000000..212fb554c4 --- /dev/null +++ b/mayan/apps/permissions/tests/mixins.py @@ -0,0 +1,23 @@ +from __future__ import unicode_literals + +from ..models import Role + +from .literals import TEST_CASE_ROLE_LABEL, TEST_ROLE_LABEL + + +class RoleTestCaseMixin(object): + def setUp(self): + super(RoleTestCaseMixin, self).setUp() + if hasattr(self, '_test_case_group'): + self.create_role() + + def create_role(self): + self._test_case_role = Role.objects.create(label=TEST_CASE_ROLE_LABEL) + + def grant_permission(self, permission): + self._test_case_role.grant(permission=permission) + + +class RoleTestMixin(object): + def _create_test_role(self): + self.test_role = Role.objects.create(label=TEST_ROLE_LABEL) diff --git a/mayan/apps/rest_api/permissions.py b/mayan/apps/rest_api/permissions.py index 88c37cd746..476b551a21 100644 --- a/mayan/apps/rest_api/permissions.py +++ b/mayan/apps/rest_api/permissions.py @@ -3,6 +3,7 @@ from __future__ import absolute_import from __future__ import unicode_literals from django.core.exceptions import PermissionDenied +from django.http import Http404 from rest_framework.permissions import BasePermission @@ -33,6 +34,10 @@ class MayanPermission(BasePermission): view, 'mayan_object_permissions', {} ).get(request.method, None) + object_permissions_raise_404 = getattr( + view, 'mayan_object_permissions_raise_404', () + ) + if required_permission: try: if hasattr(view, 'mayan_permission_attribute_check'): @@ -47,7 +52,10 @@ class MayanPermission(BasePermission): obj=obj ) except PermissionDenied: - return False + if request.method in object_permissions_raise_404: + raise Http404 + else: + return False else: return True else: diff --git a/mayan/apps/rest_api/tests/base.py b/mayan/apps/rest_api/tests/base.py index f088f2c8c9..1a7de24fd1 100644 --- a/mayan/apps/rest_api/tests/base.py +++ b/mayan/apps/rest_api/tests/base.py @@ -1,20 +1,15 @@ from __future__ import absolute_import, unicode_literals -from django.contrib.auth import get_user_model -from django.urls import reverse - from rest_framework.test import APITestCase -from mayan.apps.acls.tests.mixins import ACLBaseTestMixin +from mayan.apps.acls.tests.mixins import ACLTestCaseMixin +from mayan.apps.common.tests.mixins import ClientMethodsTestCaseMixin from mayan.apps.permissions.classes import Permission from mayan.apps.smart_settings.classes import Namespace -from mayan.apps.user_management.tests import ( - TEST_ADMIN_PASSWORD, TEST_ADMIN_USERNAME, TEST_USER_USERNAME, - TEST_USER_PASSWORD -) +from mayan.apps.user_management.tests.mixins import UserTestCaseMixin -class BaseAPITestCase(ACLBaseTestMixin, APITestCase): +class BaseAPITestCase(ClientMethodsTestCaseMixin, ACLTestCaseMixin, UserTestCaseMixin, APITestCase): """ API test case class that invalidates permissions and smart settings """ @@ -24,78 +19,4 @@ class BaseAPITestCase(ACLBaseTestMixin, APITestCase): Permission.invalidate_cache() def tearDown(self): - self.client.logout() super(BaseAPITestCase, self).tearDown() - - def delete(self, viewname=None, path=None, *args, **kwargs): - data = kwargs.pop('data', {}) - follow = kwargs.pop('follow', False) - - if viewname: - path = reverse(viewname=viewname, *args, **kwargs) - - return self.client.delete( - path=path, data=data, follow=follow - ) - - def get(self, viewname=None, path=None, *args, **kwargs): - data = kwargs.pop('data', {}) - follow = kwargs.pop('follow', False) - - if viewname: - path = reverse(viewname=viewname, *args, **kwargs) - - return self.client.get( - path=path, data=data, follow=follow - ) - - def login(self, username, password): - logged_in = self.client.login(username=username, password=password) - - user = get_user_model().objects.get(username=username) - - self.assertTrue(logged_in) - self.assertTrue(user.is_authenticated) - return user.is_authenticated - - def login_user(self): - self.login(username=TEST_USER_USERNAME, password=TEST_USER_PASSWORD) - - def login_admin_user(self): - self.login(username=TEST_ADMIN_USERNAME, password=TEST_ADMIN_PASSWORD) - - def logout(self): - self.client.logout() - - def patch(self, viewname=None, path=None, *args, **kwargs): - data = kwargs.pop('data', {}) - follow = kwargs.pop('follow', False) - - if viewname: - path = reverse(viewname=viewname, *args, **kwargs) - - return self.client.patch( - path=path, data=data, follow=follow - ) - - def post(self, viewname=None, path=None, *args, **kwargs): - data = kwargs.pop('data', {}) - follow = kwargs.pop('follow', False) - - if viewname: - path = reverse(viewname=viewname, *args, **kwargs) - - return self.client.post( - path=path, data=data, follow=follow - ) - - def put(self, viewname=None, path=None, *args, **kwargs): - data = kwargs.pop('data', {}) - follow = kwargs.pop('follow', False) - - if viewname: - path = reverse(viewname=viewname, *args, **kwargs) - - return self.client.put( - path=path, data=data, follow=follow - ) diff --git a/mayan/apps/user_management/tests/literals.py b/mayan/apps/user_management/tests/literals.py index 17577445a3..0cc6ddadc8 100644 --- a/mayan/apps/user_management/tests/literals.py +++ b/mayan/apps/user_management/tests/literals.py @@ -1,18 +1,22 @@ from __future__ import unicode_literals __all__ = ( - 'TEST_ADMIN_EMAIL', 'TEST_ADMIN_PASSWORD', 'TEST_ADMIN_USERNAME', 'TEST_GROUP_NAME', 'TEST_GROUP_NAME_EDITED', 'TEST_USER_EMAIL', 'TEST_USER_PASSWORD', 'TEST_USER_PASSWORD_EDITED', 'TEST_USER_USERNAME' ) -TEST_ADMIN_EMAIL = 'admin@example.com' -TEST_ADMIN_PASSWORD = 'test admin password' -TEST_ADMIN_USERNAME = 'test_admin' +TEST_CASE_ADMIN_EMAIL = 'admin@example.com' +TEST_CASE_ADMIN_PASSWORD = 'test admin password' +TEST_CASE_ADMIN_USERNAME = 'test_admin' + +TEST_CASE_GROUP_NAME = 'test group' +TEST_CASE_USER_EMAIL = 'user@example.com' +TEST_CASE_USER_PASSWORD = 'test user password' +TEST_CASE_USER_USERNAME = 'test_user' TEST_GROUP_NAME = 'test group' -TEST_GROUP_2_NAME = 'test group 2' TEST_GROUP_NAME_EDITED = 'test group edited' +TEST_GROUP_2_NAME = 'test group 2' TEST_GROUP_2_NAME_EDITED = 'test group 2 edited' TEST_USER_EMAIL = 'user@example.com' TEST_USER_PASSWORD = 'test user password' diff --git a/mayan/apps/user_management/tests/mixins.py b/mayan/apps/user_management/tests/mixins.py index 77ff3726c9..81643866ef 100644 --- a/mayan/apps/user_management/tests/mixins.py +++ b/mayan/apps/user_management/tests/mixins.py @@ -4,11 +4,60 @@ from django.contrib.auth import get_user_model from django.contrib.auth.models import Group from .literals import ( - TEST_GROUP_2_NAME, TEST_GROUP_2_NAME_EDITED, TEST_USER_2_EMAIL, - TEST_USER_2_PASSWORD, TEST_USER_2_USERNAME, TEST_USER_2_USERNAME_EDITED + TEST_CASE_ADMIN_EMAIL, TEST_CASE_ADMIN_PASSWORD, TEST_CASE_ADMIN_USERNAME, + TEST_CASE_GROUP_NAME, TEST_CASE_USER_EMAIL, TEST_CASE_USER_PASSWORD, + TEST_CASE_USER_USERNAME, TEST_GROUP_NAME, TEST_GROUP_2_NAME, + TEST_GROUP_2_NAME_EDITED, TEST_USER_2_EMAIL, TEST_USER_2_PASSWORD, + TEST_USER_EMAIL, TEST_USER_USERNAME, TEST_USER_PASSWORD, + TEST_USER_2_USERNAME, TEST_USER_2_USERNAME_EDITED ) +class UserTestCaseMixin(object): + auto_login_admin = False + auto_login_user = True + + def setUp(self): + super(UserTestCaseMixin, self).setUp() + if self.auto_login_user: + self._test_case_user = get_user_model().objects.create_user( + username=TEST_CASE_USER_USERNAME, email=TEST_CASE_USER_EMAIL, + password=TEST_CASE_USER_PASSWORD + ) + self.login_user() + self._test_case_group = Group.objects.create(name=TEST_GROUP_NAME) + self._test_case_group.user_set.add(self._test_case_user) + elif self.auto_login_admin: + self._test_case_admin_user = get_user_model().objects.create_superuser( + username=TEST_CASE_ADMIN_USERNAME, email=TEST_CASE_ADMIN_EMAIL, + password=TEST_CASE_ADMIN_PASSWORD + ) + self.login_admin_user() + + def tearDown(self): + self.client.logout() + super(UserTestCaseMixin, self).tearDown() + + def login(self, *args, **kwargs): + logged_in = self.client.login(*args, **kwargs) + + return logged_in + + def login_user(self): + self.login( + username=TEST_CASE_USER_USERNAME, password=TEST_CASE_USER_PASSWORD + ) + + def login_admin_user(self): + self.login( + username=TEST_CASE_ADMIN_USERNAME, + password=TEST_CASE_ADMIN_PASSWORD + ) + + def logout(self): + self.client.logout() + + class UserTestMixin(object): def _create_test_group(self): self.test_group = Group.objects.create(name=TEST_GROUP_2_NAME) @@ -23,6 +72,8 @@ class UserTestMixin(object): password=TEST_USER_2_PASSWORD ) + # Group views + def _request_test_group_create_view(self): reponse = self.post( viewname='user_management:group_create', data={ @@ -32,19 +83,38 @@ class UserTestMixin(object): self.test_group = Group.objects.filter(name=TEST_GROUP_2_NAME).first() return reponse + def _request_test_group_delete_view(self): + return self.post( + viewname='user_management:group_delete', kwargs={ + 'group_pk': self.test_group.pk + } + ) + def _request_test_group_edit_view(self): return self.post( viewname='user_management:group_edit', kwargs={ - 'pk': self.test_group.pk + 'group_pk': self.test_group.pk }, data={ 'name': TEST_GROUP_2_NAME_EDITED } ) + def _request_test_group_list_view(self): + return self.get(viewname='user_management:group_list') + + def _request_test_group_members_view(self): + return self.get( + viewname='user_management:group_members', + kwargs={'group_pk': self.test_group.pk} + ) + + # User views + def _request_test_user_create_view(self): reponse = self.post( viewname='user_management:user_create', data={ - 'username': TEST_USER_2_USERNAME + 'username': TEST_USER_2_USERNAME, + 'password': TEST_USER_2_PASSWORD } ) @@ -53,11 +123,23 @@ class UserTestMixin(object): ).first() return reponse + def _request_test_user_delete_view(self): + return self.post( + viewname='user_management:user_delete', + kwargs={'user_pk': self.test_user.pk} + ) + def _request_test_user_edit_view(self): return self.post( viewname='user_management:user_edit', kwargs={ - 'pk': self.test_user.pk + 'user_pk': self.test_user.pk }, data={ 'username': TEST_USER_2_USERNAME_EDITED } ) + + def _request_test_user_groups_view(self): + return self.get( + viewname='user_management:user_groups', + kwargs={'user_pk': self.test_user.pk} + ) From 9d8c8f48331b4488150da2c370afdea677360b29 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sun, 13 Jan 2019 22:57:59 -0400 Subject: [PATCH 007/209] Optimize permission check Convert the user permission check from a double Python loop to a single ORM query. Add methods to the Role model to grant or revoke permissions. Rename the method requester_has_this to user_has_this for clarity. Signed-off-by: Roberto Rosario --- mayan/apps/permissions/models.py | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/mayan/apps/permissions/models.py b/mayan/apps/permissions/models.py index d0eb4a7b2f..503df464aa 100644 --- a/mayan/apps/permissions/models.py +++ b/mayan/apps/permissions/models.py @@ -59,7 +59,7 @@ class StoredPermission(models.Model): def natural_key(self): return (self.namespace, self.name) - def requester_has_this(self, user): + def user_has_this(self, user): """ Helper method to check if an user has been granted this permission. The check is done sequentially over all of the user's groups and @@ -73,20 +73,13 @@ class StoredPermission(models.Model): ) return True - # Request is one of the permission's holders? - for group in user.groups.all(): - for role in group.roles.all(): - if self in role.permissions.all(): - logger.debug( - 'Permission "%s" granted to user "%s" through role "%s"', - self, user, role - ) - return True - - logger.debug( - 'Fallthru: Permission "%s" not granted to user "%s"', self, user - ) - return False + if Role.objects.filter(groups__user=user, permissions=self).exists(): + return True + else: + logger.debug( + 'Fallthru: Permission "%s" not granted to user "%s"', self, user + ) + return False @python_2_unicode_compatible @@ -120,8 +113,14 @@ class Role(models.Model): return self.label def get_absolute_url(self): - return reverse('permissions:role_list') + return reverse(viewname='permissions:role_list') + + def grant(self, permission): + self.permissions.add(permission.stored_permission) def natural_key(self): return (self.label,) natural_key.dependencies = ['auth.Group', 'permissions.StoredPermission'] + + def revoke(self, permission): + self.permissions.remove(permission.stored_permission) From 38d7b7cda36137a9e5b6f86dbb9b9afd3ce81004 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sun, 13 Jan 2019 22:59:59 -0400 Subject: [PATCH 008/209] Add check_permissions replacement Add a new class method named check_user_permission. This method is smaller as it only accepts a single permission instead of a single or a list of permission like check_permissions does. check_user_permission is meant to replace check_permissions. Signed-off-by: Roberto Rosario --- mayan/apps/permissions/classes.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/mayan/apps/permissions/classes.py b/mayan/apps/permissions/classes.py index 96220276f7..d974745bb2 100644 --- a/mayan/apps/permissions/classes.py +++ b/mayan/apps/permissions/classes.py @@ -72,15 +72,16 @@ class Permission(object): cls._permissions.values(), key=lambda x: x.namespace.name ) + # Deprecated method @classmethod - def check_permissions(cls, requester, permissions): + def check_permissions(cls, permissions, requester): try: for permission in permissions: - if permission.stored_permission.requester_has_this(requester): + if permission.stored_permission.user_has_this(user=requester): return True except TypeError: # Not a list of permissions, just one - if permissions.stored_permission.requester_has_this(requester): + if permissions.stored_permission.user_has_this(user=requester): return True logger.debug( @@ -88,6 +89,13 @@ class Permission(object): ) raise PermissionDenied(_('Insufficient permissions.')) + @classmethod + def check_user_permission(cls, permission, user): + if permission.stored_permission.user_has_this(user=user): + return True + + raise PermissionDenied(_('Insufficient permissions.')) + @classmethod def get(cls, pk, proxy_only=False): if proxy_only: From 0e800dc31491b38892a9a7e956b99efc03c23d02 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sun, 13 Jan 2019 23:15:15 -0400 Subject: [PATCH 009/209] Use keyword arguments in the permissions app Additionall rename the views GroupRoleMembersView, SetupRoleMembersView, SetupRolePermissionsView to GroupRolesView, RoleGroupsView, RolePermissionsView. Signed-off-by: Roberto Rosario --- mayan/apps/permissions/exceptions.py | 7 +- mayan/apps/permissions/icons.py | 5 +- mayan/apps/permissions/links.py | 15 ++- mayan/apps/permissions/tests/test_api.py | 108 +++++++++++--------- mayan/apps/permissions/tests/test_models.py | 14 +-- mayan/apps/permissions/tests/test_views.py | 92 ++++++++--------- mayan/apps/permissions/urls.py | 52 ++++++---- mayan/apps/permissions/views.py | 84 +++++++-------- 8 files changed, 201 insertions(+), 176 deletions(-) diff --git a/mayan/apps/permissions/exceptions.py b/mayan/apps/permissions/exceptions.py index d55e89a704..f1d9c08aeb 100644 --- a/mayan/apps/permissions/exceptions.py +++ b/mayan/apps/permissions/exceptions.py @@ -2,8 +2,11 @@ from __future__ import unicode_literals class PermissionError(Exception): - pass + """Base permission exception""" class InvalidNamespace(PermissionError): - pass + """ + Invalid namespace name. This is probably an obsolete permission namespace, + execute the management command "purgepermissions" and try again. + """ diff --git a/mayan/apps/permissions/icons.py b/mayan/apps/permissions/icons.py index 860b5b537e..594f0eff68 100644 --- a/mayan/apps/permissions/icons.py +++ b/mayan/apps/permissions/icons.py @@ -3,7 +3,10 @@ from __future__ import absolute_import, unicode_literals from mayan.apps.appearance.classes import Icon icon_permission = Icon(driver_name='fontawesome', symbol='thumbs-up') -icon_role_create = Icon(driver_name='fontawesome', symbol='plus') +icon_role_create = Icon( + driver_name='fontawesome-dual', primary_symbol='user-secret', + secondary_symbol='plus' +) icon_role_delete = Icon(driver_name='fontawesome', symbol='times') icon_role_edit = Icon(driver_name='fontawesome', symbol='pencil-alt') icon_role_groups = Icon(driver_name='fontawesome', symbol='users') diff --git a/mayan/apps/permissions/links.py b/mayan/apps/permissions/links.py index 42c194a6ae..c883137b79 100644 --- a/mayan/apps/permissions/links.py +++ b/mayan/apps/permissions/links.py @@ -3,12 +3,11 @@ from __future__ import unicode_literals from django.utils.translation import ugettext_lazy as _ from mayan.apps.navigation import Link -from mayan.apps.user_management.icons import icon_group from mayan.apps.user_management.permissions import permission_group_edit from .icons import ( - icon_permission, icon_role_create, icon_role_delete, icon_role_edit, - icon_role_groups, icon_role_list, icon_role_permissions + icon_role_create, icon_role_delete, icon_role_edit, icon_role_groups, + icon_role_list, icon_role_permissions ) from .permissions import ( permission_permission_grant, permission_permission_revoke, @@ -17,7 +16,7 @@ from .permissions import ( ) link_group_roles = Link( - args='object.id', icon_class=icon_role_list, + icon_class=icon_role_list, kwargs={'group_id': 'object.id'}, permissions=(permission_group_edit,), text=_('Roles'), view='permissions:group_roles', ) @@ -34,12 +33,12 @@ link_role_create = Link( text=_('Create new role'), view='permissions:role_create' ) link_role_delete = Link( - args='object.id', icon_class=icon_role_delete, + icon_class=icon_role_delete, kwargs={'role_id': 'object.id'}, permissions=(permission_role_delete,), tags='dangerous', text=_('Delete'), view='permissions:role_delete', ) link_role_edit = Link( - args='object.id', icon_class=icon_role_edit, + icon_class=icon_role_edit, kwargs={'role_id': 'object.id'}, permissions=(permission_role_edit,), text=_('Edit'), view='permissions:role_edit', ) @@ -48,12 +47,12 @@ link_role_list = Link( text=_('Roles'), view='permissions:role_list' ) link_role_groups = Link( - args='object.id', icon_class=icon_role_groups, + icon_class=icon_role_groups, kwargs={'role_id': 'object.id'}, permissions=(permission_role_edit,), text=_('Groups'), view='permissions:role_groups', ) link_role_permissions = Link( - args='object.id', icon_class=icon_role_permissions, + icon_class=icon_role_permissions, kwargs={'role_id': 'object.id'}, permissions=(permission_permission_grant, permission_permission_revoke), text=_('Role permissions'), view='permissions:role_permissions', ) diff --git a/mayan/apps/permissions/tests/test_api.py b/mayan/apps/permissions/tests/test_api.py index e28fbdef5b..85c3c0045f 100644 --- a/mayan/apps/permissions/tests/test_api.py +++ b/mayan/apps/permissions/tests/test_api.py @@ -5,7 +5,7 @@ from django.contrib.auth.models import Group from rest_framework import status from mayan.apps.rest_api.tests import BaseAPITestCase -from mayan.apps.user_management.tests.literals import TEST_GROUP_2_NAME +from mayan.apps.user_management.tests.literals import TEST_GROUP_NAME from ..classes import Permission from ..models import Role @@ -15,16 +15,12 @@ from ..permissions import ( ) from .literals import ( - TEST_ROLE_2_LABEL, TEST_ROLE_LABEL, TEST_ROLE_LABEL_EDITED + TEST_ROLE_LABEL, TEST_ROLE_LABEL_EDITED ) +from .mixins import RoleTestMixin -class PermissionAPITestCase(BaseAPITestCase): - def setUp(self): - super(PermissionAPITestCase, self).setUp() - self.login_user() - Permission.invalidate_cache() - +class PermissionAPITestCase(RoleTestMixin, BaseAPITestCase): def test_permissions_list_view(self): response = self.get(viewname='rest_api:permission-list') self.assertEqual(response.status_code, status.HTTP_200_OK) @@ -38,18 +34,18 @@ class PermissionAPITestCase(BaseAPITestCase): def test_roles_list_view_with_access(self): self.grant_access( - permission=permission_role_view, obj=self.role + permission=permission_role_view, obj=self.test_role ) response = self.get(viewname='rest_api:role-list') self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data['count'], 1) - self.assertEqual(response.data['results'][0]['label'], self.role.label) + self.assertEqual(response.data['results'][0]['label'], self.test_role.label) # Role create def _role_create_request(self, extra_data=None): data = { - 'label': TEST_ROLE_2_LABEL + 'label': TEST_ROLE_LABEL } if extra_data: @@ -69,20 +65,20 @@ class PermissionAPITestCase(BaseAPITestCase): response = self._role_create_request() self.assertEqual(response.status_code, status.HTTP_201_CREATED) - role = Role.objects.get(label=TEST_ROLE_2_LABEL) + role = Role.objects.get(label=TEST_ROLE_LABEL) self.assertEqual(response.data, {'label': role.label, 'id': role.pk}) self.assertEqual(Role.objects.count(), 2) - self.assertEqual(role.label, TEST_ROLE_2_LABEL) + self.assertEqual(role.label, TEST_ROLE_LABEL) - def _create_group(self): - self.group_2 = Group.objects.create(name=TEST_GROUP_2_NAME) + #def _create_group(self): + # self.test_group = Group.objects.create(name=TEST_GROUP_NAME) def _request_role_create_with_extra_data(self): self._create_group() return self._role_create_request( extra_data={ - 'groups_pk_list': '{}'.format(self.group_2.pk), + 'groups_pk_list': '{}'.format(self.test_group.pk), 'permissions_pk_list': '{}'.format(permission_role_view.pk) } ) @@ -106,7 +102,7 @@ class PermissionAPITestCase(BaseAPITestCase): role = Role.objects.get(label=TEST_ROLE_2_LABEL) self.assertEqual(role.label, TEST_ROLE_2_LABEL) self.assertQuerysetEqual( - role.groups.all(), (repr(self.group_2),) + role.groups.all(), (repr(self.test_group),) ) self.assertQuerysetEqual( role.permissions.all(), @@ -124,107 +120,118 @@ class PermissionAPITestCase(BaseAPITestCase): data.update(extra_data) return getattr(self, request_type)( - viewname='rest_api:role-detail', args=(self.role.pk,), + viewname='rest_api:role-detail', kwargs={'role_id': self.test_role.pk}, data=data ) def test_role_edit_via_patch_no_access(self): + self._create_test_role() response = self._request_role_edit(request_type='patch') self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - self.role.refresh_from_db() - self.assertEqual(self.role.label, TEST_ROLE_LABEL) + self.test_role.refresh_from_db() + self.assertEqual(self.test_role.label, TEST_ROLE_LABEL) def test_role_edit_via_patch_with_access(self): - self.grant_access(permission=permission_role_edit, obj=self.role) + self._create_test_role() + self.grant_access(permission=permission_role_edit, obj=self.test_role) response = self._request_role_edit(request_type='patch') self.assertEqual(response.status_code, status.HTTP_200_OK) - self.role.refresh_from_db() - self.assertEqual(self.role.label, TEST_ROLE_LABEL_EDITED) + self.test_role.refresh_from_db() + self.assertEqual(self.test_role.label, TEST_ROLE_LABEL_EDITED) def _request_role_edit_via_patch_with_extra_data(self): + self._create_test_role() self._create_group() return self._request_role_edit( extra_data={ - 'groups_pk_list': '{}'.format(self.group_2.pk), + 'groups_pk_list': '{}'.format(self.test_group.pk), 'permissions_pk_list': '{}'.format(permission_role_view.pk) }, request_type='patch' ) def test_role_edit_complex_via_patch_no_access(self): + self._create_test_role() + response = self._request_role_edit_via_patch_with_extra_data() self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - self.role.refresh_from_db() - self.assertEqual(self.role.label, TEST_ROLE_LABEL) + self.test_role.refresh_from_db() + self.assertEqual(self.test_role.label, TEST_ROLE_LABEL) self.assertQuerysetEqual( - self.role.groups.all(), (repr(self.group),) + self.test_role.groups.all(), (repr(self.group),) ) - self.assertQuerysetEqual(self.role.permissions.all(), ()) + self.assertQuerysetEqual(self.test_role.permissions.all(), ()) def test_role_edit_complex_via_patch_with_access(self): - self.grant_access(permission=permission_role_edit, obj=self.role) + self._create_test_role() + self.grant_access(permission=permission_role_edit, obj=self.test_role) response = self._request_role_edit_via_patch_with_extra_data() self.assertEqual(response.status_code, status.HTTP_200_OK) - self.role.refresh_from_db() - self.assertEqual(self.role.label, TEST_ROLE_LABEL_EDITED) + self.test_role.refresh_from_db() + self.assertEqual(self.test_role.label, TEST_ROLE_LABEL_EDITED) self.assertQuerysetEqual( - self.role.groups.all(), (repr(self.group_2),) + self.test_role.groups.all(), (repr(self.test_group),) ) self.assertQuerysetEqual( - self.role.permissions.all(), + self.test_role.permissions.all(), (repr(permission_role_view.stored_permission),) ) def test_role_edit_via_put_no_access(self): + self._create_test_role() response = self._request_role_edit(request_type='put') self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - self.role.refresh_from_db() - self.assertEqual(self.role.label, TEST_ROLE_LABEL) + self.test_role.refresh_from_db() + self.assertEqual(self.test_role.label, TEST_ROLE_LABEL) def test_role_edit_via_put_with_access(self): - self.grant_access(permission=permission_role_edit, obj=self.role) + self._create_test_role() + self.grant_access(permission=permission_role_edit, obj=self.test_role) response = self._request_role_edit(request_type='put') self.assertEqual(response.status_code, status.HTTP_200_OK) - self.role.refresh_from_db() - self.assertEqual(self.role.label, TEST_ROLE_LABEL_EDITED) + self.test_role.refresh_from_db() + self.assertEqual(self.test_role.label, TEST_ROLE_LABEL_EDITED) def _request_role_edit_via_put_with_extra_data(self): + self._create_test_role() self._create_group() return self._request_role_edit( extra_data={ - 'groups_pk_list': '{}'.format(self.group_2.pk), + 'groups_pk_list': '{}'.format(self.test_group.pk), 'permissions_pk_list': '{}'.format(permission_role_view.pk) }, request_type='put' ) def test_role_edit_complex_via_put_no_access(self): + self._create_test_role() response = self._request_role_edit_via_put_with_extra_data() self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - self.role.refresh_from_db() - self.assertEqual(self.role.label, TEST_ROLE_LABEL) + self.test_role.refresh_from_db() + self.assertEqual(self.test_role.label, TEST_ROLE_LABEL) self.assertQuerysetEqual( - self.role.groups.all(), (repr(self.group),) + self.test_role.groups.all(), (repr(self.group),) ) self.assertQuerysetEqual( - self.role.permissions.all(), + self.test_role.permissions.all(), () ) def test_role_edit_complex_via_put_with_access(self): - self.grant_access(permission=permission_role_edit, obj=self.role) + self._create_test_role() + self.grant_access(permission=permission_role_edit, obj=self.test_role) response = self._request_role_edit_via_put_with_extra_data() self.assertEqual(response.status_code, status.HTTP_200_OK) - self.role.refresh_from_db() - self.assertEqual(self.role.label, TEST_ROLE_LABEL_EDITED) + self.test_role.refresh_from_db() + self.assertEqual(self.test_role.label, TEST_ROLE_LABEL_EDITED) self.assertQuerysetEqual( - self.role.groups.all(), (repr(self.group_2),) + self.test_role.groups.all(), (repr(self.test_group),) ) self.assertQuerysetEqual( - self.role.permissions.all(), + self.test_role.permissions.all(), (repr(permission_role_view.stored_permission),) ) @@ -232,7 +239,8 @@ class PermissionAPITestCase(BaseAPITestCase): def _request_role_delete_view(self): return self.delete( - viewname='rest_api:role-detail', args=(self.role.pk,) + viewname='rest_api:role-detail', + kwargs={'role_id': self.test_role.pk} ) def test_role_delete_view_no_access(self): @@ -241,7 +249,7 @@ class PermissionAPITestCase(BaseAPITestCase): self.assertEqual(Role.objects.count(), 1) def test_role_delete_view_with_access(self): - self.grant_access(permission=permission_role_delete, obj=self.role) + self.grant_access(permission=permission_role_delete, obj=self.test_role) response = self._request_role_delete_view() self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) self.assertEqual(Role.objects.count(), 0) diff --git a/mayan/apps/permissions/tests/test_models.py b/mayan/apps/permissions/tests/test_models.py index 5aef04dc79..7dca7ca04b 100644 --- a/mayan/apps/permissions/tests/test_models.py +++ b/mayan/apps/permissions/tests/test_models.py @@ -14,18 +14,18 @@ class PermissionTestCase(BaseTestCase): def test_no_permissions(self): with self.assertRaises(PermissionDenied): - Permission.check_permissions( - requester=self.user, permissions=(permission_role_view,) + Permission.check_user_permission( + permission=permission_role_view, user=self._test_case_user ) def test_with_permissions(self): - self.group.user_set.add(self.user) - self.role.permissions.add(permission_role_view.stored_permission) - self.role.groups.add(self.group) + self._test_case_group.user_set.add(self._test_case_user) + self._test_case_role.permissions.add(permission_role_view.stored_permission) + self._test_case_role.groups.add(self._test_case_group) try: - Permission.check_permissions( - requester=self.user, permissions=(permission_role_view,) + Permission.check_user_permission( + permission=permission_role_view, user=self._test_case_user ) except PermissionDenied: self.fail('PermissionDenied exception was not expected.') diff --git a/mayan/apps/permissions/tests/test_views.py b/mayan/apps/permissions/tests/test_views.py index 851fdb482e..ef49a304e5 100644 --- a/mayan/apps/permissions/tests/test_views.py +++ b/mayan/apps/permissions/tests/test_views.py @@ -1,10 +1,8 @@ from __future__ import unicode_literals -from django.contrib.auth.models import Group - from mayan.apps.common.tests import GenericViewTestCase from mayan.apps.user_management.permissions import permission_group_edit -from mayan.apps.user_management.tests.literals import TEST_GROUP_2_NAME +from mayan.apps.user_management.tests import GroupTestMixin from ..models import Role from ..permissions import ( @@ -13,10 +11,11 @@ from ..permissions import ( permission_role_view, ) -from .literals import TEST_ROLE_2_LABEL, TEST_ROLE_LABEL_EDITED +from .literals import TEST_ROLE_LABEL, TEST_ROLE_LABEL_EDITED +from .mixins import RoleTestMixin -class PermissionsViewsTestCase(GenericViewTestCase): +class PermissionsViewsTestCase(GroupTestMixin, RoleTestMixin, GenericViewTestCase): def setUp(self): super(PermissionsViewsTestCase, self).setUp() self.login_user() @@ -24,7 +23,7 @@ class PermissionsViewsTestCase(GenericViewTestCase): def _request_create_role_view(self): return self.post( viewname='permissions:role_create', data={ - 'label': TEST_ROLE_2_LABEL, + 'label': TEST_ROLE_LABEL, } ) @@ -33,7 +32,7 @@ class PermissionsViewsTestCase(GenericViewTestCase): self.assertEqual(response.status_code, 403) self.assertEqual(Role.objects.count(), 1) self.assertFalse( - TEST_ROLE_2_LABEL in Role.objects.values_list('label', flat=True) + TEST_ROLE_LABEL in Role.objects.values_list('label', flat=True) ) def test_role_creation_view_with_permission(self): @@ -42,140 +41,139 @@ class PermissionsViewsTestCase(GenericViewTestCase): self.assertEqual(response.status_code, 302) self.assertEqual(Role.objects.count(), 2) self.assertTrue( - TEST_ROLE_2_LABEL in Role.objects.values_list('label', flat=True) + TEST_ROLE_LABEL in Role.objects.values_list('label', flat=True) ) def _request_role_delete_view(self): return self.post( - viewname='permissions:role_delete', args=(self.role_2.pk,), + viewname='permissions:role_delete', + kwargs={'role_id': self.test_role.pk} ) - def _create_role(self): - self.role_2 = Role.objects.create(label=TEST_ROLE_2_LABEL) - def test_role_delete_view_no_access(self): - self._create_role() + self._create_test_role() response = self._request_role_delete_view() self.assertEqual(response.status_code, 403) self.assertEqual(Role.objects.count(), 2) self.assertTrue( - TEST_ROLE_2_LABEL in Role.objects.values_list('label', flat=True) + TEST_ROLE_LABEL in Role.objects.values_list('label', flat=True) ) def test_role_delete_view_with_access(self): - self._create_role() - self.grant_access(permission=permission_role_delete, obj=self.role_2) + self._create_test_role() + self.grant_access(permission=permission_role_delete, obj=self.test_role) response = self._request_role_delete_view() self.assertEqual(response.status_code, 302) self.assertEqual(Role.objects.count(), 1) self.assertFalse( - TEST_ROLE_2_LABEL in Role.objects.values_list('label', flat=True) + TEST_ROLE_LABEL in Role.objects.values_list('label', flat=True) ) def _request_role_edit_view(self): return self.post( - viewname='permissions:role_edit', args=(self.role_2.pk,), data={ + viewname='permissions:role_edit', + kwargs={'role_id': self.test_role.pk}, data={ 'label': TEST_ROLE_LABEL_EDITED, } ) def test_role_edit_view_no_access(self): - self._create_role() + self._create_test_role() response = self._request_role_edit_view() self.assertEqual(response.status_code, 403) - self.role_2.refresh_from_db() + self.test_role.refresh_from_db() self.assertEqual(Role.objects.count(), 2) - self.assertEqual(self.role_2.label, TEST_ROLE_2_LABEL) + self.assertEqual(self.test_role.label, TEST_ROLE_LABEL) def test_role_edit_view_with_access(self): - self._create_role() - self.grant_access(permission=permission_role_edit, obj=self.role_2) + self._create_test_role() + self.grant_access(permission=permission_role_edit, obj=self.test_role) response = self._request_role_edit_view() self.assertEqual(response.status_code, 302) - self.role_2.refresh_from_db() + self.test_role.refresh_from_db() self.assertEqual(Role.objects.count(), 2) - self.assertEqual(self.role_2.label, TEST_ROLE_LABEL_EDITED) + self.assertEqual(self.test_role.label, TEST_ROLE_LABEL_EDITED) def _request_role_list_view(self): return self.get(viewname='permissions:role_list') def test_role_list_view_no_access(self): - self._create_role() + self._create_test_role() response = self._request_role_list_view() self.assertEqual(response.status_code, 200) self.assertNotContains( - response=response, text=TEST_ROLE_2_LABEL, status_code=200 + response=response, text=TEST_ROLE_LABEL, status_code=200 ) def test_role_list_view_with_access(self): - self._create_role() - self.grant_access(permission=permission_role_view, obj=self.role_2) + self._create_test_role() + self.grant_access(permission=permission_role_view, obj=self.test_role) response = self._request_role_list_view() self.assertContains( - response=response, text=TEST_ROLE_2_LABEL, status_code=200 + response=response, text=TEST_ROLE_LABEL, status_code=200 ) def _request_role_permissions_view(self): return self.get( - viewname='permissions:role_permissions', args=(self.role_2.pk,) + viewname='permissions:role_permissions', + kwargs={'role_id': self.test_role.pk} ) def test_role_permissions_view_no_access(self): - self._create_role() + self._create_test_role() response = self._request_role_permissions_view() self.assertEqual(response.status_code, 403) def test_role_permissions_view_with_permission_grant(self): - self._create_role() + self._create_test_role() self.grant_access( - permission=permission_permission_grant, obj=self.role_2 + permission=permission_permission_grant, obj=self.test_role ) response = self._request_role_permissions_view() self.assertEqual(response.status_code, 200) def test_role_permissions_view_with_permission_revoke(self): - self._create_role() + self._create_test_role() self.grant_access( - permission=permission_permission_revoke, obj=self.role_2 + permission=permission_permission_revoke, obj=self.test_role ) response = self._request_role_permissions_view() self.assertEqual(response.status_code, 200) def _request_role_groups_view(self): return self.get( - viewname='permissions:role_groups', args=(self.role_2.pk,) + viewname='permissions:role_groups', + kwargs={'role_id': self.test_role.pk} ) def test_role_groups_view_no_access(self): - self._create_role() + self._create_test_role() response = self._request_role_groups_view() self.assertEqual(response.status_code, 403) def test_role_groups_view_with_access(self): - self._create_role() - self.grant_access(permission=permission_role_edit, obj=self.role_2) + self._create_test_role() + self.grant_access(permission=permission_role_edit, obj=self.test_role) response = self._request_role_groups_view() self.assertEqual(response.status_code, 200) - def _create_group(self): - self.group_2 = Group.objects.create(name=TEST_GROUP_2_NAME) - def _request_group_roles_view(self): return self.get( - viewname='permissions:group_roles', args=(self.group_2.pk,) + viewname='permissions:group_roles', + kwargs={'group_id': self.test_group.pk} ) def test_group_roles_view_no_access(self): - self._create_group() + self._create_test_group() response = self._request_group_roles_view() self.assertEqual(response.status_code, 403) def test_group_roles_view_with_access(self): - self._create_group() - self.grant_access(permission=permission_group_edit, obj=self.group_2) + self._create_test_group() + self.grant_access(permission=permission_group_edit, obj=self.test_group) response = self._request_group_roles_view() self.assertEqual(response.status_code, 200) diff --git a/mayan/apps/permissions/urls.py b/mayan/apps/permissions/urls.py index 3c620c2059..5660c5f15c 100644 --- a/mayan/apps/permissions/urls.py +++ b/mayan/apps/permissions/urls.py @@ -4,34 +4,46 @@ from django.conf.urls import url from .api_views import APIPermissionList, APIRoleListView, APIRoleView from .views import ( - GroupRoleMembersView, RoleCreateView, RoleDeleteView, RoleEditView, - RoleListView, SetupRoleMembersView, SetupRolePermissionsView + GroupRolesView, RoleCreateView, RoleDeleteView, RoleEditView, + RoleListView, RoleGroupsView, RolePermissionsView ) urlpatterns = [ url( - r'^group/(?P\d+)/roles/$', GroupRoleMembersView.as_view(), - name='group_roles' - ), - url(r'^role/list/$', RoleListView.as_view(), name='role_list'), - url(r'^role/create/$', RoleCreateView.as_view(), name='role_create'), - url( - r'^role/(?P\d+)/permissions/$', SetupRolePermissionsView.as_view(), - name='role_permissions' - ), - url(r'^role/(?P\d+)/edit/$', RoleEditView.as_view(), name='role_edit'), - url( - r'^role/(?P\d+)/delete/$', RoleDeleteView.as_view(), - name='role_delete' + regex=r'^groups/(?P\d+)/roles/$', name='group_roles', + view=GroupRolesView.as_view() ), url( - r'^role/(?P\d+)/groups/$', SetupRoleMembersView.as_view(), - name='role_groups' + regex=r'^roles/create/$', name='role_create', + view=RoleCreateView.as_view() ), + url( + regex=r'^roles/(?P\d+)/delete/$', name='role_delete', + view=RoleDeleteView.as_view() + ), + url( + regex=r'^roles/(?P\d+)/edit/$', name='role_edit', + view=RoleEditView.as_view() + ), + url( + regex=r'^roles/(?P\d+)/groups/$', name='role_groups', + view=RoleGroupsView.as_view() + ), + url( + regex=r'^roles/(?P\d+)/permissions/$', name='role_permissions', + view=RolePermissionsView.as_view() + ), + url(regex=r'^roles/list/$', name='role_list', view=RoleListView.as_view()), ] api_urls = [ - url(r'^permissions/$', APIPermissionList.as_view(), name='permission-list'), - url(r'^roles/$', APIRoleListView.as_view(), name='role-list'), - url(r'^roles/(?P[0-9]+)/$', APIRoleView.as_view(), name='role-detail'), + url( + regex=r'^permissions/$', name='permission-list', + view=APIPermissionList.as_view(), + ), + url(regex=r'^roles/$', name='role-list', view=APIRoleListView.as_view()), + url( + regex=r'^roles/(?P[0-9]+)/$', name='role-detail', + view=APIRoleView.as_view() + ), ] diff --git a/mayan/apps/permissions/views.py b/mayan/apps/permissions/views.py index aa1ac92d52..5d70ff9d54 100644 --- a/mayan/apps/permissions/views.py +++ b/mayan/apps/permissions/views.py @@ -27,7 +27,7 @@ from .permissions import ( ) -class GroupRoleMembersView(AssignRemoveView): +class GroupRolesView(AssignRemoveView): grouped = False left_list_title = _('Available roles') right_list_title = _('Group roles') @@ -44,7 +44,7 @@ class GroupRoleMembersView(AssignRemoveView): } def get_object(self): - return get_object_or_404(klass=Group, pk=self.kwargs['pk']) + return get_object_or_404(klass=Group, pk=self.kwargs['group_id']) def left_list(self): return [ @@ -65,22 +65,24 @@ class RoleCreateView(SingleObjectCreateView): fields = ('label',) model = Role view_permission = permission_role_create - post_action_redirect = reverse_lazy('permissions:role_list') + post_action_redirect = reverse_lazy(viewname='permissions:role_list') class RoleDeleteView(SingleObjectDeleteView): model = Role object_permission = permission_role_delete - post_action_redirect = reverse_lazy('permissions:role_list') + pk_url_kwarg = 'role_id' + post_action_redirect = reverse_lazy(viewname='permissions:role_list') class RoleEditView(SingleObjectEditView): fields = ('label',) model = Role object_permission = permission_role_edit + pk_url_kwarg = 'role_id' -class SetupRoleMembersView(AssignRemoveView): +class RoleGroupsView(AssignRemoveView): grouped = False left_list_title = _('Available groups') right_list_title = _('Role groups') @@ -101,24 +103,48 @@ class SetupRoleMembersView(AssignRemoveView): } def get_object(self): - return get_object_or_404(klass=Role, pk=self.kwargs['pk']) + return get_object_or_404(klass=Role, pk=self.kwargs['role_id']) def left_list(self): return [ (force_text(group.pk), group.name) for group in set(Group.objects.all()) - set(self.get_object().groups.all()) ] + def remove(self, item): + group = get_object_or_404(klass=Group, pk=item) + self.get_object().groups.remove(group) + def right_list(self): return [ (force_text(group.pk), group.name) for group in self.get_object().groups.all() ] - def remove(self, item): - group = get_object_or_404(klass=Group, pk=item) - self.get_object().groups.remove(group) + +class RoleListView(SingleObjectListView): + model = Role + object_permission = permission_role_view + + def get_extra_context(self): + return { + 'hide_object': True, + 'no_results_icon': icon_role_list, + 'no_results_main_link': link_role_create.resolve( + context=RequestContext(request=self.request) + ), + 'no_results_text': _( + 'Roles are authorization units. They contain ' + 'user groups which inherit the role permissions for the ' + 'entire system. Roles can also part of access ' + 'controls lists. Access controls list are permissions ' + 'granted to a role for specific objects which its group ' + 'members inherit.' + ), + 'no_results_title': _('There are no roles'), + 'title': _('Roles'), + } -class SetupRolePermissionsView(AssignRemoveView): +class RolePermissionsView(AssignRemoveView): grouped = True left_list_title = _('Available permissions') right_list_title = _('Granted permissions') @@ -156,7 +182,7 @@ class SetupRolePermissionsView(AssignRemoveView): permissions=(permission_permission_grant, permission_permission_revoke), user=self.request.user, obj=self.get_object() ) - return super(SetupRolePermissionsView, self).dispatch(request, *args, **kwargs) + return super(RolePermissionsView, self).dispatch(request, *args, **kwargs) def get_extra_context(self): return { @@ -169,22 +195,17 @@ class SetupRolePermissionsView(AssignRemoveView): } def get_object(self): - return get_object_or_404(klass=Role, pk=self.kwargs['pk']) + return get_object_or_404(klass=Role, pk=self.kwargs['role_id']) def left_list(self): Permission.refresh() - return SetupRolePermissionsView.generate_choices( + return RolePermissionsView.generate_choices( entries=StoredPermission.objects.exclude( id__in=self.get_object().permissions.values_list('pk', flat=True) ) ) - def right_list(self): - return SetupRolePermissionsView.generate_choices( - entries=self.get_object().permissions.all() - ) - def remove(self, item): Permission.check_permissions( self.request.user, permissions=(permission_permission_revoke,) @@ -192,26 +213,7 @@ class SetupRolePermissionsView(AssignRemoveView): permission = get_object_or_404(klass=StoredPermission, pk=item) self.get_object().permissions.remove(permission) - -class RoleListView(SingleObjectListView): - model = Role - object_permission = permission_role_view - - def get_extra_context(self): - return { - 'hide_object': True, - 'no_results_icon': icon_role_list, - 'no_results_main_link': link_role_create.resolve( - context=RequestContext(request=self.request) - ), - 'no_results_text': _( - 'Roles are authorization units. They contain ' - 'user groups which inherit the role permissions for the ' - 'entire system. Roles can also part of access ' - 'controls lists. Access controls list are permissions ' - 'granted to a role for specific objects which its group ' - 'members inherit.' - ), - 'no_results_title': _('There are no roles'), - 'title': _('Roles'), - } + def right_list(self): + return RolePermissionsView.generate_choices( + entries=self.get_object().permissions.all() + ) From 097ac7dae6952bd905839d0447b1c7051bacdca2 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sun, 13 Jan 2019 23:58:08 -0400 Subject: [PATCH 010/209] Move permission purge code Move the code to purge obsolete permissions from the management command to the StoredPermission default manager. Signed-off-by: Roberto Rosario --- mayan/apps/permissions/classes.py | 29 +++++++------ mayan/apps/permissions/handlers.py | 7 +++- .../management/commands/purgepermissions.py | 10 +---- mayan/apps/permissions/managers.py | 7 ++++ mayan/apps/permissions/tests/literals.py | 6 +++ mayan/apps/permissions/tests/test_models.py | 41 +++++++++++++++++-- 6 files changed, 70 insertions(+), 30 deletions(-) diff --git a/mayan/apps/permissions/classes.py b/mayan/apps/permissions/classes.py index d974745bb2..64a370b9ef 100644 --- a/mayan/apps/permissions/classes.py +++ b/mayan/apps/permissions/classes.py @@ -59,7 +59,7 @@ class Permission(object): for namespace, permissions in itertools.groupby(cls.all(), lambda entry: entry.namespace): permission_options = [ - (force_text(permission.uuid), permission) for permission in permissions + (force_text(permission.pk), permission) for permission in permissions ] results.append( (namespace, permission_options) @@ -98,6 +98,8 @@ class Permission(object): @classmethod def get(cls, pk, proxy_only=False): + # TODO: Split into .get which returns the stored permission model and + # .get_volatile which returns the class of type Permission if proxy_only: return cls._permissions[pk] else: @@ -124,8 +126,8 @@ class Permission(object): self.namespace = namespace self.name = name self.label = label - self.pk = self.uuid - self.__class__._permissions[self.uuid] = self + self.pk = self.get_pk() + self.__class__._permissions[self.pk] = self def __repr__(self): return self.pk @@ -133,24 +135,21 @@ class Permission(object): def __str__(self): return force_text(self.label) + def get_pk(self): + return '%s.%s' % (self.namespace.name, self.name) + @property def stored_permission(self): - StoredPermission = apps.get_model( - app_label='permissions', model_name='StoredPermission' - ) - try: - return self.__class__._stored_permissions_cache[self.uuid] + return self.__class__._stored_permissions_cache[self.pk] except KeyError: + StoredPermission = apps.get_model( + app_label='permissions', model_name='StoredPermission' + ) + stored_permission, created = StoredPermission.objects.get_or_create( namespace=self.namespace.name, name=self.name, ) - self.__class__._stored_permissions_cache[ - self.uuid - ] = stored_permission + self.__class__._stored_permissions_cache[self.pk] = stored_permission return stored_permission - - @property - def uuid(self): - return '%s.%s' % (self.namespace.name, self.name) diff --git a/mayan/apps/permissions/handlers.py b/mayan/apps/permissions/handlers.py index 69d7887315..9a2a8d66d3 100644 --- a/mayan/apps/permissions/handlers.py +++ b/mayan/apps/permissions/handlers.py @@ -1,7 +1,10 @@ from __future__ import unicode_literals -from django.core import management +from django.apps import apps def handler_purge_permissions(**kwargs): - management.call_command('purgepermissions') + StoredPermission = apps.get_model( + app_label='permissions', model_name='StoredPermission' + ) + StoredPermission.objects.purge_obsolete() diff --git a/mayan/apps/permissions/management/commands/purgepermissions.py b/mayan/apps/permissions/management/commands/purgepermissions.py index 7c5dd04270..6d3abc9c5c 100644 --- a/mayan/apps/permissions/management/commands/purgepermissions.py +++ b/mayan/apps/permissions/management/commands/purgepermissions.py @@ -2,7 +2,6 @@ from __future__ import unicode_literals from django.core.management.base import BaseCommand -from ...classes import Permission from ...models import StoredPermission @@ -10,11 +9,4 @@ class Command(BaseCommand): help = 'Remove obsolete permissions from the database' def handle(self, *args, **options): - for permission in StoredPermission.objects.all(): - try: - Permission.get( - pk='{}.{}'.format(permission.namespace, permission.name), - proxy_only=True - ) - except KeyError: - permission.delete() + StoredPermission.objects.purge_obsolete() diff --git a/mayan/apps/permissions/managers.py b/mayan/apps/permissions/managers.py index 39cd52756c..145078094a 100644 --- a/mayan/apps/permissions/managers.py +++ b/mayan/apps/permissions/managers.py @@ -22,3 +22,10 @@ class StoredPermissionManager(models.Manager): return self.model.objects.filter( permissionholder__holder_type=ct ).filter(permissionholder__holder_id=holder.pk) + + def purge_obsolete(self): + for permission in self.all(): + try: + permission.volatile_permission + except KeyError: + permission.delete() diff --git a/mayan/apps/permissions/tests/literals.py b/mayan/apps/permissions/tests/literals.py index 42815f1718..5c392a61ea 100644 --- a/mayan/apps/permissions/tests/literals.py +++ b/mayan/apps/permissions/tests/literals.py @@ -1,5 +1,11 @@ from __future__ import unicode_literals TEST_CASE_ROLE_LABEL = 'test case role' +TEST_INVALID_PERMISSION_NAMESPACE_NAME = 'invalid namespace' +TEST_INVALID_PERMISSION_NAME = 'invalid name' +TEST_PERMISSION_NAMESPACE_LABEL = 'test namespace label' +TEST_PERMISSION_NAMESPACE_NAME = 'test namespace' +TEST_PERMISSION_LABEL = 'test name label' +TEST_PERMISSION_NAME = 'test name' TEST_ROLE_LABEL = 'test role 2' TEST_ROLE_LABEL_EDITED = 'test role label edited' diff --git a/mayan/apps/permissions/tests/test_models.py b/mayan/apps/permissions/tests/test_models.py index 7dca7ca04b..10834d7814 100644 --- a/mayan/apps/permissions/tests/test_models.py +++ b/mayan/apps/permissions/tests/test_models.py @@ -4,14 +4,18 @@ from django.core.exceptions import PermissionDenied from mayan.apps.common.tests import BaseTestCase -from ..classes import Permission +from ..classes import Permission, PermissionNamespace from ..permissions import permission_role_view +from ..models import StoredPermission + +from .literals import ( + TEST_INVALID_PERMISSION_NAMESPACE_NAME, TEST_INVALID_PERMISSION_NAME, + TEST_PERMISSION_NAMESPACE_NAME, TEST_PERMISSION_NAMESPACE_LABEL, + TEST_PERMISSION_NAME, TEST_PERMISSION_LABEL +) class PermissionTestCase(BaseTestCase): - def setUp(self): - super(PermissionTestCase, self).setUp() - def test_no_permissions(self): with self.assertRaises(PermissionDenied): Permission.check_user_permission( @@ -29,3 +33,32 @@ class PermissionTestCase(BaseTestCase): ) except PermissionDenied: self.fail('PermissionDenied exception was not expected.') + + +class StoredPermissionManagerTestCase(BaseTestCase): + create_test_case_superuser = False + create_test_case_user = False + + def test_purge_obsolete_with_invalid(self): + StoredPermission.objects.create( + namespace=TEST_INVALID_PERMISSION_NAMESPACE_NAME, + name=TEST_INVALID_PERMISSION_NAME + ) + + StoredPermission.objects.purge_obsolete() + + self.assertEqual(StoredPermission.objects.count(), 0) + + def test_purge_obsolete_with_valid(self): + test_permission_namespace = PermissionNamespace( + label=TEST_PERMISSION_NAMESPACE_LABEL, + name=TEST_PERMISSION_NAMESPACE_NAME + ) + test_permission = test_permission_namespace.add_permission( + label=TEST_PERMISSION_LABEL, name=TEST_PERMISSION_NAME + ) + test_permission.stored_permission + + StoredPermission.objects.purge_obsolete() + + self.assertEqual(StoredPermission.objects.count(), 1) From b53c026877db629d5496585904e1b30a77afe9c9 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Mon, 14 Jan 2019 00:03:26 -0400 Subject: [PATCH 011/209] Sort arguments and imports Signed-off-by: Roberto Rosario --- mayan/apps/permissions/admin.py | 2 +- mayan/apps/permissions/apps.py | 4 ++- mayan/apps/permissions/managers.py | 2 +- .../permissions/migrations/0001_initial.py | 2 +- .../migrations/0002_auto_20150628_0533.py | 2 +- mayan/apps/permissions/permissions.py | 28 +++++++++---------- mayan/apps/permissions/tests/test_api.py | 8 ++---- mayan/apps/permissions/tests/test_models.py | 8 +++--- mayan/apps/permissions/tests/test_views.py | 2 +- mayan/apps/permissions/urls.py | 2 +- mayan/apps/permissions/views.py | 4 +-- 11 files changed, 32 insertions(+), 32 deletions(-) diff --git a/mayan/apps/permissions/admin.py b/mayan/apps/permissions/admin.py index f135121177..f29de094c8 100644 --- a/mayan/apps/permissions/admin.py +++ b/mayan/apps/permissions/admin.py @@ -2,7 +2,7 @@ from __future__ import unicode_literals from django.contrib import admin -from .models import StoredPermission, Role +from .models import Role, StoredPermission @admin.register(Role) diff --git a/mayan/apps/permissions/apps.py b/mayan/apps/permissions/apps.py index b442782a0a..00612df967 100644 --- a/mayan/apps/permissions/apps.py +++ b/mayan/apps/permissions/apps.py @@ -5,7 +5,9 @@ from django.utils.translation import ugettext_lazy as _ from mayan.apps.acls import ModelPermission from mayan.apps.acls.links import link_acl_list -from mayan.apps.acls.permissions import permission_acl_edit, permission_acl_view +from mayan.apps.acls.permissions import ( + permission_acl_edit, permission_acl_view +) from mayan.apps.common import ( MayanAppConfig, menu_list_facet, menu_multi_item, menu_object, menu_secondary, menu_setup diff --git a/mayan/apps/permissions/managers.py b/mayan/apps/permissions/managers.py index 145078094a..560878b210 100644 --- a/mayan/apps/permissions/managers.py +++ b/mayan/apps/permissions/managers.py @@ -2,8 +2,8 @@ from __future__ import unicode_literals import logging -from django.db import models from django.contrib.contenttypes.models import ContentType +from django.db import models logger = logging.getLogger(__name__) diff --git a/mayan/apps/permissions/migrations/0001_initial.py b/mayan/apps/permissions/migrations/0001_initial.py index 81ef02a769..ef67a61e02 100644 --- a/mayan/apps/permissions/migrations/0001_initial.py +++ b/mayan/apps/permissions/migrations/0001_initial.py @@ -1,6 +1,6 @@ from __future__ import unicode_literals -from django.db import models, migrations +from django.db import migrations, models class Migration(migrations.Migration): diff --git a/mayan/apps/permissions/migrations/0002_auto_20150628_0533.py b/mayan/apps/permissions/migrations/0002_auto_20150628_0533.py index e2cb7b517b..375089fd1d 100644 --- a/mayan/apps/permissions/migrations/0002_auto_20150628_0533.py +++ b/mayan/apps/permissions/migrations/0002_auto_20150628_0533.py @@ -1,6 +1,6 @@ from __future__ import unicode_literals -from django.db import models, migrations +from django.db import migrations, models class Migration(migrations.Migration): diff --git a/mayan/apps/permissions/permissions.py b/mayan/apps/permissions/permissions.py index 1543ab065f..4411ca53ad 100644 --- a/mayan/apps/permissions/permissions.py +++ b/mayan/apps/permissions/permissions.py @@ -6,21 +6,21 @@ from . import PermissionNamespace namespace = PermissionNamespace(label=_('Permissions'), name='permissions') -permission_role_view = namespace.add_permission( - name='role_view', label=_('View roles') -) -permission_role_edit = namespace.add_permission( - name='role_edit', label=_('Edit roles') -) -permission_role_create = namespace.add_permission( - name='role_create', label=_('Create roles') -) -permission_role_delete = namespace.add_permission( - name='role_delete', label=_('Delete roles') -) permission_permission_grant = namespace.add_permission( - name='permission_grant', label=_('Grant permissions') + label=_('Grant permissions'), name='permission_grant' ) permission_permission_revoke = namespace.add_permission( - name='permission_revoke', label=_('Revoke permissions') + label=_('Revoke permissions'), name='permission_revoke' +) +permission_role_create = namespace.add_permission( + label=_('Create roles'), name='role_create' +) +permission_role_delete = namespace.add_permission( + label=_('Delete roles'), name='role_delete' +) +permission_role_edit = namespace.add_permission( + label=_('Edit roles'), name='role_edit' +) +permission_role_view = namespace.add_permission( + label=_('View roles'), name='role_view' ) diff --git a/mayan/apps/permissions/tests/test_api.py b/mayan/apps/permissions/tests/test_api.py index 85c3c0045f..2ed28ceffb 100644 --- a/mayan/apps/permissions/tests/test_api.py +++ b/mayan/apps/permissions/tests/test_api.py @@ -10,13 +10,11 @@ from mayan.apps.user_management.tests.literals import TEST_GROUP_NAME from ..classes import Permission from ..models import Role from ..permissions import ( - permission_role_create, permission_role_delete, - permission_role_edit, permission_role_view + permission_role_create, permission_role_delete, permission_role_edit, + permission_role_view ) -from .literals import ( - TEST_ROLE_LABEL, TEST_ROLE_LABEL_EDITED -) +from .literals import TEST_ROLE_LABEL, TEST_ROLE_LABEL_EDITED from .mixins import RoleTestMixin diff --git a/mayan/apps/permissions/tests/test_models.py b/mayan/apps/permissions/tests/test_models.py index 10834d7814..550644ae92 100644 --- a/mayan/apps/permissions/tests/test_models.py +++ b/mayan/apps/permissions/tests/test_models.py @@ -5,13 +5,13 @@ from django.core.exceptions import PermissionDenied from mayan.apps.common.tests import BaseTestCase from ..classes import Permission, PermissionNamespace -from ..permissions import permission_role_view from ..models import StoredPermission +from ..permissions import permission_role_view from .literals import ( - TEST_INVALID_PERMISSION_NAMESPACE_NAME, TEST_INVALID_PERMISSION_NAME, - TEST_PERMISSION_NAMESPACE_NAME, TEST_PERMISSION_NAMESPACE_LABEL, - TEST_PERMISSION_NAME, TEST_PERMISSION_LABEL + TEST_INVALID_PERMISSION_NAME, TEST_INVALID_PERMISSION_NAMESPACE_NAME, + TEST_PERMISSION_LABEL, TEST_PERMISSION_NAME, + TEST_PERMISSION_NAMESPACE_LABEL, TEST_PERMISSION_NAMESPACE_NAME ) diff --git a/mayan/apps/permissions/tests/test_views.py b/mayan/apps/permissions/tests/test_views.py index ef49a304e5..c4c28c7fd0 100644 --- a/mayan/apps/permissions/tests/test_views.py +++ b/mayan/apps/permissions/tests/test_views.py @@ -8,7 +8,7 @@ from ..models import Role from ..permissions import ( permission_permission_grant, permission_permission_revoke, permission_role_create, permission_role_delete, permission_role_edit, - permission_role_view, + permission_role_view ) from .literals import TEST_ROLE_LABEL, TEST_ROLE_LABEL_EDITED diff --git a/mayan/apps/permissions/urls.py b/mayan/apps/permissions/urls.py index 5660c5f15c..89e1d4b7f6 100644 --- a/mayan/apps/permissions/urls.py +++ b/mayan/apps/permissions/urls.py @@ -5,7 +5,7 @@ from django.conf.urls import url from .api_views import APIPermissionList, APIRoleListView, APIRoleView from .views import ( GroupRolesView, RoleCreateView, RoleDeleteView, RoleEditView, - RoleListView, RoleGroupsView, RolePermissionsView + RoleGroupsView, RoleListView, RolePermissionsView ) urlpatterns = [ diff --git a/mayan/apps/permissions/views.py b/mayan/apps/permissions/views.py index 5d70ff9d54..127312fe9a 100644 --- a/mayan/apps/permissions/views.py +++ b/mayan/apps/permissions/views.py @@ -22,8 +22,8 @@ from .links import link_role_create from .models import Role, StoredPermission from .permissions import ( permission_permission_grant, permission_permission_revoke, - permission_role_view, permission_role_create, permission_role_delete, - permission_role_edit + permission_role_create, permission_role_delete, permission_role_edit, + permission_role_view ) From 5d7f81047754f96647860cf71513fefda8ae31fc Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sat, 19 Jan 2019 00:05:21 -0400 Subject: [PATCH 012/209] Refactor the access control computation Rewrite the ACL queryset filtering to move most of the computation to the database manager view the ORM. Add support for cascading access control checking. Update the .check_access() method to work as a front end of the new .restrict_queryset method. The workflow for access control now follow Django convention of first generating a queryset and then attempt to .get() the desired element of the queryset. This update also allows restricting a queryset by related fields which can be Generic Foreign Keys. Signed-off-by: Roberto Rosario --- HISTORY.rst | 5 + mayan/apps/acls/apps.py | 4 + mayan/apps/acls/managers.py | 330 +++++++++++++++++++----------------- 3 files changed, 183 insertions(+), 156 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 0ff65ca33d..ba2790de20 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -217,6 +217,11 @@ - The tags app permission workflow is now reciprocal. In order to attach a tag, the user's role will need the tag attach permissions for both, the document and the tag. +- Refactor and optimize the access control computation. Most of + the computation has been moved to the database instead of doing + filtering in Python. The refactor added cascading access checking + in preparation for nested cabinet access control and the removal + of the permission proxy support which is now redundant. 3.1.9 (2018-11-01) ================== diff --git a/mayan/apps/acls/apps.py b/mayan/apps/acls/apps.py index 250fc45e33..ba66f8a458 100644 --- a/mayan/apps/acls/apps.py +++ b/mayan/apps/acls/apps.py @@ -5,6 +5,7 @@ from django.utils.translation import ugettext_lazy as _ from mayan.apps.common import MayanAppConfig, menu_object, menu_sidebar from mayan.apps.navigation import SourceColumn +from .classes import ModelPermission from .links import link_acl_create, link_acl_delete, link_acl_permissions @@ -21,6 +22,9 @@ class ACLsApp(MayanAppConfig): AccessControlList = self.get_model(model_name='AccessControlList') + ModelPermission.register_inheritance( + model=AccessControlList, related='content_object', + ) SourceColumn( attribute='role', is_identifier=True, is_sortable=True, source=AccessControlList diff --git a/mayan/apps/acls/managers.py b/mayan/apps/acls/managers.py index 7b2aae92bf..281c136626 100644 --- a/mayan/apps/acls/managers.py +++ b/mayan/apps/acls/managers.py @@ -1,15 +1,20 @@ from __future__ import absolute_import, unicode_literals import logging +import warnings +from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType from django.core.exceptions import PermissionDenied from django.db import models -from django.db.models import Q -from django.utils.translation import ugettext -from django.utils.translation import ugettext_lazy as _ +from django.db.models import CharField, Value as V, Q +from django.db.models.functions import Concat +from django.http import Http404 -from mayan.apps.common.utils import resolve_attribute, return_related +from mayan.apps.common.utils import ( + get_related_field, resolve_attribute, return_related +) +from mayan.apps.common.warnings import InterfaceWarning from mayan.apps.permissions import Permission from mayan.apps.permissions.models import StoredPermission @@ -24,173 +29,146 @@ class AccessControlListManager(models.Manager): Implement a 3 tier permission system, involving a permissions, an actor and an object """ - def check_access(self, permissions, user, obj, related=None): - if user.is_superuser or user.is_staff: - logger.debug( - 'Permissions "%s" on "%s" granted to user "%s" as superuser ' - 'or staff', permissions, obj, user + def _get_acl_filters(self, queryset, stored_permission, user, related_field_name=None): + """ + This method does the bulk of the work. It generates filters for the + AccessControlList model to determine if there are ACL entries for the + members of the queryset's model provided. + """ + # Determine which of the cases we need to address + # 1: No related field + # 2: Related field + # 3: Related field that is Generic Foreign Key + # 4: No related field, but has an inherited related field, solved by + # 5: Inherited field of a related field + # recursion, branches to #2 or #3. + # Not addressed yet + # 6: Inherited field of a related field that is Generic Foreign Key + result = [] + + if related_field_name: + related_field = get_related_field( + model=queryset.model, related_field_name=related_field_name ) - return True - try: - return Permission.check_permissions( - requester=user, permissions=permissions - ) - except PermissionDenied: - try: - stored_permissions = [ - permission.stored_permission for permission in permissions - ] - except TypeError: - # Not a list of permissions, just one - stored_permissions = (permissions.stored_permission,) + if isinstance(related_field, GenericForeignKey): + # Case 3: Generic Foreign Key, multiple ContentTypes + object + # id combinations + content_type_object_id_queryset = queryset.annotate( + ct_fk_combination=Concat( + related_field.ct_field, V('-'), related_field.fk_field, + output_field=CharField() + ) + ).values('ct_fk_combination') - if related: - obj = resolve_attribute(obj=obj, attribute=related) + field_lookup = 'pk__in' - try: - parent_accessor = ModelPermission.get_inheritance( - model=obj._meta.model + acl_filter = self.annotate( + ct_fk_combination=Concat( + 'content_type', V('-'), 'object_id', output_field=CharField() + ) + ).filter( + permissions=stored_permission, role__groups__user=user, + ct_fk_combination__in=content_type_object_id_queryset + ).values('object_id') + + result.append(Q(**{field_lookup: acl_filter})) + else: + # Case 2: Related field of a single type, single ContentType, + # multiple object id + related_field = get_related_field( + model=queryset.model, related_field_name=related_field_name ) - except AttributeError: - # AttributeError means non model objects: ie Statistics - # These can't have ACLs so we raise PermissionDenied - raise PermissionDenied( - _('Insufficient access for: %(object)s') % {'object': obj} + content_type = ContentType.objects.get_for_model( + model=related_field.related_model + ) + field_lookup = '{}_id__in'.format(related_field_name) + acl_filter = self.filter( + content_type=content_type, permissions=stored_permission, + role__groups__user=user + ).values('object_id') + result.append(Q(**{field_lookup: acl_filter})) + # Case 5: Related field, has an inherited related field itself + # Bubble up permssion check + # TODO: Add relationship support: OR or AND + # TODO: OR for document pages, version, doc, and types + # TODO: AND for new cabinet levels ACLs + try: + related_field_model_related_field_name = ModelPermission.get_inheritance( + model=related_field.related_model + ) + except KeyError: + pass + else: + related_field_name = '{}__{}'.format(related_field_name, related_field_model_related_field_name) + related_field_inherited_acl_queries = self._get_acl_filters( + queryset=queryset, stored_permission=stored_permission, + user=user, related_field_name=related_field_name + ) + result.extend(related_field_inherited_acl_queries) + else: + # Case 1: Original model, single ContentType, multiple object id + content_type = ContentType.objects.get_for_model(model=queryset.model) + field_lookup = 'id__in' + acl_filter = self.filter( + permissions=stored_permission, role__groups__user=user + ).values('id') + result.append(Q(**{field_lookup: acl_filter})) + + # Case 4: Original model, has an inherited related field + try: + related_field_name = ModelPermission.get_inheritance( + model=queryset.model ) except KeyError: pass else: - try: - return self.check_access( - obj=getattr(obj, parent_accessor), - permissions=permissions, user=user - ) - except AttributeError: - # Has no such attribute, try it as a related field - try: - return self.check_access( - obj=return_related( - instance=obj, related_field=parent_accessor - ), permissions=permissions, user=user - ) - except PermissionDenied: - pass - except PermissionDenied: - pass - - user_roles = [] - for group in user.groups.all(): - for role in group.roles.all(): - if set(stored_permissions).intersection(set(self.get_inherited_permissions(role=role, obj=obj))): - logger.debug( - 'Permissions "%s" on "%s" granted to user "%s" through role "%s" via inherited ACL', - permissions, obj, user, role - ) - return True - - user_roles.append(role) - - if not self.filter(content_type=ContentType.objects.get_for_model(model=obj), object_id=obj.pk, permissions__in=stored_permissions, role__in=user_roles).exists(): - logger.debug( - 'Permissions "%s" on "%s" denied for user "%s"', - permissions, obj, user + inherited_acl_queries = self._get_acl_filters( + queryset=queryset, stored_permission=stored_permission, + user=user, related_field_name=related_field_name ) - raise PermissionDenied(ugettext('Insufficient access for: %s') % obj) + result.extend(inherited_acl_queries) - logger.debug( - 'Permissions "%s" on "%s" granted to user "%s" through roles "%s" by direct ACL', - permissions, obj, user, user_roles - ) + return result - def filter_by_access(self, permission, user, queryset): - if user.is_superuser or user.is_staff: - logger.debug( - 'Unfiltered queryset returned to user "%s" as superuser ' - 'or staff', user - ) - return queryset + def check_access(self, permissions, user, obj, related=None, raise_404=False): + warnings.warn( + 'check_access() is deprecated, use restrict_queryset() to ' + 'produce a queryset from which to .get() the corresponding ' + 'object in the local code.', InterfaceWarning + ) try: - Permission.check_permissions( - requester=user, permissions=(permission,) - ) - except PermissionDenied: - user_roles = [] - for group in user.groups.all(): - for role in group.roles.all(): - user_roles.append(role) - - try: - parent_accessor = ModelPermission.get_inheritance( - model=queryset.model - ) - except KeyError: - parent_acl_query = Q() - else: - instance = queryset.first() - if instance: - parent_object = return_related( - instance=instance, related_field=parent_accessor - ) - - try: - # Try to see if parent_object is a function - parent_object() - except TypeError: - # Is not a function, try it as a field - parent_content_type = ContentType.objects.get_for_model( - model=parent_object - ) - parent_queryset = self.filter( - content_type=parent_content_type, - role__in=user_roles, - permissions=permission.stored_permission - ) - parent_acl_query = Q( - **{ - '{}__pk__in'.format( - parent_accessor - ): parent_queryset.values_list( - 'object_id', flat=True - ) - } - ) - else: - # Is a function. Can't perform Q object filtering. - # Perform iterative filtering. - result = [] - for entry in queryset: - try: - self.check_access( - obj=entry, permissions=permission, - user=user - ) - except PermissionDenied: - pass - else: - result.append(entry.pk) - - return queryset.filter(pk__in=result) - else: - parent_acl_query = Q() - - # Directly granted access - content_type = ContentType.objects.get_for_model( - model=queryset.model - ) - acl_query = Q(pk__in=self.filter( - content_type=content_type, role__in=user_roles, - permissions=permission.stored_permission - ).values_list('object_id', flat=True)) - logger.debug( - 'Filtered queryset returned to user "%s" based on roles "%s"', - user, user_roles - ) - - return queryset.filter(parent_acl_query | acl_query) + # permissions can be a single permission or a list of permissions + permission = permissions[0] + except TypeError: + permission = permissions else: - return queryset + warnings.warn( + 'Passing multiple permissions via the `permissions` argument ' + 'is deprecated. Pass a single permission. Use multiple call ' + 'to check against multiple permissions.', InterfaceWarning + ) + + if related: + warnings.warn( + 'Passing a related field name to check_access() is ' + 'deprecated. Register the related field using ' + 'common.classes.ModelPermission.', InterfaceWarning + ) + + queryset = self.restrict_queryset( + permission=permission, queryset=obj._meta.default_manager.all(), + user=user, related_field_name=related + ) + + if queryset.filter(pk=obj.pk).exists(): + return True + else: + if raise_404: + raise Http404 + else: + return PermissionDenied def get_inherited_permissions(self, role, obj): try: @@ -242,6 +220,46 @@ class AccessControlListManager(models.Manager): return acl + def filter_by_access(self, permission, queryset, user): + warnings.warn( + 'filter_by_access() is deprecated, use restrict_queryset().', + InterfaceWarning + ) + return self.restrict_queryset( + permission=permission, queryset=queryset, user=user + ) + + def restrict_queryset(self, permission, queryset, user, related_field_name=None): + # `related_field_name` is left only for compatibility with check_access + # once check_access() is removed the `related_field_name` argument + # will be removed too. + + # Check directly granted permission via a role + try: + Permission.check_user_permission(permission=permission, user=user) + + Permission.check_permissions( + requester=user, permissions=(permission,) + ) + except PermissionDenied: + acl_filters = self._get_acl_filters( + queryset=queryset, related_field_name=related_field_name, + stored_permission=permission.stored_permission, user=user + ) + + final_query = None + for acl_filter in acl_filters: + if final_query is None: + final_query = acl_filter + else: + final_query = final_query | acl_filter + + return queryset.filter(final_query) + else: + # User has direct permission assignment via a role, is superuser or + # is staff. Return the entire queryset. + return queryset + def revoke(self, permission, role, obj): content_type = ContentType.objects.get_for_model(model=obj) acl, created = self.get_or_create( From 354ea434aeb186b74df8ef692cca78d9026398de Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sat, 19 Jan 2019 00:09:09 -0400 Subject: [PATCH 013/209] Add keyword arguments to the ACLs app code Rename all instance of `pk` or `acl_pk` to `acl_id` to match the preferred URL parameter naming conventions of using `id` instead of `pk`. Signed-off-by: Roberto Rosario --- mayan/apps/acls/links.py | 7 +- mayan/apps/acls/models.py | 2 +- mayan/apps/acls/tests/mixins.py | 1 - mayan/apps/acls/tests/test_models.py | 109 +++++++++++++-------------- mayan/apps/acls/tests/test_views.py | 4 +- mayan/apps/acls/urls.py | 10 +-- mayan/apps/acls/views.py | 36 +++------ mayan/apps/acls/workflow_actions.py | 3 +- 8 files changed, 77 insertions(+), 95 deletions(-) diff --git a/mayan/apps/acls/links.py b/mayan/apps/acls/links.py index da2f289e10..adebdea8fb 100644 --- a/mayan/apps/acls/links.py +++ b/mayan/apps/acls/links.py @@ -29,13 +29,14 @@ def get_kwargs_factory(variable_name): link_acl_delete = Link( - args='resolved_object.pk', icon_class=icon_acl_delete, + icon_class=icon_acl_delete, kwargs={'acl_id': 'resolved_object.pk'}, permissions=(permission_acl_edit,), permissions_related='content_object', tags='dangerous', text=_('Delete'), view='acls:acl_delete', ) link_acl_list = Link( - icon_class=icon_acl_list, kwargs=get_kwargs_factory('resolved_object'), - permissions=(permission_acl_view,), text=_('ACLs'), view='acls:acl_list' + icon_class=icon_acl_list, kwargs=get_kwargs_factory( + variable_name='resolved_object' + ), permissions=(permission_acl_view,), text=_('ACLs'), view='acls:acl_list' ) link_acl_create = Link( icon_class=icon_acl_new, kwargs=get_kwargs_factory('resolved_object'), diff --git a/mayan/apps/acls/models.py b/mayan/apps/acls/models.py index e93f00f486..4a7fb10726 100644 --- a/mayan/apps/acls/models.py +++ b/mayan/apps/acls/models.py @@ -67,7 +67,7 @@ class AccessControlList(models.Model): def get_absolute_url(self): return reverse( - viewname='acls:acl_permissions', kwargs={'acl_pk': self.pk} + viewname='acls:acl_permissions', kwargs={'acl_id': self.pk} ) def get_inherited_permissions(self): diff --git a/mayan/apps/acls/tests/mixins.py b/mayan/apps/acls/tests/mixins.py index 7c32cb7287..6f4c9af305 100644 --- a/mayan/apps/acls/tests/mixins.py +++ b/mayan/apps/acls/tests/mixins.py @@ -9,7 +9,6 @@ from ..models import AccessControlList class ACLTestCaseMixin(RoleTestCaseMixin, UserTestCaseMixin): - def setUp(self): super(ACLTestCaseMixin, self).setUp() if hasattr(self, '_test_case_user'): diff --git a/mayan/apps/acls/tests/test_models.py b/mayan/apps/acls/tests/test_models.py index 1eaeb57e24..f113a4ff43 100644 --- a/mayan/apps/acls/tests/test_models.py +++ b/mayan/apps/acls/tests/test_models.py @@ -6,154 +6,147 @@ from mayan.apps.common.tests import BaseTestCase from mayan.apps.documents.models import Document, DocumentType from mayan.apps.documents.permissions import permission_document_view from mayan.apps.documents.tests import ( - TEST_DOCUMENT_TYPE_2_LABEL, TEST_DOCUMENT_TYPE_LABEL, - TEST_SMALL_DOCUMENT_PATH + DocumentTestMixin, TEST_DOCUMENT_TYPE_2_LABEL, TEST_DOCUMENT_TYPE_LABEL ) from ..models import AccessControlList -class PermissionTestCase(BaseTestCase): +class PermissionTestCase(DocumentTestMixin, BaseTestCase): + auto_create_document_type = False + def setUp(self): super(PermissionTestCase, self).setUp() - self.document_type_1 = DocumentType.objects.create( + self.test_document_type_1 = DocumentType.objects.create( label=TEST_DOCUMENT_TYPE_LABEL ) - self.document_type_2 = DocumentType.objects.create( + self.test_document_type_2 = DocumentType.objects.create( label=TEST_DOCUMENT_TYPE_2_LABEL ) - with open(TEST_SMALL_DOCUMENT_PATH, mode='rb') as file_object: - self.document_1 = self.document_type_1.new_document( - file_object=file_object - ) - - with open(TEST_SMALL_DOCUMENT_PATH, mode='rb') as file_object: - self.document_2 = self.document_type_1.new_document( - file_object=file_object - ) - - with open(TEST_SMALL_DOCUMENT_PATH, mode='rb') as file_object: - self.document_3 = self.document_type_2.new_document( - file_object=file_object - ) - - def tearDown(self): - for document_type in DocumentType.objects.all(): - document_type.delete() - super(PermissionTestCase, self).tearDown() + self.test_document_1 = self.upload_document( + document_type=self.test_document_type_1 + ) + self.test_document_2 = self.upload_document( + document_type=self.test_document_type_1 + ) + self.test_document_3 = self.upload_document( + document_type=self.test_document_type_2 + ) def test_check_access_without_permissions(self): with self.assertRaises(PermissionDenied): AccessControlList.objects.check_access( - permissions=(permission_document_view,), - user=self.user, obj=self.document_1 + obj=self.test_document_1, permissions=(permission_document_view,), + user=self._test_case_user ) def test_filtering_without_permissions(self): - self.assertQuerysetEqual( + self.assertEqual( AccessControlList.objects.filter_by_access( - permission=permission_document_view, user=self.user, - queryset=Document.objects.all() - ), [] + permission=permission_document_view, + queryset=Document.objects.all(), user=self._test_case_user, + ).count(), 0 ) def test_check_access_with_acl(self): acl = AccessControlList.objects.create( - content_object=self.document_1, role=self.role + content_object=self.test_document_1, role=self._test_case_role ) acl.permissions.add(permission_document_view.stored_permission) try: AccessControlList.objects.check_access( - permissions=(permission_document_view,), user=self.user, - obj=self.document_1 + obj=self.test_document_1, permissions=(permission_document_view,), + user=self._test_case_user ) except PermissionDenied: self.fail('PermissionDenied exception was not expected.') def test_filtering_with_permissions(self): acl = AccessControlList.objects.create( - content_object=self.document_1, role=self.role + content_object=self.test_document_1, role=self._test_case_role ) acl.permissions.add(permission_document_view.stored_permission) self.assertQuerysetEqual( AccessControlList.objects.filter_by_access( - permission=permission_document_view, user=self.user, - queryset=Document.objects.all() - ), (repr(self.document_1),) + permission=permission_document_view, + queryset=Document.objects.all(), user=self._test_case_user + ), (repr(self.test_document_1),) ) def test_check_access_with_inherited_acl(self): acl = AccessControlList.objects.create( - content_object=self.document_type_1, role=self.role + content_object=self.test_document_type_1, role=self._test_case_role ) acl.permissions.add(permission_document_view.stored_permission) try: AccessControlList.objects.check_access( - permissions=(permission_document_view,), user=self.user, - obj=self.document_1 + obj=self.test_document_1, permissions=(permission_document_view,), + user=self._test_case_user ) except PermissionDenied: self.fail('PermissionDenied exception was not expected.') def test_check_access_with_inherited_acl_and_local_acl(self): acl = AccessControlList.objects.create( - content_object=self.document_type_1, role=self.role + content_object=self.test_document_type_1, role=self._test_case_role ) acl.permissions.add(permission_document_view.stored_permission) acl = AccessControlList.objects.create( - content_object=self.document_3, role=self.role + content_object=self.test_document_3, role=self._test_case_role ) acl.permissions.add(permission_document_view.stored_permission) try: AccessControlList.objects.check_access( - permissions=(permission_document_view,), user=self.user, - obj=self.document_3 + obj=self.test_document_3, permissions=(permission_document_view,), + user=self._test_case_user ) except PermissionDenied: self.fail('PermissionDenied exception was not expected.') def test_filtering_with_inherited_permissions(self): acl = AccessControlList.objects.create( - content_object=self.document_type_1, role=self.role + content_object=self.test_document_type_1, role=self._test_case_role ) acl.permissions.add(permission_document_view.stored_permission) result = AccessControlList.objects.filter_by_access( - permission=permission_document_view, user=self.user, - queryset=Document.objects.all() + permission=permission_document_view, queryset=Document.objects.all(), + user=self._test_case_user ) # Since document_1 and document_2 are of document_type_1 # they are the only ones that should be returned - self.assertTrue(self.document_1 in result) - self.assertTrue(self.document_2 in result) - self.assertTrue(self.document_3 not in result) + self.assertTrue(self.test_document_1 in result) + self.assertTrue(self.test_document_2 in result) + self.assertTrue(self.test_document_3 not in result) def test_filtering_with_inherited_permissions_and_local_acl(self): - self.role.permissions.add(permission_document_view.stored_permission) + self._test_case_role.permissions.add( + permission_document_view.stored_permission + ) acl = AccessControlList.objects.create( - content_object=self.document_type_1, role=self.role + content_object=self.test_document_type_1, role=self._test_case_role ) acl.permissions.add(permission_document_view.stored_permission) acl = AccessControlList.objects.create( - content_object=self.document_3, role=self.role + content_object=self.test_document_3, role=self._test_case_role ) acl.permissions.add(permission_document_view.stored_permission) result = AccessControlList.objects.filter_by_access( - permission=permission_document_view, user=self.user, - queryset=Document.objects.all() + permission=permission_document_view, queryset=Document.objects.all(), + user=self._test_case_user, ) - self.assertTrue(self.document_1 in result) - self.assertTrue(self.document_2 in result) - self.assertTrue(self.document_3 in result) + self.assertTrue(self.test_document_1 in result) + self.assertTrue(self.test_document_2 in result) + self.assertTrue(self.test_document_3 in result) diff --git a/mayan/apps/acls/tests/test_views.py b/mayan/apps/acls/tests/test_views.py index 05a839f298..a41ca75164 100644 --- a/mayan/apps/acls/tests/test_views.py +++ b/mayan/apps/acls/tests/test_views.py @@ -99,7 +99,7 @@ class AccessControlListViewTestCase(RoleTestMixin, GenericDocumentViewTestCase): def _request_acl_delete_view(self): return self.post( - viewname='acls:acl_delete', kwargs={'acl_pk': self.test_acl.pk} + viewname='acls:acl_delete', kwargs={'acl_id': self.test_acl.pk} ) def test_acl_delete_view_no_permission(self): @@ -154,7 +154,7 @@ class AccessControlListViewTestCase(RoleTestMixin, GenericDocumentViewTestCase): def _request_get_acl_permissions_view(self): return self.get( viewname='acls:acl_permissions', - kwargs={'acl_pk': self.test_acl.pk} + kwargs={'acl_id': self.test_acl.pk} ) def test_acl_permissions_view_get_no_permission(self): diff --git a/mayan/apps/acls/urls.py b/mayan/apps/acls/urls.py index dc73f8a5b7..d172bb3bdc 100644 --- a/mayan/apps/acls/urls.py +++ b/mayan/apps/acls/urls.py @@ -20,11 +20,11 @@ urlpatterns = [ name='acl_list', view=ACLListView.as_view() ), url( - regex=r'^acls/(?P\d+)/delete/$', name='acl_delete', + regex=r'^acls/(?P\d+)/delete/$', name='acl_delete', view=ACLDeleteView.as_view() ), url( - regex=r'^acls/(?P\d+)/permissions/$', name='acl_permissions', + regex=r'^acls/(?P\d+)/permissions/$', name='acl_permissions', view=ACLPermissionsView.as_view() ), ] @@ -35,16 +35,16 @@ api_urls = [ name='accesscontrollist-list', view=APIObjectACLListView.as_view() ), url( - regex=r'^objects/(?P[-\w]+)/(?P[-\w]+)/(?P\d+)/acls/(?P\d+)/$', + regex=r'^objects/(?P[-\w]+)/(?P[-\w]+)/(?P\d+)/acls/(?P\d+)/$', name='accesscontrollist-detail', view=APIObjectACLView.as_view() ), url( - regex=r'^objects/(?P[-\w]+)/(?P[-\w]+)/(?P\d+)/acls/(?P\d+)/permissions/$', + regex=r'^objects/(?P[-\w]+)/(?P[-\w]+)/(?P\d+)/acls/(?P\d+)/permissions/$', name='accesscontrollist-permission-list', view=APIObjectACLPermissionListView.as_view() ), url( - regex=r'^objects/(?P[-\w]+)/(?P[-\w]+)/(?P\d+)/acls/(?P\d+)/permissions/(?P\d+)/$', + regex=r'^objects/(?P[-\w]+)/(?P[-\w]+)/(?P\d+)/acls/(?P\d+)/permissions/(?P\d+)/$', name='accesscontrollist-permission-detail', view=APIObjectACLPermissionView.as_view() ), diff --git a/mayan/apps/acls/views.py b/mayan/apps/acls/views.py index 410f75b006..2028cc93a0 100644 --- a/mayan/apps/acls/views.py +++ b/mayan/apps/acls/views.py @@ -3,7 +3,6 @@ from __future__ import absolute_import, unicode_literals import itertools import logging -from django.core.exceptions import PermissionDenied from django.shortcuts import get_object_or_404 from django.template import RequestContext from django.urls import reverse @@ -11,7 +10,7 @@ from django.utils.encoding import force_text from django.utils.translation import ugettext_lazy as _ from mayan.apps.common.mixins import ( - ContentTypeViewMixin, ExternalObjectViewMixin + ContentTypeViewMixin, ExternalObjectMixin ) from mayan.apps.common.views import ( AssignRemoveView, SingleObjectCreateView, SingleObjectDeleteView, @@ -30,7 +29,7 @@ from .permissions import permission_acl_edit, permission_acl_view logger = logging.getLogger(__name__) -class ACLCreateView(ContentTypeViewMixin, ExternalObjectViewMixin, SingleObjectCreateView): +class ACLCreateView(ContentTypeViewMixin, ExternalObjectMixin, SingleObjectCreateView): external_object_permission = permission_acl_edit external_object_pk_url_kwarg = 'object_id' form_class = ACLCreateForm @@ -77,11 +76,9 @@ class ACLCreateView(ContentTypeViewMixin, ExternalObjectViewMixin, SingleObjectC class ACLDeleteView(SingleObjectDeleteView): - object_permission = permission_acl_edit - object_permission_related = 'content_object' - object_permission_raise_404 = True model = AccessControlList - pk_url_kwarg = 'acl_pk' + object_permission = permission_acl_edit + pk_url_kwarg = 'acl_id' def get_extra_context(self): return { @@ -100,7 +97,7 @@ class ACLDeleteView(SingleObjectDeleteView): ) -class ACLListView(ContentTypeViewMixin, ExternalObjectViewMixin, SingleObjectListView): +class ACLListView(ContentTypeViewMixin, ExternalObjectMixin, SingleObjectListView): external_object_permission = permission_acl_view external_object_pk_url_kwarg = 'object_id' @@ -211,24 +208,15 @@ class ACLPermissionsView(AssignRemoveView): return StoredPermission.objects.filter(pk__in=merged_pks) def get_object(self): - acl = get_object_or_404( - klass=AccessControlList, pk=self.kwargs['acl_pk'] + return get_object_or_404( + klass=self.get_queryset(), pk=self.kwargs['acl_id'] ) - # Get the ACL, from this get the object of the ACL, from the object - # get all ACLs it holds as a filtered queryset by access. - - try: - AccessControlList.objects.check_access( - permissions=(permission_acl_edit,), obj=acl.content_object, - user=self.request.user - ) - except PermissionDenied: - queryset = AccessControlList.objects.none() - else: - queryset = acl.content_object.acls.all() - - return get_object_or_404(klass=queryset, pk=self.kwargs['acl_pk']) + def get_queryset(self): + return AccessControlList.objects.restrict_queryset( + permission=permission_acl_edit, + queryset=AccessControlList.objects.all(), user=self.request.user + ) def get_right_list_help_text(self): if self.get_object().get_inherited_permissions(): diff --git a/mayan/apps/acls/workflow_actions.py b/mayan/apps/acls/workflow_actions.py index 0d333914dd..8da80d1c18 100644 --- a/mayan/apps/acls/workflow_actions.py +++ b/mayan/apps/acls/workflow_actions.py @@ -89,7 +89,8 @@ class GrantAccessAction(WorkflowAction): try: AccessControlList.objects.check_access( - permissions=permission_acl_edit, user=request.user, obj=obj + obj=obj, permissions=permission_acl_edit, + user=request.user ) except Exception as exception: raise ValidationError(exception) From 2d9aca55c52a382acac1ee10b74cdde66612e2a6 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sat, 19 Jan 2019 00:10:40 -0400 Subject: [PATCH 014/209] Add a central module to define project warnings Add the mayan.apps.common.warnings module with an initial InterfaceWarning warning class used to mark use of deprecated internal interfaces. Signed-off-by: Roberto Rosario --- mayan/apps/common/warnings.py | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 mayan/apps/common/warnings.py diff --git a/mayan/apps/common/warnings.py b/mayan/apps/common/warnings.py new file mode 100644 index 0000000000..5365974f3e --- /dev/null +++ b/mayan/apps/common/warnings.py @@ -0,0 +1,7 @@ +from __future__ import absolute_import + + +class InterfaceWarning(UserWarning): + """ + Warning when using obsolete internal interfaces + """ From 9ed93b54af7eef5424b7047dc538945b8cfba3bb Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sat, 19 Jan 2019 00:13:19 -0400 Subject: [PATCH 015/209] Add get_related_field utility function Add the get_related_field function to resolve a model's related field reference by a path separate by Django's default field separator '__'. Signed-off-by: Roberto Rosario --- mayan/apps/common/utils.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/mayan/apps/common/utils.py b/mayan/apps/common/utils.py index c8382ea330..e5456b5a02 100644 --- a/mayan/apps/common/utils.py +++ b/mayan/apps/common/utils.py @@ -87,6 +87,26 @@ def fs_cleanup(filename, file_descriptor=None, suppress_exceptions=True): raise +def get_related_field(model, related_field_name): + try: + local_field_name, remaining_field_path = related_field_name.split( + LOOKUP_SEP, 1 + ) + except ValueError: + local_field_name = related_field_name + remaining_field_path = None + + related_field = model._meta.get_field(local_field_name) + + if remaining_field_path: + return get_related_field( + model=related_field.related_model, + related_field_name=remaining_field_path + ) + + return related_field + + def get_descriptor(file_input, read=True): try: # Is it a file like object? @@ -119,7 +139,6 @@ def get_storage_subclass(dotted_path): def deconstruct(self): return ('mayan.apps.common.classes.FakeStorageSubclass', (), {}) - return StorageSubclass From 16d8fb9feabbd6b43bff847e79ff4c6da8a91fd2 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sat, 19 Jan 2019 01:00:58 -0400 Subject: [PATCH 016/209] Modernize MOTD app Update API code to use viewsets. Update links and URLs to use keyword arguments. Signed-off-by: Roberto Rosario --- mayan/apps/motd/api_views.py | 50 +++++++------- mayan/apps/motd/apps.py | 14 ++-- mayan/apps/motd/icons.py | 7 +- mayan/apps/motd/links.py | 13 ++-- mayan/apps/motd/permissions.py | 8 +-- mayan/apps/motd/serializers.py | 5 +- mayan/apps/motd/tests/literals.py | 8 +-- mayan/apps/motd/tests/mixins.py | 41 +++++++++++ mayan/apps/motd/tests/test_api.py | 96 +++++++++++++------------ mayan/apps/motd/tests/test_models.py | 24 +++---- mayan/apps/motd/tests/test_views.py | 100 +++++++++++++++++++++++++++ mayan/apps/motd/urls.py | 29 ++++---- mayan/apps/motd/views.py | 6 +- 13 files changed, 281 insertions(+), 120 deletions(-) create mode 100644 mayan/apps/motd/tests/mixins.py create mode 100644 mayan/apps/motd/tests/test_views.py diff --git a/mayan/apps/motd/api_views.py b/mayan/apps/motd/api_views.py index 0dba8b92a4..88ad8b4871 100644 --- a/mayan/apps/motd/api_views.py +++ b/mayan/apps/motd/api_views.py @@ -1,6 +1,6 @@ from __future__ import absolute_import, unicode_literals -from rest_framework import generics +from rest_framework import viewsets from mayan.apps.rest_api.filters import MayanObjectPermissionsFilter from mayan.apps.rest_api.permissions import MayanPermission @@ -13,32 +13,34 @@ from .permissions import ( from .serializers import MessageSerializer -class APIMessageListView(generics.ListCreateAPIView): +class APIMessageViewSet(viewsets.ModelViewSet): """ - get: Returns a list of all the messages. - post: Create a new message. + create: + Create a new message. + + delete: + Delete the given message. + + edit: + Edit the given message. + + list: + Return a list of all the messages. + + retrieve: + Return the given message details. """ filter_backends = (MayanObjectPermissionsFilter,) - mayan_object_permissions = {'GET': (permission_message_view,)} - mayan_view_permissions = {'POST': (permission_message_create,)} - permission_classes = (MayanPermission,) - queryset = Message.objects.all() - serializer_class = MessageSerializer - - -class APIMessageView(generics.RetrieveUpdateDestroyAPIView): - """ - delete: Delete the selected message. - get: Return the details of the selected message. - patch: Edit the selected message. - put: Edit the selected message. - """ - filter_backends = (MayanObjectPermissionsFilter,) - mayan_object_permissions = { - 'DELETE': (permission_message_delete,), - 'GET': (permission_message_view,), - 'PATCH': (permission_message_edit,), - 'PUT': (permission_message_edit,) + lookup_url_kwarg = 'message_id' + object_permission = { + 'DELETE': permission_message_delete, + 'GET': permission_message_view, + 'PATCH': permission_message_edit, + 'PUT': permission_message_edit, } queryset = Message.objects.all() + permission_classes = (MayanPermission,) serializer_class = MessageSerializer + view_permission = { + 'POST': permission_message_create + } diff --git a/mayan/apps/motd/apps.py b/mayan/apps/motd/apps.py index fdc4b4f8b1..854fcfd0cc 100644 --- a/mayan/apps/motd/apps.py +++ b/mayan/apps/motd/apps.py @@ -38,7 +38,7 @@ class MOTDApp(MayanAppConfig): def ready(self): super(MOTDApp, self).ready() - Message = self.get_model('Message') + Message = self.get_model(model_name='Message') ModelPermission.register( model=Message, permissions=( permission_acl_edit, permission_acl_view, @@ -48,16 +48,20 @@ class MOTDApp(MayanAppConfig): ) SourceColumn( - attribute='label', is_identifier=True, source=Message + attribute='label', is_identifier=True, is_sortable=True, + source=Message ) SourceColumn( - attribute='enabled', source=Message, widget=TwoStateWidget + attribute='enabled', include_label=True, is_sortable=True, + source=Message, widget=TwoStateWidget ) SourceColumn( - attribute='start_datetime', empty_value=_('None'), source=Message + attribute='start_datetime', empty_value=_('None'), + include_label=True, is_sortable=True, source=Message ) SourceColumn( - attribute='end_datetime', empty_value=_('None'), source=Message + attribute='end_datetime', empty_value=_('None'), + include_label=True, is_sortable=True, source=Message ) menu_list_facet.bind_links( diff --git a/mayan/apps/motd/icons.py b/mayan/apps/motd/icons.py index 4a1681c28d..b7c9b72e52 100644 --- a/mayan/apps/motd/icons.py +++ b/mayan/apps/motd/icons.py @@ -2,5 +2,10 @@ from __future__ import absolute_import, unicode_literals from mayan.apps.appearance.classes import Icon -icon_message_create = Icon(driver_name='fontawesome', symbol='plus') +icon_message_create = icon_tag_create = Icon( + driver_name='fontawesome-dual', primary_symbol='bullhorn', + secondary_symbol='plus' +) +icon_message_delete = Icon(driver_name='fontawesome', symbol='times') +icon_message_edit = Icon(driver_name='fontawesome', symbol='pencil-alt') icon_message_list = Icon(driver_name='fontawesome', symbol='bullhorn') diff --git a/mayan/apps/motd/links.py b/mayan/apps/motd/links.py index 92e94542ca..b5fd4ca109 100644 --- a/mayan/apps/motd/links.py +++ b/mayan/apps/motd/links.py @@ -4,7 +4,10 @@ from django.utils.translation import ugettext_lazy as _ from mayan.apps.navigation import Link, get_cascade_condition -from .icons import icon_message_create, icon_message_list +from .icons import ( + icon_message_create, icon_message_delete, icon_message_edit, + icon_message_list +) from .permissions import ( permission_message_create, permission_message_delete, permission_message_edit, permission_message_view @@ -15,11 +18,13 @@ link_message_create = Link( text=_('Create message'), view='motd:message_create' ) link_message_delete = Link( - args='object.pk', permissions=(permission_message_delete,), - tags='dangerous', text=_('Delete'), view='motd:message_delete' + icon_class=icon_message_delete, kwargs={'message_id': 'object.pk'}, + permissions=(permission_message_delete,), tags='dangerous', + text=_('Delete'), view='motd:message_delete' ) link_message_edit = Link( - args='object.pk', permissions=(permission_message_edit,), text=_('Edit'), + icon_class=icon_message_edit, kwargs={'message_id': 'object.pk'}, + permissions=(permission_message_edit,), text=_('Edit'), view='motd:message_edit' ) link_message_list = Link( diff --git a/mayan/apps/motd/permissions.py b/mayan/apps/motd/permissions.py index ab0ff47fa9..6c22706a74 100644 --- a/mayan/apps/motd/permissions.py +++ b/mayan/apps/motd/permissions.py @@ -7,14 +7,14 @@ from mayan.apps.permissions import PermissionNamespace namespace = PermissionNamespace(label=_('Message of the day'), name='motd') permission_message_create = namespace.add_permission( - name='message_create', label=_('Create messages') + label=_('Create messages'), name='message_create' ) permission_message_delete = namespace.add_permission( - name='message_delete', label=_('Delete messages') + label=_('Delete messages'), name='message_delete' ) permission_message_edit = namespace.add_permission( - name='message_edit', label=_('Edit messages') + label=_('Edit messages'), name='message_edit' ) permission_message_view = namespace.add_permission( - name='message_view', label=_('View messages') + label=_('View messages'), name='message_view' ) diff --git a/mayan/apps/motd/serializers.py b/mayan/apps/motd/serializers.py index 1637ac91b6..e294df7646 100644 --- a/mayan/apps/motd/serializers.py +++ b/mayan/apps/motd/serializers.py @@ -8,7 +8,10 @@ from .models import Message class MessageSerializer(serializers.HyperlinkedModelSerializer): class Meta: extra_kwargs = { - 'url': {'view_name': 'rest_api:message-detail'}, + 'url': { + 'lookup_url_kwarg': 'message_id', + 'view_name': 'rest_api:message-detail' + }, } fields = ( 'end_datetime', 'enabled', 'label', 'message', 'start_datetime', diff --git a/mayan/apps/motd/tests/literals.py b/mayan/apps/motd/tests/literals.py index 431ef052bb..10051524d7 100644 --- a/mayan/apps/motd/tests/literals.py +++ b/mayan/apps/motd/tests/literals.py @@ -1,6 +1,6 @@ from __future__ import unicode_literals -TEST_LABEL = 'test label' -TEST_LABEL_EDITED = 'test label edited' -TEST_MESSAGE = 'test message' -TEST_MESSAGE_EDITED = 'test message edited' +TEST_MESSAGE_LABEL = 'test label' +TEST_MESSAGE_LABEL_EDITED = '{} edited'.format(TEST_MESSAGE_LABEL) +TEST_MESSAGE_TEXT = 'test message' +TEST_MESSAGE_TEXT_EDITED = '{} edited'.format(TEST_MESSAGE_TEXT) diff --git a/mayan/apps/motd/tests/mixins.py b/mayan/apps/motd/tests/mixins.py new file mode 100644 index 0000000000..b182b5042a --- /dev/null +++ b/mayan/apps/motd/tests/mixins.py @@ -0,0 +1,41 @@ +from __future__ import unicode_literals + +from ..models import Message + +from .literals import ( + TEST_MESSAGE_LABEL, TEST_MESSAGE_LABEL_EDITED, TEST_MESSAGE_TEXT, + TEST_MESSAGE_TEXT_EDITED +) + + +class MOTDTestMixin(object): + def _create_test_message(self): + self.test_message = Message.objects.create( + label=TEST_MESSAGE_LABEL, message=TEST_MESSAGE_TEXT + ) + + def _request_message_create_view(self): + return self.post( + viewname='motd:message_create', data={ + 'label': TEST_MESSAGE_LABEL, 'message': TEST_MESSAGE_TEXT + } + ) + + def _request_message_delete_view(self): + return self.post( + viewname='motd:message_delete', + kwargs={'message_id': self.test_message.pk} + ) + + def _request_message_edit_view(self): + return self.post( + viewname='motd:message_edit', + kwargs={'message_id': self.test_message.pk}, + data={ + 'label': TEST_MESSAGE_LABEL_EDITED, + 'message': TEST_MESSAGE_TEXT_EDITED + } + ) + + def _request_message_list_view(self): + return self.get(viewname='motd:message_list') diff --git a/mayan/apps/motd/tests/test_api.py b/mayan/apps/motd/tests/test_api.py index fa4b89760f..16bfdb86ec 100644 --- a/mayan/apps/motd/tests/test_api.py +++ b/mayan/apps/motd/tests/test_api.py @@ -11,24 +11,18 @@ from ..permissions import ( ) from .literals import ( - TEST_LABEL, TEST_LABEL_EDITED, TEST_MESSAGE, TEST_MESSAGE_EDITED + TEST_MESSAGE_LABEL, TEST_MESSAGE_LABEL_EDITED, TEST_MESSAGE_TEXT, + TEST_MESSAGE_TEXT_EDITED ) +from .mixins import MOTDTestMixin -class MOTDAPITestCase(BaseAPITestCase): - def setUp(self): - super(MOTDAPITestCase, self).setUp() - self.login_user() - - def _create_message(self): - return Message.objects.create( - label=TEST_LABEL, message=TEST_MESSAGE - ) - +class MOTDAPITestCase(MOTDTestMixin, BaseAPITestCase): def _request_message_create_view(self): return self.post( viewname='rest_api:message-list', data={ - 'label': TEST_LABEL, 'message': TEST_MESSAGE + 'label': TEST_MESSAGE_LABEL, + 'message': TEST_MESSAGE_TEXT } ) @@ -44,103 +38,107 @@ class MOTDAPITestCase(BaseAPITestCase): message = Message.objects.first() self.assertEqual(response.data['id'], message.pk) - self.assertEqual(response.data['label'], TEST_LABEL) - self.assertEqual(response.data['message'], TEST_MESSAGE) + self.assertEqual(response.data['label'], TEST_MESSAGE_LABEL) + self.assertEqual(response.data['message'], TEST_MESSAGE_TEXT) self.assertEqual(Message.objects.count(), 1) - self.assertEqual(message.label, TEST_LABEL) - self.assertEqual(message.message, TEST_MESSAGE) + self.assertEqual(message.label, TEST_MESSAGE_LABEL) + self.assertEqual(message.message, TEST_MESSAGE_TEXT) def _request_message_delete_view(self): return self.delete( - viewname='rest_api:message-detail', args=(self.message.pk,) + viewname='rest_api:message-detail', + kwargs={'message_id': self.test_message.pk}, ) def test_message_delete_view_no_access(self): - self.message = self._create_message() + self._create_test_message() response = self._request_message_delete_view() self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) self.assertEqual(Message.objects.count(), 1) def test_message_delete_view_with_access(self): - self.message = self._create_message() - self.grant_access(permission=permission_message_delete, obj=self.message) + self._create_test_message() + self.grant_access(permission=permission_message_delete, obj=self.test_message) response = self._request_message_delete_view() self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) self.assertEqual(Message.objects.count(), 0) def _request_message_detail_view(self): return self.get( - viewname='rest_api:message-detail', args=(self.message.pk,) + viewname='rest_api:message-detail', + kwargs={'message_id': self.test_message.pk}, ) def test_message_detail_view_no_access(self): - self.message = self._create_message() + self._create_test_message() response = self._request_message_detail_view() self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) def test_message_detail_view_with_access(self): - self.message = self._create_message() - self.grant_access(permission=permission_message_view, obj=self.message) + self._create_test_message() + self.grant_access(permission=permission_message_view, obj=self.test_message) response = self._request_message_detail_view() self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data['label'], TEST_LABEL) + self.assertEqual(response.data['label'], TEST_MESSAGE_LABEL) def _request_message_edit_via_patch_view(self): return self.patch( - viewname='rest_api:message-detail', args=(self.message.pk,), + viewname='rest_api:message-detail', + kwargs={'message_id': self.test_message.pk}, data={ - 'label': TEST_LABEL_EDITED, - 'message': TEST_MESSAGE_EDITED + 'label': TEST_MESSAGE_LABEL_EDITED, + 'message': TEST_MESSAGE_TEXT_EDITED } ) def test_message_edit_via_patch_view_no_access(self): - self.message = self._create_message() + self._create_test_message() response = self._request_message_edit_via_patch_view() self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - self.message.refresh_from_db() + self.test_message.refresh_from_db() - self.assertEqual(self.message.label, TEST_LABEL) - self.assertEqual(self.message.message, TEST_MESSAGE) + self.assertEqual(self.test_message.label, TEST_MESSAGE_LABEL) + self.assertEqual(self.test_message.message, TEST_MESSAGE_TEXT) def test_message_edit_via_patch_view_with_access(self): - self.message = self._create_message() - self.grant_access(permission=permission_message_edit, obj=self.message) + self._create_test_message() + self.grant_access(permission=permission_message_edit, obj=self.test_message) response = self._request_message_edit_via_patch_view() self.assertEqual(response.status_code, status.HTTP_200_OK) - self.message.refresh_from_db() - self.assertEqual(self.message.label, TEST_LABEL_EDITED) - self.assertEqual(self.message.message, TEST_MESSAGE_EDITED) + self.test_message.refresh_from_db() + self.assertEqual(self.test_message.label, TEST_MESSAGE_LABEL_EDITED) + self.assertEqual(self.test_message.message, TEST_MESSAGE_TEXT_EDITED) def _request_message_edit_via_put_view(self): return self.put( - viewname='rest_api:message-detail', args=(self.message.pk,), + viewname='rest_api:message-detail', + kwargs={'message_id': self.test_message.pk}, data={ - 'label': TEST_LABEL_EDITED, - 'message': TEST_MESSAGE_EDITED + 'label': TEST_MESSAGE_LABEL_EDITED, + 'message': TEST_MESSAGE_TEXT_EDITED } ) def test_message_edit_via_put_view_no_access(self): - self.message = self._create_message() + self._create_test_message() response = self._request_message_edit_via_put_view() self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - self.message.refresh_from_db() + self.test_message.refresh_from_db() - self.assertEqual(self.message.label, TEST_LABEL) - self.assertEqual(self.message.message, TEST_MESSAGE) + self.assertEqual(self.test_message.label, TEST_MESSAGE_LABEL) + self.assertEqual(self.test_message.message, TEST_MESSAGE_TEXT) def test_message_edit_via_put_view_with_access(self): - self.message = self._create_message() - self.grant_access(permission=permission_message_edit, obj=self.message) + self._create_test_message() + self.grant_access(permission=permission_message_edit, obj=self.test_message) response = self._request_message_edit_via_put_view() self.assertEqual(response.status_code, status.HTTP_200_OK) - self.message.refresh_from_db() - self.assertEqual(self.message.label, TEST_LABEL_EDITED) - self.assertEqual(self.message.message, TEST_MESSAGE_EDITED) + self.test_message.refresh_from_db() + self.assertEqual(self.test_message.label, TEST_MESSAGE_LABEL_EDITED) + self.assertEqual(self.test_message.message, TEST_MESSAGE_TEXT_EDITED) diff --git a/mayan/apps/motd/tests/test_models.py b/mayan/apps/motd/tests/test_models.py index 457cacedac..cdff233b4e 100644 --- a/mayan/apps/motd/tests/test_models.py +++ b/mayan/apps/motd/tests/test_models.py @@ -7,14 +7,12 @@ from django.utils import timezone from ..models import Message -from .literals import TEST_LABEL, TEST_MESSAGE +from .mixins import MOTDTestMixin -class MOTDTestCase(TestCase): +class MOTDTestCase(MOTDTestMixin, TestCase): def setUp(self): - self.motd = Message.objects.create( - label=TEST_LABEL, message=TEST_MESSAGE - ) + self._create_test_message() def test_basic(self): queryset = Message.objects.get_for_now() @@ -22,25 +20,25 @@ class MOTDTestCase(TestCase): self.assertEqual(queryset.exists(), True) def test_start_datetime(self): - self.motd.start_datetime = timezone.now() - timedelta(days=1) - self.motd.save() + self.test_message.start_datetime = timezone.now() - timedelta(days=1) + self.test_message.save() queryset = Message.objects.get_for_now() - self.assertEqual(queryset.first(), self.motd) + self.assertEqual(queryset.first(), self.test_message) def test_end_datetime(self): - self.motd.start_datetime = timezone.now() - timedelta(days=2) - self.motd.end_datetime = timezone.now() - timedelta(days=1) - self.motd.save() + self.test_message.start_datetime = timezone.now() - timedelta(days=2) + self.test_message.end_datetime = timezone.now() - timedelta(days=1) + self.test_message.save() queryset = Message.objects.get_for_now() self.assertEqual(queryset.exists(), False) def test_enable(self): - self.motd.enabled = False - self.motd.save() + self.test_message.enabled = False + self.test_message.save() queryset = Message.objects.get_for_now() diff --git a/mayan/apps/motd/tests/test_views.py b/mayan/apps/motd/tests/test_views.py new file mode 100644 index 0000000000..3d9384a193 --- /dev/null +++ b/mayan/apps/motd/tests/test_views.py @@ -0,0 +1,100 @@ +from __future__ import unicode_literals + +from django.utils.encoding import force_text + +from mayan.apps.common.tests import GenericViewTestCase + +from ..models import Message +from ..permissions import ( + permission_message_create, permission_message_delete, + permission_message_edit, permission_message_view +) + +from .literals import ( + TEST_MESSAGE_LABEL, TEST_MESSAGE_LABEL_EDITED, TEST_MESSAGE_TEXT, + TEST_MESSAGE_TEXT_EDITED +) +from .mixins import MOTDTestMixin + + +class MOTDViewTestCase(MOTDTestMixin, GenericViewTestCase): + def test_message_create_view_no_permissions(self): + response = self._request_message_create_view() + self.assertEqual(response.status_code, 403) + + self.assertEqual(Message.objects.count(), 0) + + def test_message_create_view_with_permissions(self): + self.grant_permission(permission=permission_message_create) + response = self._request_message_create_view() + self.assertEqual(response.status_code, 302) + + self.assertEqual(Message.objects.count(), 1) + message = Message.objects.first() + self.assertEqual(message.label, TEST_MESSAGE_LABEL) + self.assertEqual(message.message, TEST_MESSAGE_TEXT) + + def test_message_delete_view_no_permissions(self): + self._create_test_message() + + response = self._request_message_delete_view() + self.assertEqual(response.status_code, 404) + self.assertEqual(Message.objects.count(), 1) + + def test_message_delete_view_with_access(self): + self._create_test_message() + + self.grant_access( + obj=self.test_message, permission=permission_message_delete + ) + + response = self._request_message_delete_view() + self.assertEqual(response.status_code, 302) + + self.assertEqual(Message.objects.count(), 0) + + def test_message_edit_view_no_permissions(self): + self._create_test_message() + + response = self._request_message_edit_view() + self.assertEqual(response.status_code, 404) + self.test_message.refresh_from_db() + self.assertEqual(self.test_message.label, TEST_MESSAGE_LABEL) + self.assertEqual(self.test_message.message, TEST_MESSAGE_TEXT) + + def test_message_edit_view_with_access(self): + self._create_test_message() + + self.grant_access( + obj=self.test_message, permission=permission_message_edit + ) + + response = self._request_message_edit_view() + self.assertEqual(response.status_code, 302) + self.test_message.refresh_from_db() + self.assertEqual(self.test_message.label, TEST_MESSAGE_LABEL_EDITED) + self.assertEqual( + self.test_message.message, TEST_MESSAGE_TEXT_EDITED + ) + + def test_message_list_view_no_permissions(self): + self._create_test_message() + + response = self._request_message_list_view() + self.assertNotContains( + response=response, text=force_text(self.test_message), + status_code=200 + ) + + def test_message_list_view_with_access(self): + self._create_test_message() + + self.grant_access( + obj=self.test_message, permission=permission_message_view + ) + + response = self._request_message_list_view() + self.assertContains( + response=response, text=force_text(self.test_message), + status_code=200 + ) diff --git a/mayan/apps/motd/urls.py b/mayan/apps/motd/urls.py index 7ee0ff1740..24f26246d6 100644 --- a/mayan/apps/motd/urls.py +++ b/mayan/apps/motd/urls.py @@ -2,27 +2,30 @@ from __future__ import unicode_literals from django.conf.urls import url -from .api_views import APIMessageListView, APIMessageView +from .api_views import APIMessageViewSet from .views import ( MessageCreateView, MessageDeleteView, MessageEditView, MessageListView ) urlpatterns = [ - url(r'^list/$', MessageListView.as_view(), name='message_list'), - url(r'^create/$', MessageCreateView.as_view(), name='message_create'), url( - r'^(?P\d+)/edit/$', MessageEditView.as_view(), name='message_edit' + regex=r'^messages/$', name='message_list', + view=MessageListView.as_view() ), url( - r'^(?P\d+)/delete/$', MessageDeleteView.as_view(), - name='message_delete' + regex=r'^messages/create/$', name='message_create', + view=MessageCreateView.as_view() + ), + url( + regex=r'^messages/(?P\d+)/delete/$', name='message_delete', + view=MessageDeleteView.as_view() + ), + url( + regex=r'^messages/(?P\d+)/edit/$', name='message_edit', + view=MessageEditView.as_view() ), ] -api_urls = [ - url(r'^messages/$', APIMessageListView.as_view(), name='message-list'), - url( - r'^messages/(?P[0-9]+)/$', APIMessageView.as_view(), - name='message-detail' - ), -] +api_router_entries = ( + {'prefix': r'messages', 'viewset': APIMessageViewSet, 'basename': 'message'}, +) diff --git a/mayan/apps/motd/views.py b/mayan/apps/motd/views.py index d16475647f..0abc8b2266 100644 --- a/mayan/apps/motd/views.py +++ b/mayan/apps/motd/views.py @@ -36,7 +36,8 @@ class MessageCreateView(SingleObjectCreateView): class MessageDeleteView(SingleObjectDeleteView): model = Message object_permission = permission_message_delete - post_action_redirect = reverse_lazy('motd:message_list') + pk_url_kwarg = 'message_id' + post_action_redirect = reverse_lazy(viewname='motd:message_list') def get_extra_context(self): return { @@ -50,7 +51,8 @@ class MessageEditView(SingleObjectEditView): fields = ('label', 'message', 'enabled', 'start_datetime', 'end_datetime') model = Message object_permission = permission_message_edit - post_action_redirect = reverse_lazy('motd:message_list') + pk_url_kwarg = 'message_id' + post_action_redirect = reverse_lazy(viewname='motd:message_list') def get_extra_context(self): return { From 7c4ae1aef0024edcef652370331e6a465c7abee4 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sat, 19 Jan 2019 01:04:59 -0400 Subject: [PATCH 017/209] Update common app API to viewsets Update the API entries for content types and templates to use viewsets and the new api_router_entries URL registraion method. Signed-off-by: Roberto Rosario --- mayan/apps/common/api_views.py | 39 +++++++++++++++++----------------- mayan/apps/common/urls.py | 28 ++++++++++-------------- 2 files changed, 31 insertions(+), 36 deletions(-) diff --git a/mayan/apps/common/api_views.py b/mayan/apps/common/api_views.py index 9c632282a2..ab762843a8 100644 --- a/mayan/apps/common/api_views.py +++ b/mayan/apps/common/api_views.py @@ -2,42 +2,43 @@ from __future__ import unicode_literals from django.contrib.contenttypes.models import ContentType -from rest_framework import generics +from rest_framework import viewsets from rest_framework.permissions import IsAuthenticated from .classes import Template from .serializers import ContentTypeSerializer, TemplateSerializer -class APIContentTypeList(generics.ListAPIView): +class APIContentTypeViewSet(viewsets.ReadOnlyModelViewSet): """ - Returns a list of all the available content types. + list: + Return a list of all the available content types. + + retrieve: + Return the given content type details. """ - serializer_class = ContentTypeSerializer + lookup_field = 'pk' + lookup_url_kwarg = 'content_type_id' queryset = ContentType.objects.order_by('app_label', 'model') + serializer_class = ContentTypeSerializer -class APITemplateListView(generics.ListAPIView): +class APITemplateViewSet(viewsets.ReadOnlyModelViewSet): """ - Returns a list of partial templates. - get: Returns a list of partial templates. + list: + Return a list of partial templates. + + retrieve: + Return the given partial template details. """ - serializer_class = TemplateSerializer + lookup_url_kwarg = 'name' permission_classes = (IsAuthenticated,) - - def get_queryset(self): - return Template.all(rendered=True, request=self.request) - - -class APITemplateView(generics.RetrieveAPIView): - """ - Returns the selected partial template details. - get: Retrieve the details of the partial template. - """ serializer_class = TemplateSerializer - permission_classes = (IsAuthenticated,) def get_object(self): return Template.get(name=self.kwargs['name']).render( request=self.request ) + + def get_queryset(self): + return Template.all(rendered=True, request=self.request) diff --git a/mayan/apps/common/urls.py b/mayan/apps/common/urls.py index a857ab1bce..9d42b6f802 100644 --- a/mayan/apps/common/urls.py +++ b/mayan/apps/common/urls.py @@ -3,9 +3,7 @@ from __future__ import unicode_literals from django.conf.urls import url from django.views.i18n import javascript_catalog, set_language -from .api_views import ( - APIContentTypeList, APITemplateListView, APITemplateView -) +from .api_views import APIContentTypeViewSet, APITemplateViewSet from .views import ( AboutView, CheckVersionView, CurrentUserLocaleProfileDetailsView, CurrentUserLocaleProfileEditView, FaviconRedirectView, HomeView, @@ -67,17 +65,13 @@ urlpatterns += [ ), ] -api_urls = [ - url( - regex=r'^content_types/$', name='content-type-list', - view=APIContentTypeList.as_view() - ), - url( - regex=r'^templates/$', name='template-list', - view=APITemplateListView.as_view() - ), - url( - regex=r'^templates/(?P[-\w]+)/$', name='template-detail', - view=APITemplateView.as_view() - ), -] +api_router_entries = ( + { + 'prefix': r'content_types', 'viewset': APIContentTypeViewSet, + 'basename': 'content_type' + }, + { + 'prefix': r'templates', 'viewset': APITemplateViewSet, + 'basename': 'template' + }, +) From 383d0fcc38a629e53ceca727a89d4715d060d845 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sat, 19 Jan 2019 01:12:39 -0400 Subject: [PATCH 018/209] Remove support for raising 404 Remove explict support for raising 404 error when the object access fails. The new method to use is to restrict the queryset using the .restrict_queryset manager method and then .get() the desired object. If the object access control failed then the desired object will not be found in the queryset and an error 404 will be raised. The end result is the same: error 404, the method to raise the error is what differs now. Signed-off-by: Roberto Rosario --- mayan/apps/common/literals.py | 2 +- mayan/apps/common/mixins.py | 43 +++++++++++++---------------------- mayan/apps/common/views.py | 4 ++-- 3 files changed, 19 insertions(+), 30 deletions(-) diff --git a/mayan/apps/common/literals.py b/mayan/apps/common/literals.py index 80b781da2c..3a6c613a74 100644 --- a/mayan/apps/common/literals.py +++ b/mayan/apps/common/literals.py @@ -11,7 +11,7 @@ MESSAGE_SQLITE_WARNING = _( 'for development and testing, not for production.' ) PYPI_URL = 'https://pypi.python.org/pypi' - +PK_LIST_SEPARATOR = ',' TEXT_LIST_AS_ITEMS_PARAMETER = '_list_mode' TEXT_LIST_AS_ITEMS_VARIABLE_NAME = 'list_as_items' TEXT_CHOICE_ITEMS = 'items' diff --git a/mayan/apps/common/mixins.py b/mayan/apps/common/mixins.py index 103dc36dbe..67132941e2 100644 --- a/mayan/apps/common/mixins.py +++ b/mayan/apps/common/mixins.py @@ -16,8 +16,8 @@ from mayan.apps.permissions import Permission from .exceptions import ActionError from .forms import DynamicForm from .literals import ( - TEXT_CHOICE_ITEMS, TEXT_CHOICE_LIST, TEXT_LIST_AS_ITEMS_PARAMETER, - TEXT_LIST_AS_ITEMS_VARIABLE_NAME + PK_LIST_SEPARATOR, TEXT_CHOICE_ITEMS, TEXT_CHOICE_LIST, + TEXT_LIST_AS_ITEMS_PARAMETER, TEXT_LIST_AS_ITEMS_VARIABLE_NAME ) __all__ = ( @@ -73,7 +73,7 @@ class ExtraContextMixin(object): return context -class ExternalObjectViewMixin(object): +class ExternalObjectMixin(object): external_object_class = None external_object_permission = None external_object_pk_url_kwarg = 'pk' @@ -214,7 +214,7 @@ class MultipleObjectMixin(object): model = None object_permission = None pk_list_key = 'id_list' - pk_list_separator = ',' + pk_list_separator = PK_LIST_SEPARATOR pk_url_kwarg = 'pk' queryset = None slug_url_kwarg = 'slug' @@ -334,7 +334,7 @@ class ObjectListPermissionFilterMixin(object): if not self.access_object_retrieve_method and self.object_permission: return AccessControlList.objects.filter_by_access( - obj=self.object_permission, queryset=queryset, + permission=self.object_permission, queryset=queryset, user=self.request.user ) else: @@ -359,38 +359,27 @@ class ObjectNameMixin(object): class ObjectPermissionCheckMixin(object): """ - If object_permission_raise_404 is True an HTTP 404 error will be raised - instead of the normal 403. + Filter the queryset of the view by the `object_permission` provided. + If no `object_permission` is provide the queryset will be returned + as is. """ object_permission = None - object_permission_raise_404 = False - def get_permission_object(self): - return self.get_object() + def get_queryset(self): + queryset = super(ObjectPermissionCheckMixin, self).get_queryset() - def dispatch(self, request, *args, **kwargs): if self.object_permission: - try: - AccessControlList.objects.check_access( - obj=self.get_permission_object(), - permissions=self.object_permission, - related=getattr(self, 'object_permission_related', None), - user=request.user - ) - except PermissionDenied: - if self.object_permission_raise_404: - raise Http404 - else: - raise + return AccessControlList.objects.restrict_queryset( + permission=self.object_permission, queryset=queryset, + user=self.request.user + ) - return super( - ObjectPermissionCheckMixin, self - ).dispatch(request, *args, **kwargs) + return queryset class RedirectionMixin(object): - post_action_redirect = None action_cancel_redirect = None + post_action_redirect = None def dispatch(self, request, *args, **kwargs): post_action_redirect = self.get_post_action_redirect() diff --git a/mayan/apps/common/views.py b/mayan/apps/common/views.py index 8bdc694e66..ca5e47fd2c 100644 --- a/mayan/apps/common/views.py +++ b/mayan/apps/common/views.py @@ -16,7 +16,7 @@ from django.views.generic import RedirectView, TemplateView from mayan.apps.acls.models import AccessControlList from mayan.apps.common.mixins import ( - ContentTypeViewMixin, ExternalObjectViewMixin + ContentTypeViewMixin, ExternalObjectMixin ) from .exceptions import NotLatestVersion, UnknownLatestVersion @@ -174,7 +174,7 @@ class ObjectErrorLogEntryListClearView(ConfirmView): ) -class ObjectErrorLogEntryListView(ContentTypeViewMixin, ExternalObjectViewMixin, SingleObjectListView): +class ObjectErrorLogEntryListView(ContentTypeViewMixin, ExternalObjectMixin, SingleObjectListView): #TODO: Update for MERC 6. Return 404. """ def dispatch(self, request, *args, **kwargs): From a15f0b7641905de345fe84f30769ebf3c2c96db6 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Wed, 9 Jan 2019 12:23:39 -0400 Subject: [PATCH 019/209] Improve FilteredSelectionForm Improve the configuration process of the FilteredSelectionForm form by adding Meta child class support. The child Meta class is defined in FilteredSelectionFormOptions. Signed-off-by: Roberto Rosario --- mayan/apps/common/forms.py | 114 +++++++++++++++++++++++-------------- 1 file changed, 70 insertions(+), 44 deletions(-) diff --git a/mayan/apps/common/forms.py b/mayan/apps/common/forms.py index fc8146ebac..4a331e01ab 100644 --- a/mayan/apps/common/forms.py +++ b/mayan/apps/common/forms.py @@ -131,48 +131,76 @@ class FileDisplayForm(forms.Form): self.fields['text'].initial = file_object.read() +class FilteredSelectionFormOptions(object): + # Dictionary list of option names and default values + option_definitions = ( + {'allow_multiple': False}, + {'field_name': None}, + {'help_text': None}, + {'label': None}, + {'model': None}, + {'permission': None}, + {'queryset': None}, + {'user': None}, + {'widget_class': None}, + {'widget_attributes': {'size': '10'}}, + ) + + def __init__(self, form, kwargs, options=None): + """ + Option definitions will be iterated. The option value will be + determined in the following order: as passed via keyword + arguments during form intialization, as form get_... method or + finally as static Meta options. This is to allow a form with + Meta options or method to be overrided at initialization + and increase the usability of a single class. + """ + for option_definition in self.option_definitions: + name = option_definition.keys()[0] + default_value = option_definition.values()[0] + + try: + # Check for a runtime value via kwargs + value = kwargs.pop(name) + except KeyError: + try: + # Check if there is a get_... method + value = getattr(self, 'get_{}'.format(name))() + except AttributeError: + try: + # Check the meta class options + value = getattr(options, name) + except AttributeError: + value = default_value + + setattr(self, name, value) + + class FilteredSelectionForm(forms.Form): """ Form to select the from a list of choice filtered by access. Can be configure to allow single or multiple selection. """ - _field_name = None - _label = None - _help_text = None - _permission = None - _queryset = None - _widget_class = None - _widget_attributes = None - def __init__(self, *args, **kwargs): - field_name = self._field_name or kwargs.pop('field_name', None) - label = self._label or kwargs.pop('label', None) - help_text = self._help_text or kwargs.pop('help_text', None) - permission = self._permission or kwargs.pop('permission', None) - queryset = self.get_queryset() or kwargs.pop('queryset', None) - - if queryset is None: - model = kwargs.pop('model', None) - if not model: - raise ImproperlyConfigured( - 'Must provide a queryset or a model.' - ) - - queryset = model.objects.all() - - user = self.get_user() or kwargs.pop('user', None) - widget_class = self._widget_class or kwargs.pop('widget_class', None) - widget_attributes = self._widget_attributes or kwargs.pop( - 'widget_attributes', {'size': '10'} + opts = FilteredSelectionFormOptions( + form=self, kwargs=kwargs, options=getattr(self, 'Meta', None) ) - if not widget_class: - if self._allow_multiple is not None: - allow_multiple = self._allow_multiple - else: - allow_multiple = self.kwargs.pop('allow_multiple', False) + if opts.queryset is None: + if not opts.model: + raise ImproperlyConfigured( + '{} requires a queryset or a model to be specified as ' + 'a meta option or passed during initialization.'.format( + self.__class__ + ) + ) - if allow_multiple: + queryset = opts.model.objects.all() + else: + queryset = opts.queryset + + if not opts.widget_class: + if opts.allow_multiple: extra_kwargs = {} field_class = forms.ModelMultipleChoiceField widget_class = forms.widgets.SelectMultiple @@ -180,26 +208,24 @@ class FilteredSelectionForm(forms.Form): extra_kwargs = {'empty_label': None} field_class = forms.ModelChoiceField widget_class = forms.widgets.Select + else: + widget_class = opts.widget_class super(FilteredSelectionForm, self).__init__(*args, **kwargs) - if permission: + if opts.permission: queryset = AccessControlList.objects.filter_by_access( - permission=permission, queryset=queryset, user=user + permission=opts.permission, queryset=queryset, + user=opts.user ) - self.fields[field_name] = field_class( - help_text=help_text, label=label, + self.fields[opts.field_name] = field_class( + help_text=opts.help_text, label=opts.label, queryset=queryset, required=True, - widget=widget_class(attrs=widget_attributes), **extra_kwargs + widget=widget_class(attrs=opts.widget_attributes), + **extra_kwargs ) - def get_queryset(self): - return self._queryset - - def get_user(self): - return None - class LicenseForm(FileDisplayForm): DIRECTORY = () From 3f97bc1a680020d3f09780f65a5494bd630ad84b Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sat, 19 Jan 2019 01:21:04 -0400 Subject: [PATCH 020/209] Update ContentTypeSerializer URL arguments Signed-off-by: Roberto Rosario --- mayan/apps/common/serializers.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/mayan/apps/common/serializers.py b/mayan/apps/common/serializers.py index c176ce5a45..0983e55d9a 100644 --- a/mayan/apps/common/serializers.py +++ b/mayan/apps/common/serializers.py @@ -5,9 +5,15 @@ from django.contrib.contenttypes.models import ContentType from rest_framework import serializers -class ContentTypeSerializer(serializers.ModelSerializer): +class ContentTypeSerializer(serializers.HyperlinkedModelSerializer): class Meta: - fields = ('app_label', 'id', 'model') + extra_kwargs = { + 'url': { + 'lookup_field': 'pk', 'lookup_url_kwarg': 'content_type_id', + 'view_name': 'rest_api:content_type-detail' + } + } + fields = ('app_label', 'id', 'model', 'url') model = ContentType @@ -15,3 +21,7 @@ class TemplateSerializer(serializers.Serializer): hex_hash = serializers.CharField(read_only=True) name = serializers.CharField(read_only=True) html = serializers.CharField(read_only=True) + url = serializers.HyperlinkedIdentityField( + lookup_field='name', lookup_url_kwarg='name', + view_name='rest_api:template-detail' + ) From 79742e82f950589cee6216d3d4e8be0317a7ec9d Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sat, 19 Jan 2019 01:21:26 -0400 Subject: [PATCH 021/209] Add missing logger instance Signed-off-by: Roberto Rosario --- mayan/apps/common/templatetags/common_tags.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/mayan/apps/common/templatetags/common_tags.py b/mayan/apps/common/templatetags/common_tags.py index 488032667f..0f365b0b65 100644 --- a/mayan/apps/common/templatetags/common_tags.py +++ b/mayan/apps/common/templatetags/common_tags.py @@ -1,5 +1,7 @@ from __future__ import unicode_literals +import logging + from django.template import Context, Library, VariableDoesNotExist, Variable from django.template.defaultfilters import truncatechars from django.template.loader import get_template @@ -14,6 +16,7 @@ from ..icons import icon_list_mode_items, icon_list_mode_list from ..literals import MESSAGE_SQLITE_WARNING from ..utils import check_for_sqlite, resolve_attribute +logger = logging.getLogger(__name__) register = Library() From 53f3261dae09d0ae2b20f740c7099764394fe123 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sat, 19 Jan 2019 03:56:56 -0400 Subject: [PATCH 022/209] Fix keyword argument name Signed-off-by: Roberto Rosario --- mayan/apps/converter/managers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mayan/apps/converter/managers.py b/mayan/apps/converter/managers.py index 86cccddb59..cd16f1ab51 100644 --- a/mayan/apps/converter/managers.py +++ b/mayan/apps/converter/managers.py @@ -25,7 +25,7 @@ class TransformationManager(models.Manager): """ Copy transformation from source to all targets """ - content_type = ContentType.objects.get_for_model(obj=source) + content_type = ContentType.objects.get_for_model(model=source) # Get transformations transformations = self.filter( From 6376445cc4cbb8bc9201677fdd63d3e77a804469 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sat, 19 Jan 2019 04:08:45 -0400 Subject: [PATCH 023/209] Update document comments app Add keyword arguments to the app links. Remove use of `raise_404`. Update URL parameters to use document_id and comment_id. Signed-off-by: Roberto Rosario --- mayan/apps/document_comments/api_views.py | 12 ++++++------ mayan/apps/document_comments/apps.py | 10 +++++----- mayan/apps/document_comments/events.py | 6 +++--- mayan/apps/document_comments/icons.py | 2 +- mayan/apps/document_comments/links.py | 7 ++++--- mayan/apps/document_comments/permissions.py | 6 +++--- mayan/apps/document_comments/serializers.py | 8 ++++---- mayan/apps/document_comments/tests/mixins.py | 6 +++--- .../apps/document_comments/tests/test_api.py | 4 ---- .../document_comments/tests/test_events.py | 4 ---- .../document_comments/tests/test_views.py | 8 ++------ mayan/apps/document_comments/urls.py | 18 +++++++++--------- mayan/apps/document_comments/views.py | 19 +++++++++---------- mayan/apps/mailer/views.py | 3 --- 14 files changed, 49 insertions(+), 64 deletions(-) diff --git a/mayan/apps/document_comments/api_views.py b/mayan/apps/document_comments/api_views.py index b7aaa195a2..bb8e14ccf9 100644 --- a/mayan/apps/document_comments/api_views.py +++ b/mayan/apps/document_comments/api_views.py @@ -2,7 +2,7 @@ from __future__ import absolute_import, unicode_literals from rest_framework import generics -from mayan.apps.common.mixins import ExternalObjectViewMixin +from mayan.apps.common.mixins import ExternalObjectMixin from mayan.apps.documents.models import Document from .permissions import ( @@ -12,13 +12,13 @@ from .permissions import ( from .serializers import CommentSerializer, WritableCommentSerializer -class APICommentListView(ExternalObjectViewMixin, generics.ListCreateAPIView): +class APICommentListView(ExternalObjectMixin, generics.ListCreateAPIView): """ get: Returns a list of all the document comments. post: Create a new document comment. """ - external_object_pk_url_kwarg = 'document_pk' external_object_class = Document + external_object_pk_url_kwarg = 'document_id' def get_document(self): return self.get_external_object() @@ -59,14 +59,14 @@ class APICommentListView(ExternalObjectViewMixin, generics.ListCreateAPIView): return context -class APICommentView(ExternalObjectViewMixin, generics.RetrieveDestroyAPIView): +class APICommentView(ExternalObjectMixin, generics.RetrieveDestroyAPIView): """ delete: Delete the selected document comment. get: Returns the details of the selected document comment. """ - external_object_pk_url_kwarg = 'document_pk' external_object_class = Document - lookup_url_kwarg = 'comment_pk' + external_object_pk_url_kwarg = 'document_id' + lookup_url_kwarg = 'comment_id' serializer_class = CommentSerializer def get_document(self): diff --git a/mayan/apps/document_comments/apps.py b/mayan/apps/document_comments/apps.py index df27c74dd1..a32ed30073 100644 --- a/mayan/apps/document_comments/apps.py +++ b/mayan/apps/document_comments/apps.py @@ -60,18 +60,18 @@ class DocumentCommentsApp(MayanAppConfig): SourceColumn(source=Comment, label=_('Date'), attribute='submit_date') SourceColumn( - source=Comment, label=_('User'), - func=lambda context: context['object'].user.get_full_name() if context['object'].user.get_full_name() else context['object'].user + func=lambda context: context['object'].user.get_full_name() if context['object'].user.get_full_name() else context['object'].user, + label=_('User'), source=Comment ) SourceColumn(source=Comment, label=_('Comment'), attribute='comment') document_page_search.add_model_field( + label=_('Comments'), field='document_version__document__comments__comment', - label=_('Comments') ) document_search.add_model_field( - field='comments__comment', - label=_('Comments') + label=_('Comments'), + field='comments__comment' ) menu_sidebar.bind_links( diff --git a/mayan/apps/document_comments/events.py b/mayan/apps/document_comments/events.py index ba34f362e9..13cafa936f 100644 --- a/mayan/apps/document_comments/events.py +++ b/mayan/apps/document_comments/events.py @@ -5,12 +5,12 @@ from django.utils.translation import ugettext_lazy as _ from mayan.apps.events import EventTypeNamespace namespace = EventTypeNamespace( - name='document_comments', label=_('Document comments') + label=_('Document comments'), name='document_comments' ) event_document_comment_created = namespace.add_event_type( - name='create', label=_('Document comment created') + label=_('Document comment created'), name='create' ) event_document_comment_deleted = namespace.add_event_type( - name='delete', label=_('Document comment deleted') + label=_('Document comment deleted'), name='delete' ) diff --git a/mayan/apps/document_comments/icons.py b/mayan/apps/document_comments/icons.py index 25f210b038..292b5a7614 100644 --- a/mayan/apps/document_comments/icons.py +++ b/mayan/apps/document_comments/icons.py @@ -2,9 +2,9 @@ from __future__ import absolute_import, unicode_literals from mayan.apps.appearance.classes import Icon -icon_comments_for_document = Icon(driver_name='fontawesome', symbol='comment') icon_comment_add = Icon( driver_name='fontawesome-dual', primary_symbol='comment', secondary_symbol='plus' ) icon_comment_delete = Icon(driver_name='fontawesome', symbol='times') +icon_comments_for_document = Icon(driver_name='fontawesome', symbol='comment') diff --git a/mayan/apps/document_comments/links.py b/mayan/apps/document_comments/links.py index c7cea63471..58238f88e1 100644 --- a/mayan/apps/document_comments/links.py +++ b/mayan/apps/document_comments/links.py @@ -13,17 +13,18 @@ from .permissions import ( ) link_comment_add = Link( - args='object.pk', icon_class=icon_comment_add, + icon_class=icon_comment_add, kwargs={'document_id': 'object.pk'}, permissions=(permission_comment_create,), text=_('Add comment'), view='comments:comment_add', ) link_comment_delete = Link( - args='object.pk', icon_class=icon_comment_delete, + icon_class=icon_comment_delete, kwargs={'comment_id': 'object.pk'}, permissions=(permission_comment_delete,), tags='dangerous', text=_('Delete'), view='comments:comment_delete', ) link_comments_for_document = Link( - args='resolved_object.pk', icon_class=icon_comments_for_document, + icon_class=icon_comments_for_document, + kwargs={'document_id': 'resolved_object.pk'}, permissions=(permission_comment_view,), text=_('Comments'), view='comments:comments_for_document', ) diff --git a/mayan/apps/document_comments/permissions.py b/mayan/apps/document_comments/permissions.py index 3edd1a7dff..a5fa60a7d2 100644 --- a/mayan/apps/document_comments/permissions.py +++ b/mayan/apps/document_comments/permissions.py @@ -7,11 +7,11 @@ from mayan.apps.permissions import PermissionNamespace namespace = PermissionNamespace(label=_('Comments'), name='comments') permission_comment_create = namespace.add_permission( - name='comment_create', label=_('Create new comments') + label=_('Create new comments'), name='comment_create' ) permission_comment_delete = namespace.add_permission( - name='comment_delete', label=_('Delete comments') + label=_('Delete comments'), name='comment_delete' ) permission_comment_view = namespace.add_permission( - name='comment_view', label=_('View comments') + label=_('View comments'), name='comment_view' ) diff --git a/mayan/apps/document_comments/serializers.py b/mayan/apps/document_comments/serializers.py index a231bb22c9..4ab9d418a8 100644 --- a/mayan/apps/document_comments/serializers.py +++ b/mayan/apps/document_comments/serializers.py @@ -25,14 +25,14 @@ class CommentSerializer(serializers.HyperlinkedModelSerializer): def get_document_comments_url(self, instance): return reverse( viewname='rest_api:comment-list', kwargs={ - 'document_pk': instance.document.pk, + 'document_id': instance.document.pk, }, request=self.context['request'], format=self.context['format'] ) def get_url(self, instance): return reverse( viewname='rest_api:comment-detail', kwargs={ - 'document_pk': instance.document.pk, 'comment_pk': instance.pk + 'document_id': instance.document.pk, 'comment_pk': instance.pk }, request=self.context['request'], format=self.context['format'] ) @@ -59,13 +59,13 @@ class WritableCommentSerializer(serializers.ModelSerializer): def get_document_comments_url(self, instance): return reverse( viewname='rest_api:comment-list', kwargs={ - 'document_pk': instance.document.pk + 'document_id': instance.document.pk }, request=self.context['request'], format=self.context['format'] ) def get_url(self, instance): return reverse( viewname='rest_api:comment-detail', kwargs={ - 'document_pk': instance.document.pk, 'comment_pk': instance.pk + 'document_id': instance.document.pk, 'comment_id': instance.pk }, request=self.context['request'], format=self.context['format'] ) diff --git a/mayan/apps/document_comments/tests/mixins.py b/mayan/apps/document_comments/tests/mixins.py index 906c32bee1..fac80d37e7 100644 --- a/mayan/apps/document_comments/tests/mixins.py +++ b/mayan/apps/document_comments/tests/mixins.py @@ -9,13 +9,13 @@ class CommentsTestMixin(object): def _create_comment(self, user=None): self.test_comment = self.document.comments.create( comment=TEST_COMMENT_TEXT, - user=user or self.user or self.admin_user + user=user or self._test_case_user or self.admin_user ) def _request_document_comment_add_view(self): response = self.post( viewname='comments:comment_add', - kwargs={'document_pk': self.document.pk}, + kwargs={'document_id': self.document.pk}, data={'comment': TEST_COMMENT_TEXT} ) self.test_comment = Comment.objects.filter( @@ -27,5 +27,5 @@ class CommentsTestMixin(object): def _request_document_comment_delete_view(self): return self.post( viewname='comments:comment_delete', - kwargs={'comment_pk': self.test_comment.pk}, + kwargs={'comment_id': self.test_comment.pk}, ) diff --git a/mayan/apps/document_comments/tests/test_api.py b/mayan/apps/document_comments/tests/test_api.py index dc898249a7..15ef08722a 100644 --- a/mayan/apps/document_comments/tests/test_api.py +++ b/mayan/apps/document_comments/tests/test_api.py @@ -16,10 +16,6 @@ from .mixins import CommentsTestMixin class CommentAPITestCase(CommentsTestMixin, DocumentTestMixin, BaseAPITestCase): - def setUp(self): - super(CommentAPITestCase, self).setUp() - self.login_user() - def _request_api_comment_create_view(self): return self.post( viewname='rest_api:comment-list', diff --git a/mayan/apps/document_comments/tests/test_events.py b/mayan/apps/document_comments/tests/test_events.py index 8618baf8aa..05c9225655 100644 --- a/mayan/apps/document_comments/tests/test_events.py +++ b/mayan/apps/document_comments/tests/test_events.py @@ -13,10 +13,6 @@ from .mixins import CommentsTestMixin class CommentEventsTestCase(CommentsTestMixin, GenericDocumentViewTestCase): - def setUp(self): - super(CommentEventsTestCase, self).setUp() - self.login_user() - def test_comment_created_event_no_permissions(self): Action.objects.all().delete() diff --git a/mayan/apps/document_comments/tests/test_views.py b/mayan/apps/document_comments/tests/test_views.py index 59b0882ca1..de242c57d8 100644 --- a/mayan/apps/document_comments/tests/test_views.py +++ b/mayan/apps/document_comments/tests/test_views.py @@ -12,10 +12,6 @@ from .mixins import CommentsTestMixin class CommentsViewsTestCase(CommentsTestMixin, GenericDocumentViewTestCase): - def setUp(self): - super(CommentsViewsTestCase, self).setUp() - self.login_user() - def test_document_comment_add_view_no_permission(self): response = self._request_document_comment_add_view() self.assertEqual(response.status_code, 404) @@ -31,7 +27,7 @@ class CommentsViewsTestCase(CommentsTestMixin, GenericDocumentViewTestCase): def _create_test_comment(self): self.test_comment = self.document.comments.create( - user=self.user, comment=TEST_COMMENT_TEXT + user=self._test_case_user, comment=TEST_COMMENT_TEXT ) def test_document_comment_delete_view_no_permission(self): @@ -54,7 +50,7 @@ class CommentsViewsTestCase(CommentsTestMixin, GenericDocumentViewTestCase): def _request_document_comment_list_view(self): return self.get( viewname='comments:comments_for_document', - kwargs={'document_pk': self.document.pk} + kwargs={'document_id': self.document.pk} ) def test_document_comment_list_view_no_permissions(self): diff --git a/mayan/apps/document_comments/urls.py b/mayan/apps/document_comments/urls.py index 7ac96fb8f2..2e06d4eed3 100644 --- a/mayan/apps/document_comments/urls.py +++ b/mayan/apps/document_comments/urls.py @@ -10,26 +10,26 @@ from .views import ( urlpatterns = [ url( - regex=r'^comments/(?P\d+)/delete/$', name='comment_delete', + regex=r'^comments/(?P\d+)/delete/$', name='comment_delete', view=DocumentCommentDeleteView.as_view() ), url( - regex=r'^documents/(?P\d+)/comments/add/$', - name='comment_add', view=DocumentCommentCreateView.as_view() - ), - url( - regex=r'^documents/(?P\d+)/comments/$', + regex=r'^documents/(?P\d+)/comments/$', name='comments_for_document', view=DocumentCommentListView.as_view() ), + url( + regex=r'^documents/(?P\d+)/comments/add/$', + name='comment_add', view=DocumentCommentCreateView.as_view() + ) ] api_urls = [ url( - regex=r'^documents/(?P[0-9]+)/comments/$', + regex=r'^documents/(?P\d+)/comments/$', name='comment-list', view=APICommentListView.as_view() ), url( - regex=r'^documents/(?P[0-9]+)/comments/(?P[0-9]+)/$', + regex=r'^documents/(?P\d+)/comments/(?P\d+)/$', name='comment-detail', view=APICommentView.as_view() - ), + ) ] diff --git a/mayan/apps/document_comments/views.py b/mayan/apps/document_comments/views.py index ae1f2d0c45..f009630156 100644 --- a/mayan/apps/document_comments/views.py +++ b/mayan/apps/document_comments/views.py @@ -7,7 +7,7 @@ from django.utils.translation import ugettext_lazy as _ from mayan.apps.common.generics import ( SingleObjectCreateView, SingleObjectDeleteView, SingleObjectListView ) -from mayan.apps.common.mixins import ExternalObjectViewMixin +from mayan.apps.common.mixins import ExternalObjectMixin from mayan.apps.documents.models import Document from .icons import icon_comments_for_document @@ -19,11 +19,11 @@ from .permissions import ( ) -class DocumentCommentCreateView(ExternalObjectViewMixin, SingleObjectCreateView): - fields = ('comment',) - external_object_pk_url_kwarg = 'document_pk' +class DocumentCommentCreateView(ExternalObjectMixin, SingleObjectCreateView): external_object_class = Document external_object_permission = permission_comment_create + external_object_pk_url_kwarg = 'document_id' + fields = ('comment',) model = Comment def get_document(self): @@ -43,7 +43,7 @@ class DocumentCommentCreateView(ExternalObjectViewMixin, SingleObjectCreateView) def get_post_action_redirect(self): return reverse( viewname='comments:comments_for_document', kwargs={ - 'document_pk': self.kwargs['document_pk'] + 'document_id': self.kwargs['document_id'] } ) @@ -55,9 +55,8 @@ class DocumentCommentCreateView(ExternalObjectViewMixin, SingleObjectCreateView) class DocumentCommentDeleteView(SingleObjectDeleteView): model = Comment - pk_url_kwarg = 'comment_pk' + pk_url_kwarg = 'comment_id' object_permission = permission_comment_delete - object_permission_raise_404 = True def get_delete_extra_data(self): return {'_user': self.request.user} @@ -71,15 +70,15 @@ class DocumentCommentDeleteView(SingleObjectDeleteView): def get_post_action_redirect(self): return reverse( viewname='comments:comments_for_document', kwargs={ - 'document_pk': self.get_object().document.pk + 'document_id': self.get_object().document.pk } ) -class DocumentCommentListView(ExternalObjectViewMixin, SingleObjectListView): - external_object_pk_url_kwarg = 'document_pk' +class DocumentCommentListView(ExternalObjectMixin, SingleObjectListView): external_object_class = Document external_object_permission = permission_comment_view + external_object_pk_url_kwarg = 'document_id' def get_document(self): return self.get_external_object() diff --git a/mayan/apps/mailer/views.py b/mayan/apps/mailer/views.py index fcb47ff8b3..b57da3eef4 100644 --- a/mayan/apps/mailer/views.py +++ b/mayan/apps/mailer/views.py @@ -47,7 +47,6 @@ class MailDocumentView(MultipleObjectFormActionView): form_class = DocumentMailForm model = Document object_permission = permission_mailing_send_document - object_permission_raise_404 = True pk_url_kwarg = 'document_pk' success_message = _('%(count)d document queued for email delivery') success_message_plural = _( @@ -173,7 +172,6 @@ class UserMailingCreateView(SingleObjectDynamicFormCreateView): class UserMailingDeleteView(SingleObjectDeleteView): model = UserMailer object_permission = permission_user_mailer_delete - object_permission_raise_404 = True pk_url_kwarg = 'mailer_pk' post_action_redirect = reverse_lazy(viewname='mailer:user_mailer_list') @@ -187,7 +185,6 @@ class UserMailingEditView(SingleObjectDynamicFormEditView): form_class = UserMailerDynamicForm model = UserMailer object_permission = permission_user_mailer_edit - object_permission_raise_404 = True pk_url_kwarg = 'mailer_pk' def get_extra_context(self): From 622972fd85dd3af212e2750d4c19f7546bcb939b Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sat, 19 Jan 2019 04:21:28 -0400 Subject: [PATCH 024/209] Update file metadata app Add keyword arguments to links and test views. Update URL parameters to use the _id form. Signed-off-by: Roberto Rosario --- mayan/apps/file_metadata/drivers/exiftool.py | 2 +- mayan/apps/file_metadata/events.py | 2 +- mayan/apps/file_metadata/links.py | 11 +++++--- mayan/apps/file_metadata/permissions.py | 10 +++---- mayan/apps/file_metadata/settings.py | 2 +- mayan/apps/file_metadata/tests/test_views.py | 13 ++++----- mayan/apps/file_metadata/urls.py | 29 ++++++++++---------- mayan/apps/file_metadata/views.py | 16 +++++++---- 8 files changed, 47 insertions(+), 38 deletions(-) diff --git a/mayan/apps/file_metadata/drivers/exiftool.py b/mayan/apps/file_metadata/drivers/exiftool.py index 6d7de4465d..8a75b7c0fe 100644 --- a/mayan/apps/file_metadata/drivers/exiftool.py +++ b/mayan/apps/file_metadata/drivers/exiftool.py @@ -35,7 +35,7 @@ class EXIFToolDriver(FileMetadataDriver): try: document_version.save_to_file(filepath=temp_filename) result = self.command_exiftool(temp_filename) - return json.loads(result.stdout)[0] + return json.loads(s=result.stdout)[0] finally: fs_cleanup(filename=temp_filename) diff --git a/mayan/apps/file_metadata/events.py b/mayan/apps/file_metadata/events.py index 6e4a4d5fcb..57daccffe2 100644 --- a/mayan/apps/file_metadata/events.py +++ b/mayan/apps/file_metadata/events.py @@ -5,7 +5,7 @@ from django.utils.translation import ugettext_lazy as _ from mayan.apps.events import EventTypeNamespace namespace = EventTypeNamespace( - name='file_metadata', label=_('File metadata') + label=_('File metadata'), name='file_metadata' ) event_file_metadata_document_version_submit = namespace.add_event_type( diff --git a/mayan/apps/file_metadata/links.py b/mayan/apps/file_metadata/links.py index 59eda9b631..ef884470c7 100644 --- a/mayan/apps/file_metadata/links.py +++ b/mayan/apps/file_metadata/links.py @@ -13,17 +13,20 @@ from .permissions import ( ) link_document_driver_list = Link( - args='resolved_object.id', icon_class=icon_file_metadata, + icon_class=icon_file_metadata, + kwargs={'document_id': 'resolved_object.id'}, permissions=(permission_file_metadata_view,), text=_('File metadata'), view='file_metadata:document_driver_list', ) link_document_file_metadata_list = Link( - args=('resolved_object.id',), icon_class=icon_file_metadata, + icon_class=icon_file_metadata, + kwargs={'document_version_driver_id': 'resolved_object.id'}, permissions=(permission_file_metadata_view,), text=_('Attributes'), view='file_metadata:document_version_driver_file_metadata_list', ) link_document_submit = Link( - args='resolved_object.id', icon_class=icon_document_submit, + icon_class=icon_document_submit, + kwargs={'document_id': 'resolved_object.id'}, permissions=(permission_file_metadata_submit,), text=_('Submit for file metadata'), view='file_metadata:document_submit' ) @@ -32,8 +35,8 @@ link_document_multiple_submit = Link( view='file_metadata:document_multiple_submit' ) link_document_type_file_metadata_settings = Link( - args='resolved_object.id', icon_class=icon_file_metadata, + kwargs={'document_type_id': 'resolved_object.id'}, permissions=(permission_document_type_file_metadata_setup,), text=_('Setup file metadata'), view='file_metadata:document_type_settings', diff --git a/mayan/apps/file_metadata/permissions.py b/mayan/apps/file_metadata/permissions.py index c0d156f888..4251531074 100644 --- a/mayan/apps/file_metadata/permissions.py +++ b/mayan/apps/file_metadata/permissions.py @@ -7,14 +7,14 @@ from mayan.apps.permissions import PermissionNamespace namespace = PermissionNamespace(label=_('File metadata'), name='file_metadata') permission_document_type_file_metadata_setup = namespace.add_permission( - name='file_metadata_document_type_setup', - label=_('Change document type file metadata settings') + label=_('Change document type file metadata settings'), + name='file_metadata_document_type_setup' ) permission_file_metadata_submit = namespace.add_permission( - name='file_metadata_submit', label=_( + label=_( 'Submit document for file metadata processing' - ) + ), name='file_metadata_submit' ) permission_file_metadata_view = namespace.add_permission( - name='file_metadata_view', label=_('View file metadata') + label=_('View file metadata'), name='file_metadata_view' ) diff --git a/mayan/apps/file_metadata/settings.py b/mayan/apps/file_metadata/settings.py index 692ab7a12f..ceb98a3928 100644 --- a/mayan/apps/file_metadata/settings.py +++ b/mayan/apps/file_metadata/settings.py @@ -6,7 +6,7 @@ from mayan.apps.smart_settings import Namespace from .literals import DEFAULT_EXIF_PATH -namespace = Namespace(name='file_metadata', label=_('File metadata')) +namespace = Namespace(label=_('File metadata'), name='file_metadata') setting_drivers_arguments = namespace.add_setting( global_name='FILE_METADATA_DRIVERS_ARGUMENTS', diff --git a/mayan/apps/file_metadata/tests/test_views.py b/mayan/apps/file_metadata/tests/test_views.py index 08a5bb910e..5971bf657c 100644 --- a/mayan/apps/file_metadata/tests/test_views.py +++ b/mayan/apps/file_metadata/tests/test_views.py @@ -20,8 +20,8 @@ class FileMetadataViewsTestCase(GenericDocumentViewTestCase): def _request_document_version_driver_list_view(self): return self.get( - args=(self.document.pk,), viewname='file_metadata:document_driver_list', + kwargs={'document_id': self.document.pk} ) def test_document_version_driver_list_view_no_permission(self): @@ -39,10 +39,8 @@ class FileMetadataViewsTestCase(GenericDocumentViewTestCase): def _request_document_version_file_metadata_list_view(self): return self.get( - args=( - self.document.latest_version.file_metadata_drivers.first().pk, - ), viewname='file_metadata:document_version_driver_file_metadata_list', + kwargs={'document_version_driver_id': self.document.latest_version.file_metadata_drivers.first().pk} ) def test_document_version_file_metadata_list_view_no_permission(self): @@ -62,7 +60,8 @@ class FileMetadataViewsTestCase(GenericDocumentViewTestCase): def _request_document_submit_view(self): return self.post( - viewname='file_metadata:document_submit', args=(self.document.pk,) + viewname='file_metadata:document_submit', + kwargs={'document_id': self.document.pk} ) def test_document_submit_view_no_permission(self): @@ -86,7 +85,7 @@ class FileMetadataViewsTestCase(GenericDocumentViewTestCase): def _request_multiple_document_submit_view(self): return self.post( - viewname='file_metadata:document_submit_multiple', + viewname='file_metadata:document_multiple_submit', data={ 'id_list': self.document.pk, } @@ -120,7 +119,7 @@ class DocumentTypeViewsTestCase(GenericDocumentViewTestCase): def _request_document_type_settings_view(self): return self.get( viewname='file_metadata:document_type_settings', - args=(self.document.document_type.pk,) + kwargs={'document_type_id': self.document.document_type.pk} ) def test_document_type_settings_view_no_permission(self): diff --git a/mayan/apps/file_metadata/urls.py b/mayan/apps/file_metadata/urls.py index c322d4f1ed..5236811808 100644 --- a/mayan/apps/file_metadata/urls.py +++ b/mayan/apps/file_metadata/urls.py @@ -9,29 +9,30 @@ from .views import ( urlpatterns = [ url( - r'^documents/(?P\d+)/drivers/$', DocumentDriverListView.as_view(), - name='document_driver_list' + regex=r'^documents/(?P\d+)/drivers/$', + name='document_driver_list', view=DocumentDriverListView.as_view() + ), url( - r'^documents/(?P\d+)/submit/$', DocumentSubmitView.as_view(), - name='document_submit' + regex=r'^documents/(?P\d+)/submit/$', + name='document_submit', view=DocumentSubmitView.as_view() ), url( - r'^documents/multiple/submit/$', DocumentSubmitView.as_view(), - name='document_multiple_submit' + regex=r'^documents/multiple/submit/$', name='document_multiple_submit', + view=DocumentSubmitView.as_view() ), url( - r'^document_types/(?P\d+)/ocr/settings/$', - DocumentTypeSettingsEditView.as_view(), - name='document_type_settings' + regex=r'^document_types/(?P\d+)/ocr/settings/$', + name='document_type_settings', + view=DocumentTypeSettingsEditView.as_view() ), url( - r'^document_types/submit/$', DocumentTypeSubmitView.as_view(), - name='document_type_submit' + regex=r'^document_types/submit/$', name='document_type_submit', + view=DocumentTypeSubmitView.as_view() ), url( - r'^document_version_driver/(?P\d+)/attributes/$', - DocumentVersionDriverEntryFileMetadataListView.as_view(), - name='document_version_driver_file_metadata_list' + regex=r'^document_version_driver/(?P\d+)/attributes/$', + name='document_version_driver_file_metadata_list', + view=DocumentVersionDriverEntryFileMetadataListView.as_view() ), ] diff --git a/mayan/apps/file_metadata/views.py b/mayan/apps/file_metadata/views.py index c810062ae7..1efe7adaa4 100644 --- a/mayan/apps/file_metadata/views.py +++ b/mayan/apps/file_metadata/views.py @@ -45,7 +45,9 @@ class DocumentDriverListView(SingleObjectListView): } def get_object(self): - document = get_object_or_404(klass=Document, pk=self.kwargs['pk']) + document = get_object_or_404( + klass=Document, pk=self.kwargs['document_id'] + ) AccessControlList.objects.check_access( permissions=permission_file_metadata_view, user=self.request.user, obj=document @@ -72,7 +74,8 @@ class DocumentVersionDriverEntryFileMetadataListView(SingleObjectListView): def get_object(self): document_version_driver_entry = get_object_or_404( - klass=DocumentVersionDriverEntry, pk=self.kwargs['pk'] + klass=DocumentVersionDriverEntry, + pk=self.kwargs['document_version_driver_id'] ) AccessControlList.objects.check_access( obj=document_version_driver_entry.document_version, @@ -88,6 +91,7 @@ class DocumentVersionDriverEntryFileMetadataListView(SingleObjectListView): class DocumentSubmitView(MultipleObjectConfirmActionView): model = Document object_permission = permission_file_metadata_submit + pk_url_kwarg = 'document_id' success_message = '%(count)d document submitted to the file metadata queue.' success_message_plural = '%(count)d documents submitted to the file metadata queue.' @@ -111,10 +115,12 @@ class DocumentSubmitView(MultipleObjectConfirmActionView): class DocumentTypeSettingsEditView(SingleObjectEditView): fields = ('auto_process',) object_permission = permission_document_type_file_metadata_setup - post_action_redirect = reverse_lazy('documents:document_type_list') + post_action_redirect = reverse_lazy(viewname='documents:document_type_list') def get_document_type(self): - return get_object_or_404(klass=DocumentType, pk=self.kwargs['pk']) + return get_object_or_404( + klass=DocumentType, pk=self.kwargs['document_type_id'] + ) def get_extra_context(self): return { @@ -135,7 +141,7 @@ class DocumentTypeSubmitView(FormView): ) } form_class = DocumentTypeFilteredSelectForm - post_action_redirect = reverse_lazy('common:tools_list') + post_action_redirect = reverse_lazy(viewname='common:tools_list') def get_form_extra_kwargs(self): return { From 14fd5f02a82213fb8d23828ab8dd5d8db8dbca84 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sat, 19 Jan 2019 04:22:27 -0400 Subject: [PATCH 025/209] Remove unused code from events app Signed-off-by: Roberto Rosario --- mayan/apps/events/classes.py | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/mayan/apps/events/classes.py b/mayan/apps/events/classes.py index ac4a822fc1..065dc325ef 100644 --- a/mayan/apps/events/classes.py +++ b/mayan/apps/events/classes.py @@ -190,8 +190,6 @@ class ModelEventType(object): """ Class to allow matching a model to a specific set of events. """ - _inheritances = {} - _proxies = {} _registry = {} @classmethod @@ -211,11 +209,6 @@ class ModelEventType(object): if class_events: events.extend(class_events) - proxy = cls._proxies.get(type(instance)) - - if proxy: - events.extend(cls._registry.get(proxy)) - pks = [ event.id for event in set(events) ] @@ -224,20 +217,8 @@ class ModelEventType(object): event_type_list=StoredEventType.objects.filter(name__in=pks) ) - @classmethod - def get_inheritance(cls, model): - return cls._inheritances[model] - @classmethod def register(cls, model, event_types): cls._registry.setdefault(model, []) for event_type in event_types: cls._registry[model].append(event_type) - - @classmethod - def register_inheritance(cls, model, related): - cls._inheritances[model] = related - - @classmethod - def register_proxy(cls, source, model): - cls._proxies[model] = source From fc29309f68e7b2f0297a95fbee10f5ce4ba84018 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sun, 20 Jan 2019 18:08:47 -0400 Subject: [PATCH 026/209] Update Django GPG app Add keyword arguments to all calls. Rename URL parameters to be explicit ("key_id"). Add key delete view test. Update tests to use a mixin for repeated key creation code. Grant permissions and access the proper way using self.grant_permission and self.grant_access. Signed-off-by: Roberto Rosario --- mayan/apps/django_gpg/api_views.py | 2 + mayan/apps/django_gpg/classes.py | 36 +++++----- mayan/apps/django_gpg/forms.py | 4 +- mayan/apps/django_gpg/links.py | 16 +++-- mayan/apps/django_gpg/literals.py | 1 + mayan/apps/django_gpg/models.py | 4 +- mayan/apps/django_gpg/permissions.py | 14 ++-- mayan/apps/django_gpg/serializers.py | 1 + mayan/apps/django_gpg/settings.py | 6 +- mayan/apps/django_gpg/tests/literals.py | 12 ++++ mayan/apps/django_gpg/tests/mixins.py | 9 +++ mayan/apps/django_gpg/tests/test_api.py | 6 +- mayan/apps/django_gpg/tests/test_models.py | 49 +++++-------- mayan/apps/django_gpg/tests/test_views.py | 81 +++++++++++++--------- mayan/apps/django_gpg/urls.py | 37 +++++----- mayan/apps/django_gpg/views.py | 13 ++-- 16 files changed, 168 insertions(+), 123 deletions(-) create mode 100644 mayan/apps/django_gpg/tests/mixins.py diff --git a/mayan/apps/django_gpg/api_views.py b/mayan/apps/django_gpg/api_views.py index 47014873da..dc7fcfa4f7 100644 --- a/mayan/apps/django_gpg/api_views.py +++ b/mayan/apps/django_gpg/api_views.py @@ -31,6 +31,8 @@ class APIKeyView(generics.RetrieveDestroyAPIView): get: Return the details of the selected key. """ filter_backends = (MayanObjectPermissionsFilter,) + lookup_field = 'pk' + lookup_url_kwarg = 'key_id' mayan_object_permissions = { 'DELETE': (permission_key_delete,), 'GET': (permission_key_view,), diff --git a/mayan/apps/django_gpg/classes.py b/mayan/apps/django_gpg/classes.py index 510cc2892d..7243b63744 100644 --- a/mayan/apps/django_gpg/classes.py +++ b/mayan/apps/django_gpg/classes.py @@ -17,6 +17,24 @@ class GPGBackend(object): self.kwargs = kwargs +class KeyStub(object): + def __init__(self, raw): + self.fingerprint = raw['keyid'] + self.key_type = raw['type'] + self.date = date.fromtimestamp(int(raw['date'])) + if raw['expires']: + self.expires = date.fromtimestamp(int(raw['expires'])) + else: + self.expires = None + self.length = raw['length'] + self.user_id = raw['uids'] + + @property + def key_id(self): + return self.fingerprint[-8:] + key_id.fget.short_description = _('Key ID') + + class PythonGNUPGBackend(GPGBackend): @staticmethod def _import_key(gpg, **kwargs): @@ -136,24 +154,6 @@ class PythonGNUPGBackend(GPGBackend): ) -class KeyStub(object): - def __init__(self, raw): - self.fingerprint = raw['keyid'] - self.key_type = raw['type'] - self.date = date.fromtimestamp(int(raw['date'])) - if raw['expires']: - self.expires = date.fromtimestamp(int(raw['expires'])) - else: - self.expires = None - self.length = raw['length'] - self.user_id = raw['uids'] - - @property - def key_id(self): - return self.fingerprint[-8:] - key_id.fget.short_description = _('Key ID') - - class SignatureVerification(object): def __init__(self, raw): self.user_id = raw['username'] diff --git a/mayan/apps/django_gpg/forms.py b/mayan/apps/django_gpg/forms.py index 1d72806e93..8d7795b2d9 100644 --- a/mayan/apps/django_gpg/forms.py +++ b/mayan/apps/django_gpg/forms.py @@ -44,6 +44,6 @@ class KeyDetailForm(DetailForm): class KeySearchForm(forms.Form): term = forms.CharField( - label=_('Term'), - help_text=_('Name, e-mail, key ID or key fingerprint to look for.') + help_text=_('Name, e-mail, key ID or key fingerprint to look for.'), + label=_('Term') ) diff --git a/mayan/apps/django_gpg/links.py b/mayan/apps/django_gpg/links.py index 32eb85ff78..08264795d3 100644 --- a/mayan/apps/django_gpg/links.py +++ b/mayan/apps/django_gpg/links.py @@ -11,16 +11,18 @@ from .permissions import ( ) link_key_delete = Link( - args=('resolved_object.pk',), permissions=(permission_key_delete,), - tags='dangerous', text=_('Delete'), view='django_gpg:key_delete', + kwargs={'key_id': 'resolved_object.pk'}, + permissions=(permission_key_delete,), tags='dangerous', text=_('Delete'), + view='django_gpg:key_delete' ) link_key_detail = Link( - args=('resolved_object.pk',), permissions=(permission_key_view,), - text=_('Details'), view='django_gpg:key_detail', + kwargs={'key_id': 'resolved_object.pk'}, permissions=(permission_key_view,), + text=_('Details'), view='django_gpg:key_detail' ) link_key_download = Link( - args=('resolved_object.pk',), permissions=(permission_key_download,), - text=_('Download'), view='django_gpg:key_download', + kwargs={'key_id': 'resolved_object.pk'}, + permissions=(permission_key_download,), text=_('Download'), + view='django_gpg:key_download' ) link_key_query = Link( icon_class=icon_keyserver_search, @@ -28,7 +30,7 @@ link_key_query = Link( view='django_gpg:key_query' ) link_key_receive = Link( - args='object.key_id', keep_query=True, + keep_query=True, kwargs={'key_id': 'object.key_id'}, permissions=(permission_key_receive,), text=_('Import'), view='django_gpg:key_receive', ) diff --git a/mayan/apps/django_gpg/literals.py b/mayan/apps/django_gpg/literals.py index ea85b945ab..ed97da11c6 100644 --- a/mayan/apps/django_gpg/literals.py +++ b/mayan/apps/django_gpg/literals.py @@ -9,6 +9,7 @@ if platform.system() == 'OpenBSD': else: DEFAULT_GPG_PATH = '/usr/bin/gpg1' +DEFAULT_KEYSERVER = 'pool.sks-keyservers.net' DEFAULT_SETTING_GPG_BACKEND = 'mayan.apps.django_gpg.classes.PythonGNUPGBackend' ERROR_MSG_BAD_PASSPHRASE = 'BAD_PASSPHRASE' diff --git a/mayan/apps/django_gpg/models.py b/mayan/apps/django_gpg/models.py index 6ab1aab7f2..c59748b63d 100644 --- a/mayan/apps/django_gpg/models.py +++ b/mayan/apps/django_gpg/models.py @@ -77,7 +77,9 @@ class Key(models.Model): raise ValidationError(_('Key already exists.')) def get_absolute_url(self): - return reverse('django_gpg:key_detail', args=(self.pk,)) + return reverse( + viewname='django_gpg:key_detail', kwargs={'key_pk': self.pk} + ) @property def key_id(self): diff --git a/mayan/apps/django_gpg/permissions.py b/mayan/apps/django_gpg/permissions.py index ee129ab114..c2a2ab2cb9 100644 --- a/mayan/apps/django_gpg/permissions.py +++ b/mayan/apps/django_gpg/permissions.py @@ -7,23 +7,23 @@ from mayan.apps.permissions import PermissionNamespace namespace = PermissionNamespace(label=_('Key management'), name='django_gpg') permission_key_delete = namespace.add_permission( - name='key_delete', label=_('Delete keys') + label=_('Delete keys'), name='key_delete' ) permission_key_download = namespace.add_permission( - name='key_download', label=_('Download keys') + label=_('Download keys'), name='key_download' ) permission_key_receive = namespace.add_permission( - name='key_receive', label=_('Import keys from keyservers') + label=_('Import keys from keyservers'), name='key_receive' ) permission_key_sign = namespace.add_permission( - name='key_sign', label=_('Use keys to sign content') + label=_('Use keys to sign content'), name='key_sign' ) permission_key_upload = namespace.add_permission( - name='key_upload', label=_('Upload keys') + label=_('Upload keys'), name='key_upload' ) permission_key_view = namespace.add_permission( - name='key_view', label=_('View keys') + label=_('View keys'), name='key_view' ) permission_keyserver_query = namespace.add_permission( - name='keyserver_query', label=_('Query keyservers') + label=_('Query keyservers'), name='keyserver_query' ) diff --git a/mayan/apps/django_gpg/serializers.py b/mayan/apps/django_gpg/serializers.py index df3c965690..6191ea6f24 100644 --- a/mayan/apps/django_gpg/serializers.py +++ b/mayan/apps/django_gpg/serializers.py @@ -8,6 +8,7 @@ from .models import Key class KeySerializer(serializers.ModelSerializer): class Meta: extra_kwargs = { + 'lookup_url_kwarg': 'key_id', 'url': {'view_name': 'rest_api:key-detail'}, } fields = ( diff --git a/mayan/apps/django_gpg/settings.py b/mayan/apps/django_gpg/settings.py index 87171619db..0559a1504f 100644 --- a/mayan/apps/django_gpg/settings.py +++ b/mayan/apps/django_gpg/settings.py @@ -4,7 +4,9 @@ from django.utils.translation import ugettext_lazy as _ from mayan.apps.smart_settings import Namespace -from .literals import DEFAULT_GPG_PATH, DEFAULT_SETTING_GPG_BACKEND +from .literals import ( + DEFAULT_GPG_PATH, DEFAULT_KEYSERVER, DEFAULT_SETTING_GPG_BACKEND +) namespace = Namespace(name='django_gpg', label=_('Signatures')) @@ -23,6 +25,6 @@ setting_gpg_backend_arguments = namespace.add_setting( ) ) setting_keyserver = namespace.add_setting( - global_name='SIGNATURES_KEYSERVER', default='pool.sks-keyservers.net', + global_name='SIGNATURES_KEYSERVER', default=DEFAULT_KEYSERVER, help_text=_('Keyserver used to query for keys.') ) diff --git a/mayan/apps/django_gpg/tests/literals.py b/mayan/apps/django_gpg/tests/literals.py index 3bcbe01ab3..c7dca32a6a 100644 --- a/mayan/apps/django_gpg/tests/literals.py +++ b/mayan/apps/django_gpg/tests/literals.py @@ -4,6 +4,18 @@ import os from django.conf import settings +MOCK_SEARCH_KEYS_RESPONSE = [ + { + 'algo': u'1', + 'date': u'1311475606', + 'expires': u'1643601600', + 'keyid': u'607138F1AECC5A5CA31CB7715F3F7F75D210724D', + 'length': u'2048', + 'type': u'pub', + 'uids': [u'Roberto Rosario '] + } +] + TEST_DETACHED_SIGNATURE = os.path.join( settings.BASE_DIR, 'apps', 'django_gpg', 'tests', 'contrib', 'test_files', 'test_file.txt.asc' diff --git a/mayan/apps/django_gpg/tests/mixins.py b/mayan/apps/django_gpg/tests/mixins.py new file mode 100644 index 0000000000..01294af113 --- /dev/null +++ b/mayan/apps/django_gpg/tests/mixins.py @@ -0,0 +1,9 @@ +from ..models import Key + +from .literals import TEST_KEY_DATA + + +class KeyTestMixin(object): + def _create_test_key(self): + # Creating a Key instance is analogous to importing a key + self.test_key = Key.objects.create(key_data=TEST_KEY_DATA) diff --git a/mayan/apps/django_gpg/tests/test_api.py b/mayan/apps/django_gpg/tests/test_api.py index f46c7034eb..14abdd85f3 100644 --- a/mayan/apps/django_gpg/tests/test_api.py +++ b/mayan/apps/django_gpg/tests/test_api.py @@ -32,7 +32,7 @@ class KeyAPITestCase(BaseAPITestCase): def test_key_create_view_no_permission(self): response = self._request_key_create_view() self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - self.assertEqual(Key.objects.all().count(), 0) + self.assertEqual(Key.objects.count(), 0) def test_key_create_view_with_permission(self): self.grant_permission(permission=permission_key_upload) @@ -50,7 +50,7 @@ class KeyAPITestCase(BaseAPITestCase): def _request_key_delete_view(self): return self.delete( - viewname='rest_api:key-detail', args=(self.key.pk,) + viewname='rest_api:key-detail', kwargs={'key_id': self.key.pk} ) def test_key_delete_view_no_access(self): @@ -72,7 +72,7 @@ class KeyAPITestCase(BaseAPITestCase): def _request_key_detail_view(self): return self.get( - viewname='rest_api:key-detail', args=(self.key.pk,) + viewname='rest_api:key-detail', kwargs={'key_id': self.key.pk} ) def test_key_detail_view_no_access(self): diff --git a/mayan/apps/django_gpg/tests/test_models.py b/mayan/apps/django_gpg/tests/test_models.py index 763166f905..9273cfd065 100644 --- a/mayan/apps/django_gpg/tests/test_models.py +++ b/mayan/apps/django_gpg/tests/test_models.py @@ -17,22 +17,12 @@ from ..exceptions import ( from ..models import Key from .literals import ( - TEST_DETACHED_SIGNATURE, TEST_FILE, TEST_KEY_DATA, TEST_KEY_FINGERPRINT, - TEST_KEY_PASSPHRASE, TEST_RECEIVE_KEY, TEST_SEARCH_FINGERPRINT, - TEST_SEARCH_UID, TEST_SIGNED_FILE, TEST_SIGNED_FILE_CONTENT + MOCK_SEARCH_KEYS_RESPONSE, TEST_DETACHED_SIGNATURE, TEST_FILE, + TEST_KEY_FINGERPRINT, TEST_KEY_PASSPHRASE, TEST_RECEIVE_KEY, + TEST_SEARCH_FINGERPRINT, TEST_SEARCH_UID, TEST_SIGNED_FILE, + TEST_SIGNED_FILE_CONTENT ) - -MOCK_SEARCH_KEYS_RESPONSE = [ - { - 'algo': u'1', - 'date': u'1311475606', - 'expires': u'1643601600', - 'keyid': u'607138F1AECC5A5CA31CB7715F3F7F75D210724D', - 'length': u'2048', - 'type': u'pub', - 'uids': [u'Roberto Rosario '] - } -] +from .mixins import KeyTestMixin def mock_recv_keys(self, keyserver, *keyids): @@ -45,12 +35,11 @@ def mock_recv_keys(self, keyserver, *keyids): return ImportResult() -class KeyTestCase(BaseTestCase): +class KeyTestCase(KeyTestMixin, BaseTestCase): def test_key_instance_creation(self): - # Creating a Key instance is analogous to importing a key - key = Key.objects.create(key_data=TEST_KEY_DATA) + self._create_test_key() - self.assertEqual(key.fingerprint, TEST_KEY_FINGERPRINT) + self.assertEqual(self.test_key.fingerprint, TEST_KEY_FINGERPRINT) @mock.patch.object(gnupg.GPG, 'search_keys', autospec=True) def test_key_search(self, search_keys): @@ -92,7 +81,7 @@ class KeyTestCase(BaseTestCase): self.assertTrue(result.key_id in TEST_KEY_FINGERPRINT) def test_embedded_verification_with_key(self): - Key.objects.create(key_data=TEST_KEY_DATA) + self._create_test_key() with open(TEST_SIGNED_FILE, mode='rb') as signed_file: result = Key.objects.verify_file(signed_file) @@ -100,7 +89,7 @@ class KeyTestCase(BaseTestCase): self.assertEqual(result.fingerprint, TEST_KEY_FINGERPRINT) def test_embedded_verification_with_correct_fingerprint(self): - Key.objects.create(key_data=TEST_KEY_DATA) + self._create_test_key() with open(TEST_SIGNED_FILE, mode='rb') as signed_file: result = Key.objects.verify_file( @@ -111,14 +100,14 @@ class KeyTestCase(BaseTestCase): self.assertEqual(result.fingerprint, TEST_KEY_FINGERPRINT) def test_embedded_verification_with_incorrect_fingerprint(self): - Key.objects.create(key_data=TEST_KEY_DATA) + self._create_test_key() with open(TEST_SIGNED_FILE, mode='rb') as signed_file: with self.assertRaises(KeyDoesNotExist): Key.objects.verify_file(signed_file, key_fingerprint='999') def test_signed_file_decryption(self): - Key.objects.create(key_data=TEST_KEY_DATA) + self._create_test_key() with open(TEST_SIGNED_FILE, mode='rb') as signed_file: result = Key.objects.decrypt_file(file_object=signed_file) @@ -145,7 +134,7 @@ class KeyTestCase(BaseTestCase): self.assertTrue(result.key_id in TEST_KEY_FINGERPRINT) def test_detached_verification_with_key(self): - Key.objects.create(key_data=TEST_KEY_DATA) + self._create_test_key() with open(TEST_DETACHED_SIGNATURE, mode='rb') as signature_file: with open(TEST_FILE, mode='rb') as test_file: @@ -157,29 +146,29 @@ class KeyTestCase(BaseTestCase): self.assertEqual(result.fingerprint, TEST_KEY_FINGERPRINT) def test_detached_signing_no_passphrase(self): - key = Key.objects.create(key_data=TEST_KEY_DATA) + self._create_test_key() with self.assertRaises(NeedPassphrase): with open(TEST_FILE, mode='rb') as test_file: - key.sign_file( + self.test_key.sign_file( file_object=test_file, detached=True, ) def test_detached_signing_bad_passphrase(self): - key = Key.objects.create(key_data=TEST_KEY_DATA) + self._create_test_key() with self.assertRaises(PassphraseError): with open(TEST_FILE, mode='rb') as test_file: - key.sign_file( + self.test_key.sign_file( file_object=test_file, detached=True, passphrase='bad passphrase' ) def test_detached_signing_with_passphrase(self): - key = Key.objects.create(key_data=TEST_KEY_DATA) + self._create_test_key() with open(TEST_FILE, mode='rb') as test_file: - detached_signature = key.sign_file( + detached_signature = self.test_key.sign_file( file_object=test_file, detached=True, passphrase=TEST_KEY_PASSPHRASE ) diff --git a/mayan/apps/django_gpg/tests/test_views.py b/mayan/apps/django_gpg/tests/test_views.py index 39cb0164be..39c29ea516 100644 --- a/mayan/apps/django_gpg/tests/test_views.py +++ b/mayan/apps/django_gpg/tests/test_views.py @@ -5,62 +5,79 @@ from django_downloadview.test import assert_download_response from mayan.apps.common.tests import GenericViewTestCase from ..models import Key -from ..permissions import permission_key_download, permission_key_upload +from ..permissions import ( + permission_key_delete, permission_key_download, permission_key_upload +) from .literals import TEST_KEY_DATA, TEST_KEY_FINGERPRINT +from .mixins import KeyTestMixin -class KeyViewTestCase(GenericViewTestCase): - def test_key_download_view_no_permission(self): - key = Key.objects.create(key_data=TEST_KEY_DATA) - - self.login_user() - - response = self.get( - viewname='django_gpg:key_download', args=(key.pk,) +class KeyViewTestCase(KeyTestMixin, GenericViewTestCase): + def _request_key_delete_view(self): + return self.post( + viewname='django_gpg:key_delete', + kwargs={'key_id': self.test_key.pk} ) - self.assertEqual(response.status_code, 403) + def test_key_delete_view_no_permission(self): + self._create_test_key() + + response = self._request_key_delete_view() + self.assertEqual(response.status_code, 404) + + def test_key_delete_view_with_access(self): + self._create_test_key() + + self.grant_access(obj=self.test_key, permission=permission_key_delete) + + response = self._request_key_delete_view() + self.assertEqual(response.status_code, 302) + + self.assertEqual(Key.objects.count(), 0) + + def _request_key_download_view(self): + return self.get( + viewname='django_gpg:key_download', + kwargs={'key_id': self.test_key.pk} + ) + + def test_key_download_view_no_permission(self): + self._create_test_key() + + response = self._request_key_download_view() + self.assertEqual(response.status_code, 404) def test_key_download_view_with_permission(self): - key = Key.objects.create(key_data=TEST_KEY_DATA) + self._create_test_key() - self.login_user() - - self.role.permissions.add(permission_key_download.stored_permission) + self.grant_access(obj=self.test_key, permission=permission_key_download) self.expected_content_type = 'application/octet-stream; charset=utf-8' - response = self.get( - viewname='django_gpg:key_download', args=(key.pk,) - ) + response = self._request_key_download_view() assert_download_response( - self, response=response, content=key.key_data, - basename=key.key_id, + test_case=self, response=response, content=self.test_key.key_data, + basename=self.test_key.key_id, ) - def test_key_upload_view_no_permission(self): - self.login_user() - - response = self.post( + def _request_key_upload_view(self): + return self.post( viewname='django_gpg:key_upload', data={'key_data': TEST_KEY_DATA} ) + def test_key_upload_view_no_permission(self): + response = self._request_key_upload_view() self.assertEqual(response.status_code, 403) + self.assertEqual(Key.objects.count(), 0) def test_key_upload_view_with_permission(self): - self.login_user() + self.grant_permission(permission=permission_key_upload) - self.role.permissions.add(permission_key_upload.stored_permission) - - response = self.post( - viewname='django_gpg:key_upload', data={'key_data': TEST_KEY_DATA}, - follow=True - ) - - self.assertContains(response, 'created', status_code=200) + response = self._request_key_upload_view() + self.assertEqual(response.status_code, 302) self.assertEqual(Key.objects.count(), 1) self.assertEqual(Key.objects.first().fingerprint, TEST_KEY_FINGERPRINT) diff --git a/mayan/apps/django_gpg/urls.py b/mayan/apps/django_gpg/urls.py index ac21c86d2d..eaad4a617b 100644 --- a/mayan/apps/django_gpg/urls.py +++ b/mayan/apps/django_gpg/urls.py @@ -11,39 +11,44 @@ from .views import ( urlpatterns = [ url( - r'^(?P\d+)/$', KeyDetailView.as_view(), name='key_detail' + regex=r'^keys/(?P\d+)/$', name='key_detail', + view=KeyDetailView.as_view() ), url( - r'^(?P\d+)/delete/$', KeyDeleteView.as_view(), name='key_delete' + regex=r'^keys/(?P\d+)/delete/$', name='key_delete', + view=KeyDeleteView.as_view() ), url( - r'^(?P\d+)/download/$', KeyDownloadView.as_view(), - name='key_download' + regex=r'^keys/(?P\d+)/download/$', name='key_download', + view=KeyDownloadView.as_view() ), url( - r'^list/private/$', PrivateKeyListView.as_view(), - name='key_private_list' + regex=r'^keys/private/$', name='key_private_list', + view=PrivateKeyListView.as_view() ), url( - r'^list/public/$', PublicKeyListView.as_view(), name='key_public_list' + regex=r'^keys/public/$', name='key_public_list', + view=PublicKeyListView.as_view() ), url( - r'^upload/$', KeyUploadView.as_view(), name='key_upload' + regex=r'^keys/upload/$', name='key_upload', + view=KeyUploadView.as_view() ), - url(r'^query/$', KeyQueryView.as_view(), name='key_query'), + url(regex=r'^keys/query/$', name='key_query', view=KeyQueryView.as_view()), url( - r'^query/results/$', KeyQueryResultView.as_view(), - name='key_query_results' + regex=r'^keys/query/results/$', name='key_query_results', + view=KeyQueryResultView.as_view() ), url( - r'^receive/(?P.+)/$', KeyReceive.as_view(), name='key_receive' - ), + regex=r'^keys/receive/(?P.+)/$', name='key_receive', + view=KeyReceive.as_view() + ) ] api_urls = [ url( - r'^keys/(?P[0-9]+)/$', APIKeyView.as_view(), - name='key-detail' + regex=r'^keys/(?P\d+)/$', name='key-detail', + view=APIKeyView.as_view() ), - url(r'^keys/$', APIKeyListView.as_view(), name='key-list'), + url(regex=r'^keys/$', name='key-list', view=APIKeyListView.as_view()) ] diff --git a/mayan/apps/django_gpg/views.py b/mayan/apps/django_gpg/views.py index 54842cd3b1..5c22ec9d4d 100644 --- a/mayan/apps/django_gpg/views.py +++ b/mayan/apps/django_gpg/views.py @@ -30,12 +30,13 @@ logger = logging.getLogger(__name__) class KeyDeleteView(SingleObjectDeleteView): model = Key object_permission = permission_key_delete + pk_url_kwarg = 'key_id' def get_post_action_redirect(self): if self.get_object().key_type == KEY_TYPE_PUBLIC: - return reverse_lazy('django_gpg:key_public_list') + return reverse_lazy(viewname='django_gpg:key_public_list') else: - return reverse_lazy('django_gpg:key_private_list') + return reverse_lazy(viewname='django_gpg:key_private_list') def get_extra_context(self): return {'title': _('Delete key: %s') % self.get_object()} @@ -45,6 +46,7 @@ class KeyDetailView(SingleObjectDetailView): form_class = KeyDetailForm model = Key object_permission = permission_key_view + pk_url_kwarg = 'key_id' def get_extra_context(self): return { @@ -55,6 +57,7 @@ class KeyDetailView(SingleObjectDetailView): class KeyDownloadView(SingleObjectDownloadView): model = Key object_permission = permission_key_download + pk_url_kwarg = 'key_id' def get_file(self): key = self.get_object() @@ -63,7 +66,7 @@ class KeyDownloadView(SingleObjectDownloadView): class KeyReceive(ConfirmView): - post_action_redirect = reverse_lazy('django_gpg:key_public_list') + post_action_redirect = reverse_lazy(viewname='django_gpg:key_public_list') view_permission = permission_key_receive def get_extra_context(self): @@ -105,7 +108,7 @@ class KeyQueryView(SimpleView): def get_extra_context(self): return { 'form': self.get_form(), - 'form_action': reverse('django_gpg:key_query_results'), + 'form_action': reverse(viewname='django_gpg:key_query_results'), 'submit_icon_class': icon_keyserver_search, 'submit_label': _('Search'), 'submit_method': 'GET', @@ -144,7 +147,7 @@ class KeyQueryResultView(SingleObjectListView): class KeyUploadView(SingleObjectCreateView): fields = ('key_data',) model = Key - post_action_redirect = reverse_lazy('django_gpg:key_public_list') + post_action_redirect = reverse_lazy(viewname='django_gpg:key_public_list') view_permission = permission_key_upload def get_extra_context(self): From c0b34067ef6e22950f374692a6b7e4c822e2ef83 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Mon, 21 Jan 2019 00:29:28 -0400 Subject: [PATCH 027/209] Update document parsing app Update URL parameters to the "_id" forms. Add keyword arguments. Remove use of is_path in the DOCUMENT_PARSING_PDFTOTEXT_PATH setting. Signed-off-by: Roberto Rosario --- mayan/apps/document_parsing/api_views.py | 7 +-- mayan/apps/document_parsing/events.py | 2 +- mayan/apps/document_parsing/exceptions.py | 1 - mayan/apps/document_parsing/links.py | 17 ++++--- mayan/apps/document_parsing/permissions.py | 8 ++-- mayan/apps/document_parsing/queues.py | 7 +-- mayan/apps/document_parsing/settings.py | 3 +- mayan/apps/document_parsing/tests/test_api.py | 13 ++--- .../apps/document_parsing/tests/test_views.py | 27 ++++------- mayan/apps/document_parsing/urls.py | 48 ++++++++++--------- mayan/apps/document_parsing/views.py | 13 +++-- 11 files changed, 75 insertions(+), 71 deletions(-) diff --git a/mayan/apps/document_parsing/api_views.py b/mayan/apps/document_parsing/api_views.py index ea97b8b9ad..c56e09c6f8 100644 --- a/mayan/apps/document_parsing/api_views.py +++ b/mayan/apps/document_parsing/api_views.py @@ -17,7 +17,7 @@ class APIDocumentPageContentView(generics.RetrieveAPIView): """ Returns the content of the selected document page. """ - lookup_url_kwarg = 'page_pk' + lookup_url_kwarg = 'document_page_id' mayan_object_permissions = { 'GET': (permission_content_view,), } @@ -25,11 +25,12 @@ class APIDocumentPageContentView(generics.RetrieveAPIView): serializer_class = DocumentPageContentSerializer def get_document(self): - return get_object_or_404(klass=Document, pk=self.kwargs['document_pk']) + return get_object_or_404(klass=Document, pk=self.kwargs['document_id']) def get_document_version(self): return get_object_or_404( - klass=self.get_document().versions.all(), pk=self.kwargs['version_pk'] + klass=self.get_document().versions.all(), + pk=self.kwargs['document_version_id'] ) def get_queryset(self): diff --git a/mayan/apps/document_parsing/events.py b/mayan/apps/document_parsing/events.py index 84a9c86d6d..3700c44f05 100644 --- a/mayan/apps/document_parsing/events.py +++ b/mayan/apps/document_parsing/events.py @@ -5,7 +5,7 @@ from django.utils.translation import ugettext_lazy as _ from mayan.apps.events import EventTypeNamespace namespace = EventTypeNamespace( - name='document_parsing', label=_('Document parsing') + label=_('Document parsing'), name='document_parsing' ) event_parsing_document_version_submit = namespace.add_event_type( diff --git a/mayan/apps/document_parsing/exceptions.py b/mayan/apps/document_parsing/exceptions.py index ebc1b0d0ca..6798f85c5e 100644 --- a/mayan/apps/document_parsing/exceptions.py +++ b/mayan/apps/document_parsing/exceptions.py @@ -5,4 +5,3 @@ class ParserError(Exception): """ Base exception for file parsers """ - pass diff --git a/mayan/apps/document_parsing/links.py b/mayan/apps/document_parsing/links.py index a4e27960c8..ac2cf20d7b 100644 --- a/mayan/apps/document_parsing/links.py +++ b/mayan/apps/document_parsing/links.py @@ -16,22 +16,26 @@ from .permissions import ( ) link_document_content = Link( - args='resolved_object.id', icon_class=icon_document_content, + icon_class=icon_document_content, + kwargs={'document_id': 'resolved_object.id'}, permissions=(permission_content_view,), text=_('Content'), view='document_parsing:document_content', ) link_document_page_content = Link( - args='resolved_object.id', icon_class=icon_document_content, + icon_class=icon_document_content, + kwargs={'document_page_id': 'resolved_object.id'}, permissions=(permission_content_view,), text=_('Content'), view='document_parsing:document_page_content', ) link_document_parsing_errors_list = Link( - args='resolved_object.id', icon_class=icon_document_parsing_errors_list, + icon_class=icon_document_parsing_errors_list, + kwargs={'document_id': 'resolved_object.id'}, permissions=(permission_content_view,), text=_('Parsing errors'), view='document_parsing:document_parsing_error_list' ) link_document_content_download = Link( - args='resolved_object.id', icon_class=icon_document_content_download, + icon_class=icon_document_content_download, + kwargs={'document_id': 'resolved_object.id'}, permissions=(permission_content_view,), text=_('Download content'), view='document_parsing:document_content_download' ) @@ -40,13 +44,14 @@ link_document_multiple_submit = Link( view='document_parsing:document_multiple_submit' ) link_document_submit = Link( - args='resolved_object.id', icon_class=icon_document_submit, + icon_class=icon_document_submit, + kwargs={'document_id': 'resolved_object.id'}, permissions=(permission_parse_document,), text=_('Submit for parsing'), view='document_parsing:document_submit' ) link_document_type_parsing_settings = Link( - args='resolved_object.id', icon_class=icon_document_type_parsing_settings, + kwargs={'document_type_id': 'resolved_object.id'}, permissions=(permission_document_type_parsing_setup,), text=_('Setup parsing'), view='document_parsing:document_type_parsing_settings', diff --git a/mayan/apps/document_parsing/permissions.py b/mayan/apps/document_parsing/permissions.py index d06198a9b7..b38b9ac4ea 100644 --- a/mayan/apps/document_parsing/permissions.py +++ b/mayan/apps/document_parsing/permissions.py @@ -7,12 +7,12 @@ from mayan.apps.permissions import PermissionNamespace namespace = PermissionNamespace(label=_('Document parsing'), name='document_parsing') permission_content_view = namespace.add_permission( - name='content_view', label=_('View the content of a document') + label=_('View the content of a document'), name='content_view' ) permission_document_type_parsing_setup = namespace.add_permission( - name='document_type_setup', - label=_('Change document type parsing settings') + label=_('Change document type parsing settings'), + name='document_type_setup' ) permission_parse_document = namespace.add_permission( - name='parse_document', label=_('Parse the content of a document') + label=_('Parse the content of a document'), name='parse_document' ) diff --git a/mayan/apps/document_parsing/queues.py b/mayan/apps/document_parsing/queues.py index 8fa72c2cc4..f9a4c6cc85 100644 --- a/mayan/apps/document_parsing/queues.py +++ b/mayan/apps/document_parsing/queues.py @@ -4,8 +4,9 @@ from django.utils.translation import ugettext_lazy as _ from mayan.apps.task_manager.classes import CeleryQueue -queue_ocr = CeleryQueue(name='parsing', label=_('Parsing')) +queue_ocr = CeleryQueue(label=_('Parsing'), name='parsing') + queue_ocr.add_task_type( - name='mayan.apps.document_parsing.tasks.task_parse_document_version', - label=_('Document version parsing') + label=_('Document version parsing'), + name='mayan.apps.document_parsing.tasks.task_parse_document_version' ) diff --git a/mayan/apps/document_parsing/settings.py b/mayan/apps/document_parsing/settings.py index fd920cafcc..cde88864a5 100644 --- a/mayan/apps/document_parsing/settings.py +++ b/mayan/apps/document_parsing/settings.py @@ -6,7 +6,7 @@ from mayan.apps.smart_settings import Namespace from .literals import DEFAULT_PDFTOTEXT_PATH -namespace = Namespace(name='document_parsing', label=_('Document parsing')) +namespace = Namespace(label=_('Document parsing'), name='document_parsing') setting_auto_parsing = namespace.add_setting( global_name='DOCUMENT_PARSING_AUTO_PARSING', default=True, @@ -21,5 +21,4 @@ setting_pdftotext_path = namespace.add_setting( 'File path to poppler\'s pdftotext program used to extract text ' 'from PDF files.' ), - is_path=True ) diff --git a/mayan/apps/document_parsing/tests/test_api.py b/mayan/apps/document_parsing/tests/test_api.py index e67f9e46d2..9294f1963c 100644 --- a/mayan/apps/document_parsing/tests/test_api.py +++ b/mayan/apps/document_parsing/tests/test_api.py @@ -16,17 +16,14 @@ TEST_DOCUMENT_CONTENT = 'Sample text' class DocumentParsingAPITestCase(DocumentTestMixin, BaseAPITestCase): test_document_filename = TEST_HYBRID_DOCUMENT - def setUp(self): - super(DocumentParsingAPITestCase, self).setUp() - self.login_user() - def _request_document_page_content_view(self): return self.get( viewname='rest_api:document-page-content-view', - args=( - self.document.pk, self.document.latest_version.pk, - self.document.latest_version.pages.first().pk, - ) + kargs={ + 'document_id': self.document.pk, + 'version_id': self.document.latest_version.pk, + 'page_id': self.document.latest_version.pages.first().pk + } ) def test_get_document_version_page_content_no_access(self): diff --git a/mayan/apps/document_parsing/tests/test_views.py b/mayan/apps/document_parsing/tests/test_views.py index a7bcb0a1c5..d7b5e629ca 100644 --- a/mayan/apps/document_parsing/tests/test_views.py +++ b/mayan/apps/document_parsing/tests/test_views.py @@ -22,19 +22,16 @@ class DocumentContentViewsTestCase(GenericDocumentViewTestCase): # Ensure we use a PDF file test_document_filename = TEST_HYBRID_DOCUMENT - def setUp(self): - super(DocumentContentViewsTestCase, self).setUp() - self.login_user() - def _request_document_content_view(self): return self.get( - 'document_parsing:document_content', args=(self.document.pk,) + viewname='document_parsing:document_content', + kwargs={'document_id': self.document.pk} ) def test_document_content_view_no_permissions(self): response = self._request_document_content_view() - self.assertEqual(response.status_code, 403) + self.assertEqual(response.status_code, 404) def test_document_content_view_with_access(self): self.grant_access( @@ -48,15 +45,15 @@ class DocumentContentViewsTestCase(GenericDocumentViewTestCase): def _request_document_page_content_view(self): return self.get( - viewname='document_parsing:document_page_content', args=( - self.document.pages.first().pk, - ) + viewname='document_parsing:document_page_content', kwargs={ + 'document_page_id': self.document.pages.first().pk + } ) def test_document_page_content_view_no_permissions(self): response = self._request_document_page_content_view() - self.assertEqual(response.status_code, 403) + self.assertEqual(response.status_code, 404) def test_document_page_content_view_with_access(self): self.grant_access( @@ -71,12 +68,12 @@ class DocumentContentViewsTestCase(GenericDocumentViewTestCase): def _request_document_content_download_view(self): return self.get( viewname='document_parsing:document_content_download', - args=(self.document.pk,) + kwargs={'document_id': self.document.pk} ) def test_document_parsing_download_view_no_permission(self): response = self._request_document_content_download_view() - self.assertEqual(response.status_code, 403) + self.assertEqual(response.status_code, 404) def test_download_view_with_access(self): self.expected_content_type = 'application/octet-stream; charset=utf-8' @@ -98,14 +95,10 @@ class DocumentTypeViewsTestCase(GenericDocumentViewTestCase): # Ensure we use a PDF file test_document_filename = TEST_HYBRID_DOCUMENT - def setUp(self): - super(DocumentTypeViewsTestCase, self).setUp() - self.login_user() - def _request_document_type_parsing_settings_view(self): return self.get( viewname='document_parsing:document_type_parsing_settings', - args=(self.document.document_type.pk,) + kwargs={'document_type_id': self.document.document_type.pk} ) def test_document_type_parsing_settings_view_no_permission(self): diff --git a/mayan/apps/document_parsing/urls.py b/mayan/apps/document_parsing/urls.py index ce121d9fa5..eb4c70988a 100644 --- a/mayan/apps/document_parsing/urls.py +++ b/mayan/apps/document_parsing/urls.py @@ -11,46 +11,50 @@ from .views import ( urlpatterns = [ url( - r'^documents/(?P\d+)/content/$', DocumentContentView.as_view(), - name='document_content' + regex=r'^documents/(?P\d+)/content/$', + name='document_content', view=DocumentContentView.as_view() ), url( - r'^documents/pages/(?P\d+)/content/$', - DocumentPageContentView.as_view(), name='document_page_content' + regex=r'^documents/pages/(?P\d+)/content/$', + name='document_page_content', view=DocumentPageContentView.as_view() ), url( - r'^documents/(?P\d+)/content/download/$', - DocumentContentDownloadView.as_view(), name='document_content_download' + regex=r'^documents/(?P\d+)/content/download/$', + name='document_content_download', + view=DocumentContentDownloadView.as_view() ), url( - r'^documents/(?P\d+)/submit/$', DocumentSubmitView.as_view(), - name='document_submit' + regex=r'^documents/(?P\d+)/submit/$', + name='document_submit', view=DocumentSubmitView.as_view() ), url( - r'^documents/multiple/submit/$', DocumentSubmitView.as_view(), - name='document_multiple_submit' + regex=r'^documents/multiple/submit/$', name='document_multiple_submit', + view=DocumentSubmitView.as_view() ), url( - r'^documents/(?P\d+)/errors/$', - DocumentParsingErrorsListView.as_view(), - name='document_parsing_error_list' + regex=r'^documents/(?P\d+)/errors/$', + name='document_parsing_error_list', + view=DocumentParsingErrorsListView.as_view() ), url( - r'^document_types/submit/$', DocumentTypeSubmitView.as_view(), - name='document_type_submit' + regex=r'^document_types/submit/$', name='document_type_submit', + view=DocumentTypeSubmitView.as_view() ), url( - r'^document_types/(?P\d+)/parsing/settings/$', - DocumentTypeSettingsEditView.as_view(), - name='document_type_parsing_settings' + regex=r'^document_types/(?P\d+)/parsing/settings/$', + name='document_type_parsing_settings', + view=DocumentTypeSettingsEditView.as_view() ), - url(r'^errors/all/$', ParseErrorListView.as_view(), name='error_list'), + url( + regex=r'^errors/all/$', name='error_list', + view=ParseErrorListView.as_view() + ) ] api_urls = [ url( - r'^documents/(?P\d+)/versions/(?P\d+)/pages/(?P\d+)/content/$', - APIDocumentPageContentView.as_view(), + regex=r'^documents/(?P\d+)/versions/(?P\d+)/pages/(?P\d+)/content/$', + view=APIDocumentPageContentView.as_view(), name='document-page-content-view' - ), + ) ] diff --git a/mayan/apps/document_parsing/views.py b/mayan/apps/document_parsing/views.py index df6af457b0..341b65ff87 100644 --- a/mayan/apps/document_parsing/views.py +++ b/mayan/apps/document_parsing/views.py @@ -28,6 +28,7 @@ class DocumentContentView(SingleObjectDetailView): form_class = DocumentContentForm model = Document object_permission = permission_content_view + pk_url_kwarg = 'document_id' def dispatch(self, request, *args, **kwargs): result = super(DocumentContentView, self).dispatch( @@ -48,6 +49,7 @@ class DocumentContentView(SingleObjectDetailView): class DocumentContentDownloadView(SingleObjectDownloadView): model = Document object_permission = permission_content_view + pk_url_kwarg = 'document_id' def get_file(self): file_object = DocumentContentDownloadView.TextIteratorIO( @@ -62,6 +64,7 @@ class DocumentPageContentView(SingleObjectDetailView): form_class = DocumentPageContentForm model = DocumentPage object_permission = permission_content_view + pk_url_kwarg = 'document_page_id' def dispatch(self, request, *args, **kwargs): result = super(DocumentPageContentView, self).dispatch( @@ -84,7 +87,7 @@ class DocumentParsingErrorsListView(SingleObjectListView): view_permission = permission_content_view def get_document(self): - return get_object_or_404(klass=Document, pk=self.kwargs['pk']) + return get_object_or_404(klass=Document, pk=self.kwargs['document_id']) def get_extra_context(self): return { @@ -141,10 +144,12 @@ class DocumentSubmitView(MultipleObjectConfirmActionView): class DocumentTypeSettingsEditView(SingleObjectEditView): fields = ('auto_parsing',) object_permission = permission_document_type_parsing_setup - post_action_redirect = reverse_lazy('documents:document_type_list') + post_action_redirect = reverse_lazy(viewname='documents:document_type_list') def get_document_type(self): - return get_object_or_404(klass=DocumentType, pk=self.kwargs['pk']) + return get_object_or_404( + klass=DocumentType, pk=self.kwargs['document_type_id'] + ) def get_extra_context(self): return { @@ -163,7 +168,7 @@ class DocumentTypeSubmitView(FormView): 'title': _('Submit all documents of a type for parsing') } form_class = DocumentTypeFilteredSelectForm - post_action_redirect = reverse_lazy('common:tools_list') + post_action_redirect = reverse_lazy(viewname='common:tools_list') def get_form_extra_kwargs(self): return { From 2e5d05403a40fe761125b3de7eb0ce4e527635d1 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Mon, 21 Jan 2019 02:00:22 -0400 Subject: [PATCH 028/209] Update linking app Add keyword arguments. Update URL parameters to the '_id' form. Movernize tests and update them to use the latest test case improvements. Signed-off-by: Roberto Rosario --- mayan/apps/linking/api_views.py | 10 +- mayan/apps/linking/forms.py | 2 +- mayan/apps/linking/links.py | 42 ++++--- mayan/apps/linking/permissions.py | 12 +- mayan/apps/linking/tests/mixins.py | 13 ++ mayan/apps/linking/tests/test_api.py | 10 +- mayan/apps/linking/tests/test_models.py | 16 +-- mayan/apps/linking/tests/test_views.py | 156 +++++++++++------------- mayan/apps/linking/urls.py | 101 +++++++-------- mayan/apps/linking/views.py | 87 +++++++------ 10 files changed, 233 insertions(+), 216 deletions(-) create mode 100644 mayan/apps/linking/tests/mixins.py diff --git a/mayan/apps/linking/api_views.py b/mayan/apps/linking/api_views.py index 5a6e0aaec0..2c2c126315 100644 --- a/mayan/apps/linking/api_views.py +++ b/mayan/apps/linking/api_views.py @@ -32,7 +32,7 @@ class APIResolvedSmartLinkDocumentListView(generics.ListAPIView): serializer_class = ResolvedSmartLinkDocumentSerializer def get_document(self): - document = get_object_or_404(klass=Document, pk=self.kwargs['pk']) + document = get_object_or_404(klass=Document, pk=self.kwargs['document_pk']) AccessControlList.objects.check_access( permissions=permission_document_view, user=self.request.user, @@ -86,7 +86,7 @@ class APIResolvedSmartLinkView(generics.RetrieveAPIView): serializer_class = ResolvedSmartLinkSerializer def get_document(self): - document = get_object_or_404(klass=Document, pk=self.kwargs['pk']) + document = get_object_or_404(klass=Document, pk=self.kwargs['document_pk']) AccessControlList.objects.check_access( permissions=permission_document_view, user=self.request.user, @@ -123,7 +123,7 @@ class APIResolvedSmartLinkListView(generics.ListAPIView): serializer_class = ResolvedSmartLinkSerializer def get_document(self): - document = get_object_or_404(klass=Document, pk=self.kwargs['pk']) + document = get_object_or_404(klass=Document, pk=self.kwargs['document_pk']) AccessControlList.objects.check_access( permissions=permission_document_view, user=self.request.user, @@ -182,7 +182,7 @@ class APISmartLinkConditionListView(generics.ListCreateAPIView): else: permission_required = permission_smart_link_edit - smart_link = get_object_or_404(klass=SmartLink, pk=self.kwargs['pk']) + smart_link = get_object_or_404(klass=SmartLink, pk=self.kwargs['smart_link_pk']) AccessControlList.objects.check_access( permissions=permission_required, user=self.request.user, @@ -225,7 +225,7 @@ class APISmartLinkConditionView(generics.RetrieveUpdateDestroyAPIView): else: permission_required = permission_smart_link_edit - smart_link = get_object_or_404(klass=SmartLink, pk=self.kwargs['pk']) + smart_link = get_object_or_404(klass=SmartLink, pk=self.kwargs['smart_link_pk']) AccessControlList.objects.check_access( permissions=permission_required, user=self.request.user, diff --git a/mayan/apps/linking/forms.py b/mayan/apps/linking/forms.py index 78ce42d758..14b5fc85be 100644 --- a/mayan/apps/linking/forms.py +++ b/mayan/apps/linking/forms.py @@ -45,5 +45,5 @@ class SmartLinkConditionForm(forms.ModelForm): ) class Meta: - model = SmartLinkCondition exclude = ('smart_link',) + model = SmartLinkCondition diff --git a/mayan/apps/linking/links.py b/mayan/apps/linking/links.py index a82cff8677..911a6c47df 100644 --- a/mayan/apps/linking/links.py +++ b/mayan/apps/linking/links.py @@ -17,23 +17,25 @@ from .permissions import ( ) link_smart_link_condition_create = Link( - args='object.pk', icon_class=icon_smart_link_condition_create, + icon_class=icon_smart_link_condition_create, + kwargs={'smart_link_id': 'object.pk'}, permissions=(permission_smart_link_edit,), text=_('Create condition'), - view='linking:smart_link_condition_create', + view='linking:smart_link_condition_create' ) link_smart_link_condition_delete = Link( - args='resolved_object.pk', permissions=(permission_smart_link_edit,), - tags='dangerous', text=_('Delete'), - view='linking:smart_link_condition_delete', + kwargs={'smart_link_condition_id': 'resolved_object.pk'}, + permissions=(permission_smart_link_edit,), tags='dangerous', + text=_('Delete'), view='linking:smart_link_condition_delete' ) link_smart_link_condition_edit = Link( - args='resolved_object.pk', permissions=(permission_smart_link_edit,), - text=_('Edit'), view='linking:smart_link_condition_edit', + kwargs={'smart_link_condition_id': 'resolved_object.pk'}, + permissions=(permission_smart_link_edit,), text=_('Edit'), + view='linking:smart_link_condition_edit' ) link_smart_link_condition_list = Link( - args='object.pk', icon_class=icon_smart_link_condition, + icon_class=icon_smart_link_condition, kwargs={'smart_link_id': 'object.pk'}, permissions=(permission_smart_link_edit,), text=_('Conditions'), - view='linking:smart_link_condition_list', + view='linking:smart_link_condition_list' ) link_smart_link_create = Link( icon_class=icon_smart_link_create, @@ -41,35 +43,37 @@ link_smart_link_create = Link( text=_('Create new smart link'), view='linking:smart_link_create' ) link_smart_link_delete = Link( - args='object.pk', permissions=(permission_smart_link_delete,), - tags='dangerous', text=_('Delete'), view='linking:smart_link_delete', + kwargs={'smart_link_id': 'object.pk'}, + permissions=(permission_smart_link_delete,), + tags='dangerous', text=_('Delete'), view='linking:smart_link_delete' ) link_smart_link_document_types = Link( - args='object.pk', icon_class=icon_document_type, + icon_class=icon_document_type, kwargs={'document_type_id': 'object.pk'}, permissions=(permission_smart_link_edit,), text=_('Document types'), view='linking:smart_link_document_types', ) link_smart_link_edit = Link( - args='object.pk', permissions=(permission_smart_link_edit,), + kwargs={'smart_link_id': 'object.pk'}, + permissions=(permission_smart_link_edit,), text=_('Edit'), view='linking:smart_link_edit', ) link_smart_link_instance_view = Link( - args=('document.pk', 'object.pk',), + kwargs={'document_id': 'document.pk', 'smart_link_id': 'object.pk'}, permissions=(permission_smart_link_view,), text=_('Documents'), - view='linking:smart_link_instance_view', + view='linking:resolved_smart_link_details' ) link_smart_link_instances_for_document = Link( - args='resolved_object.pk', icon_class=icon_smart_link_instances_for_document, + kwargs={'document_id': 'resolved_object.pk'}, permissions=(permission_document_view,), text=_('Smart links'), - view='linking:smart_link_instances_for_document', + view='linking:resolved_smart_links_for_document', ) link_smart_link_list = Link( - permissions=(permission_smart_link_create,), text=_('Smart links'), + permissions=(permission_smart_link_view,), text=_('Smart links'), view='linking:smart_link_list' ) link_smart_link_setup = Link( icon_class=icon_smart_link_setup, - permissions=(permission_smart_link_create,), text=_('Smart links'), + permissions=(permission_smart_link_view,), text=_('Smart links'), view='linking:smart_link_list' ) diff --git a/mayan/apps/linking/permissions.py b/mayan/apps/linking/permissions.py index 0d6d3a47a7..3bae121d04 100644 --- a/mayan/apps/linking/permissions.py +++ b/mayan/apps/linking/permissions.py @@ -6,15 +6,15 @@ from mayan.apps.permissions import PermissionNamespace namespace = PermissionNamespace(label=_('Smart links'), name='linking') -permission_smart_link_view = namespace.add_permission( - name='smart_link_view', label=_('View existing smart links') -) permission_smart_link_create = namespace.add_permission( - name='smart_link_create', label=_('Create new smart links') + label=_('Create new smart links'), name='smart_link_create' ) permission_smart_link_delete = namespace.add_permission( - name='smart_link_delete', label=_('Delete smart links') + label=_('Delete smart links'), name='smart_link_delete' ) permission_smart_link_edit = namespace.add_permission( - name='smart_link_edit', label=_('Edit smart links') + label=_('Edit smart links'), name='smart_link_edit' +) +permission_smart_link_view = namespace.add_permission( + label=_('View existing smart links'), name='smart_link_view' ) diff --git a/mayan/apps/linking/tests/mixins.py b/mayan/apps/linking/tests/mixins.py new file mode 100644 index 0000000000..73b4aff8d9 --- /dev/null +++ b/mayan/apps/linking/tests/mixins.py @@ -0,0 +1,13 @@ +from __future__ import unicode_literals + +from ..models import SmartLink + +from .literals import TEST_SMART_LINK_DYNAMIC_LABEL, TEST_SMART_LINK_LABEL + + +class SmartLinkTestMixin(object): + def _create_test_smart_link(self): + self.test_smart_link = SmartLink.objects.create( + label=TEST_SMART_LINK_LABEL, + dynamic_label=TEST_SMART_LINK_DYNAMIC_LABEL + ) diff --git a/mayan/apps/linking/tests/test_api.py b/mayan/apps/linking/tests/test_api.py index f08ef7ad10..c534f72f8d 100644 --- a/mayan/apps/linking/tests/test_api.py +++ b/mayan/apps/linking/tests/test_api.py @@ -66,7 +66,7 @@ class SmartLinkAPITestCase(DocumentTestMixin, BaseAPITestCase): return self.post( viewname='rest_api:smartlink-list', data={ 'label': TEST_SMART_LINK_LABEL, - 'document_types_pk_list': self.document_type.pk + 'document_types_id_list': self.document_type.pk }, ) @@ -135,7 +135,7 @@ class SmartLinkAPITestCase(DocumentTestMixin, BaseAPITestCase): viewname='rest_api:smartlink-detail', args=(self.smart_link.pk,), data={ 'label': TEST_SMART_LINK_LABEL_EDITED, - 'document_types_pk_list': self.document_type.pk + 'document_types_id_list': self.document_type.pk } ) @@ -163,7 +163,7 @@ class SmartLinkAPITestCase(DocumentTestMixin, BaseAPITestCase): viewname='rest_api:smartlink-detail', args=(self.smart_link.pk,), data={ 'label': TEST_SMART_LINK_LABEL_EDITED, - 'document_types_pk_list': self.document_type.pk + 'document_types_id_list': self.document_type.pk } ) @@ -253,7 +253,9 @@ class SmartLinkConditionAPITestCase(BaseAPITestCase): self._create_smart_link() self._create_smart_link_condition() self._create_document() - self.grant_access(permission=permission_smart_link_view, obj=self.smart_link) + self.grant_access( + obj=self.smart_link, permission=permission_smart_link_view + ) response = self._request_resolved_smart_link_detail_view() self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) self.assertFalse('label' in response.data) diff --git a/mayan/apps/linking/tests/test_models.py b/mayan/apps/linking/tests/test_models.py index d8fece55c0..05bf6b5726 100644 --- a/mayan/apps/linking/tests/test_models.py +++ b/mayan/apps/linking/tests/test_models.py @@ -2,20 +2,16 @@ from __future__ import unicode_literals from mayan.apps.documents.tests import GenericDocumentTestCase -from ..models import SmartLink - -from .literals import TEST_SMART_LINK_DYNAMIC_LABEL, TEST_SMART_LINK_LABEL +from .mixins import SmartLinkTestMixin -class SmartLinkTestCase(GenericDocumentTestCase): +class SmartLinkTestCase(SmartLinkTestMixin, GenericDocumentTestCase): def test_dynamic_label(self): - smart_link = SmartLink.objects.create( - label=TEST_SMART_LINK_LABEL, - dynamic_label=TEST_SMART_LINK_DYNAMIC_LABEL - ) - smart_link.document_types.add(self.document_type) + self._create_test_smart_link() + + self.test_smart_link.document_types.add(self.document_type) self.assertEqual( - smart_link.get_dynamic_label(document=self.document), + self.test_smart_link.get_dynamic_label(document=self.document), self.document.label ) diff --git a/mayan/apps/linking/tests/test_views.py b/mayan/apps/linking/tests/test_views.py index 38e40bae3f..b4e758df5a 100644 --- a/mayan/apps/linking/tests/test_views.py +++ b/mayan/apps/linking/tests/test_views.py @@ -14,121 +14,116 @@ from .literals import ( TEST_SMART_LINK_DYNAMIC_LABEL, TEST_SMART_LINK_LABEL, TEST_SMART_LINK_LABEL_EDITED ) +from .mixins import SmartLinkTestMixin -class SmartLinkViewTestCase(GenericViewTestCase): - def setUp(self): - super(SmartLinkViewTestCase, self).setUp() - self.login_user() - - def test_smart_link_create_view_no_permission(self): - response = self.post( - 'linking:smart_link_create', data={ +class SmartLinkViewTestCase(SmartLinkTestMixin, GenericViewTestCase): + def _request_smart_link_create_view(self): + return self.post( + viewname='linking:smart_link_create', data={ 'label': TEST_SMART_LINK_LABEL } ) - self.assertEquals(response.status_code, 403) + def test_smart_link_create_view_no_permission(self): + response = self._request_smart_link_create_view() + self.assertEqual(response.status_code, 403) + self.assertEqual(SmartLink.objects.count(), 0) def test_smart_link_create_view_with_permission(self): - self.role.permissions.add( - permission_smart_link_create.stored_permission - ) + self.grant_permission(permission=permission_smart_link_create) + + response = self._request_smart_link_create_view() + self.assertEqual(response.status_code, 302) - response = self.post( - 'linking:smart_link_create', data={ - 'label': TEST_SMART_LINK_LABEL - }, follow=True - ) - self.assertContains(response, text='created', status_code=200) self.assertEqual(SmartLink.objects.count(), 1) self.assertEqual( SmartLink.objects.first().label, TEST_SMART_LINK_LABEL ) - def test_smart_link_delete_view_no_permission(self): - smart_link = SmartLink.objects.create(label=TEST_SMART_LINK_LABEL) - - response = self.post( - 'linking:smart_link_delete', args=(smart_link.pk,) + def _request_smart_link_delete_view(self): + return self.post( + viewname='linking:smart_link_delete', + kwargs={'smart_link_id': self.test_smart_link.pk} ) - self.assertEqual(response.status_code, 403) + + def test_smart_link_delete_view_no_permission(self): + self._create_test_smart_link() + + response = self._request_smart_link_delete_view() + self.assertEqual(response.status_code, 404) + self.assertEqual(SmartLink.objects.count(), 1) - def test_smart_link_delete_view_with_permission(self): - self.role.permissions.add( - permission_smart_link_delete.stored_permission + def test_smart_link_delete_view_with_access(self): + self._create_test_smart_link() + self.grant_access( + obj=self.test_smart_link, permission=permission_smart_link_delete ) + response = self._request_smart_link_delete_view() + self.assertEqual(response.status_code, 302) - smart_link = SmartLink.objects.create(label=TEST_SMART_LINK_LABEL) - - response = self.post( - 'linking:smart_link_delete', args=(smart_link.pk,), follow=True - ) - - self.assertContains(response, text='deleted', status_code=200) self.assertEqual(SmartLink.objects.count(), 0) - def test_smart_link_edit_view_no_permission(self): - smart_link = SmartLink.objects.create(label=TEST_SMART_LINK_LABEL) - - response = self.post( - 'linking:smart_link_edit', args=(smart_link.pk,), data={ + def _request_smart_link_edit_view(self): + return self.post( + viewname='linking:smart_link_edit', + kwargs={'smart_link_id': self.test_smart_link.pk}, data={ 'label': TEST_SMART_LINK_LABEL_EDITED } ) - self.assertEqual(response.status_code, 403) - smart_link = SmartLink.objects.get(pk=smart_link.pk) - self.assertEqual(smart_link.label, TEST_SMART_LINK_LABEL) - def test_smart_link_edit_view_with_permission(self): - self.role.permissions.add( - permission_smart_link_edit.stored_permission + def test_smart_link_edit_view_no_permission(self): + self._create_test_smart_link() + + response = self._request_smart_link_edit_view() + self.assertEqual(response.status_code, 404) + + self.test_smart_link.refresh_from_db() + self.assertEqual(self.test_smart_link.label, TEST_SMART_LINK_LABEL) + + def test_smart_link_edit_view_with_access(self): + self._create_test_smart_link() + + self.grant_access( + obj=self.test_smart_link, permission=permission_smart_link_edit ) + response = self._request_smart_link_edit_view() + self.assertEqual(response.status_code, 302) - smart_link = SmartLink.objects.create(label=TEST_SMART_LINK_LABEL) - - response = self.post( - 'linking:smart_link_edit', args=(smart_link.pk,), data={ - 'label': TEST_SMART_LINK_LABEL_EDITED - }, follow=True + self.test_smart_link.refresh_from_db() + self.assertEqual( + self.test_smart_link.label, TEST_SMART_LINK_LABEL_EDITED ) - smart_link = SmartLink.objects.get(pk=smart_link.pk) - self.assertContains(response, text='update', status_code=200) - self.assertEqual(smart_link.label, TEST_SMART_LINK_LABEL_EDITED) - class SmartLinkDocumentsViewTestCase(GenericDocumentViewTestCase): - def setUp(self): - super(SmartLinkDocumentsViewTestCase, self).setUp() - self.login_user() - - def setup_smart_links(self): - smart_link = SmartLink.objects.create( + def _setup_smart_links(self): + self.test_smart_link = SmartLink.objects.create( label=TEST_SMART_LINK_LABEL, dynamic_label=TEST_SMART_LINK_DYNAMIC_LABEL ) - smart_link.document_types.add(self.document_type) + self.test_smart_link.document_types.add(self.document_type) - smart_link_2 = SmartLink.objects.create( + self.test_smart_link_2 = SmartLink.objects.create( label=TEST_SMART_LINK_LABEL, dynamic_label=TEST_SMART_LINK_DYNAMIC_LABEL ) - smart_link_2.document_types.add(self.document_type) + self.test_smart_link_2.document_types.add(self.document_type) + + def _request_document_resolved_smart_links_for_document_view(self): + return self.get( + viewname='linking:resolved_smart_links_for_document', + kwargs={'document_id': self.document.pk} + ) def test_document_smart_link_list_view_no_permission(self): - self.setup_smart_links() + self._setup_smart_links() - self.role.permissions.add( - permission_document_view.stored_permission - ) + self.grant_access(obj=self.document, permission=permission_document_view) - response = self.get( - 'linking:smart_link_instances_for_document', - args=(self.document.pk,) - ) + response = self._request_document_resolved_smart_links_for_document_view() # Text must appear 3 times, 2 for the window titles and template # heading. The two smart links are not shown. @@ -137,20 +132,13 @@ class SmartLinkDocumentsViewTestCase(GenericDocumentViewTestCase): ) def test_document_smart_link_list_view_with_permission(self): - self.setup_smart_links() + self._setup_smart_links() - self.role.permissions.add( - permission_smart_link_view.stored_permission - ) - - self.role.permissions.add( - permission_document_view.stored_permission - ) - - response = self.get( - 'linking:smart_link_instances_for_document', - args=(self.document.pk,) + self.grant_access(obj=self.document, permission=permission_document_view) + self.grant_access( + obj=self.test_smart_link, permission=permission_smart_link_view ) + response = self._request_document_resolved_smart_links_for_document_view() # Text must appear 5 times: 3 for the window titles and template # heading, plus 2 for the test. diff --git a/mayan/apps/linking/urls.py b/mayan/apps/linking/urls.py index 6bdf4f04ca..a9b846f9b8 100644 --- a/mayan/apps/linking/urls.py +++ b/mayan/apps/linking/urls.py @@ -17,86 +17,89 @@ from .views import ( urlpatterns = [ url( - r'^document/(?P\d+)/list/$', DocumentSmartLinkListView.as_view(), - name='smart_link_instances_for_document' + regex=r'^smart_links/$', name='smart_link_list', + view=SmartLinkListView.as_view() ), url( - r'^document/(?P\d+)/(?P\d+)/$', - ResolvedSmartLinkView.as_view(), name='smart_link_instance_view' - ), - - url( - r'^setup/list/$', SmartLinkListView.as_view(), name='smart_link_list' + regex=r'^smart_links/create/$', name='smart_link_create', + view=SmartLinkCreateView.as_view() ), url( - r'^setup/create/$', SmartLinkCreateView.as_view(), - name='smart_link_create' + regex=r'^smart_links/(?P\d+)/delete/$', + name='smart_link_delete', view=SmartLinkDeleteView.as_view() ), url( - r'^setup/(?P\d+)/delete/$', - SmartLinkDeleteView.as_view(), name='smart_link_delete' + regex=r'^smart_links/(?P\d+)/edit/$', + name='smart_link_edit', view=SmartLinkEditView.as_view() ), url( - r'^setup/(?P\d+)/edit/$', SmartLinkEditView.as_view(), - name='smart_link_edit' + regex=r'^smart_links/(?P\d+)/document_types/$', + name='smart_link_document_types', + view=SetupSmartLinkDocumentTypesView.as_view() ), url( - r'^setup/(?P\d+)/document_types/$', - SetupSmartLinkDocumentTypesView.as_view(), - name='smart_link_document_types' - ), - - url( - r'^setup/(?P\d+)/condition/list/$', - SmartLinkConditionListView.as_view(), name='smart_link_condition_list' + regex=r'^smart_links/(?P\d+)/conditions/$', + name='smart_link_condition_list', + view=SmartLinkConditionListView.as_view() ), url( - r'^setup/(?P\d+)/condition/create/$', - SmartLinkConditionCreateView.as_view(), - name='smart_link_condition_create' + regex=r'^smart_links/(?P\d+)/conditions/create/$', + name='smart_link_condition_create', + view=SmartLinkConditionCreateView.as_view() ), url( - r'^setup/smart_link/condition/(?P\d+)/edit/$', - SmartLinkConditionEditView.as_view(), name='smart_link_condition_edit' + regex=r'^smart_links/conditions/(?P\d+)/edit/$', + name='smart_link_condition_edit', + view=SmartLinkConditionEditView.as_view() ), url( - r'^setup/smart_link/condition/(?P\d+)/delete/$', - SmartLinkConditionDeleteView.as_view(), - name='smart_link_condition_delete' + regex=r'^smart_links/conditions/(?P\d+)/delete/$', + name='smart_link_condition_delete', + view=SmartLinkConditionDeleteView.as_view() ), + url( + regex=r'^documents/(?P\d+)/resolved_smart_links/$', + name='resolved_smart_links_for_document', + view=DocumentSmartLinkListView.as_view() + ), + url( + regex=r'^documents/(?P\d+)/resolved_smart_links/(?P\d+)/$', + name='resolved_smart_link_details', view=ResolvedSmartLinkView.as_view() + ) ] api_urls = [ url( - r'^smart_links/$', APISmartLinkListView.as_view(), - name='smartlink-list' + regex=r'^smart_links/$', name='smartlink-list', + view=APISmartLinkListView.as_view() ), url( - r'^smart_links/(?P[0-9]+)/$', APISmartLinkView.as_view(), - name='smartlink-detail' + regex=r'^smart_links/(?P\d+)/$', + name='smartlink-detail', view=APISmartLinkView.as_view() ), url( - r'^smart_links/(?P[0-9]+)/conditions/$', - APISmartLinkConditionListView.as_view(), name='smartlinkcondition-list' + regex=r'^smart_links/(?P\d+)/conditions/$', + name='smartlinkcondition-list', + view=APISmartLinkConditionListView.as_view() ), url( - r'^smart_links/(?P[0-9]+)/conditions/(?P[0-9]+)/$', - APISmartLinkConditionView.as_view(), - name='smartlinkcondition-detail' + regex=r'^smart_links/(?P\d+)/conditions/(?P\d+)/$', + name='smartlinkcondition-detail', + view=APISmartLinkConditionView.as_view() ), url( - r'^documents/(?P[0-9]+)/resolved_smart_links/$', - APIResolvedSmartLinkListView.as_view(), - name='resolvedsmartlink-list' + regex=r'^documents/(?P\d+)/resolved_smart_links/$', + name='resolvedsmartlink-list', + view=APIResolvedSmartLinkListView.as_view() ), url( - r'^documents/(?P[0-9]+)/resolved_smart_links/(?P[0-9]+)/$', - APIResolvedSmartLinkView.as_view(), - name='resolvedsmartlink-detail' + regex=r'^documents/(?P\d+)/resolved_smart_links/(?P\d+)/$', + name='resolvedsmartlink-detail', + view=APIResolvedSmartLinkView.as_view() ), url( - r'^documents/(?P[0-9]+)/resolved_smart_links/(?P[0-9]+)/documents/$', - APIResolvedSmartLinkDocumentListView.as_view(), - name='resolvedsmartlinkdocument-list' - ), + regex=r'^documents/(?P\d+)/resolved_smart_links/(?P\d+)/documents/$', + name='resolvedsmartlinkdocument-list', + view=APIResolvedSmartLinkDocumentListView.as_view() + ) ] diff --git a/mayan/apps/linking/views.py b/mayan/apps/linking/views.py index ae6fd359de..238b1c729b 100644 --- a/mayan/apps/linking/views.py +++ b/mayan/apps/linking/views.py @@ -33,20 +33,20 @@ logger = logging.getLogger(__name__) class ResolvedSmartLinkView(DocumentListView): def dispatch(self, request, *args, **kwargs): self.document = get_object_or_404( - klass=Document, pk=self.kwargs['document_pk'] + klass=Document, pk=self.kwargs['document_id'] ) self.smart_link = get_object_or_404( - klass=SmartLink, pk=self.kwargs['smart_link_pk'] + klass=SmartLink, pk=self.kwargs['smart_link_id'] ) AccessControlList.objects.check_access( - permissions=permission_document_view, user=request.user, - obj=self.document + obj=self.document, permissions=permission_document_view, + user=request.user ) AccessControlList.objects.check_access( - permissions=permission_smart_link_view, user=request.user, - obj=self.smart_link + obj=self.smart_link, permissions=permission_smart_link_view, + user=request.user ) return super( @@ -55,20 +55,24 @@ class ResolvedSmartLinkView(DocumentListView): def get_document_queryset(self): try: - queryset = self.smart_link.get_linked_document_for(self.document) + queryset = self.smart_link.get_linked_document_for( + document=self.document + ) except Exception as exception: queryset = Document.objects.none() try: AccessControlList.objects.check_access( - permissions=permission_smart_link_edit, user=self.request.user, - obj=self.smart_link + obj=self.smart_link, permissions=permission_smart_link_edit, + user=self.request.user ) except PermissionDenied: pass else: messages.error( - self.request, _('Smart link query error: %s' % exception) + message=_( + 'Smart link query error: %s' % exception + ), request=self.request, ) return queryset @@ -114,12 +118,14 @@ class SetupSmartLinkDocumentTypesView(AssignRemoveView): } def get_object(self): - return get_object_or_404(klass=SmartLink, pk=self.kwargs['pk']) + return get_object_or_404( + klass=SmartLink, pk=self.kwargs['smart_link_id'] + ) def left_list(self): # TODO: filter document type list by user ACL return AssignRemoveView.generate_choices( - DocumentType.objects.exclude( + choices=DocumentType.objects.exclude( pk__in=self.get_object().document_types.all() ) ) @@ -130,7 +136,7 @@ class SetupSmartLinkDocumentTypesView(AssignRemoveView): def right_list(self): # TODO: filter document type list by user ACL return AssignRemoveView.generate_choices( - self.get_object().document_types.all() + choices=self.get_object().document_types.all() ) @@ -166,11 +172,13 @@ class SmartLinkListView(SingleObjectListView): class DocumentSmartLinkListView(SmartLinkListView): def dispatch(self, request, *args, **kwargs): - self.document = get_object_or_404(klass=Document, pk=self.kwargs['pk']) + self.document = get_object_or_404( + klass=Document, pk=self.kwargs['document_id'] + ) AccessControlList.objects.check_access( - permissions=permission_document_view, user=request.user, - obj=self.document + obj=self.document, permissions=permission_document_view, + user=request.user ) return super( @@ -202,15 +210,16 @@ class DocumentSmartLinkListView(SmartLinkListView): class SmartLinkCreateView(SingleObjectCreateView): extra_context = {'title': _('Create new smart link')} form_class = SmartLinkForm - post_action_redirect = reverse_lazy('linking:smart_link_list') + post_action_redirect = reverse_lazy(viewname='linking:smart_link_list') view_permission = permission_smart_link_create class SmartLinkEditView(SingleObjectEditView): form_class = SmartLinkForm model = SmartLink - post_action_redirect = reverse_lazy('linking:smart_link_list') - view_permission = permission_smart_link_edit + object_permission = permission_smart_link_edit + pk_url_kwarg = 'smart_link_id' + post_action_redirect = reverse_lazy(viewname='linking:smart_link_list') def get_extra_context(self): return { @@ -221,8 +230,9 @@ class SmartLinkEditView(SingleObjectEditView): class SmartLinkDeleteView(SingleObjectDeleteView): model = SmartLink - post_action_redirect = reverse_lazy('linking:smart_link_list') - view_permission = permission_smart_link_delete + object_permission = permission_smart_link_delete + pk_url_kwarg = 'smart_link_id' + post_action_redirect = reverse_lazy(viewname='linking:smart_link_list') def get_extra_context(self): return { @@ -262,7 +272,9 @@ class SmartLinkConditionListView(SingleObjectListView): return self.get_smart_link().conditions.all() def get_smart_link(self): - return get_object_or_404(klass=SmartLink, pk=self.kwargs['pk']) + return get_object_or_404( + klass=SmartLink, pk=self.kwargs['smart_link_id'] + ) class SmartLinkConditionCreateView(SingleObjectCreateView): @@ -270,8 +282,8 @@ class SmartLinkConditionCreateView(SingleObjectCreateView): def dispatch(self, request, *args, **kwargs): AccessControlList.objects.check_access( - permissions=permission_smart_link_edit, user=request.user, - obj=self.get_smart_link() + obj=self.get_smart_link(), permissions=permission_smart_link_edit, + user=request.user ) return super( @@ -291,16 +303,17 @@ class SmartLinkConditionCreateView(SingleObjectCreateView): def get_post_action_redirect(self): return reverse( - 'linking:smart_link_condition_list', args=( - self.get_smart_link().pk, - ) + viewname='linking:smart_link_condition_list', + kwargs={'smart_link_id': self.get_smart_link().pk} ) def get_queryset(self): return self.get_smart_link().conditions.all() def get_smart_link(self): - return get_object_or_404(klass=SmartLink, pk=self.kwargs['pk']) + return get_object_or_404( + klass=SmartLink, pk=self.kwargs['smart_link_id'] + ) class SmartLinkConditionEditView(SingleObjectEditView): @@ -309,8 +322,8 @@ class SmartLinkConditionEditView(SingleObjectEditView): def dispatch(self, request, *args, **kwargs): AccessControlList.objects.check_access( - permissions=permission_smart_link_edit, user=request.user, - obj=self.get_object().smart_link + obj=self.get_object().smart_link, + permissions=permission_smart_link_edit, user=request.user ) return super( @@ -327,9 +340,8 @@ class SmartLinkConditionEditView(SingleObjectEditView): def get_post_action_redirect(self): return reverse( - 'linking:smart_link_condition_list', args=( - self.get_object().smart_link.pk, - ) + viewname='linking:smart_link_condition_list', + kwargs={'smart_link_id': self.get_object().smart_link.pk} ) @@ -338,8 +350,8 @@ class SmartLinkConditionDeleteView(SingleObjectDeleteView): def dispatch(self, request, *args, **kwargs): AccessControlList.objects.check_access( - permissions=permission_smart_link_edit, user=request.user, - obj=self.get_object().smart_link + obj=self.get_object().smart_link, + permissions=permission_smart_link_edit, user=request.user ) return super( @@ -358,7 +370,6 @@ class SmartLinkConditionDeleteView(SingleObjectDeleteView): def get_post_action_redirect(self): return reverse( - 'linking:smart_link_condition_list', args=( - self.get_object().smart_link.pk, - ) + viewname='linking:smart_link_condition_list', + kwargs={'smart_link_id': self.get_object().smart_link.pk} ) From 027a85388533fd1df163ddd4a04b594bde647981 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Mon, 21 Jan 2019 02:35:00 -0400 Subject: [PATCH 029/209] Update events app Add keyword arguments. Update URLs for uniformity. Update URL parameters to the '_id' form. Update views to remove use of .check_access(). Fix escape sequence warning in migration 0005. Signed-off-by: Roberto Rosario --- mayan/apps/events/apps.py | 16 +-- mayan/apps/events/icons.py | 2 +- mayan/apps/events/links.py | 4 +- .../migrations/0005_auto_20170731_0452.py | 12 +- mayan/apps/events/permissions.py | 2 +- mayan/apps/events/tests/test_api.py | 2 +- mayan/apps/events/tests/test_views.py | 60 +++----- mayan/apps/events/urls.py | 73 +++++----- mayan/apps/events/views.py | 134 +++++++++--------- mayan/apps/events/widgets.py | 2 +- 10 files changed, 142 insertions(+), 165 deletions(-) diff --git a/mayan/apps/events/apps.py b/mayan/apps/events/apps.py index b063e6b35e..a320a2b454 100644 --- a/mayan/apps/events/apps.py +++ b/mayan/apps/events/apps.py @@ -16,9 +16,7 @@ from .links import ( link_events_list, link_notification_mark_read, link_notification_mark_read_all, link_user_notifications_list ) -from .widgets import ( - widget_event_actor_link, widget_event_type_link -) +from .widgets import widget_event_actor_link, widget_event_type_link class EventsApp(MayanAppConfig): @@ -56,10 +54,10 @@ class EventsApp(MayanAppConfig): ) SourceColumn( - source=StoredEventType, label=_('Namespace'), attribute='namespace' + attribute='namespace', label=_('Namespace'), source=StoredEventType ) SourceColumn( - source=StoredEventType, label=_('Label'), attribute='label' + attribute='label', label=_('Label'), source=StoredEventType ) SourceColumn( @@ -67,12 +65,12 @@ class EventsApp(MayanAppConfig): is_sortable=True, label=_('Date and time'), source=Notification ) SourceColumn( - func=widget_event_actor_link, kwargs={'attribute': 'action'}, - label=_('Actor'), source=Notification + func=widget_event_actor_link, label=_('Actor'), + kwargs={'attribute': 'action'}, source=Notification ) SourceColumn( - func=widget_event_type_link, kwargs={'attribute': 'action'}, - label=_('Event'), source=Notification + func=widget_event_type_link, label=_('Event'), + kwargs={'attribute': 'action'}, source=Notification ) SourceColumn( attribute='action.target', label=_('Target'), source=Notification, diff --git a/mayan/apps/events/icons.py b/mayan/apps/events/icons.py index bbbc1bde38..5473c4508e 100644 --- a/mayan/apps/events/icons.py +++ b/mayan/apps/events/icons.py @@ -5,8 +5,8 @@ from mayan.apps.appearance.classes import Icon icon_event_types_subscriptions_list = Icon( driver_name='fontawesome', symbol='rss' ) -icon_events_list = Icon(driver_name='fontawesome', symbol='list-ol') icon_events_for_object = Icon(driver_name='fontawesome', symbol='list-ol') +icon_events_list = Icon(driver_name='fontawesome', symbol='list-ol') icon_object_event_types_user_subcriptions_list = Icon( driver_name='fontawesome', symbol='rss' ) diff --git a/mayan/apps/events/links.py b/mayan/apps/events/links.py index 6854bd3277..0a2b8c6fd5 100644 --- a/mayan/apps/events/links.py +++ b/mayan/apps/events/links.py @@ -20,7 +20,7 @@ def get_kwargs_factory(variable_name): ) content_type = ContentType.objects.get_for_model( - context[variable_name] + model=context[variable_name] ) return { 'app_label': '"{}"'.format(content_type.app_label), @@ -54,7 +54,7 @@ link_event_types_subscriptions_list = Link( view='events:event_types_user_subcriptions_list' ) link_notification_mark_read = Link( - args='object.pk', text=_('Mark as seen'), + kwargs={'notification_id': 'object.pk'}, text=_('Mark as seen'), view='events:notification_mark_read' ) link_notification_mark_read_all = Link( diff --git a/mayan/apps/events/migrations/0005_auto_20170731_0452.py b/mayan/apps/events/migrations/0005_auto_20170731_0452.py index 7626b31cda..9305e8d69e 100644 --- a/mayan/apps/events/migrations/0005_auto_20170731_0452.py +++ b/mayan/apps/events/migrations/0005_auto_20170731_0452.py @@ -38,12 +38,12 @@ def operation_revert_event_types_names(apps, schema_editor): StoredEventType = apps.get_model('events', 'StoredEventType') known_namespaces = { - 'documents\.': 'documents_', - 'checkouts\.': 'checkouts_', - 'document_comments\.': 'document_comment_', - 'document_parsing\.': 'parsing_document_', - 'ocr\.': 'ocr_', - 'tags\.': 'tag_', + r'documents\.': 'documents_', + r'checkouts\.': 'checkouts_', + r'document_comments\.': 'document_comment_', + r'document_parsing\.': 'parsing_document_', + r'ocr\.': 'ocr_', + r'tags\.': 'tag_', } pattern = re.compile('|'.join(known_namespaces.keys())) diff --git a/mayan/apps/events/permissions.py b/mayan/apps/events/permissions.py index dc04070ce0..50e001f65e 100644 --- a/mayan/apps/events/permissions.py +++ b/mayan/apps/events/permissions.py @@ -6,5 +6,5 @@ from mayan.apps.permissions import PermissionNamespace namespace = PermissionNamespace(label=_('Events'), name='events') permission_events_view = namespace.add_permission( - name='events_view', label=_('Access the events of an object') + label=_('Access the events of an object'), name='events_view' ) diff --git a/mayan/apps/events/tests/test_api.py b/mayan/apps/events/tests/test_api.py index b11e6e85e0..d0ffffd967 100644 --- a/mayan/apps/events/tests/test_api.py +++ b/mayan/apps/events/tests/test_api.py @@ -7,5 +7,5 @@ from mayan.apps.rest_api.tests import BaseAPITestCase class EventAPITestCase(BaseAPITestCase): def test_evet_type_list_view(self): - response = self.client.get(reverse('rest_api:event-type-list')) + response = self.client.get(reverse(viewname='rest_api:event-type-list')) self.assertEqual(response.status_code, 200) diff --git a/mayan/apps/events/tests/test_views.py b/mayan/apps/events/tests/test_views.py index c7bad2f229..c830ff202e 100644 --- a/mayan/apps/events/tests/test_views.py +++ b/mayan/apps/events/tests/test_views.py @@ -10,59 +10,33 @@ from ..permissions import permission_events_view class EventsViewTestCase(GenericDocumentViewTestCase): def setUp(self): super(EventsViewTestCase, self).setUp() + self.test_object = self.document - content_type = ContentType.objects.get_for_model(self.document) + content_type = ContentType.objects.get_for_model(model=self.test_object) self.view_arguments = { 'app_label': content_type.app_label, 'model': content_type.model, - 'object_id': self.document.pk + 'object_id': self.test_object.pk } + def _request_events_for_object_view(self): + return self.get( + viewname='events:events_for_object', kwargs=self.view_arguments + ) + def test_events_for_object_view_no_permission(self): - self.login_user() - - document = self.document.add_as_recent_document_for_user( - self.user - ).document - - content_type = ContentType.objects.get_for_model(document) - - view_arguments = { - 'app_label': content_type.app_label, - 'model': content_type.model, - 'object_id': document.pk - } - - response = self.get( - viewname='events:events_for_object', kwargs=view_arguments + response = self._request_events_for_object_view() + self.assertNotContains( + response=response, text=self.test_object.label, status_code=404 ) - self.assertNotContains(response, text=document.label, status_code=403) - self.assertNotContains(response, text='otal:', status_code=403) - - def test_events_for_object_view_with_permission(self): - self.login_user() - - self.role.permissions.add( - permission_events_view.stored_permission + def test_events_for_object_view_with_access(self): + self.grant_access( + obj=self.test_object, permission=permission_events_view ) - document = self.document.add_as_recent_document_for_user( - self.user - ).document - - content_type = ContentType.objects.get_for_model(document) - - view_arguments = { - 'app_label': content_type.app_label, - 'model': content_type.model, - 'object_id': document.pk - } - - response = self.get( - viewname='events:events_for_object', kwargs=view_arguments + response = self._request_events_for_object_view() + self.assertContains( + response=response, text=self.test_object.label, status_code=200 ) - - self.assertContains(response, text=document.label, status_code=200) - self.assertNotContains(response, text='otal: 0', status_code=200) diff --git a/mayan/apps/events/urls.py b/mayan/apps/events/urls.py index e60cb1cc71..f93a84eaac 100644 --- a/mayan/apps/events/urls.py +++ b/mayan/apps/events/urls.py @@ -15,69 +15,70 @@ from .views import ( ) urlpatterns = [ - url(r'^all/$', EventListView.as_view(), name='events_list'), + url(regex=r'^events/$', name='events_list', view=EventListView.as_view()), url( - r'^for/(?P[-\w]+)/(?P[-\w]+)/(?P\d+)/$', - ObjectEventListView.as_view(), name='events_for_object' + regex=r'^events/by_verb/(?P[\w\-\.]+)/$', name='events_by_verb', + view=VerbEventListView.as_view() ), url( - r'^by_verb/(?P[\w\-\.]+)/$', VerbEventListView.as_view(), - name='events_by_verb' + regex=r'^objects/(?P[-\w]+)/(?P[-\w]+)/(?P\d+)/events/$', + name='events_for_object', view=ObjectEventListView.as_view() ), url( - r'^notifications/(?P\d+)/mark_read/$', - NotificationMarkRead.as_view(), name='notification_mark_read' + regex=r'^user/events/$', name='current_user_events', + view=CurrentUserEventListView.as_view() ), url( - r'^notifications/all/mark_read/$', - NotificationMarkReadAll.as_view(), name='notification_mark_read_all' + regex=r'^user/event_types/subscriptions/$', + name='event_types_user_subcriptions_list', + view=EventTypeSubscriptionListView.as_view() ), url( - r'^user/(?P[-\w]+)/(?P[-\w]+)/(?P\d+)/subscriptions/$', - ObjectEventTypeSubscriptionListView.as_view(), - name='object_event_types_user_subcriptions_list' + regex=r'^user/notifications/$', name='user_notifications_list', + view=NotificationListView.as_view() ), url( - r'^user/$', CurrentUserEventListView.as_view(), - name='current_user_events' + regex=r'^user/notifications/(?P\d+)/mark_read/$', + name='notification_mark_read', view=NotificationMarkRead.as_view() ), url( - r'^user/event_types/subscriptions/$', - EventTypeSubscriptionListView.as_view(), - name='event_types_user_subcriptions_list' + regex=r'^user/notifications/all/mark_read/$', + name='notification_mark_read_all', + view=NotificationMarkReadAll.as_view() ), url( - r'^user/notifications/$', NotificationListView.as_view(), - name='user_notifications_list' - ), + regex=r'^user/subscriptions/for/(?P[-\w]+)/(?P[-\w]+)/(?P\d+)/$', + name='object_event_types_user_subcriptions_list', + view=ObjectEventTypeSubscriptionListView.as_view() + ) ] api_urls = [ url( - r'^event_type_namespaces/(?P[-\w]+)/$', - APIEventTypeNamespaceDetailView.as_view(), - name='event-type-namespace-detail' + regex=r'^event_type_namespaces/(?P[-\w]+)/$', + name='event-type-namespace-detail', + view=APIEventTypeNamespaceDetailView.as_view() ), url( - r'^event_type_namespaces/(?P[-\w]+)/event_types/$', - APIEventTypeNamespaceEventTypeListView.as_view(), - name='event-type-namespace-event-type-list' + regex=r'^event_type_namespaces/(?P[-\w]+)/event_types/$', + name='event-type-namespace-event-type-list', + view=APIEventTypeNamespaceEventTypeListView.as_view() ), url( - r'^event_type_namespaces/$', APIEventTypeNamespaceListView.as_view(), - name='event-type-namespace-list' + regex=r'^event_type_namespaces/$', name='event-type-namespace-list', + view=APIEventTypeNamespaceListView.as_view() ), url( - r'^event_types/$', APIEventTypeListView.as_view(), - name='event-type-list' + regex=r'^event_types/$', name='event-type-list', + view=APIEventTypeListView.as_view() ), - url(r'^events/$', APIEventListView.as_view(), name='event-list'), + url(regex=r'^events/$', name='event-list', view=APIEventListView.as_view()), url( - r'^notifications/$', APINotificationListView.as_view(), - name='notification-list' + regex=r'^notifications/$', name='notification-list', + view=APINotificationListView.as_view() ), url( - r'^objects/(?P[-\w]+)/(?P[-\w]+)/(?P\d+)/events/$', - APIObjectEventListView.as_view(), name='object-event-list' - ), + regex=r'^objects/(?P[-\w]+)/(?P[-\w]+)/(?P\d+)/events/$', + name='object-event-list', view=APIObjectEventListView.as_view() + ) ] diff --git a/mayan/apps/events/views.py b/mayan/apps/events/views.py index 8b42157733..49603b33bb 100644 --- a/mayan/apps/events/views.py +++ b/mayan/apps/events/views.py @@ -2,7 +2,7 @@ from __future__ import absolute_import, unicode_literals from django.contrib import messages from django.contrib.contenttypes.models import ContentType -from django.http import Http404, HttpResponseRedirect +from django.http import HttpResponseRedirect from django.shortcuts import get_object_or_404 from django.template import RequestContext from django.urls import reverse @@ -18,9 +18,7 @@ from .classes import EventType, ModelEventType from .forms import ( EventTypeUserRelationshipFormSet, ObjectEventTypeUserRelationshipFormSet ) -from .icons import ( - icon_events_list, icon_user_notifications_list -) +from .icons import icon_events_list, icon_user_notifications_list from .links import link_event_types_subscriptions_list from .models import StoredEventType from .permissions import permission_events_view @@ -46,7 +44,9 @@ class EventTypeSubscriptionListView(FormView): def dispatch(self, *args, **kwargs): EventType.refresh() - return super(EventTypeSubscriptionListView, self).dispatch(*args, **kwargs) + return super( + EventTypeSubscriptionListView, self + ).dispatch(*args, **kwargs) def form_valid(self, form): try: @@ -54,21 +54,19 @@ class EventTypeSubscriptionListView(FormView): instance.save() except Exception as exception: messages.error( - self.request, - _('Error updating event subscription; %s') % exception + message=_('Error updating event subscription; %s') % exception, + request=self.request ) else: messages.success( - self.request, _('Event subscriptions updated successfully') + message=_('Event subscriptions updated successfully'), + request=self.request ) return super( EventTypeSubscriptionListView, self ).form_valid(form=form) - def get_object(self): - return self.request.user - def get_extra_context(self): return { 'form_display_mode_table': True, @@ -83,20 +81,25 @@ class EventTypeSubscriptionListView(FormView): initial = [] for element in self.get_queryset(): - initial.append({ - 'user': obj, - 'main_model': self.main_model, - 'stored_event_type': element, - }) + initial.append( + { + 'user': obj, + 'main_model': self.main_model, + 'stored_event_type': element, + } + ) return initial + def get_object(self): + return self.request.user + def get_queryset(self): # Return the queryset by name from the sorted list of the class event_type_ids = [event_type.id for event_type in EventType.all()] return self.submodel.objects.filter(name__in=event_type_ids) def get_post_action_redirect(self): - return reverse('common:current_user_details') + return reverse(viewname='common:current_user_details') class NotificationListView(SingleObjectListView): @@ -124,8 +127,12 @@ class NotificationListView(SingleObjectListView): class NotificationMarkRead(SimpleView): def dispatch(self, *args, **kwargs): - self.get_queryset().filter(pk=self.kwargs['pk']).update(read=True) - return HttpResponseRedirect(reverse('events:user_notifications_list')) + self.get_queryset().filter( + pk=self.kwargs['notification_id'] + ).update(read=True) + return HttpResponseRedirect( + redirect_to=reverse(viewname='events:user_notifications_list') + ) def get_queryset(self): return self.request.user.notifications.all() @@ -134,24 +141,16 @@ class NotificationMarkRead(SimpleView): class NotificationMarkReadAll(SimpleView): def dispatch(self, *args, **kwargs): self.get_queryset().update(read=True) - return HttpResponseRedirect(reverse('events:user_notifications_list')) + return HttpResponseRedirect( + redirect_to=reverse(viewname='events:user_notifications_list') + ) def get_queryset(self): return self.request.user.notifications.all() class ObjectEventListView(EventListView): - view_permissions = None - - def _get_object(self): - content_type = get_object_or_404( - klass=ContentType, app_label=self.kwargs['app_label'], - model=self.kwargs['model'] - ) - - return get_object_or_404( - klass=content_type.model_class(), pk=self.kwargs['object_id'] - ) + view_permission = None def get_extra_context(self): context = super(ObjectEventListView, self).get_extra_context() @@ -171,12 +170,19 @@ class ObjectEventListView(EventListView): return context def get_object(self): - obj = self._get_object() - AccessControlList.objects.check_access( - permissions=permission_events_view, user=self.request.user, - obj=obj + content_type = get_object_or_404( + klass=ContentType, app_label=self.kwargs['app_label'], + model=self.kwargs['model'] + ) + + queryset = AccessControlList.objects.restrict_queryset( + permission=permission_events_view, + queryset=content_type.model_class().objects.all(), + user=self.request.user + ) + return get_object_or_404( + klass=queryset, pk=self.kwargs['object_id'] ) - return obj def get_object_list(self): return any_stream(self.get_object()) @@ -197,40 +203,21 @@ class ObjectEventTypeSubscriptionListView(FormView): instance.save() except Exception as exception: messages.error( - self.request, - _('Error updating object event subscription; %s') % exception + message=_( + 'Error updating object event subscription; %s' + ) % exception, request=self.request ) else: messages.success( - self.request, _( + message=_( 'Object event subscriptions updated successfully.' - ) + ), request=self.request ) return super( ObjectEventTypeSubscriptionListView, self ).form_valid(form=form) - def get_object(self): - object_content_type = get_object_or_404( - klass=ContentType, app_label=self.kwargs['app_label'], - model=self.kwargs['model'] - ) - - try: - content_object = object_content_type.get_object_for_this_type( - pk=self.kwargs['object_id'] - ) - except object_content_type.model_class().DoesNotExist: - raise Http404 - - AccessControlList.objects.check_access( - permissions=permission_events_view, user=self.request.user, - obj=content_object - ) - - return content_object - def get_extra_context(self): return { 'form_display_mode_table': True, @@ -245,13 +232,30 @@ class ObjectEventTypeSubscriptionListView(FormView): initial = [] for element in self.get_queryset(): - initial.append({ - 'user': self.request.user, - 'object': obj, - 'stored_event_type': element, - }) + initial.append( + { + 'user': self.request.user, + 'object': obj, + 'stored_event_type': element, + } + ) return initial + def get_object(self): + content_type = get_object_or_404( + klass=ContentType, app_label=self.kwargs['app_label'], + model=self.kwargs['model'] + ) + + queryset = AccessControlList.objects.restrict_queryset( + permission=permission_events_view, + queryset=content_type.model_class().objects.all(), + user=self.request.user + ) + return get_object_or_404( + klass=queryset, pk=self.kwargs['object_id'] + ) + def get_queryset(self): return ModelEventType.get_for_instance(instance=self.get_object()) diff --git a/mayan/apps/events/widgets.py b/mayan/apps/events/widgets.py index 2acacdfd37..5b3d75a9fa 100644 --- a/mayan/apps/events/widgets.py +++ b/mayan/apps/events/widgets.py @@ -49,7 +49,7 @@ def widget_event_type_link(context, attribute=None): return mark_safe( '%(label)s' % { - 'url': reverse('events:events_by_verb', kwargs={'verb': entry.verb}), + 'url': reverse(viewname='events:events_by_verb', kwargs={'verb': entry.verb}), 'label': EventType.get(name=entry.verb) } ) From 09edab50272247ead19c9e8b44ee24bc8df53f22 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Mon, 21 Jan 2019 02:50:34 -0400 Subject: [PATCH 030/209] Update lock managet app Add keyword arguments. Sort imports. Move settings and test literals to their own module. Signed-off-by: Roberto Rosario --- mayan/apps/lock_manager/backends/file_lock.py | 10 +++--- mayan/apps/lock_manager/decorators.py | 2 +- mayan/apps/lock_manager/exceptions.py | 5 ++- mayan/apps/lock_manager/literals.py | 4 +++ .../lock_manager/migrations/0001_initial.py | 2 +- .../migrations/0002_auto_20150604_2219.py | 2 +- mayan/apps/lock_manager/models.py | 2 +- mayan/apps/lock_manager/runtime.py | 2 +- mayan/apps/lock_manager/settings.py | 5 ++- mayan/apps/lock_manager/tests/literals.py | 3 ++ .../apps/lock_manager/tests/test_backends.py | 34 +++++++++++-------- 11 files changed, 43 insertions(+), 28 deletions(-) create mode 100644 mayan/apps/lock_manager/literals.py create mode 100644 mayan/apps/lock_manager/tests/literals.py diff --git a/mayan/apps/lock_manager/backends/file_lock.py b/mayan/apps/lock_manager/backends/file_lock.py index 29e4b16f79..459d91e044 100644 --- a/mayan/apps/lock_manager/backends/file_lock.py +++ b/mayan/apps/lock_manager/backends/file_lock.py @@ -1,8 +1,8 @@ from __future__ import unicode_literals import hashlib -import logging import json +import logging import os import threading import time @@ -23,7 +23,9 @@ lock = threading.Lock() logger = logging.getLogger(__name__) lock_file = os.path.join( - setting_temporary_directory.value, hashlib.sha256(force_bytes(settings.SECRET_KEY)).hexdigest() + setting_temporary_directory.value, hashlib.sha256( + force_bytes(settings.SECRET_KEY) + ).hexdigest() ) open(lock_file, 'a').close() logger.debug('lock_file: %s', lock_file) @@ -76,7 +78,7 @@ class FileLock(LockingBackend): data = file_object.read() if data: - file_locks = json.loads(data) + file_locks = json.loads(s=data) else: file_locks = {} @@ -103,7 +105,7 @@ class FileLock(LockingBackend): with open(self.__class__.lock_file, 'r+') as file_object: locks.lock(f=file_object, flags=locks.LOCK_EX) try: - file_locks = json.loads(file_object.read()) + file_locks = json.loads(s=file_object.read()) except EOFError: file_locks = {} diff --git a/mayan/apps/lock_manager/decorators.py b/mayan/apps/lock_manager/decorators.py index 87aa4cc396..ac21ec11d1 100644 --- a/mayan/apps/lock_manager/decorators.py +++ b/mayan/apps/lock_manager/decorators.py @@ -1,7 +1,7 @@ from __future__ import absolute_import, unicode_literals -import time import random +import time from .exceptions import LockError diff --git a/mayan/apps/lock_manager/exceptions.py b/mayan/apps/lock_manager/exceptions.py index cede7b1642..027a277943 100644 --- a/mayan/apps/lock_manager/exceptions.py +++ b/mayan/apps/lock_manager/exceptions.py @@ -1,2 +1,5 @@ +from __future__ import unicode_literals + + class LockError(Exception): - pass + """Raised when trying to acquire an existing lock""" diff --git a/mayan/apps/lock_manager/literals.py b/mayan/apps/lock_manager/literals.py new file mode 100644 index 0000000000..fa84c33e02 --- /dev/null +++ b/mayan/apps/lock_manager/literals.py @@ -0,0 +1,4 @@ +from __future__ import unicode_literals + +DEFAULT_BACKEND = 'mayan.apps.lock_manager.backends.file_lock.FileLock' +DEFAULT_LOCK_TIMEOUT_VALUE = 30 diff --git a/mayan/apps/lock_manager/migrations/0001_initial.py b/mayan/apps/lock_manager/migrations/0001_initial.py index 68d0e3f782..4c7013ecb3 100644 --- a/mayan/apps/lock_manager/migrations/0001_initial.py +++ b/mayan/apps/lock_manager/migrations/0001_initial.py @@ -1,6 +1,6 @@ from __future__ import unicode_literals -from django.db import models, migrations +from django.db import migrations, models class Migration(migrations.Migration): diff --git a/mayan/apps/lock_manager/migrations/0002_auto_20150604_2219.py b/mayan/apps/lock_manager/migrations/0002_auto_20150604_2219.py index d0be8deab5..4f47307d5c 100644 --- a/mayan/apps/lock_manager/migrations/0002_auto_20150604_2219.py +++ b/mayan/apps/lock_manager/migrations/0002_auto_20150604_2219.py @@ -1,6 +1,6 @@ from __future__ import unicode_literals -from django.db import models, migrations +from django.db import migrations, models class Migration(migrations.Migration): diff --git a/mayan/apps/lock_manager/models.py b/mayan/apps/lock_manager/models.py index d409036201..610c60ba35 100644 --- a/mayan/apps/lock_manager/models.py +++ b/mayan/apps/lock_manager/models.py @@ -38,7 +38,7 @@ class Lock(models.Model): """ try: lock = Lock.objects.get( - name=self.name, creation_datetime=self.creation_datetime + creation_datetime=self.creation_datetime, name=self.name ) except Lock.DoesNotExist: # Our lock has expired and was reassigned diff --git a/mayan/apps/lock_manager/runtime.py b/mayan/apps/lock_manager/runtime.py index 3c4a54de02..27e8d636b5 100644 --- a/mayan/apps/lock_manager/runtime.py +++ b/mayan/apps/lock_manager/runtime.py @@ -2,4 +2,4 @@ from django.utils.module_loading import import_string from .settings import setting_backend -locking_backend = import_string(setting_backend.value) +locking_backend = import_string(dotted_path=setting_backend.value) diff --git a/mayan/apps/lock_manager/settings.py b/mayan/apps/lock_manager/settings.py index defbc79a99..d401b4c462 100644 --- a/mayan/apps/lock_manager/settings.py +++ b/mayan/apps/lock_manager/settings.py @@ -4,10 +4,9 @@ from django.utils.translation import ugettext_lazy as _ from mayan.apps.smart_settings import Namespace -DEFAULT_BACKEND = 'mayan.apps.lock_manager.backends.file_lock.FileLock' -DEFAULT_LOCK_TIMEOUT_VALUE = 30 +from .literals import DEFAULT_BACKEND, DEFAULT_LOCK_TIMEOUT_VALUE -namespace = Namespace(name='lock_manager', label=_('Lock manager')) +namespace = Namespace(label=_('Lock manager'), name='lock_manager') setting_backend = namespace.add_setting( default=DEFAULT_BACKEND, diff --git a/mayan/apps/lock_manager/tests/literals.py b/mayan/apps/lock_manager/tests/literals.py new file mode 100644 index 0000000000..d7d23429e6 --- /dev/null +++ b/mayan/apps/lock_manager/tests/literals.py @@ -0,0 +1,3 @@ +from __future__ import unicode_literals + +TEST_LOCK_NAME = 'test lock name' diff --git a/mayan/apps/lock_manager/tests/test_backends.py b/mayan/apps/lock_manager/tests/test_backends.py index 97844c1207..a0c41599d0 100644 --- a/mayan/apps/lock_manager/tests/test_backends.py +++ b/mayan/apps/lock_manager/tests/test_backends.py @@ -7,51 +7,53 @@ from django.utils.module_loading import import_string from ..exceptions import LockError -TEST_LOCK_1 = 'test lock 1' +from .literals import TEST_LOCK_NAME class FileLockTestCase(TestCase): backend_string = 'mayan.apps.lock_manager.backends.file_lock.FileLock' def setUp(self): - self.locking_backend = import_string(self.backend_string) + self.locking_backend = import_string(dotted_path=self.backend_string) def test_exclusive(self): - lock_1 = self.locking_backend.acquire_lock(name=TEST_LOCK_1) + lock_1 = self.locking_backend.acquire_lock(name=TEST_LOCK_NAME) with self.assertRaises(LockError): - self.locking_backend.acquire_lock(name=TEST_LOCK_1) + self.locking_backend.acquire_lock(name=TEST_LOCK_NAME) # Cleanup lock_1.release() def test_release(self): - lock_1 = self.locking_backend.acquire_lock(name=TEST_LOCK_1) + lock_1 = self.locking_backend.acquire_lock(name=TEST_LOCK_NAME) lock_1.release() - lock_2 = self.locking_backend.acquire_lock(name=TEST_LOCK_1) + lock_2 = self.locking_backend.acquire_lock(name=TEST_LOCK_NAME) # Cleanup lock_2.release() def test_timeout_expired(self): - self.locking_backend.acquire_lock(name=TEST_LOCK_1, timeout=1) + self.locking_backend.acquire_lock(name=TEST_LOCK_NAME, timeout=1) # lock_1 not release and not expired, should raise LockError with self.assertRaises(LockError): - self.locking_backend.acquire_lock(name=TEST_LOCK_1) + self.locking_backend.acquire_lock(name=TEST_LOCK_NAME) time.sleep(1.01) # lock_1 not release but has expired, should not raise LockError - lock_2 = self.locking_backend.acquire_lock(name=TEST_LOCK_1) + lock_2 = self.locking_backend.acquire_lock(name=TEST_LOCK_NAME) # Cleanup lock_2.release() def test_double_release(self): - lock_1 = self.locking_backend.acquire_lock(name=TEST_LOCK_1) + lock_1 = self.locking_backend.acquire_lock(name=TEST_LOCK_NAME) lock_1.release() def test_release_expired(self): - lock_1 = self.locking_backend.acquire_lock(name=TEST_LOCK_1, timeout=1) + lock_1 = self.locking_backend.acquire_lock( + name=TEST_LOCK_NAME, timeout=1 + ) time.sleep(1.01) lock_1.release() # No exception is raised even though the lock has expired. @@ -60,11 +62,13 @@ class FileLockTestCase(TestCase): # would be successfull, even after an extended lapse of time def test_release_expired_reaquired(self): - self.locking_backend.acquire_lock(name=TEST_LOCK_1, timeout=1) + self.locking_backend.acquire_lock(name=TEST_LOCK_NAME, timeout=1) time.sleep(1.01) - # TEST_LOCK_1 is expired so trying to acquire it should not return an - # error. - lock_2 = self.locking_backend.acquire_lock(name=TEST_LOCK_1, timeout=1) + # TEST_LOCK_NAME is expired so trying to acquire it should not return + # an error. + lock_2 = self.locking_backend.acquire_lock( + name=TEST_LOCK_NAME, timeout=1 + ) # Cleanup lock_2.release() From 166183dff96b458b01a8d123c5943cc6ace8e8c7 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Mon, 21 Jan 2019 03:31:19 -0400 Subject: [PATCH 031/209] Update metadata app Sort arguments. Add keyword arguments. Update URL parameters to the '_id' form. Remove use of .check_access() from views. Sort methods. Signed-off-by: Roberto Rosario --- mayan/apps/metadata/apps.py | 8 +- mayan/apps/metadata/events.py | 23 +-- mayan/apps/metadata/handlers.py | 46 ++--- mayan/apps/metadata/links.py | 38 ++-- mayan/apps/metadata/models.py | 5 +- mayan/apps/metadata/parsers.py | 8 +- mayan/apps/metadata/permissions.py | 21 ++- mayan/apps/metadata/queues.py | 10 +- mayan/apps/metadata/serializers.py | 31 ++-- mayan/apps/metadata/settings.py | 2 +- mayan/apps/metadata/tests/mixins.py | 13 +- mayan/apps/metadata/tests/test_views.py | 24 ++- .../apps/metadata/tests/test_wizard_steps.py | 4 +- mayan/apps/metadata/urls.py | 80 ++++----- mayan/apps/metadata/views.py | 169 ++++++++++-------- 15 files changed, 252 insertions(+), 230 deletions(-) diff --git a/mayan/apps/metadata/apps.py b/mayan/apps/metadata/apps.py index 7e2a641989..6058d3b977 100644 --- a/mayan/apps/metadata/apps.py +++ b/mayan/apps/metadata/apps.py @@ -33,9 +33,9 @@ from .events import ( event_metadata_type_relationship ) from .handlers import ( - handler_post_document_type_metadata_type_add, + handler_index_document, handler_post_document_type_metadata_type_add, handler_post_document_type_metadata_type_delete, - handler_post_document_type_change_metadata, handler_index_document, + handler_post_document_type_change, ) from .links import ( link_document_metadata_add, link_document_metadata_edit, @@ -255,8 +255,8 @@ class MetadataApp(MayanAppConfig): sender=DocumentTypeMetadataType ) post_document_type_change.connect( - dispatch_uid='metadata_handler_post_document_type_change_metadata', - receiver=handler_post_document_type_change_metadata, + dispatch_uid='metadata_handler_post_document_type_change', + receiver=handler_post_document_type_change, sender=Document ) post_save.connect( diff --git a/mayan/apps/metadata/events.py b/mayan/apps/metadata/events.py index 769eb07119..73c650ee74 100644 --- a/mayan/apps/metadata/events.py +++ b/mayan/apps/metadata/events.py @@ -4,31 +4,24 @@ from django.utils.translation import ugettext_lazy as _ from mayan.apps.events import EventTypeNamespace -namespace = EventTypeNamespace(name='metadata', label=_('Metadata')) +namespace = EventTypeNamespace(label=_('Metadata'), name='metadata') event_document_metadata_added = namespace.add_event_type( - name='document_metadata_added', label=_( - 'Document metadata added' - ) + label=_('Document metadata added'), name='document_metadata_added' ) event_document_metadata_edited = namespace.add_event_type( - name='document_metadata_edited', label=_( - 'Document metadata edited' - ) + label=_('Document metadata edited'), name='document_metadata_edited' ) event_document_metadata_removed = namespace.add_event_type( - name='document_metadata_removed', label=_( - 'Document metadata removed' - ) + label=_('Document metadata removed'), name='document_metadata_removed' ) event_metadata_type_created = namespace.add_event_type( - name='metadata_type_created', label=_('Metadata type created') + label=_('Metadata type created'), name='metadata_type_created' ) event_metadata_type_edited = namespace.add_event_type( - name='metadata_type_edited', label=_('Metadata type edited') + label=_('Metadata type edited'), name='metadata_type_edited' ) event_metadata_type_relationship = namespace.add_event_type( - name='metadata_type_relationship', label=_( - 'Metadata type relationship updated' - ) + label=_('Metadata type relationship updated'), + name='metadata_type_relationship' ) diff --git a/mayan/apps/metadata/handlers.py b/mayan/apps/metadata/handlers.py index efe21b0faa..97da1133a1 100644 --- a/mayan/apps/metadata/handlers.py +++ b/mayan/apps/metadata/handlers.py @@ -11,29 +11,7 @@ from .tasks import task_add_required_metadata_type, task_remove_metadata_type logger = logging.getLogger(__name__) -def handler_post_document_type_metadata_type_add(sender, instance, created, **kwargs): - logger.debug('instance: %s', instance) - - if created and instance.required: - task_add_required_metadata_type.apply_async( - kwargs={ - 'document_type_id': instance.document_type.pk, - 'metadata_type_id': instance.metadata_type.pk - } - ) - - -def handler_post_document_type_metadata_type_delete(sender, instance, **kwargs): - logger.debug('instance: %s', instance) - task_remove_metadata_type.apply_async( - kwargs={ - 'document_type_id': instance.document_type.pk, - 'metadata_type_id': instance.metadata_type.pk - } - ) - - -def handler_post_document_type_change_metadata(sender, instance, **kwargs): +def handler_post_document_type_change(sender, instance, **kwargs): logger.debug('received post_document_type_change') logger.debug('instance: %s', instance) @@ -73,6 +51,28 @@ def handler_post_document_type_change_metadata(sender, instance, **kwargs): ) +def handler_post_document_type_metadata_type_add(sender, instance, created, **kwargs): + logger.debug('instance: %s', instance) + + if created and instance.required: + task_add_required_metadata_type.apply_async( + kwargs={ + 'document_type_id': instance.document_type.pk, + 'metadata_type_id': instance.metadata_type.pk + } + ) + + +def handler_post_document_type_metadata_type_delete(sender, instance, **kwargs): + logger.debug('instance: %s', instance) + task_remove_metadata_type.apply_async( + kwargs={ + 'document_type_id': instance.document_type.pk, + 'metadata_type_id': instance.metadata_type.pk + } + ) + + def handler_index_document(sender, **kwargs): task_index_document.apply_async( kwargs=dict(document_id=kwargs['instance'].document.pk) diff --git a/mayan/apps/metadata/links.py b/mayan/apps/metadata/links.py index c7c1181a5f..75869e0525 100644 --- a/mayan/apps/metadata/links.py +++ b/mayan/apps/metadata/links.py @@ -22,15 +22,27 @@ from .permissions import ( ) link_document_metadata_add = Link( - args='object.pk', icon_class=icon_document_metadata_add, + icon_class=icon_document_metadata_add, kwargs={'document_id': 'object.pk'}, permissions=(permission_document_metadata_add,), text=_('Add metadata'), view='metadata:document_metadata_add', ) link_document_metadata_edit = Link( - args='object.pk', icon_class=icon_document_metadata_edit, + icon_class=icon_document_metadata_edit, kwargs={'document_id': 'object.pk'}, permissions=(permission_document_metadata_edit,), text=_('Edit metadata'), view='metadata:document_metadata_edit' ) +link_document_metadata_remove = Link( + icon_class=icon_document_metadata_remove, + kwargs={'document_id': 'object.pk'}, + permissions=(permission_document_metadata_remove,), + text=_('Remove metadata'), view='metadata:document_metadata_remove', +) +link_document_metadata_view = Link( + icon_class=icon_document_metadata_view, + kwargs={'document_id': 'resolved_object.pk'}, + permissions=(permission_document_metadata_view,), text=_('Metadata'), + view='metadata:document_metadata_view', +) link_document_multiple_metadata_add = Link( icon_class=icon_document_multiple_metadata_add, text=_('Add metadata'), view='metadata:document_multiple_metadata_add' @@ -44,23 +56,15 @@ link_document_multiple_metadata_remove = Link( text=_('Remove metadata'), view='metadata:document_multiple_metadata_remove' ) -link_document_metadata_remove = Link( - args='object.pk', icon_class=icon_document_metadata_remove, - permissions=(permission_document_metadata_remove,), - text=_('Remove metadata'), view='metadata:document_metadata_remove', -) -link_document_metadata_view = Link( - args='resolved_object.pk', icon_class=icon_document_metadata_view, - permissions=(permission_document_metadata_view,), text=_('Metadata'), - view='metadata:document_metadata_view', -) link_document_type_metadata_types = Link( - args='resolved_object.pk', icon_class=icon_document_type_metadata_types, + icon_class=icon_document_type_metadata_types, + kwargs={'document_type_id': 'resolved_object.pk'}, permissions=(permission_document_type_edit,), text=_('Metadata types'), view='metadata:document_type_metadata_types', ) link_metadata_type_document_types = Link( - args='resolved_object.pk', icon_class=icon_document_type, + icon_class=icon_document_type, + kwargs={'metadata_type_id': 'resolved_object.pk'}, permissions=(permission_document_type_edit,), text=_('Document types'), view='metadata:metadata_type_document_types', ) @@ -70,12 +74,14 @@ link_metadata_type_create = Link( view='metadata:metadata_type_create' ) link_metadata_type_delete = Link( - args='object.pk', icon_class=icon_metadata_type_delete, + icon_class=icon_metadata_type_delete, + kwargs={'metadata_type_id': 'object.pk'}, permissions=(permission_metadata_type_delete,), tags='dangerous', text=_('Delete'), view='metadata:metadata_type_delete', ) link_metadata_type_edit = Link( - args='object.pk', icon_class=icon_metadata_type_edit, + icon_class=icon_metadata_type_edit, + kwargs={'metadata_type_id': 'object.pk'}, permissions=(permission_metadata_type_edit,), text=_('Edit'), view='metadata:metadata_type_edit' ) diff --git a/mayan/apps/metadata/models.py b/mayan/apps/metadata/models.py index c7ac3e883b..f5ccdb319e 100644 --- a/mayan/apps/metadata/models.py +++ b/mayan/apps/metadata/models.py @@ -98,7 +98,10 @@ class MetadataType(models.Model): return self.label def get_absolute_url(self): - return reverse('metadata:metadata_type_edit', kwargs={'pk': self.pk}) + return reverse( + viewname='metadata:metadata_type_edit', + kwargs={'metadata_type_id': self.pk} + ) if PY2: # Python 2 non unicode version diff --git a/mayan/apps/metadata/parsers.py b/mayan/apps/metadata/parsers.py index a3f7beb984..f1fb558a3b 100644 --- a/mayan/apps/metadata/parsers.py +++ b/mayan/apps/metadata/parsers.py @@ -8,10 +8,6 @@ from django.core.exceptions import ValidationError class MetadataParser(object): _registry = [] - @classmethod - def register(cls, parser): - cls._registry.append(parser) - @classmethod def get_all(cls): return cls._registry @@ -24,6 +20,10 @@ class MetadataParser(object): def get_import_paths(cls): return [validator.get_import_path() for validator in cls.get_all()] + @classmethod + def register(cls, parser): + cls._registry.append(parser) + def execute(self, input_data): raise NotImplementedError diff --git a/mayan/apps/metadata/permissions.py b/mayan/apps/metadata/permissions.py index 0d809d89a1..e1c35fa460 100644 --- a/mayan/apps/metadata/permissions.py +++ b/mayan/apps/metadata/permissions.py @@ -4,35 +4,34 @@ from django.utils.translation import ugettext_lazy as _ from mayan.apps.permissions import PermissionNamespace -namespace = PermissionNamespace(name='metadata', label=_('Metadata')) +namespace = PermissionNamespace(label=_('Metadata'), name='metadata') permission_document_metadata_add = namespace.add_permission( - name='metadata_document_add', label=_('Add metadata to a document') + label=_('Add metadata to a document'), name='metadata_document_add' ) permission_document_metadata_edit = namespace.add_permission( - name='metadata_document_edit', label=_('Edit a document\'s metadata') + label=_('Edit a document\'s metadata'), name='metadata_document_edit' ) permission_document_metadata_remove = namespace.add_permission( - name='metadata_document_remove', - label=_('Remove metadata from a document') + label=_('Remove metadata from a document'), name='metadata_document_remove' ) permission_document_metadata_view = namespace.add_permission( - name='metadata_document_view', label=_('View metadata from a document') + label=_('View metadata from a document'), name='metadata_document_view' ) setup_namespace = PermissionNamespace( - name='metadata_setup', label=_('Metadata setup') + label=_('Metadata setup'), name='metadata_setup' ) permission_metadata_type_create = setup_namespace.add_permission( - name='metadata_type_create', label=_('Create new metadata types') + label=_('Create new metadata types'), name='metadata_type_create' ) permission_metadata_type_delete = setup_namespace.add_permission( - name='metadata_type_delete', label=_('Delete metadata types') + label=_('Delete metadata types'), name='metadata_type_delete' ) permission_metadata_type_edit = setup_namespace.add_permission( - name='metadata_type_edit', label=_('Edit metadata types') + label=_('Edit metadata types'), name='metadata_type_edit' ) permission_metadata_type_view = setup_namespace.add_permission( - name='metadata_type_view', label=_('View metadata types') + label=_('View metadata types'), name='metadata_type_view' ) diff --git a/mayan/apps/metadata/queues.py b/mayan/apps/metadata/queues.py index 16775657fb..c57d66cc4c 100644 --- a/mayan/apps/metadata/queues.py +++ b/mayan/apps/metadata/queues.py @@ -5,13 +5,13 @@ from django.utils.translation import ugettext_lazy as _ from mayan.apps.task_manager.classes import CeleryQueue queue_metadata = CeleryQueue( - name='metadata', label=_('Metadata') + label=_('Metadata'), name='metadata' ) queue_metadata.add_task_type( - name='mayan.apps.metadata.tasks.task_remove_metadata_type', - label=_('Remove metadata type') + label=_('Remove metadata type'), + name='mayan.apps.metadata.tasks.task_remove_metadata_type' ) queue_metadata.add_task_type( - name='mayan.apps.metadata.tasks.task_add_required_metadata_type', - label=_('Add required metadata type') + label=_('Add required metadata type'), + name='mayan.apps.metadata.tasks.task_add_required_metadata_type' ) diff --git a/mayan/apps/metadata/serializers.py b/mayan/apps/metadata/serializers.py index c611e55556..c8f47d2bf9 100644 --- a/mayan/apps/metadata/serializers.py +++ b/mayan/apps/metadata/serializers.py @@ -40,9 +40,9 @@ class DocumentTypeMetadataTypeSerializer(serializers.HyperlinkedModelSerializer) def get_url(self, instance): return reverse( - 'rest_api:documenttypemetadatatype-detail', args=( - instance.document_type.pk, instance.pk - ), request=self.context['request'], format=self.context['format'] + viewname='rest_api:documenttypemetadatatype-detail', kwargs={ + 'document_type_pk': instance.document_type.pk, 'metadata_type': instance.pk + }, request=self.context['request'], format=self.context['format'] ) @@ -61,9 +61,9 @@ class NewDocumentTypeMetadataTypeSerializer(serializers.ModelSerializer): def get_url(self, instance): return reverse( - 'rest_api:documenttypemetadatatype-detail', args=( - instance.document_type.pk, instance.pk - ), request=self.context['request'], format=self.context['format'] + viewname='rest_api:documenttypemetadatatype-detail', kwargs={ + 'document_type': instance.document_type.pk, 'metadata_type': instance.pk + }, request=self.context['request'], format=self.context['format'] ) def validate(self, attrs): @@ -92,9 +92,10 @@ class WritableDocumentTypeMetadataTypeSerializer(serializers.ModelSerializer): def get_url(self, instance): return reverse( - 'rest_api:documenttypemetadatatype-detail', args=( - instance.document_type.pk, instance.pk - ), request=self.context['request'], format=self.context['format'] + viewname='rest_api:documenttypemetadatatype-detail', kwargs={ + 'document_type_pk': instance.document_type.pk, + 'metadata_type': instance.pk + }, request=self.context['request'], format=self.context['format'] ) @@ -110,9 +111,9 @@ class DocumentMetadataSerializer(serializers.HyperlinkedModelSerializer): def get_url(self, instance): return reverse( - 'rest_api:documentmetadata-detail', args=( - instance.document.pk, instance.pk - ), request=self.context['request'], format=self.context['format'] + viewname='rest_api:documentmetadata-detail', kwargs={ + 'document_pk': instance.document.pk, 'metadata_pk': instance.pk + }, request=self.context['request'], format=self.context['format'] ) def validate(self, attrs): @@ -141,9 +142,9 @@ class NewDocumentMetadataSerializer(serializers.ModelSerializer): def get_url(self, instance): return reverse( - 'rest_api:documentmetadata-detail', args=( - instance.document.pk, instance.pk - ), request=self.context['request'], format=self.context['format'] + viewname='rest_api:documentmetadata-detail', kwargs={ + 'document_pk': instance.document.pk, 'metadata_pk': instance.pk + }, request=self.context['request'], format=self.context['format'] ) def validate(self, attrs): diff --git a/mayan/apps/metadata/settings.py b/mayan/apps/metadata/settings.py index 9dfaffa4e5..ce7aa6de35 100644 --- a/mayan/apps/metadata/settings.py +++ b/mayan/apps/metadata/settings.py @@ -7,7 +7,7 @@ from mayan.apps.smart_settings import Namespace from .parsers import MetadataParser from .validators import MetadataValidator -namespace = Namespace(name='metadata', label=_('Metadata')) +namespace = Namespace(label=_('Metadata'), name='metadata') setting_available_validators = namespace.add_setting( global_name='METADATA_AVAILABLE_VALIDATORS', diff --git a/mayan/apps/metadata/tests/mixins.py b/mayan/apps/metadata/tests/mixins.py index 83ef32c2b6..1ec5dcd64e 100644 --- a/mayan/apps/metadata/tests/mixins.py +++ b/mayan/apps/metadata/tests/mixins.py @@ -33,15 +33,15 @@ class MetadataTestsMixin(object): def _request_metadata_type_delete_view(self): return self.post( - viewname='metadata:metadata_type_delete', args=( - self.metadata_type.pk, - ), + viewname='metadata:metadata_type_delete', + kwargs={'metadata_type_id': self.metadata_type.pk} ) def _request_metadata_type_edit_view(self): return self.post( - viewname='metadata:metadata_type_edit', args=( - self.metadata_type.pk,), data={ + viewname='metadata:metadata_type_edit', + kwargs={'metadata_type_id': self.metadata_type.pk}, + data={ 'label': TEST_METADATA_TYPE_LABEL_EDITED, 'name': TEST_METADATA_TYPE_NAME_EDITED } @@ -53,7 +53,8 @@ class MetadataTestsMixin(object): return self.post( viewname='metadata:metadata_type_document_types', - args=(self.metadata_type.pk,), data={ + kwargs={'metadata_type_id': self.metadata_type.pk}, + data={ 'form-TOTAL_FORMS': '1', 'form-INITIAL_FORMS': '0', 'form-0-relationship_type': 'required', diff --git a/mayan/apps/metadata/tests/test_views.py b/mayan/apps/metadata/tests/test_views.py index 7eff3c794b..b76afbbdf4 100644 --- a/mayan/apps/metadata/tests/test_views.py +++ b/mayan/apps/metadata/tests/test_views.py @@ -38,13 +38,13 @@ class DocumentMetadataTestCase(MetadataTestsMixin, GenericDocumentViewTestCase): def _request_get_document_metadata_add_view(self): return self.get( viewname='metadata:document_metadata_add', - kwargs={'pk': self.document.pk}, + kwargs={'document_id': self.document.pk}, ) def _request_post_document_metadata_add_view(self): return self.post( viewname='metadata:document_metadata_add', - kwargs={'pk': self.document.pk}, + kwargs={'document_id': self.document.pk}, data={'metadata_type': self.metadata_type.pk} ) @@ -109,14 +109,14 @@ class DocumentMetadataTestCase(MetadataTestsMixin, GenericDocumentViewTestCase): response = self.get( viewname='metadata:document_metadata_edit', - kwargs={'pk': self.document.pk} + kwargs={'document_id': self.document.pk} ) self.assertContains(response, 'Edit', status_code=200) response = self.post( viewname='metadata:document_metadata_edit', - kwargs={'pk': self.document.pk}, data={ + kwargs={'document_id': self.document.pk}, data={ 'form-0-id': document_metadata_2.metadata_type.pk, 'form-0-update': True, 'form-0-value': TEST_DOCUMENT_METADATA_VALUE_2, @@ -136,13 +136,13 @@ class DocumentMetadataTestCase(MetadataTestsMixin, GenericDocumentViewTestCase): def _request_get_document_document_metadata_remove_view(self): return self.get( viewname='metadata:document_metadata_remove', - kwargs={'pk': self.document.pk} + kwargs={'document_id': self.document.pk} ) def _request_post_document_document_metadata_remove_view(self): return self.post( viewname='metadata:document_metadata_remove', - kwargs={'pk': self.document.pk}, data={ + kwargs={'document_id': self.document.pk}, data={ 'form-0-id': self.document_metadata.metadata_type.pk, 'form-0-update': True, 'form-TOTAL_FORMS': '1', @@ -338,7 +338,7 @@ class DocumentMetadataTestCase(MetadataTestsMixin, GenericDocumentViewTestCase): self.post( viewname='metadata:document_metadata_add', - kwargs={'pk': self.document.pk}, data={ + kwargs={'document_id': self.document.pk}, data={ 'metadata_type': [self.metadata_type.pk, metadata_type_2.pk], } ) @@ -385,7 +385,7 @@ class MetadataTypeViewTestCase(DocumentTestMixin, MetadataTestsMixin, GenericVie response = self._request_metadata_type_delete_view() - self.assertEqual(response.status_code, 403) + self.assertEqual(response.status_code, 404) self.assertQuerysetEqual( qs=MetadataType.objects.values('label', 'name'), values=[ @@ -414,7 +414,7 @@ class MetadataTypeViewTestCase(DocumentTestMixin, MetadataTestsMixin, GenericVie response = self._request_metadata_type_edit_view() - self.assertEqual(response.status_code, 403) + self.assertEqual(response.status_code, 404) self.assertQuerysetEqual( qs=MetadataType.objects.values('label', 'name'), values=[ @@ -472,8 +472,7 @@ class MetadataTypeViewTestCase(DocumentTestMixin, MetadataTestsMixin, GenericVie self.upload_document() response = self._request_metadata_type_relationship_edit_view() - - self.assertEqual(response.status_code, 403) + self.assertEqual(response.status_code, 404) self.document_type.refresh_from_db() @@ -489,8 +488,7 @@ class MetadataTypeViewTestCase(DocumentTestMixin, MetadataTestsMixin, GenericVie ) response = self._request_metadata_type_relationship_edit_view() - - self.assertEqual(response.status_code, 403) + self.assertEqual(response.status_code, 404) self.document_type.refresh_from_db() diff --git a/mayan/apps/metadata/tests/test_wizard_steps.py b/mayan/apps/metadata/tests/test_wizard_steps.py index 045324a457..b513ba4922 100644 --- a/mayan/apps/metadata/tests/test_wizard_steps.py +++ b/mayan/apps/metadata/tests/test_wizard_steps.py @@ -36,7 +36,7 @@ class DocumentUploadMetadataTestCase(MetadataTypeTestMixin, GenericDocumentViewT ) def test_upload_interactive_with_unicode_metadata(self): - url = furl(reverse('sources:upload_interactive')) + url = furl(reverse(viewname='sources:upload_interactive')) url.args['metadata0_id'] = self.metadata_type.pk url.args['metadata0_value'] = TEST_METADATA_VALUE_UNICODE @@ -60,7 +60,7 @@ class DocumentUploadMetadataTestCase(MetadataTypeTestMixin, GenericDocumentViewT ) def test_upload_interactive_with_ampersand_metadata(self): - url = furl(reverse('sources:upload_interactive')) + url = furl(reverse(viewname='sources:upload_interactive')) url.args['metadata0_id'] = self.metadata_type.pk url.args['metadata0_value'] = TEST_METADATA_VALUE_WITH_AMPERSAND diff --git a/mayan/apps/metadata/urls.py b/mayan/apps/metadata/urls.py index bf5a94dfa2..d070ed5576 100644 --- a/mayan/apps/metadata/urls.py +++ b/mayan/apps/metadata/urls.py @@ -17,8 +17,34 @@ from .views import ( urlpatterns = [ url( - regex=r'^documents/(?P\d+)/edit/$', name='document_metadata_edit', - view=DocumentMetadataEditView.as_view() + regex=r'^metadata_types/$', name='metadata_type_list', + view=MetadataTypeListView.as_view() + ), + url( + regex=r'^metadata_types/create/$', name='metadata_type_create', + view=MetadataTypeCreateView.as_view() + ), + url( + regex=r'^metadata_types/(?P\d+)/delete/$', + name='metadata_type_delete', view=MetadataTypeDeleteView.as_view() + ), + url( + regex=r'^metadata_types/(?P\d+)/edit/$', + name='metadata_type_edit', view=MetadataTypeEditView.as_view() + ), + url( + regex=r'^metadata_types/(?P\d+)/document_types/$', + name='metadata_type_document_types', + view=SetupMetadataTypesDocumentTypes.as_view() + ), + url( + regex=r'^document_types/(?P\d+)/metadata_types/$', + name='document_type_metadata_types', + view=SetupDocumentTypeMetadataTypes.as_view() + ), + url( + regex=r'^documents/(?P\d+)/edit/$', + name='document_metadata_edit', view=DocumentMetadataEditView.as_view() ), url( regex=r'^documents/multiple/edit/$', @@ -26,12 +52,13 @@ urlpatterns = [ view=DocumentMetadataEditView.as_view() ), url( - regex=r'^documents/(?P\d+)/view/$', name='document_metadata_view', + regex=r'^documents/(?P\d+)/view/$', + name='document_metadata_view', view=DocumentMetadataListView.as_view() ), url( - regex=r'^documents/(?P\d+)/add/$', name='document_metadata_add', - view=DocumentMetadataAddView.as_view() + regex=r'^documents/(?P\d+)/add/$', + name='document_metadata_add', view=DocumentMetadataAddView.as_view() ), url( regex=r'^documents/multiple/add/$', @@ -39,7 +66,7 @@ urlpatterns = [ view=DocumentMetadataAddView.as_view() ), url( - regex=r'^documents/(?P\d+)/remove/$', + regex=r'^documents/(?P\d+)/remove/$', name='document_metadata_remove', view=DocumentMetadataRemoveView.as_view() ), @@ -47,34 +74,7 @@ urlpatterns = [ regex=r'^documents/multiple/remove/$', name='document_multiple_metadata_remove', view=DocumentMetadataRemoveView.as_view() - ), - - url( - regex=r'^types/list/$', name='metadata_type_list', - view=MetadataTypeListView.as_view() - ), - url( - regex=r'^types/create/$', name='metadata_type_create', - view=MetadataTypeCreateView.as_view() - ), - url( - regex=r'^types/(?P\d+)/edit/$', name='metadata_type_edit', - view=MetadataTypeEditView.as_view() - ), - url( - regex=r'^types/(?P\d+)/delete/$', name='metadata_type_delete', - view=MetadataTypeDeleteView.as_view() - ), - url( - regex=r'^document_types/(?P\d+)/metadata_types/$', - name='document_type_metadata_types', - view=SetupDocumentTypeMetadataTypes.as_view() - ), - url( - regex=r'^metadata_types/(?P\d+)/document_types/$', - name='metadata_type_document_types', - view=SetupMetadataTypesDocumentTypes.as_view() - ), + ) ] api_urls = [ @@ -83,28 +83,28 @@ api_urls = [ view=APIMetadataTypeListView.as_view() ), url( - regex=r'^metadata_types/(?P\d+)/$', + regex=r'^metadata_types/(?P\d+)/$', name='metadatatype-detail', view=APIMetadataTypeView.as_view() ), url( - regex=r'^document_types/(?P\d+)/metadata_types/$', + regex=r'^document_types/(?P\d+)/metadata_types/$', name='documenttypemetadatatype-list', view=APIDocumentTypeMetadataTypeListView.as_view() ), url( - regex=r'^document_types/(?P\d+)/metadata_types/(?P\d+)/$', + regex=r'^document_types/(?P\d+)/metadata_types/(?P\d+)/$', name='documenttypemetadatatype-detail', view=APIDocumentTypeMetadataTypeView.as_view() ), url( - regex=r'^documents/(?P\d+)/metadata/$', + regex=r'^documents/(?P\d+)/metadata/$', name='documentmetadata-list', view=APIDocumentMetadataListView.as_view() ), url( - regex=r'^documents/(?P\d+)/metadata/(?P\d+)/$', + regex=r'^documents/(?P\d+)/metadata/(?P\d+)/$', name='documentmetadata-detail', view=APIDocumentMetadataView.as_view() - ), + ) ] diff --git a/mayan/apps/metadata/views.py b/mayan/apps/metadata/views.py index 3c988b9f4d..8bf89b90a6 100644 --- a/mayan/apps/metadata/views.py +++ b/mayan/apps/metadata/views.py @@ -45,6 +45,7 @@ class DocumentMetadataAddView(MultipleObjectFormActionView): form_class = DocumentMetadataAddForm model = Document object_permission = permission_document_metadata_add + pk_url_kwarg = 'document_id' success_message = _('Metadata add request performed on %(count)d document') success_message_plural = _( 'Metadata add request performed on %(count)d documents' @@ -62,9 +63,11 @@ class DocumentMetadataAddView(MultipleObjectFormActionView): if len(set([document.document_type.pk for document in queryset])) > 1: messages.error( - request, _('Selected documents must be of the same type.') + message=_( + 'Selected documents must be of the same type.' + ), request=request ) - return HttpResponseRedirect(self.previous_url) + return HttpResponseRedirect(redirect_to=self.previous_url) return result @@ -75,14 +78,14 @@ class DocumentMetadataAddView(MultipleObjectFormActionView): if self.action_count == 1: return HttpResponseRedirect( - reverse( + redirect_to=reverse( viewname='metadata:document_metadata_edit', - args=(queryset.first().pk,) + kwargs={'document_id': queryset.first().pk} ) ) elif self.action_count > 1: return HttpResponseRedirect( - '%s?%s' % ( + redirect_to='%s?%s' % ( reverse( viewname='metadata:document_multiple_metadata_edit' ), urlencode( @@ -106,9 +109,9 @@ class DocumentMetadataAddView(MultipleObjectFormActionView): result = { 'submit_label': _('Add'), 'title': ungettext( - 'Add metadata types to document', - 'Add metadata types to documents', - queryset.count() + singular='Add metadata types to document', + plural='Add metadata types to documents', + number=queryset.count() ) } @@ -169,8 +172,7 @@ class DocumentMetadataAddView(MultipleObjectFormActionView): created = True except Exception as exception: messages.error( - self.request, - _( + message=_( 'Error adding metadata type ' '"%(metadata_type)s" to document: ' '%(document)s; %(exception)s' @@ -180,29 +182,30 @@ class DocumentMetadataAddView(MultipleObjectFormActionView): 'exception': ', '.join( getattr(exception, 'messages', exception) ) - } + }, + request=self.request ) else: if created: messages.success( - self.request, - _( + message=_( 'Metadata type: %(metadata_type)s ' 'successfully added to document %(document)s.' ) % { 'metadata_type': metadata_type, 'document': instance - } + }, + request=self.request ) else: messages.warning( - self.request, _( + message=_( 'Metadata type: %(metadata_type)s already ' 'present in document %(document)s.' ) % { 'metadata_type': metadata_type, 'document': instance - } + }, request=self.request ) @@ -210,6 +213,7 @@ class DocumentMetadataEditView(MultipleObjectFormActionView): form_class = DocumentMetadataFormSet model = Document object_permission = permission_document_metadata_edit + pk_url_kwarg = 'document_id' success_message = _( 'Metadata edit request performed on %(count)d document' ) @@ -229,9 +233,11 @@ class DocumentMetadataEditView(MultipleObjectFormActionView): if len(set([document.document_type.pk for document in queryset])) > 1: messages.error( - request, _('Selected documents must be of the same type.') + message=_( + 'Selected documents must be of the same type.' + ), request=request ) - return HttpResponseRedirect(self.previous_url) + return HttpResponseRedirect(redirect_to=self.previous_url) return result @@ -242,14 +248,14 @@ class DocumentMetadataEditView(MultipleObjectFormActionView): if self.action_count == 1: return HttpResponseRedirect( - reverse( + redirect_to=reverse( viewname='metadata:document_metadata_edit', - args=(queryset.first().pk,) + kwargs={'document_id': queryset.first().pk} ) ) elif self.action_count > 1: return HttpResponseRedirect( - '%s?%s' % ( + redirect_to='%s?%s' % ( reverse( viewname='metadata:document_multiple_metadata_edit' ), urlencode( @@ -303,9 +309,9 @@ class DocumentMetadataEditView(MultipleObjectFormActionView): 'no_results_title': _('There is no metadata to edit'), 'submit_label': _('Edit'), 'title': ungettext( - 'Edit document metadata', - 'Edit documents metadata', - queryset.count() + singular='Edit document metadata', + plural='Edit documents metadata', + number=queryset.count() ) } @@ -371,36 +377,31 @@ class DocumentMetadataEditView(MultipleObjectFormActionView): exception_message = force_text(error) messages.error( - self.request, _( + message=_( 'Error editing metadata for document: ' '%(document)s; %(exception)s.' ) % { 'document': instance, 'exception': exception_message - } + }, request=self.request ) else: messages.success( - self.request, - _( + message=_( 'Metadata for document %s edited successfully.' - ) % instance + ) % instance, request=self.request ) class DocumentMetadataListView(SingleObjectListView): - def dispatch(self, request, *args, **kwargs): - AccessControlList.objects.check_access( - permissions=permission_document_metadata_view, - user=self.request.user, obj=self.get_document() - ) - - return super(DocumentMetadataListView, self).dispatch( - request, *args, **kwargs - ) - def get_document(self): - return get_object_or_404(klass=Document, pk=self.kwargs['pk']) + queryset = AccessControlList.objects.restrict_queryset( + permission=permission_document_metadata_view, + queryset=Document.objects.all(), + user=self.request.user + ) + + return get_object_or_404(klass=queryset, pk=self.kwargs['document_id']) def get_extra_context(self): document = self.get_document() @@ -431,6 +432,7 @@ class DocumentMetadataRemoveView(MultipleObjectFormActionView): form_class = DocumentMetadataRemoveFormSet model = Document object_permission = permission_document_metadata_remove + pk_url_kwarg = 'document_id' success_message = _( 'Metadata remove request performed on %(count)d document' ) @@ -450,9 +452,11 @@ class DocumentMetadataRemoveView(MultipleObjectFormActionView): if len(set([document.document_type.pk for document in queryset])) > 1: messages.error( - request, _('Selected documents must be of the same type.') + message=_( + 'Selected documents must be of the same type.' + ), request=request ) - return HttpResponseRedirect(self.previous_url) + return HttpResponseRedirect(redirect_to=self.previous_url) return result @@ -463,14 +467,14 @@ class DocumentMetadataRemoveView(MultipleObjectFormActionView): if self.action_count == 1: return HttpResponseRedirect( - reverse( + redirect_to=reverse( viewname='metadata:document_metadata_edit', - args=(queryset.first().pk,) + kwargs={'document_id': queryset.first().pk} ) ) elif self.action_count > 1: return HttpResponseRedirect( - '%s?%s' % ( + redirect_to='%s?%s' % ( reverse( viewname='metadata:document_multiple_metadata_edit' ), urlencode( @@ -495,9 +499,9 @@ class DocumentMetadataRemoveView(MultipleObjectFormActionView): 'form_display_mode_table': True, 'submit_label': _('Remove'), 'title': ungettext( - 'Remove metadata types from the document', - 'Remove metadata types from the documents', - queryset.count() + singular='Remove metadata types from the document', + plural='Remove metadata types from the documents', + number=queryset.count() ) } @@ -553,24 +557,22 @@ class DocumentMetadataRemoveView(MultipleObjectFormActionView): ) document_metadata.delete(_user=self.request.user) messages.success( - self.request, - _( + message=_( 'Successfully remove metadata type "%(metadata_type)s" from document: %(document)s.' ) % { 'metadata_type': metadata_type, 'document': instance - } + }, request=self.request ) except Exception as exception: messages.error( - self.request, - _( + message=_( 'Error removing metadata type "%(metadata_type)s" from document: %(document)s; %(exception)s' ) % { 'metadata_type': metadata_type, 'document': instance, 'exception': ', '.join(exception.messages) - } + }, request=self.request ) @@ -593,6 +595,7 @@ class MetadataTypeCreateView(SingleObjectCreateView): class MetadataTypeDeleteView(SingleObjectDeleteView): model = MetadataType object_permission = permission_metadata_type_delete + pk_url_kwarg = 'metadata_type_id' post_action_redirect = reverse_lazy( viewname='metadata:metadata_type_list' ) @@ -609,6 +612,7 @@ class MetadataTypeEditView(SingleObjectEditView): form_class = MetadataTypeForm model = MetadataType object_permission = permission_metadata_type_edit + pk_url_kwarg = 'metadata_type_id' post_action_redirect = reverse_lazy( viewname='metadata:metadata_type_list' ) @@ -663,12 +667,14 @@ class SetupDocumentTypeMetadataTypes(FormView): instance.save() except Exception as exception: messages.error( - self.request, - _('Error updating relationship; %s') % exception + message=_('Error updating relationship; %s') % exception, + request=self.request ) else: messages.success( - self.request, _('Relationships updated successfully') + message=_( + 'Relationships updated successfully' + ), request=self.request ) return super( @@ -703,24 +709,27 @@ class SetupDocumentTypeMetadataTypes(FormView): initial = [] for element in self.get_queryset(): - initial.append({ - 'document_type': obj, - 'main_model': self.main_model, - 'metadata_type': element, - }) + initial.append( + { + 'document_type': obj, + 'main_model': self.main_model, + 'metadata_type': element, + } + ) return initial def get_object(self): - obj = get_object_or_404(klass=self.model, pk=self.kwargs['pk']) - - AccessControlList.objects.check_access( - permissions=(permission_metadata_type_edit,), - user=self.request.user, obj=obj + queryset = AccessControlList.objects.restrict_queryset( + permission=permission_metadata_type_edit, + queryset=self.model.objects.all(), + user=self.request.user + ) + return get_object_or_404( + klass=queryset, pk=self.kwargs['document_type_id'] ) - return obj def get_post_action_redirect(self): - return reverse('documents:document_type_list') + return reverse(viewname='documents:document_type_list') def get_queryset(self): queryset = self.submodel.objects.all() @@ -749,12 +758,24 @@ class SetupMetadataTypesDocumentTypes(SetupDocumentTypeMetadataTypes): initial = [] for element in self.get_queryset(): - initial.append({ - 'document_type': element, - 'main_model': self.main_model, - 'metadata_type': obj, - }) + initial.append( + { + 'document_type': element, + 'main_model': self.main_model, + 'metadata_type': obj, + } + ) return initial + def get_object(self): + queryset = AccessControlList.objects.restrict_queryset( + permission=permission_metadata_type_edit, + queryset=self.model.objects.all(), + user=self.request.user + ) + return get_object_or_404( + klass=queryset, pk=self.kwargs['metadata_type_id'] + ) + def get_post_action_redirect(self): return reverse(viewname='metadata:metadata_type_list') From ad7c77b4f34c6edffa288c41e3497977b9144ab1 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Mon, 21 Jan 2019 03:53:55 -0400 Subject: [PATCH 032/209] Update dynamic search app Sort methods. Update use of .filter_by_access() to .restrict_queryset(). Change the method to so the final object filtering. Instead of expressing the pk list and remove the duplicated using a set, pass the queryset as a subquery to the object filter. This moves the processing to the database instead of holding a list of an unknown number of primary keys in the memory. Add keyword arguments. Update tests to use the latest user test case mixin interface. Signed-off-by: Roberto Rosario --- mayan/apps/dynamic_search/classes.py | 78 +++++++++---------- mayan/apps/dynamic_search/links.py | 11 +-- mayan/apps/dynamic_search/settings.py | 3 +- mayan/apps/dynamic_search/tests/test_api.py | 6 +- .../apps/dynamic_search/tests/test_models.py | 24 +++--- mayan/apps/dynamic_search/tests/test_views.py | 34 +++----- mayan/apps/dynamic_search/urls.py | 29 +++---- mayan/apps/dynamic_search/views.py | 11 ++- 8 files changed, 95 insertions(+), 101 deletions(-) diff --git a/mayan/apps/dynamic_search/classes.py b/mayan/apps/dynamic_search/classes.py index a831121a5a..a37d1a3f21 100644 --- a/mayan/apps/dynamic_search/classes.py +++ b/mayan/apps/dynamic_search/classes.py @@ -107,6 +107,38 @@ class SearchModel(object): def __str__(self): return force_text(self.label) + def add_model_field(self, *args, **kwargs): + """ + Add a search field that directly belongs to the parent SearchModel + """ + search_field = SearchField(self, *args, **kwargs) + self.search_fields.append(search_field) + + def get_fields_simple_list(self): + """ + Returns a list of the fields for the SearchModel + """ + result = [] + for search_field in self.search_fields: + result.append((search_field.get_full_name(), search_field.label)) + + return result + + def get_full_name(self): + return '%s.%s' % (self.app_label, self.model_name) + + def get_search_field(self, full_name): + try: + return self.search_fields[full_name] + except KeyError: + raise KeyError('No search field named: %s' % full_name) + + def get_search_query(self, query_string, global_and_search=False): + return SearchQuery( + query_string=query_string, search_model=self, + global_and_search=global_and_search + ) + @property def label(self): if not self._label: @@ -123,38 +155,6 @@ class SearchModel(object): def pk(self): return self.get_full_name() - def add_model_field(self, *args, **kwargs): - """ - Add a search field that directly belongs to the parent SearchModel - """ - search_field = SearchField(self, *args, **kwargs) - self.search_fields.append(search_field) - - def get_full_name(self): - return '%s.%s' % (self.app_label, self.model_name) - - def get_fields_simple_list(self): - """ - Returns a list of the fields for the SearchModel - """ - result = [] - for search_field in self.search_fields: - result.append((search_field.get_full_name(), search_field.label)) - - return result - - def get_search_field(self, full_name): - try: - return self.search_fields[full_name] - except KeyError: - raise KeyError('No search field named: %s' % full_name) - - def get_search_query(self, query_string, global_and_search=False): - return SearchQuery( - query_string=query_string, search_model=self, - global_and_search=global_and_search - ) - def search(self, query_string, user, global_and_search=False): AccessControlList = apps.get_model( app_label='acls', model_name='AccessControlList' @@ -165,18 +165,14 @@ class SearchModel(object): ) queryset = self.model.objects.filter( - pk__in=set( - self.model.objects.filter(search_query.query).values_list( - 'pk', flat=True - )[ - :setting_limit.value - ] - ) + pk__in=self.model.objects.filter(search_query.query).values('pk')[ + :setting_limit.value + ] ) if self.permission: - queryset = AccessControlList.objects.filter_by_access( - permission=self.permission, user=user, queryset=queryset + queryset = AccessControlList.objects.restrict_queryset( + permission=self.permission, queryset=queryset, user=user ) return queryset diff --git a/mayan/apps/dynamic_search/links.py b/mayan/apps/dynamic_search/links.py index 4ef031a533..d23c47d8be 100644 --- a/mayan/apps/dynamic_search/links.py +++ b/mayan/apps/dynamic_search/links.py @@ -5,13 +5,14 @@ from django.utils.translation import ugettext_lazy as _ from mayan.apps.navigation import Link link_search = Link( - text=_('Search'), view='search:search', args='search_model.get_full_name' + kwargs={'search_model': 'search_model.get_full_name'}, text=_('Search'), + view='search:search' ) link_search_advanced = Link( - text=_('Advanced search'), view='search:search_advanced', - args='search_model.get_full_name' + kwargs={'search_model': 'search_model.get_full_name'}, + text=_('Advanced search'), view='search:search_advanced' ) link_search_again = Link( - text=_('Search again'), view='search:search_again', - args='search_model.get_full_name', keep_query=True + kwargs={'search_model': 'search_model.get_full_name'}, keep_query=True, + text=_('Search again'), view='search:search_again' ) diff --git a/mayan/apps/dynamic_search/settings.py b/mayan/apps/dynamic_search/settings.py index eaa47d5036..c6bff778a9 100644 --- a/mayan/apps/dynamic_search/settings.py +++ b/mayan/apps/dynamic_search/settings.py @@ -4,7 +4,8 @@ from django.utils.translation import ugettext_lazy as _ from mayan.apps.smart_settings import Namespace -namespace = Namespace(name='dynamic_search', label=_('Search')) +namespace = Namespace(label=_('Search'), name='dynamic_search') + setting_limit = namespace.add_setting( global_name='SEARCH_LIMIT', default=100, help_text=_('Maximum amount search hits to fetch and display.') diff --git a/mayan/apps/dynamic_search/tests/test_api.py b/mayan/apps/dynamic_search/tests/test_api.py index a47a73b8c4..9af5df8eea 100644 --- a/mayan/apps/dynamic_search/tests/test_api.py +++ b/mayan/apps/dynamic_search/tests/test_api.py @@ -23,9 +23,9 @@ class SearchAPITestCase(DocumentTestMixin, BaseAPITestCase): return self.get( path='{}?q={}'.format( reverse( - 'rest_api:search-view', args=( - document_search.get_full_name(), - ) + viewname='rest_api:search-view', kwargs={ + 'search_model': document_search.get_full_name() + } ), self.document.label ) ) diff --git a/mayan/apps/dynamic_search/tests/test_models.py b/mayan/apps/dynamic_search/tests/test_models.py index 38b1afac0a..c84c44e17d 100644 --- a/mayan/apps/dynamic_search/tests/test_models.py +++ b/mayan/apps/dynamic_search/tests/test_models.py @@ -9,6 +9,8 @@ from mayan.apps.documents.tests import ( class DocumentSearchTestCase(DocumentTestMixin, BaseTestCase): auto_upload_document = False + create_test_case_superuser = True + create_test_case_user = False test_document_filename = TEST_DOCUMENT_FILENAME def test_simple_search_after_related_name_change(self): @@ -18,7 +20,7 @@ class DocumentSearchTestCase(DocumentTestMixin, BaseTestCase): """ self.document = self.upload_document() queryset = document_search.search( - {'q': 'Mayan'}, user=self.admin_user + {'q': 'Mayan'}, user=self._test_case_superuser ) self.assertEqual(queryset.count(), 1) self.assertTrue(self.document in queryset) @@ -27,7 +29,7 @@ class DocumentSearchTestCase(DocumentTestMixin, BaseTestCase): # Test versions__filename self.document = self.upload_document() queryset = document_search.search( - {'label': self.document.label}, user=self.admin_user + {'label': self.document.label}, user=self._test_case_superuser ) self.assertEqual(queryset.count(), 1) self.assertTrue(self.document in queryset) @@ -35,7 +37,7 @@ class DocumentSearchTestCase(DocumentTestMixin, BaseTestCase): # Test versions__mimetype queryset = document_search.search( {'versions__mimetype': self.document.file_mimetype}, - user=self.admin_user + user=self._test_case_superuser ) self.assertEqual(queryset.count(), 1) self.assertTrue(self.document in queryset) @@ -48,7 +50,7 @@ class DocumentSearchTestCase(DocumentTestMixin, BaseTestCase): self.document_2.save() queryset = document_search.search( - {'q': 'Mayan OR second'}, user=self.admin_user + {'q': 'Mayan OR second'}, user=self._test_case_superuser ) self.assertEqual(queryset.count(), 2) self.assertTrue(self.document in queryset) @@ -61,12 +63,12 @@ class DocumentSearchTestCase(DocumentTestMixin, BaseTestCase): self.document_2.save() queryset = document_search.search( - {'q': 'non_valid second'}, user=self.admin_user + {'q': 'non_valid second'}, user=self._test_case_superuser ) self.assertEqual(queryset.count(), 0) queryset = document_search.search( - {'q': 'second non_valid'}, user=self.admin_user + {'q': 'second non_valid'}, user=self._test_case_superuser ) self.assertEqual(queryset.count(), 0) @@ -77,26 +79,26 @@ class DocumentSearchTestCase(DocumentTestMixin, BaseTestCase): self.document_2.save() queryset = document_search.search( - {'q': '-non_valid second'}, user=self.admin_user + {'q': '-non_valid second'}, user=self._test_case_superuser ) self.assertEqual(queryset.count(), 1) queryset = document_search.search( - {'label': '-second'}, user=self.admin_user + {'label': '-second'}, user=self._test_case_superuser ) self.assertEqual(queryset.count(), 0) queryset = document_search.search( - {'label': '-second -Mayan'}, user=self.admin_user + {'label': '-second -Mayan'}, user=self._test_case_superuser ) self.assertEqual(queryset.count(), 0) queryset = document_search.search( - {'label': '-second OR -Mayan'}, user=self.admin_user + {'label': '-second OR -Mayan'}, user=self._test_case_superuser ) self.assertEqual(queryset.count(), 1) queryset = document_search.search( - {'label': '-non_valid -second'}, user=self.admin_user + {'label': '-non_valid -second'}, user=self._test_case_superuser ) self.assertEqual(queryset.count(), 0) diff --git a/mayan/apps/dynamic_search/tests/test_views.py b/mayan/apps/dynamic_search/tests/test_views.py index 6319ae1faa..43bd7f4f59 100644 --- a/mayan/apps/dynamic_search/tests/test_views.py +++ b/mayan/apps/dynamic_search/tests/test_views.py @@ -1,44 +1,32 @@ from __future__ import unicode_literals from mayan.apps.common.tests import GenericViewTestCase -from mayan.apps.documents.models import DocumentType from mayan.apps.documents.search import document_search -from mayan.apps.documents.tests import ( - TEST_DOCUMENT_TYPE_LABEL, TEST_SMALL_DOCUMENT_PATH -) +from mayan.apps.documents.tests import DocumentTestMixin -class Issue46TestCase(GenericViewTestCase): +class Issue46TestCase(DocumentTestMixin, GenericViewTestCase): """ Functional tests to make sure issue 46 is fixed """ + auto_upload_document = False + auto_login_superuser = True + create_test_case_superuser = True + create_test_case_user = False + def setUp(self): super(Issue46TestCase, self).setUp() - self.login_admin_user() self.document_count = 4 - self.document_type = DocumentType.objects.create( - label=TEST_DOCUMENT_TYPE_LABEL - ) - # Upload many instances of the same test document for i in range(self.document_count): - with open(TEST_SMALL_DOCUMENT_PATH, mode='rb') as file_object: - self.document_type.new_document( - file_object=file_object, - label='test document', - ) - - def tearDown(self): - for document_type in DocumentType.objects.all(): - document_type.delete() - super(Issue46TestCase, self).tearDown() + self.test_document = self.upload_document() def test_advanced_search_past_first_page(self): # Make sure all documents are returned by the search queryset = document_search.search( - {'label': 'test document'}, user=self.admin_user + {'label': self.test_document.label}, user=self._test_case_superuser ) self.assertEqual(queryset.count(), self.document_count) @@ -46,7 +34,7 @@ class Issue46TestCase(GenericViewTestCase): # Functional test for the first page of advanced results response = self.get( viewname='search:results', - args=(document_search.get_full_name(),), + kwargs={'search_model': document_search.get_full_name()}, data={'label': 'test'} ) @@ -66,7 +54,7 @@ class Issue46TestCase(GenericViewTestCase): # Functional test for the second page of advanced results response = self.get( viewname='search:results', - args=(document_search.get_full_name(),), + kwargs={'search_model': document_search.get_full_name()}, data={'label': 'test', 'page': 2} ) diff --git a/mayan/apps/dynamic_search/urls.py b/mayan/apps/dynamic_search/urls.py index c19b27a663..a2fd2eb969 100644 --- a/mayan/apps/dynamic_search/urls.py +++ b/mayan/apps/dynamic_search/urls.py @@ -6,32 +6,35 @@ from .api_views import APIAdvancedSearchView, APISearchModelList, APISearchView from .views import AdvancedSearchView, ResultsView, SearchAgainView, SearchView urlpatterns = [ - url(r'^(?P[\.\w]+)/$', SearchView.as_view(), name='search'), url( - r'^advanced/(?P[\.\w]+)/$', AdvancedSearchView.as_view(), - name='search_advanced' + regex=r'^(?P[\.\w]+)/$', name='search', + view=SearchView.as_view() ), url( - r'^again/(?P[\.\w]+)/$', SearchAgainView.as_view(), - name='search_again' + regex=r'^advanced/(?P[\.\w]+)/$', name='search_advanced', + view=AdvancedSearchView.as_view() ), url( - r'^results/(?P[\.\w]+)/$', ResultsView.as_view(), - name='results' + regex=r'^again/(?P[\.\w]+)/$', name='search_again', + view=SearchAgainView.as_view() ), + url( + regex=r'^results/(?P[\.\w]+)/$', name='results', + view=ResultsView.as_view() + ) ] api_urls = [ url( - r'^search_models/$', APISearchModelList.as_view(), - name='searchmodel-list' + regex=r'^search_models/$', name='searchmodel-list', + view=APISearchModelList.as_view() ), url( - r'^search/(?P[\.\w]+)/$', APISearchView.as_view(), - name='search-view' + regex=r'^search/(?P[\.\w]+)/$', name='search-view', + view=APISearchView.as_view() ), url( - r'^search/advanced/(?P[\.\w]+)/$', APIAdvancedSearchView.as_view(), - name='advanced-search-view' + regex=r'^search/advanced/(?P[\.\w]+)/$', + name='advanced-search-view', view=APIAdvancedSearchView.as_view() ), ] diff --git a/mayan/apps/dynamic_search/views.py b/mayan/apps/dynamic_search/views.py index 6fc08a179e..5a475ce8ea 100644 --- a/mayan/apps/dynamic_search/views.py +++ b/mayan/apps/dynamic_search/views.py @@ -47,7 +47,8 @@ class ResultsView(SearchModelMixin, SingleObjectListView): global_and_search = False return self.search_model.get_search_query( - query_string=self.request.GET, global_and_search=global_and_search + global_and_search=global_and_search, + query_string=self.request.GET ) def get_object_list(self): @@ -63,8 +64,8 @@ class ResultsView(SearchModelMixin, SingleObjectListView): global_and_search = False queryset = self.search_model.search( - query_string=self.request.GET, user=self.request.user, - global_and_search=global_and_search + global_and_search=global_and_search, + query_string=self.request.GET, user=self.request.user ) return queryset @@ -79,7 +80,9 @@ class SearchView(SearchModelMixin, SimpleView): return { 'form': self.get_form(), 'form_action': reverse( - 'search:results', args=(self.search_model.get_full_name(),) + viewname='search:results', kwargs={ + 'search_model': self.search_model.get_full_name() + } ), 'search_model': self.search_model, 'submit_icon_class': icon_search_submit, From 50333d1326c8185e3a71b85c71bb0b48c47b1dde Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Mon, 21 Jan 2019 04:06:31 -0400 Subject: [PATCH 033/209] Update smart settings app Sort arguments. Add keyword arguments. Signed-off-by: Roberto Rosario --- mayan/apps/smart_settings/apps.py | 14 +++++++------- mayan/apps/smart_settings/classes.py | 8 ++++---- mayan/apps/smart_settings/links.py | 10 ++++++---- mayan/apps/smart_settings/permissions.py | 4 ++-- .../smart_settings/tests/test_view_permissions.py | 14 ++++++-------- mayan/apps/smart_settings/urls.py | 14 +++++++------- mayan/apps/smart_settings/views.py | 12 ++++++------ 7 files changed, 38 insertions(+), 38 deletions(-) diff --git a/mayan/apps/smart_settings/apps.py b/mayan/apps/smart_settings/apps.py index 8ef1a6fad3..42e11f5168 100644 --- a/mayan/apps/smart_settings/apps.py +++ b/mayan/apps/smart_settings/apps.py @@ -28,19 +28,19 @@ class SmartSettingsApp(MayanAppConfig): Namespace.initialize() SourceColumn( - source=Namespace, label=_('Setting count'), - func=lambda context: len(context['object'].settings) + func=lambda context: len(context['object'].settings), + label=_('Setting count'), source=Namespace ) SourceColumn( - source=Setting, label=_('Name'), - func=lambda context: setting_widget(context['object']) + func=lambda context: setting_widget(context['object']), + label=_('Name'), source=Setting ) SourceColumn( - source=Setting, label=_('Value'), attribute='serialized_value' + attribute='serialized_value', label=_('Value'), source=Setting ) SourceColumn( - source=Setting, label=_('Overrided by environment variable?'), - func=lambda context: _('Yes') if context['object'].environment_variable else _('No') + func=lambda context: _('Yes') if context['object'].environment_variable else _('No'), + label=_('Overrided by environment variable?'), source=Setting ) menu_secondary.bind_links( diff --git a/mayan/apps/smart_settings/classes.py b/mayan/apps/smart_settings/classes.py index 81a9700b7e..4e1888f30a 100644 --- a/mayan/apps/smart_settings/classes.py +++ b/mayan/apps/smart_settings/classes.py @@ -34,14 +34,14 @@ class Namespace(object): exception ) - @classmethod - def get_all(cls): - return sorted(cls._registry.values(), key=lambda x: x.label) - @classmethod def get(cls, name): return cls._registry[name] + @classmethod + def get_all(cls): + return sorted(cls._registry.values(), key=lambda x: x.label) + @classmethod def invalidate_cache_all(cls): for namespace in cls.get_all(): diff --git a/mayan/apps/smart_settings/links.py b/mayan/apps/smart_settings/links.py index b166801492..67c66e53f3 100644 --- a/mayan/apps/smart_settings/links.py +++ b/mayan/apps/smart_settings/links.py @@ -12,8 +12,9 @@ link_namespace_list = Link( text=_('Settings'), view='settings:namespace_list' ) link_namespace_detail = Link( - args='resolved_object.name', permissions=(permission_settings_view,), - text=_('Settings'), view='settings:namespace_detail', + kwargs={'namespace_name': 'resolved_object.name'}, + permissions=(permission_settings_view,), text=_('Settings'), + view='settings:namespace_detail' ) # Duplicate the link to use a different name link_namespace_root_list = Link( @@ -21,6 +22,7 @@ link_namespace_root_list = Link( text=_('Namespaces'), view='settings:namespace_list' ) link_setting_edit = Link( - args='resolved_object.global_name', permissions=(permission_settings_edit,), - text=_('Edit'), view='settings:setting_edit_view', + kwargs={'setting_global_name': 'resolved_object.global_name'}, + permissions=(permission_settings_edit,), text=_('Edit'), + view='settings:setting_edit_view' ) diff --git a/mayan/apps/smart_settings/permissions.py b/mayan/apps/smart_settings/permissions.py index c28d9a417b..ea66346062 100644 --- a/mayan/apps/smart_settings/permissions.py +++ b/mayan/apps/smart_settings/permissions.py @@ -7,8 +7,8 @@ from mayan.apps.permissions import PermissionNamespace namespace = PermissionNamespace(label=_('Smart settings'), name='smart_settings') permission_settings_edit = namespace.add_permission( - name='permission_settings_edit', label=_('Edit settings') + label=_('Edit settings'), name='permission_settings_edit' ) permission_settings_view = namespace.add_permission( - name='permission_settings_view', label=_('View settings') + label=_('View settings'), name='permission_settings_view' ) diff --git a/mayan/apps/smart_settings/tests/test_view_permissions.py b/mayan/apps/smart_settings/tests/test_view_permissions.py index 4ef5e91e2b..2ae0df8fbc 100644 --- a/mayan/apps/smart_settings/tests/test_view_permissions.py +++ b/mayan/apps/smart_settings/tests/test_view_permissions.py @@ -6,27 +6,25 @@ from ..permissions import permission_settings_view class SmartSettingViewPermissionsTestCase(GenericViewTestCase): - def setUp(self): - super(SmartSettingViewPermissionsTestCase, self).setUp() - self.login_user() - def test_view_access_denied(self): - response = self.get('settings:namespace_list') + response = self.get(viewname='settings:namespace_list') self.assertEqual(response.status_code, 403) response = self.get( - 'settings:namespace_detail', args=('common',) + viewname='settings:namespace_detail', + kwargs={'namespace_name': 'common'} ) self.assertEqual(response.status_code, 403) def test_view_access_permitted(self): self.grant_permission(permission=permission_settings_view) - response = self.get('settings:namespace_list') + response = self.get(viewname='settings:namespace_list') self.assertEqual(response.status_code, 200) response = self.get( - 'settings:namespace_detail', args=('common',) + viewname='settings:namespace_detail', + kwargs={'namespace_name': 'common'} ) self.assertEqual(response.status_code, 200) diff --git a/mayan/apps/smart_settings/urls.py b/mayan/apps/smart_settings/urls.py index 7e108ac756..a7c351f737 100644 --- a/mayan/apps/smart_settings/urls.py +++ b/mayan/apps/smart_settings/urls.py @@ -6,15 +6,15 @@ from .views import NamespaceDetailView, NamespaceListView, SettingEditView urlpatterns = [ url( - r'^namespace/all/$', NamespaceListView.as_view(), - name='namespace_list' + regex=r'^namespaces/$', name='namespace_list', + view=NamespaceListView.as_view() ), url( - r'^namespace/(?P\w+)/$', - NamespaceDetailView.as_view(), name='namespace_detail' + regex=r'^namespaces/(?P\w+)/$', + name='namespace_detail', view=NamespaceDetailView.as_view() ), url( - r'^edit/(?P\w+)/$', - SettingEditView.as_view(), name='setting_edit_view' - ), + regex=r'^settings/(?P\w+)/edit/$', + name='setting_edit_view', view=SettingEditView.as_view() + ) ] diff --git a/mayan/apps/smart_settings/views.py b/mayan/apps/smart_settings/views.py index 5835182ee1..d723b9db92 100644 --- a/mayan/apps/smart_settings/views.py +++ b/mayan/apps/smart_settings/views.py @@ -39,7 +39,7 @@ class NamespaceDetailView(SingleObjectListView): def get_namespace(self): try: - return Namespace.get(self.kwargs['namespace_name']) + return Namespace.get(name=self.kwargs['namespace_name']) except KeyError: raise Http404( _('Namespace: %s, not found') % self.kwargs['namespace_name'] @@ -57,7 +57,7 @@ class SettingEditView(FormView): self.get_object().value = form.cleaned_data['value'] Setting.save_configuration() messages.success( - self.request, _('Setting updated successfully.') + message=_('Setting updated successfully.'), request=self.request ) return super(SettingEditView, self).form_valid(form=form) @@ -72,11 +72,11 @@ class SettingEditView(FormView): return {'setting': self.get_object()} def get_object(self): - return Setting.get(self.kwargs['setting_global_name']) + return Setting.get(global_name=self.kwargs['setting_global_name']) def get_post_action_redirect(self): return reverse( - 'settings:namespace_detail', args=( - self.get_object().namespace.name, - ) + viewname='settings:namespace_detail', kwargs={ + 'namespace_name': self.get_object().namespace.name + } ) From 83a9b5a60af9056fdc1ba6559f8b78a2df6186e6 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Mon, 21 Jan 2019 19:24:00 -0400 Subject: [PATCH 034/209] Update OCR app Add keyword arguments. Update URL parameters to the "_id" form. Updated view tests. Signed-off-by: Roberto Rosario --- mayan/apps/ocr/api_views.py | 6 +-- mayan/apps/ocr/events.py | 2 +- mayan/apps/ocr/exceptions.py | 4 +- mayan/apps/ocr/links.py | 18 ++++++--- mayan/apps/ocr/permissions.py | 10 ++--- mayan/apps/ocr/queues.py | 5 ++- mayan/apps/ocr/runtime.py | 2 +- mayan/apps/ocr/settings.py | 2 +- mayan/apps/ocr/tasks.py | 2 +- mayan/apps/ocr/tests/literals.py | 5 +++ mayan/apps/ocr/tests/test_api.py | 16 +++++--- mayan/apps/ocr/tests/test_models.py | 9 ++--- mayan/apps/ocr/tests/test_views.py | 20 +++++----- mayan/apps/ocr/urls.py | 61 ++++++++++++++++------------- mayan/apps/ocr/views.py | 37 ++++++++++++----- 15 files changed, 119 insertions(+), 80 deletions(-) diff --git a/mayan/apps/ocr/api_views.py b/mayan/apps/ocr/api_views.py index 3cde1b2f36..69b3c44507 100644 --- a/mayan/apps/ocr/api_views.py +++ b/mayan/apps/ocr/api_views.py @@ -38,7 +38,7 @@ class APIDocumentVersionOCRView(generics.GenericAPIView): """ post: Submit a document version for OCR. """ - lookup_url_kwarg = 'version_pk' + lookup_url_kwarg = 'document_version_pk' mayan_object_permissions = { 'POST': (permission_ocr_document,) } @@ -66,7 +66,7 @@ class APIDocumentPageOCRContentView(generics.RetrieveAPIView): """ get: Returns the OCR content of the selected document page. """ - lookup_url_kwarg = 'page_pk' + lookup_url_kwarg = 'document_page_pk' mayan_object_permissions = { 'GET': (permission_ocr_content_view,), } @@ -78,7 +78,7 @@ class APIDocumentPageOCRContentView(generics.RetrieveAPIView): def get_document_version(self): return get_object_or_404( - klass=self.get_document().versions.all(), pk=self.kwargs['version_pk'] + klass=self.get_document().versions.all(), pk=self.kwargs['document_version_pk'] ) def get_queryset(self): diff --git a/mayan/apps/ocr/events.py b/mayan/apps/ocr/events.py index a77827bb47..c793b6edee 100644 --- a/mayan/apps/ocr/events.py +++ b/mayan/apps/ocr/events.py @@ -4,7 +4,7 @@ from django.utils.translation import ugettext_lazy as _ from mayan.apps.events import EventTypeNamespace -namespace = EventTypeNamespace(name='ocr', label=_('OCR')) +namespace = EventTypeNamespace(label=_('OCR'), name='ocr') event_ocr_document_version_submit = namespace.add_event_type( label=_('Document version submitted for OCR'), diff --git a/mayan/apps/ocr/exceptions.py b/mayan/apps/ocr/exceptions.py index 686aa75914..a02c62db32 100644 --- a/mayan/apps/ocr/exceptions.py +++ b/mayan/apps/ocr/exceptions.py @@ -3,6 +3,6 @@ from __future__ import unicode_literals class OCRError(Exception): """ - Raised by the OCR backend + Raised by the OCR backend for unexpected events that stop the + OCR processing. """ - pass diff --git a/mayan/apps/ocr/links.py b/mayan/apps/ocr/links.py index 700554ba40..cf67bdaf50 100644 --- a/mayan/apps/ocr/links.py +++ b/mayan/apps/ocr/links.py @@ -16,17 +16,20 @@ from .permissions import ( ) link_document_page_ocr_content = Link( - args='resolved_object.id', icon_class=icon_document_content, + icon_class=icon_document_content, + kwargs={'document_page_id': 'resolved_object.id'}, permissions=(permission_ocr_content_view,), text=_('OCR'), view='ocr:document_page_content', ) link_document_ocr_content = Link( - args='resolved_object.id', icon_class=icon_document_content, + icon_class=icon_document_content, + kwargs={'document_id': 'resolved_object.id'}, permissions=(permission_ocr_content_view,), text=_('OCR'), view='ocr:document_content', ) link_document_submit = Link( - args='resolved_object.id', icon_class=icon_document_submit, + icon_class=icon_document_submit, + kwargs={'document_id': 'resolved_object.id'}, permissions=(permission_ocr_document,), text=_('Submit for OCR'), view='ocr:document_submit' ) @@ -35,7 +38,8 @@ link_document_multiple_submit = Link( view='ocr:document_multiple_submit' ) link_document_type_ocr_settings = Link( - args='resolved_object.id', icon_class=icon_document_type_ocr_settings, + icon_class=icon_document_type_ocr_settings, + kwargs={'document_type_id': 'resolved_object.id'}, permissions=(permission_document_type_ocr_setup,), text=_('Setup OCR'), view='ocr:document_type_settings', ) @@ -49,12 +53,14 @@ link_entry_list = Link( text=_('OCR errors'), view='ocr:entry_list' ) link_document_ocr_errors_list = Link( - args='resolved_object.id', icon_class=icon_document_ocr_errors_list, + icon_class=icon_document_ocr_errors_list, + kwargs={'document_id': 'resolved_object.id'}, permissions=(permission_ocr_content_view,), text=_('OCR errors'), view='ocr:document_error_list' ) link_document_ocr_download = Link( - args='resolved_object.id', icon_class=icon_document_ocr_download, + icon_class=icon_document_ocr_download, + kwargs={'document_id': 'resolved_object.id'}, permissions=(permission_ocr_content_view,), text=_('Download OCR text'), view='ocr:document_download' ) diff --git a/mayan/apps/ocr/permissions.py b/mayan/apps/ocr/permissions.py index 8e6c0f6ed4..b5121fd779 100644 --- a/mayan/apps/ocr/permissions.py +++ b/mayan/apps/ocr/permissions.py @@ -7,13 +7,13 @@ from mayan.apps.permissions import PermissionNamespace namespace = PermissionNamespace(label=_('OCR'), name='ocr') permission_ocr_document = namespace.add_permission( - name='ocr_document', label=_('Submit documents for OCR') + label=_('Submit documents for OCR'), name='ocr_document' ) permission_ocr_content_view = namespace.add_permission( - name='ocr_content_view', - label=_('View the transcribed text from document') + label=_('View the transcribed text from document'), + name='ocr_content_view' ) permission_document_type_ocr_setup = namespace.add_permission( - name='ocr_document_type_setup', - label=_('Change document type OCR settings') + label=_('Change document type OCR settings'), + name='ocr_document_type_setup' ) diff --git a/mayan/apps/ocr/queues.py b/mayan/apps/ocr/queues.py index 537dae91c1..3749339961 100644 --- a/mayan/apps/ocr/queues.py +++ b/mayan/apps/ocr/queues.py @@ -4,7 +4,8 @@ from django.utils.translation import ugettext_lazy as _ from mayan.apps.task_manager.classes import CeleryQueue -queue_ocr = CeleryQueue(name='ocr', label=_('OCR')) +queue_ocr = CeleryQueue(label=_('OCR'), name='ocr') + queue_ocr.add_task_type( - name='mayan.apps.ocr.tasks.task_do_ocr', label=_('Document version OCR') + label=_('Document version OCR'), name='mayan.apps.ocr.tasks.task_do_ocr' ) diff --git a/mayan/apps/ocr/runtime.py b/mayan/apps/ocr/runtime.py index 36192e4156..55e6a36a35 100644 --- a/mayan/apps/ocr/runtime.py +++ b/mayan/apps/ocr/runtime.py @@ -5,5 +5,5 @@ from django.utils.module_loading import import_string from .settings import setting_ocr_backend, setting_ocr_backend_arguments ocr_backend = import_string( - setting_ocr_backend.value + dotted_path=setting_ocr_backend.value )(**setting_ocr_backend_arguments.value) diff --git a/mayan/apps/ocr/settings.py b/mayan/apps/ocr/settings.py index debf8be200..e9df89c4a8 100644 --- a/mayan/apps/ocr/settings.py +++ b/mayan/apps/ocr/settings.py @@ -4,7 +4,7 @@ from django.utils.translation import ugettext_lazy as _ from mayan.apps.smart_settings import Namespace -namespace = Namespace(name='ocr', label=_('OCR')) +namespace = Namespace(label=_('OCR'), name='ocr') setting_ocr_backend = namespace.add_setting( global_name='OCR_BACKEND', default='mayan.apps.ocr.backends.pyocr.PyOCR', diff --git a/mayan/apps/ocr/tasks.py b/mayan/apps/ocr/tasks.py index 77b340e3f5..d0a2fce4ff 100644 --- a/mayan/apps/ocr/tasks.py +++ b/mayan/apps/ocr/tasks.py @@ -28,7 +28,7 @@ def task_do_ocr(self, document_version_pk): logger.debug('trying to acquire lock: %s', lock_id) # Acquire lock to avoid doing OCR on the same document version more # than once concurrently - lock = locking_backend.acquire_lock(lock_id, LOCK_EXPIRE) + lock = locking_backend.acquire_lock(name=lock_id, timeout=LOCK_EXPIRE) logger.debug('acquired lock: %s', lock_id) document_version = None try: diff --git a/mayan/apps/ocr/tests/literals.py b/mayan/apps/ocr/tests/literals.py index 8fae0ef951..62b888408d 100644 --- a/mayan/apps/ocr/tests/literals.py +++ b/mayan/apps/ocr/tests/literals.py @@ -1,4 +1,9 @@ +# -*- coding: utf-8 -*- from __future__ import unicode_literals +TEST_DOCUMENT_CONTENT = 'Mayan EDMS Documentation' +TEST_DOCUMENT_CONTENT_DEU_1 = 'Repository für elektronische Dokumente.' +TEST_DOCUMENT_CONTENT_DEU_2 = 'Es bietet einen' + TEST_OCR_INDEX_NODE_TEMPLATE = '{% if "mayan" in document.get_ocr_content().lower() %}mayan{% endif %}' TEST_OCR_INDEX_NODE_TEMPLATE_LEVEL = 'mayan' diff --git a/mayan/apps/ocr/tests/test_api.py b/mayan/apps/ocr/tests/test_api.py index 3aaaa90ce8..d50c6e1953 100644 --- a/mayan/apps/ocr/tests/test_api.py +++ b/mayan/apps/ocr/tests/test_api.py @@ -23,7 +23,7 @@ class OCRAPITestCase(DocumentTestMixin, BaseAPITestCase): def _request_document_ocr_submit_view(self): return self.post( viewname='rest_api:document-ocr-submit-view', - args=(self.document.pk,) + kwargs={'document_id': self.document.pk} ) def test_submit_document_no_access(self): @@ -42,7 +42,10 @@ class OCRAPITestCase(DocumentTestMixin, BaseAPITestCase): def _request_document_version_ocr_submit_view(self): return self.post( viewname='rest_api:document-version-ocr-submit-view', - args=(self.document.pk, self.document.latest_version.pk,) + kwargs={ + 'document_id': self.document.pk, + 'document_version_id': self.document.latest_version.pk + } ) def test_submit_document_version_no_access(self): @@ -61,10 +64,11 @@ class OCRAPITestCase(DocumentTestMixin, BaseAPITestCase): def _request_document_page_content_view(self): return self.get( viewname='rest_api:document-page-ocr-content-view', - args=( - self.document.pk, self.document.latest_version.pk, - self.document.latest_version.pages.first().pk, - ) + kwargs={ + 'document_id': self.document.pk, + 'document_version_id': self.document.latest_version.pk, + 'document_page_id': self.document.latest_version.pages.first().pk + } ) def test_get_document_version_page_content_no_access(self): diff --git a/mayan/apps/ocr/tests/test_models.py b/mayan/apps/ocr/tests/test_models.py index 4bc8add466..29dfe50345 100644 --- a/mayan/apps/ocr/tests/test_models.py +++ b/mayan/apps/ocr/tests/test_models.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - from __future__ import unicode_literals from django.test import override_settings @@ -11,9 +9,10 @@ from mayan.apps.documents.tests import ( DocumentTestMixin, TEST_DEU_DOCUMENT_PATH, TEST_DOCUMENT_TYPE_LABEL ) -TEST_DOCUMENT_CONTENT = 'Mayan EDMS Documentation' -TEST_DOCUMENT_CONTENT_DEU_1 = 'Repository für elektronische Dokumente.' -TEST_DOCUMENT_CONTENT_DEU_2 = 'Es bietet einen' +from .literals import ( + TEST_DOCUMENT_CONTENT, TEST_DOCUMENT_CONTENT_DEU_1, + TEST_DOCUMENT_CONTENT_DEU_2 +) @override_settings(OCR_AUTO_OCR=True) diff --git a/mayan/apps/ocr/tests/test_views.py b/mayan/apps/ocr/tests/test_views.py index b52ec3a695..7e62121b8c 100644 --- a/mayan/apps/ocr/tests/test_views.py +++ b/mayan/apps/ocr/tests/test_views.py @@ -7,7 +7,7 @@ from ..permissions import ( permission_ocr_document, ) -TEST_DOCUMENT_CONTENT = 'Mayan EDMS Documentation' +from .literals import TEST_DOCUMENT_CONTENT class OCRViewsTestCase(GenericDocumentViewTestCase): @@ -22,14 +22,14 @@ class OCRViewsTestCase(GenericDocumentViewTestCase): def _request_document_content_view(self): return self.get( viewname='ocr:document_content', - kwargs={'pk': self.document.pk} + kwargs={'document_id': self.document.pk} ) def test_document_content_view_no_permissions(self): self.document.submit_for_ocr() response = self._request_document_content_view() - self.assertEqual(response.status_code, 403) + self.assertEqual(response.status_code, 404) def test_document_content_view_with_access(self): self.document.submit_for_ocr() @@ -46,14 +46,14 @@ class OCRViewsTestCase(GenericDocumentViewTestCase): def _request_document_page_content_view(self): return self.get( viewname='ocr:document_page_content', - kwargs={'pk': self.document.pages.first().pk} + kwargs={'document_page_id': self.document.pages.first().pk} ) def test_document_page_content_view_no_permissions(self): self.document.submit_for_ocr() response = self._request_document_page_content_view() - self.assertEqual(response.status_code, 403) + self.assertEqual(response.status_code, 404) def test_document_page_content_view_with_access(self): self.document.submit_for_ocr() @@ -70,7 +70,7 @@ class OCRViewsTestCase(GenericDocumentViewTestCase): def _request_document_submit_view(self): return self.post( viewname='ocr:document_submit', - kwargs={'pk': self.document.pk} + kwargs={'document_id': self.document.pk} ) def test_document_submit_view_no_permission(self): @@ -110,13 +110,13 @@ class OCRViewsTestCase(GenericDocumentViewTestCase): def _request_document_ocr_download_view(self): return self.get( viewname='ocr:document_download', - kwargs={'pk': self.document.pk} + kwargs={'document_id': self.document.pk} ) def test_document_ocr_download_view_no_permission(self): self.document.submit_for_ocr() response = self._request_document_ocr_download_view() - self.assertEqual(response.status_code, 403) + self.assertEqual(response.status_code, 404) def test_document_ocr_download_view_with_permission(self): self.document.submit_for_ocr() @@ -144,12 +144,12 @@ class DocumentTypeViewsTestCase(GenericDocumentViewTestCase): def _request_document_type_ocr_settings_view(self): return self.get( viewname='ocr:document_type_settings', - kwargs={'pk': self.document.document_type.pk} + kwargs={'document_type_id': self.document.document_type.pk} ) def test_document_type_ocr_settings_view_no_permission(self): response = self._request_document_type_ocr_settings_view() - self.assertEqual(response.status_code, 403) + self.assertEqual(response.status_code, 404) def test_document_type_ocr_settings_view_with_access(self): self.grant_access( diff --git a/mayan/apps/ocr/urls.py b/mayan/apps/ocr/urls.py index 857bb64bde..86fe690972 100644 --- a/mayan/apps/ocr/urls.py +++ b/mayan/apps/ocr/urls.py @@ -14,54 +14,61 @@ from .views import ( urlpatterns = [ url( - r'^documents/pages/(?P\d+)/content/$', - DocumentPageOCRContentView.as_view(), name='document_page_content' + regex=r'^document_types/ocr/submit/$', name='document_type_submit', + view=DocumentTypeSubmitView.as_view() ), url( - r'^documents/(?P\d+)/content/$', DocumentOCRContentView.as_view(), - name='document_content' + regex=r'^document_types/(?P\d+)/ocr/settings/$', + name='document_type_settings', + view=DocumentTypeSettingsEditView.as_view() ), url( - r'^documents/(?P\d+)/submit/$', DocumentSubmitView.as_view(), - name='document_submit' + regex=r'^documents/(?P\d+)/ocr/content/$', + name='document_content', view=DocumentOCRContentView.as_view() ), url( - r'^documents/(?P\d+)/ocr/errors/$', - DocumentOCRErrorsListView.as_view(), name='document_error_list' + regex=r'^documents/(?P\d+)/ocr/download/$', + name='document_download', view=DocumentOCRDownloadView.as_view() ), url( - r'^documents/(?P\d+)/ocr/download/$', - DocumentOCRDownloadView.as_view(), name='document_download' + regex=r'^documents/(?P\d+)/ocr/errors/$', + name='document_error_list', + view=DocumentOCRErrorsListView.as_view() ), url( - r'^documents/multiple/submit/$', DocumentSubmitView.as_view(), - name='document_multiple_submit' + regex=r'^documents/(?P\d+)/ocr/submit/$', + name='document_submit', view=DocumentSubmitView.as_view() ), url( - r'^document_types/submit/$', DocumentTypeSubmitView.as_view(), - name='document_type_submit' + regex=r'^documents/multiple/ocr/submit/$', + name='document_multiple_submit', + view=DocumentSubmitView.as_view() ), url( - r'^document_types/(?P\d+)/ocr/settings/$', - DocumentTypeSettingsEditView.as_view(), - name='document_type_settings' + regex=r'^documents/pages/(?P\d+)/ocr/content/$', + name='document_page_content', + view=DocumentPageOCRContentView.as_view() ), - url(r'^errors/$', EntryListView.as_view(), name='entry_list'), + url( + regex=r'^errors/$', name='entry_list', + view=EntryListView.as_view() + ) ] api_urls = [ url( - r'^documents/(?P\d+)/submit/$', APIDocumentOCRView.as_view(), - name='document-ocr-submit-view' + regex=r'^documents/(?P\d+)/ocr/submit/$', + name='document-ocr-submit-view', + view=APIDocumentOCRView.as_view() ), url( - r'^documents/(?P\d+)/versions/(?P\d+)/ocr/$', - APIDocumentVersionOCRView.as_view(), - name='document-version-ocr-submit-view' + regex=r'^documents/(?P\d+)/versions/(?P\d+)/ocr/$', + name='document-version-ocr-submit-view', + view=APIDocumentVersionOCRView.as_view() ), url( - r'^documents/(?P\d+)/versions/(?P\d+)/pages/(?P\d+)/ocr/$', - APIDocumentPageOCRContentView.as_view(), - name='document-page-ocr-content-view' - ), + regex=r'^documents/(?P\d+)/versions/(?P\d+)/pages/(?P\d+)/ocr/$', + name='document-page-ocr-content-view', + view=APIDocumentPageOCRContentView.as_view() + ) ] diff --git a/mayan/apps/ocr/views.py b/mayan/apps/ocr/views.py index 94c70173a9..0f45a27b35 100644 --- a/mayan/apps/ocr/views.py +++ b/mayan/apps/ocr/views.py @@ -6,6 +6,7 @@ from django.shortcuts import get_object_or_404 from django.urls import reverse_lazy from django.utils.translation import ugettext_lazy as _, ungettext +from mayan.apps.acls.models import AccessControlList from mayan.apps.common.generics import ( FormView, MultipleObjectConfirmActionView, SingleObjectDetailView, SingleObjectDownloadView, SingleObjectEditView, SingleObjectListView @@ -26,6 +27,7 @@ class DocumentOCRContentView(SingleObjectDetailView): form_class = DocumentOCRContentForm model = Document object_permission = permission_ocr_content_view + pk_url_kwarg = 'document_id' def dispatch(self, request, *args, **kwargs): result = super(DocumentOCRContentView, self).dispatch( @@ -46,6 +48,7 @@ class DocumentOCRContentView(SingleObjectDetailView): class DocumentOCRDownloadView(SingleObjectDownloadView): model = Document object_permission = permission_ocr_content_view + pk_url_kwarg = 'document_id' def get_file(self): file_object = DocumentOCRDownloadView.TextIteratorIO( @@ -60,7 +63,9 @@ class DocumentOCRErrorsListView(SingleObjectListView): object_permission = permission_ocr_document def get_document(self): - return get_object_or_404(klass=Document, pk=self.kwargs['pk']) + return get_object_or_404( + klass=Document, pk=self.kwargs['document_id'] + ) def get_extra_context(self): return { @@ -77,6 +82,7 @@ class DocumentPageOCRContentView(SingleObjectDetailView): form_class = DocumentPageOCRContentForm model = DocumentPage object_permission = permission_ocr_content_view + pk_url_kwarg = 'document_page_id' def dispatch(self, request, *args, **kwargs): result = super(DocumentPageOCRContentView, self).dispatch( @@ -98,6 +104,7 @@ class DocumentPageOCRContentView(SingleObjectDetailView): class DocumentSubmitView(MultipleObjectConfirmActionView): model = Document object_permission = permission_ocr_document + pk_url_kwarg = 'document_id' success_message = '%(count)d document submitted to the OCR queue.' success_message_plural = '%(count)d documents submitted to the OCR queue.' @@ -106,9 +113,9 @@ class DocumentSubmitView(MultipleObjectConfirmActionView): result = { 'title': ungettext( - 'Submit the selected document to the OCR queue?', - 'Submit the selected documents to the OCR queue?', - queryset.count() + singular='Submit the selected document to the OCR queue?', + plural='Submit the selected documents to the OCR queue?', + number=queryset.count() ) } @@ -121,10 +128,20 @@ class DocumentSubmitView(MultipleObjectConfirmActionView): class DocumentTypeSettingsEditView(SingleObjectEditView): fields = ('auto_ocr',) object_permission = permission_document_type_ocr_setup - post_action_redirect = reverse_lazy('documents:document_type_list') + post_action_redirect = reverse_lazy( + viewname='documents:document_type_list' + ) def get_document_type(self): - return get_object_or_404(klass=DocumentType, pk=self.kwargs['pk']) + queryset = AccessControlList.objects.restrict_queryset( + permission=permission_document_type_ocr_setup, + queryset=DocumentType.objects.all(), + user=self.request.user + ) + + return get_object_or_404( + klass=queryset, pk=self.kwargs['document_type_id'] + ) def get_extra_context(self): return { @@ -143,7 +160,7 @@ class DocumentTypeSubmitView(FormView): 'title': _('Submit all documents of a type for OCR') } form_class = DocumentTypeFilteredSelectForm - post_action_redirect = reverse_lazy('common:tools_list') + post_action_redirect = reverse_lazy(viewname='common:tools_list') def get_form_extra_kwargs(self): return { @@ -160,14 +177,14 @@ class DocumentTypeSubmitView(FormView): count += 1 messages.success( - self.request, _( + message=_( '%(count)d documents added to the OCR queue.' ) % { 'count': count, - } + }, request=self.request ) - return HttpResponseRedirect(self.get_success_url()) + return HttpResponseRedirect(redirect_to=self.get_success_url()) class EntryListView(SingleObjectListView): From 55356c4781b7ac7a7d02f51d55e996ea94f4015c Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Mon, 21 Jan 2019 20:06:34 -0400 Subject: [PATCH 035/209] Update document state app Sort arguments. Add keyword arguments. Update URL parameters to the '_id' form. Signed-off-by: Roberto Rosario --- mayan/apps/document_states/api_views.py | 20 +- mayan/apps/document_states/apps.py | 26 +- mayan/apps/document_states/error_logs.py | 2 +- mayan/apps/document_states/forms.py | 2 +- mayan/apps/document_states/models.py | 10 +- mayan/apps/document_states/permissions.py | 12 +- mayan/apps/document_states/queues.py | 12 +- mayan/apps/document_states/serializers.py | 76 ++--- mayan/apps/document_states/settings.py | 2 +- .../apps/document_states/tests/test_views.py | 64 ++-- mayan/apps/document_states/urls.py | 290 +++++++++--------- mayan/apps/document_states/views.py | 130 +++++--- .../apps/document_states/workflow_actions.py | 2 +- 13 files changed, 350 insertions(+), 298 deletions(-) diff --git a/mayan/apps/document_states/api_views.py b/mayan/apps/document_states/api_views.py index a828e92570..65a637072a 100644 --- a/mayan/apps/document_states/api_views.py +++ b/mayan/apps/document_states/api_views.py @@ -41,7 +41,7 @@ class APIDocumentTypeWorkflowListView(generics.ListAPIView): serializer_class = WorkflowSerializer def get_document_type(self): - document_type = get_object_or_404(klass=DocumentType, pk=self.kwargs['pk']) + document_type = get_object_or_404(klass=DocumentType, pk=self.kwargs['document_type_pk']) AccessControlList.objects.check_access( permissions=permission_document_type_view, user=self.request.user, @@ -106,7 +106,7 @@ class APIWorkflowDocumentTypeList(generics.ListCreateAPIView): else: permission_required = permission_workflow_edit - workflow = get_object_or_404(klass=Workflow, pk=self.kwargs['pk']) + workflow = get_object_or_404(klass=Workflow, pk=self.kwargs['workflow_pk']) AccessControlList.objects.check_access( permissions=permission_required, user=self.request.user, @@ -159,7 +159,7 @@ class APIWorkflowDocumentTypeView(generics.RetrieveDestroyAPIView): else: permission_required = permission_workflow_edit - workflow = get_object_or_404(klass=Workflow, pk=self.kwargs['pk']) + workflow = get_object_or_404(klass=Workflow, pk=self.kwargs['workflow_pk']) AccessControlList.objects.check_access( permissions=permission_required, user=self.request.user, @@ -297,7 +297,7 @@ class APIWorkflowStateListView(generics.ListCreateAPIView): else: permission_required = permission_workflow_edit - workflow = get_object_or_404(klass=Workflow, pk=self.kwargs['pk']) + workflow = get_object_or_404(klass=Workflow, pk=self.kwargs['workflow_pk']) AccessControlList.objects.check_access( permissions=permission_required, user=self.request.user, @@ -340,7 +340,7 @@ class APIWorkflowStateView(generics.RetrieveUpdateDestroyAPIView): else: permission_required = permission_workflow_edit - workflow = get_object_or_404(klass=Workflow, pk=self.kwargs['pk']) + workflow = get_object_or_404(klass=Workflow, pk=self.kwargs['workflow_pk']) AccessControlList.objects.check_access( permissions=permission_required, user=self.request.user, @@ -393,7 +393,7 @@ class APIWorkflowTransitionListView(generics.ListCreateAPIView): else: permission_required = permission_workflow_edit - workflow = get_object_or_404(klass=Workflow, pk=self.kwargs['pk']) + workflow = get_object_or_404(klass=Workflow, pk=self.kwargs['workflow_pk']) AccessControlList.objects.check_access( permissions=permission_required, user=self.request.user, @@ -447,7 +447,7 @@ class APIWorkflowTransitionView(generics.RetrieveUpdateDestroyAPIView): else: permission_required = permission_workflow_edit - workflow = get_object_or_404(klass=Workflow, pk=self.kwargs['pk']) + workflow = get_object_or_404(klass=Workflow, pk=self.kwargs['workflow_pk']) AccessControlList.objects.check_access( permissions=permission_required, user=self.request.user, @@ -471,7 +471,7 @@ class APIWorkflowInstanceListView(generics.ListAPIView): } def get_document(self): - document = get_object_or_404(klass=Document, pk=self.kwargs['pk']) + document = get_object_or_404(klass=Document, pk=self.kwargs['document_pk']) AccessControlList.objects.check_access( permissions=permission_workflow_view, user=self.request.user, @@ -496,7 +496,7 @@ class APIWorkflowInstanceView(generics.RetrieveAPIView): serializer_class = WorkflowInstanceSerializer def get_document(self): - document = get_object_or_404(klass=Document, pk=self.kwargs['pk']) + document = get_object_or_404(klass=Document, pk=self.kwargs['document_pk']) AccessControlList.objects.check_access( permissions=permission_workflow_view, user=self.request.user, @@ -515,7 +515,7 @@ class APIWorkflowInstanceLogEntryListView(generics.ListCreateAPIView): post: Transition a document workflow by creating a new document workflow log entry. """ def get_document(self): - document = get_object_or_404(klass=Document, pk=self.kwargs['pk']) + document = get_object_or_404(klass=Document, pk=self.kwargs['document_pk']) if self.request.method == 'GET': """ diff --git a/mayan/apps/document_states/apps.py b/mayan/apps/document_states/apps.py index a00df2d808..83744b2826 100644 --- a/mayan/apps/document_states/apps.py +++ b/mayan/apps/document_states/apps.py @@ -73,16 +73,24 @@ class DocumentStatesApp(MayanAppConfig): app_label='common', model_name='ErrorLogEntry' ) - Workflow = self.get_model('Workflow') - WorkflowInstance = self.get_model('WorkflowInstance') - WorkflowInstanceLogEntry = self.get_model('WorkflowInstanceLogEntry') - WorkflowRuntimeProxy = self.get_model('WorkflowRuntimeProxy') - WorkflowState = self.get_model('WorkflowState') - WorkflowStateAction = self.get_model('WorkflowStateAction') - WorkflowStateRuntimeProxy = self.get_model('WorkflowStateRuntimeProxy') - WorkflowTransition = self.get_model('WorkflowTransition') + Workflow = self.get_model(model_name='Workflow') + WorkflowInstance = self.get_model(model_name='WorkflowInstance') + WorkflowInstanceLogEntry = self.get_model( + model_name='WorkflowInstanceLogEntry' + ) + WorkflowRuntimeProxy = self.get_model( + model_name='WorkflowRuntimeProxy' + ) + WorkflowState = self.get_model(model_name='WorkflowState') + WorkflowStateAction = self.get_model( + model_name='WorkflowStateAction' + ) + WorkflowStateRuntimeProxy = self.get_model( + model_name='WorkflowStateRuntimeProxy' + ) + WorkflowTransition = self.get_model(model_name='WorkflowTransition') WorkflowTransitionTriggerEvent = self.get_model( - 'WorkflowTransitionTriggerEvent' + model_name='WorkflowTransitionTriggerEvent' ) Document.add_to_class( diff --git a/mayan/apps/document_states/error_logs.py b/mayan/apps/document_states/error_logs.py index b87b9ef298..447d9cee17 100644 --- a/mayan/apps/document_states/error_logs.py +++ b/mayan/apps/document_states/error_logs.py @@ -5,5 +5,5 @@ from django.utils.translation import ugettext_lazy as _ from mayan.apps.common.classes import ErrorLogNamespace error_log_state_actions = ErrorLogNamespace( - name='workflow_state_actions', label=_('Workflow state actions') + label=_('Workflow state actions'), name='workflow_state_actions' ) diff --git a/mayan/apps/document_states/forms.py b/mayan/apps/document_states/forms.py index 44359762e6..1368c58e71 100644 --- a/mayan/apps/document_states/forms.py +++ b/mayan/apps/document_states/forms.py @@ -50,7 +50,7 @@ class WorkflowStateActionDynamicForm(DynamicModelForm): WorkflowStateActionDynamicForm, self ).__init__(*args, **kwargs) if self.instance.action_data: - for key, value in json.loads(self.instance.action_data).items(): + for key, value in json.loads(s=self.instance.action_data).items(): self.fields[key].initial = value return result diff --git a/mayan/apps/document_states/models.py b/mayan/apps/document_states/models.py index c3f8f1a92b..f8e179daf8 100644 --- a/mayan/apps/document_states/models.py +++ b/mayan/apps/document_states/models.py @@ -87,7 +87,10 @@ class Workflow(models.Model): def get_api_image_url(self, *args, **kwargs): final_url = furl() final_url.args = kwargs - final_url.path = reverse('rest_api:workflow-image', args=(self.pk,)) + final_url.path = reverse( + viewname='rest_api:workflow-image', + kwargs={'workflow_id': self.pk} + ) final_url.args['_hash'] = self.get_hash() return final_url.tostr() @@ -321,7 +324,7 @@ class WorkflowStateAction(models.Model): get_class_label.short_description = _('Action type') def loads(self): - return json.loads(self.action_data) + return json.loads(s=self.action_data) @python_2_unicode_compatible @@ -403,7 +406,8 @@ class WorkflowInstance(models.Model): def get_absolute_url(self): return reverse( - 'document_states:workflow_instance_detail', args=(str(self.pk),) + viewname='document_states:workflow_instance_detail', + kwargs={'workflow_instance_id': self.pk} ) def get_context(self): diff --git a/mayan/apps/document_states/permissions.py b/mayan/apps/document_states/permissions.py index 43f974cde1..cef7c72d6d 100644 --- a/mayan/apps/document_states/permissions.py +++ b/mayan/apps/document_states/permissions.py @@ -7,23 +7,23 @@ from mayan.apps.permissions import PermissionNamespace namespace = PermissionNamespace(label=_('Document workflows'), name='document_states') permission_workflow_create = namespace.add_permission( - name='workflow_create', label=_('Create workflows') + label=_('Create workflows'), name='workflow_create' ) permission_workflow_delete = namespace.add_permission( - name='workflow_delte', label=_('Delete workflows') + label=_('Delete workflows'), name='workflow_delte' ) permission_workflow_edit = namespace.add_permission( - name='workflow_edit', label=_('Edit workflows') + label=_('Edit workflows'), name='workflow_edit' ) permission_workflow_view = namespace.add_permission( - name='workflow_view', label=_('View workflows') + label=_('View workflows'), name='workflow_view' ) # Translators: This text refers to the permission to grant user the ability to # 'transition workflows' from one state to another, to move the workflow # forwards permission_workflow_transition = namespace.add_permission( - name='workflow_transition', label=_('Transition workflows') + label=_('Transition workflows'), name='workflow_transition' ) permission_workflow_tools = namespace.add_permission( - name='workflow_tools', label=_('Execute workflow tools') + label=_('Execute workflow tools'), name='workflow_tools' ) diff --git a/mayan/apps/document_states/queues.py b/mayan/apps/document_states/queues.py index 753a612421..40ec12bff7 100644 --- a/mayan/apps/document_states/queues.py +++ b/mayan/apps/document_states/queues.py @@ -5,17 +5,17 @@ from django.utils.translation import ugettext_lazy as _ from mayan.apps.task_manager.classes import CeleryQueue queue_document_states = CeleryQueue( - name='document_states', label=_('Document states') + label=_('Document states'), name='document_states' ) queue_document_states_fast = CeleryQueue( - name='document_states_fast', label=_('Document states fast') + label=_('Document states fast'), name='document_states_fast' ) queue_document_states.add_task_type( - name='mayan.apps.document_states.tasks.task_launch_all_workflows', - label=_('Launch all workflows') + label=_('Launch all workflows'), + name='mayan.apps.document_states.tasks.task_launch_all_workflows' ) queue_document_states_fast.add_task_type( - name='mayan.apps.document_states.tasks.task_generate_document_state_image', - label=_('Generate workflow previews') + label=_('Generate workflow previews'), + name='mayan.apps.document_states.tasks.task_generate_document_state_image' ) diff --git a/mayan/apps/document_states/serializers.py b/mayan/apps/document_states/serializers.py index 78a055254b..8d89577c5e 100644 --- a/mayan/apps/document_states/serializers.py +++ b/mayan/apps/document_states/serializers.py @@ -48,9 +48,9 @@ class WorkflowDocumentTypeSerializer(DocumentTypeSerializer): def get_workflow_document_type_url(self, instance): return reverse( - 'rest_api:workflow-document-type-detail', args=( - self.context['workflow'].pk, instance.pk - ), request=self.context['request'], format=self.context['format'] + viewname='rest_api:workflow-document-type-detail', kwargs={ + 'workflow_pk': self.context['workflow'].pk, 'document_type_pk': instance.pk + }, request=self.context['request'], format=self.context['format'] ) @@ -70,16 +70,16 @@ class WorkflowStateSerializer(serializers.HyperlinkedModelSerializer): def get_url(self, instance): return reverse( - 'rest_api:workflowstate-detail', args=( - instance.workflow.pk, instance.pk - ), request=self.context['request'], format=self.context['format'] + viewname='rest_api:workflowstate-detail', kwargs={ + 'workflow_pk': instance.workflow.pk, 'workflow_state_pk': instance.pk + }, request=self.context['request'], format=self.context['format'] ) def get_workflow_url(self, instance): return reverse( - 'rest_api:workflow-detail', args=( - instance.workflow.pk, - ), request=self.context['request'], format=self.context['format'] + viewname='rest_api:workflow-detail', kwargs={ + 'workflow_pk': instance.workflow.pk, + }, request=self.context['request'], format=self.context['format'] ) @@ -98,16 +98,16 @@ class WorkflowTransitionSerializer(serializers.HyperlinkedModelSerializer): def get_url(self, instance): return reverse( - 'rest_api:workflowtransition-detail', args=( - instance.workflow.pk, instance.pk - ), request=self.context['request'], format=self.context['format'] + viewname='rest_api:workflowtransition-detail', kwargs={ + 'workflow_pk': instance.workflow.pk, 'transition_pk': instance.pk + }, request=self.context['request'], format=self.context['format'] ) def get_workflow_url(self, instance): return reverse( - 'rest_api:workflow-detail', args=( - instance.workflow.pk, - ), request=self.context['request'], format=self.context['format'] + viewname='rest_api:workflow-detail', kwargs={ + 'workflow_pk': instance.workflow.pk, + }, request=self.context['request'], format=self.context['format'] ) @@ -145,16 +145,16 @@ class WritableWorkflowTransitionSerializer(serializers.ModelSerializer): def get_url(self, instance): return reverse( - 'rest_api:workflowtransition-detail', args=( - instance.workflow.pk, instance.pk - ), request=self.context['request'], format=self.context['format'] + viewname='rest_api:workflowtransition-detail', kwargs={ + 'workflow_pk': instance.workflow.pk, 'transition_pk': instance.pk + }, request=self.context['request'], format=self.context['format'] ) def get_workflow_url(self, instance): return reverse( - 'rest_api:workflow-detail', args=( - instance.workflow.pk, - ), request=self.context['request'], format=self.context['format'] + viewname='rest_api:workflow-detail', kwargs={ + 'workflow_pk': instance.workflow.pk, + }, request=self.context['request'], format=self.context['format'] ) def update(self, instance, validated_data): @@ -190,9 +190,9 @@ class WorkflowSerializer(serializers.HyperlinkedModelSerializer): def get_image_url(self, instance): return reverse( - 'rest_api:workflow-image', args=( - instance.pk, - ), request=self.context['request'], format=self.context['format'] + viewname='rest_api:workflow-image', kwargs={ + 'workflow_pk': instance.pk + }, request=self.context['request'], format=self.context['format'] ) @@ -210,10 +210,10 @@ class WorkflowInstanceLogEntrySerializer(serializers.ModelSerializer): def get_document_workflow_url(self, instance): return reverse( - 'rest_api:workflowinstance-detail', args=( - instance.workflow_instance.document.pk, - instance.workflow_instance.pk, - ), request=self.context['request'], format=self.context['format'] + viewname='rest_api:workflowinstance-detail', kwargs={ + 'document_pk': instance.workflow_instance.document.pk, + 'transition_pk': instance.workflow_instance.pk + }, request=self.context['request'], format=self.context['format'] ) @@ -248,16 +248,16 @@ class WorkflowInstanceSerializer(serializers.ModelSerializer): def get_document_workflow_url(self, instance): return reverse( - 'rest_api:workflowinstance-detail', args=( - instance.document.pk, instance.pk, - ), request=self.context['request'], format=self.context['format'] + viewname='rest_api:workflowinstance-detail', kwargs={ + 'document_pk': instance.document.pk, 'transition_pk': instance.pk, + }, request=self.context['request'], format=self.context['format'] ) def get_log_entries_url(self, instance): return reverse( - 'rest_api:workflowinstancelogentry-list', args=( - instance.document.pk, instance.pk, - ), request=self.context['request'], format=self.context['format'] + viewname='rest_api:workflowinstancelogentry-list', kwargs={ + 'document_pk': instance.document.pk, 'workflow_pk': instance.pk, + }, request=self.context['request'], format=self.context['format'] ) @@ -339,10 +339,10 @@ class WritableWorkflowInstanceLogEntrySerializer(serializers.ModelSerializer): def get_document_workflow_url(self, instance): return reverse( - 'rest_api:workflowinstance-detail', args=( - instance.workflow_instance.document.pk, - instance.workflow_instance.pk, - ), request=self.context['request'], format=self.context['format'] + viewname='rest_api:workflowinstance-detail', kwargs={ + 'document_pk': instance.workflow_instance.document.pk, + 'workflow_pk': instance.workflow_instance.pk, + }, request=self.context['request'], format=self.context['format'] ) def validate(self, attrs): diff --git a/mayan/apps/document_states/settings.py b/mayan/apps/document_states/settings.py index 49a68baa30..b90ae5e382 100644 --- a/mayan/apps/document_states/settings.py +++ b/mayan/apps/document_states/settings.py @@ -7,7 +7,7 @@ from django.utils.translation import ugettext_lazy as _ from mayan.apps.smart_settings import Namespace -namespace = Namespace(name='document_states', label=_('Workflows')) +namespace = Namespace(label=_('Workflows'), name='document_states') settings_workflow_image_cache_time = namespace.add_setting( global_name='WORKFLOWS_IMAGE_CACHE_TIME', default='31556926', diff --git a/mayan/apps/document_states/tests/test_views.py b/mayan/apps/document_states/tests/test_views.py index 0b40454a27..d25fa91971 100644 --- a/mayan/apps/document_states/tests/test_views.py +++ b/mayan/apps/document_states/tests/test_views.py @@ -49,9 +49,8 @@ class DocumentStateViewTestCase(WorkflowTestMixin, GenericViewTestCase): def _request_workflow_delete_view(self): return self.post( - viewname='document_states:setup_workflow_delete', args=( - self.workflow.pk, - ), + viewname='document_states:setup_workflow_delete', + kwargs={'workflow_id': self.workflow.pk} ) def test_workflow_delete_view_no_access(self): @@ -69,9 +68,9 @@ class DocumentStateViewTestCase(WorkflowTestMixin, GenericViewTestCase): def _request_workflow_edit_view(self): return self.post( - viewname='document_states:setup_workflow_edit', args=( - self.workflow.pk, - ), data={ + viewname='document_states:setup_workflow_edit', + kwargs={'workflow_id': self.workflow.pk}, + data={ 'label': TEST_WORKFLOW_LABEL_EDITED, 'internal_name': self.workflow.internal_name } @@ -80,7 +79,7 @@ class DocumentStateViewTestCase(WorkflowTestMixin, GenericViewTestCase): def test_workflow_edit_view_no_access(self): self._create_workflow() response = self._request_workflow_edit_view() - self.assertEquals(response.status_code, 403) + self.assertEquals(response.status_code, 404) self.workflow.refresh_from_db() self.assertEqual(self.workflow.label, TEST_WORKFLOW_LABEL) @@ -112,15 +111,14 @@ class DocumentStateViewTestCase(WorkflowTestMixin, GenericViewTestCase): def _request_workflow_preview_view(self): return self.get( - viewname='document_states:workflow_preview', args=( - self.workflow.pk, - ), + viewname='document_states:workflow_preview', + kwargs={'workflow_id': self.workflow.pk} ) def test_workflow_preview_view_no_access(self): self._create_workflow() response = self._request_workflow_preview_view() - self.assertEquals(response.status_code, 403) + self.assertEquals(response.status_code, 404) self.assertTrue(self.workflow in Workflow.objects.all()) def test_workflow_preview_view_with_access(self): @@ -138,7 +136,8 @@ class DocumentStateStateViewTestCase(WorkflowTestMixin, GenericViewTestCase): def _request_workflow_state_create_view(self): return self.post( viewname='document_states:setup_workflow_state_create', - args=(self.workflow.pk,), data={ + kwargs={'workflow_id': self.workflow.pk}, + data={ 'label': TEST_WORKFLOW_STATE_LABEL, 'completion': TEST_WORKFLOW_STATE_COMPLETION, } @@ -152,7 +151,9 @@ class DocumentStateStateViewTestCase(WorkflowTestMixin, GenericViewTestCase): def test_create_workflow_state_with_access(self): self._create_workflow() - self.grant_access(permission=permission_workflow_edit, obj=self.workflow) + self.grant_access( + permission=permission_workflow_edit, obj=self.workflow + ) response = self._request_workflow_state_create_view() self.assertEquals(response.status_code, 302) self.assertEquals(WorkflowState.objects.count(), 1) @@ -167,14 +168,14 @@ class DocumentStateStateViewTestCase(WorkflowTestMixin, GenericViewTestCase): def _request_workflow_state_delete_view(self): return self.post( viewname='document_states:setup_workflow_state_delete', - args=(self.workflow_state.pk,) + kwargs={'workflow_state_id': self.workflow_state.pk} ) def test_delete_workflow_state_no_access(self): self._create_workflow() self._create_workflow_states() response = self._request_workflow_state_delete_view() - self.assertEquals(response.status_code, 403) + self.assertEquals(response.status_code, 404) self.assertEquals(WorkflowState.objects.count(), 2) def test_delete_workflow_state_with_access(self): @@ -188,7 +189,8 @@ class DocumentStateStateViewTestCase(WorkflowTestMixin, GenericViewTestCase): def _request_workflow_state_edit_view(self): return self.post( viewname='document_states:setup_workflow_state_edit', - args=(self.workflow_state.pk,), data={ + kwargs={'workflow_state_id': self.workflow_state.pk}, + data={ 'label': TEST_WORKFLOW_STATE_LABEL_EDITED } ) @@ -197,7 +199,7 @@ class DocumentStateStateViewTestCase(WorkflowTestMixin, GenericViewTestCase): self._create_workflow() self._create_workflow_states() response = self._request_workflow_state_edit_view() - self.assertEquals(response.status_code, 403) + self.assertEquals(response.status_code, 404) self.assertEquals(self.workflow_state.label, TEST_WORKFLOW_STATE_LABEL) def test_edit_workflow_state_with_access(self): @@ -212,14 +214,14 @@ class DocumentStateStateViewTestCase(WorkflowTestMixin, GenericViewTestCase): def _request_workflow_state_list_view(self): return self.get( viewname='document_states:setup_workflow_state_list', - args=(self.workflow.pk,) + kwargs={'workflow_id': self.workflow.pk} ) def test_workflow_state_list_no_access(self): self._create_workflow() self._create_workflow_states() response = self._request_workflow_state_list_view() - self.assertEquals(response.status_code, 403) + self.assertEquals(response.status_code, 404) def test_workflow_state_list_with_access(self): self._create_workflow() @@ -260,7 +262,7 @@ class DocumentStateToolViewTestCase(GenericDocumentViewTestCase): def _request_workflow_launch_view(self): return self.post( - 'document_states:tool_launch_all_workflows', + viewname='document_states:tool_launch_all_workflows', ) def test_tool_launch_all_workflows_view_no_permission(self): @@ -295,7 +297,7 @@ class DocumentStateTransitionViewTestCase(WorkflowTestMixin, GenericDocumentView def _request_workflow_transition_create_view(self): return self.post( viewname='document_states:setup_workflow_transition_create', - args=(self.workflow.pk,), data={ + kwargs={'workflow_id': self.workflow.pk}, data={ 'label': TEST_WORKFLOW_TRANSITION_LABEL, 'origin_state': self.workflow_initial_state.pk, 'destination_state': self.workflow_state.pk, @@ -306,7 +308,7 @@ class DocumentStateTransitionViewTestCase(WorkflowTestMixin, GenericDocumentView self._create_workflow() self._create_workflow_states() response = self._request_workflow_transition_create_view() - self.assertEquals(response.status_code, 403) + self.assertEquals(response.status_code, 404) self.assertEquals(WorkflowTransition.objects.count(), 0) def test_create_workflow_transition_with_access(self): @@ -332,7 +334,7 @@ class DocumentStateTransitionViewTestCase(WorkflowTestMixin, GenericDocumentView def _request_workflow_transition_delete_view(self): return self.post( viewname='document_states:setup_workflow_transition_delete', - args=(self.workflow_transition.pk,) + kwargs={'workflow_transition_id': self.workflow_transition.pk} ) def test_delete_workflow_transition_no_access(self): @@ -340,7 +342,7 @@ class DocumentStateTransitionViewTestCase(WorkflowTestMixin, GenericDocumentView self._create_workflow_states() self._create_workflow_transition() response = self._request_workflow_transition_delete_view() - self.assertEquals(response.status_code, 403) + self.assertEquals(response.status_code, 404) self.assertTrue(self.workflow_transition in WorkflowTransition.objects.all()) def test_delete_workflow_transition_with_access(self): @@ -355,7 +357,8 @@ class DocumentStateTransitionViewTestCase(WorkflowTestMixin, GenericDocumentView def _request_workflow_transition_edit_view(self): return self.post( viewname='document_states:setup_workflow_transition_edit', - args=(self.workflow_transition.pk,), data={ + kwargs={'workflow_transition_id': self.workflow_transition.pk}, + data={ 'label': TEST_WORKFLOW_TRANSITION_LABEL_EDITED, 'origin_state': self.workflow_initial_state.pk, 'destination_state': self.workflow_state.pk, @@ -367,7 +370,7 @@ class DocumentStateTransitionViewTestCase(WorkflowTestMixin, GenericDocumentView self._create_workflow_states() self._create_workflow_transition() response = self._request_workflow_transition_edit_view() - self.assertEquals(response.status_code, 403) + self.assertEquals(response.status_code, 404) self.workflow_transition.refresh_from_db() self.assertEqual( self.workflow_transition.label, TEST_WORKFLOW_TRANSITION_LABEL @@ -388,7 +391,7 @@ class DocumentStateTransitionViewTestCase(WorkflowTestMixin, GenericDocumentView def _request_workflow_transition_list_view(self): return self.get( viewname='document_states:setup_workflow_transition_list', - args=(self.workflow.pk,) + kwargs={'workflow_id': self.workflow.pk} ) def test_workflow_transition_list_no_access(self): @@ -411,7 +414,8 @@ class DocumentStateTransitionViewTestCase(WorkflowTestMixin, GenericDocumentView def _request_workflow_transition(self): return self.post( viewname='document_states:workflow_instance_transition', - args=(self.workflow_instance.pk,), data={ + kwargs={'workflow_instance_id': self.workflow_instance.pk}, + data={ 'transition': self.workflow_transition.pk, } ) @@ -481,7 +485,7 @@ class DocumentStateTransitionEventViewTestCase(WorkflowTestMixin, GenericDocumen def _request_workflow_transition_event_list_view(self): return self.get( viewname='document_states:setup_workflow_transition_events', - args=(self.workflow_transition.pk,) + kwargs={'workflow_transition_id': self.workflow_transition.pk} ) def test_workflow_transition_event_list_no_access(self): @@ -489,7 +493,7 @@ class DocumentStateTransitionEventViewTestCase(WorkflowTestMixin, GenericDocumen self._create_workflow_states() self._create_workflow_transition() response = self._request_workflow_transition_event_list_view() - self.assertEquals(response.status_code, 403) + self.assertEquals(response.status_code, 404) def test_workflow_transition_event_list_with_access(self): self._create_workflow() diff --git a/mayan/apps/document_states/urls.py b/mayan/apps/document_states/urls.py index bb36b6fda5..48baa4d5e8 100644 --- a/mayan/apps/document_states/urls.py +++ b/mayan/apps/document_states/urls.py @@ -29,207 +29,209 @@ from .views import ( urlpatterns = [ url( - r'^document/(?P\d+)/workflows/$', - DocumentWorkflowInstanceListView.as_view(), - name='document_workflow_instance_list' + regex=r'^workflows/$', name='setup_workflow_list', + view=SetupWorkflowListView.as_view() ), url( - r'^document/workflows/(?P\d+)/$', - WorkflowInstanceDetailView.as_view(), name='workflow_instance_detail' + regex=r'^workflows/create/$', name='setup_workflow_create', + view=SetupWorkflowCreateView.as_view() ), url( - r'^document/workflows/(?P\d+)/transition/$', - WorkflowInstanceTransitionView.as_view(), - name='workflow_instance_transition' + regex=r'^workflows/(?P\d+)/delete/$', + name='setup_workflow_delete', view=SetupWorkflowDeleteView.as_view() ), url( - r'^setup/all/$', SetupWorkflowListView.as_view(), - name='setup_workflow_list' + regex=r'^workflows/(?P\d+)/edit/$', + name='setup_workflow_edit', view=SetupWorkflowEditView.as_view() ), url( - r'^setup/create/$', SetupWorkflowCreateView.as_view(), - name='setup_workflow_create' + regex=r'^workflows/(?P\d+)/preview/$', + name='workflow_preview', view=WorkflowPreviewView.as_view() ), url( - r'^setup/workflow/(?P\d+)/edit/$', SetupWorkflowEditView.as_view(), - name='setup_workflow_edit' + regex=r'^workflows/(?P\d+)/document_types/$', + name='setup_workflow_document_types', + view=SetupWorkflowDocumentTypesView.as_view() ), url( - r'^setup/workflow/(?P\d+)/delete/$', SetupWorkflowDeleteView.as_view(), - name='setup_workflow_delete' + regex=r'^workflows/(?P\d+)/states/$', + name='setup_workflow_state_list', + view=SetupWorkflowStateListView.as_view() ), url( - r'^setup/workflow/(?P\d+)/documents/$', - WorkflowDocumentListView.as_view(), - name='setup_workflow_document_list' + regex=r'^workflows/(?P\d+)/states/create/$', + name='setup_workflow_state_create', + view=SetupWorkflowStateCreateView.as_view() ), url( - r'^setup/workflow/(?P\d+)/document_types/$', - SetupWorkflowDocumentTypesView.as_view(), - name='setup_workflow_document_types' + regex=r'^workflows/states/(?P\d+)/delete/$', + name='setup_workflow_state_delete', + view=SetupWorkflowStateDeleteView.as_view() ), url( - r'^setup/workflow/(?P\d+)/states/$', SetupWorkflowStateListView.as_view(), - name='setup_workflow_state_list' - ), - url( - r'^setup/workflow/(?P\d+)/states/create/$', - SetupWorkflowStateCreateView.as_view(), - name='setup_workflow_state_create' - ), - url( - r'^setup/workflow/(?P\d+)/transitions/$', - SetupWorkflowTransitionListView.as_view(), - name='setup_workflow_transition_list' - ), - url( - r'^setup/workflow/(?P\d+)/transitions/create/$', - SetupWorkflowTransitionCreateView.as_view(), - name='setup_workflow_transition_create' - ), - url( - r'^setup/workflow/(?P\d+)/transitions/events/$', - SetupWorkflowTransitionTriggerEventListView.as_view(), - name='setup_workflow_transition_events' - ), - url( - r'^setup/workflow/state/(?P\d+)/delete/$', - SetupWorkflowStateDeleteView.as_view(), - name='setup_workflow_state_delete' - ), - url( - r'^setup/workflow/state/(?P\d+)/edit/$', - SetupWorkflowStateEditView.as_view(), - name='setup_workflow_state_edit' - ), - url( - r'^setup/workflow/state/(?P\d+)/actions/$', - SetupWorkflowStateActionListView.as_view(), - name='setup_workflow_state_action_list' - ), - url( - r'^setup/workflow/state/(?P\d+)/actions/$', - SetupWorkflowStateActionListView.as_view(), - name='setup_workflow_state_action_list' - ), - url( - r'^setup/workflow/state/(?P\d+)/actions/selection/$', - SetupWorkflowStateActionSelectionView.as_view(), - name='setup_workflow_state_action_selection' - ), - url( - r'^setup/workflow/state/(?P\d+)/actions/(?P[a-zA-Z0-9_.]+)/create/$', - SetupWorkflowStateActionCreateView.as_view(), - name='setup_workflow_state_action_create' + regex=r'^workflows/states/(?P\d+)/edit/$', + name='setup_workflow_state_edit', + view=SetupWorkflowStateEditView.as_view() ), url( - r'^setup/workflow/state/actions/(?P\d+)/delete/$', - SetupWorkflowStateActionDeleteView.as_view(), + regex=r'^workflows/states/(?P\d+)/actions/$', + name='setup_workflow_state_action_list', + view=SetupWorkflowStateActionListView.as_view() + ), + url( + regex=r'^workflows/states/(?P\d+)/actions/selection/$', + name='setup_workflow_state_action_selection', + view=SetupWorkflowStateActionSelectionView.as_view(), + ), + url( + regex=r'^workflows/states/(?P\d+)/actions/(?P[a-zA-Z0-9_.]+)/create/$', + name='setup_workflow_state_action_create', + view=SetupWorkflowStateActionCreateView.as_view() + ), + url( + regex=r'^workflows/states/actions/(?P\d+)/delete/$', + view=SetupWorkflowStateActionDeleteView.as_view(), name='setup_workflow_state_action_delete' ), url( - r'^setup/workflow/state/actions/(?P\d+)/edit/$', - SetupWorkflowStateActionEditView.as_view(), - name='setup_workflow_state_action_edit' - ), - - - url( - r'^setup/workflow/transitions/(?P\d+)/delete/$', - SetupWorkflowTransitionDeleteView.as_view(), - name='setup_workflow_transition_delete' - ), - url( - r'^setup/workflow/transitions/(?P\d+)/edit/$', - SetupWorkflowTransitionEditView.as_view(), - name='setup_workflow_transition_edit' - ), - url( - r'^tools/workflow/all/launch/$', - ToolLaunchAllWorkflows.as_view(), - name='tool_launch_all_workflows' - ), - url( - r'all/$', - WorkflowListView.as_view(), - name='workflow_list' - ), - url( - r'^(?P\d+)/documents/$', - WorkflowDocumentListView.as_view(), - name='workflow_document_list' + regex=r'^workflows/states/actions/(?P\d+)/edit/$', + name='setup_workflow_state_action_edit', + view=SetupWorkflowStateActionEditView.as_view() ), url( - r'^(?P\d+)/states/$', - WorkflowStateListView.as_view(), - name='workflow_state_list' + regex=r'^workflows/(?P\d+)/transitions/$', + name='setup_workflow_transition_list', + view=SetupWorkflowTransitionListView.as_view() ), url( - r'^(?P\d+)/preview/$', - WorkflowPreviewView.as_view(), - name='workflow_preview' + regex=r'^workflows/(?P\d+)/transitions/create/$', + name='setup_workflow_transition_create', + view=SetupWorkflowTransitionCreateView.as_view() ), url( - r'^state/(?P\d+)/documents/$', - WorkflowStateDocumentListView.as_view(), - name='workflow_state_document_list' + regex=r'^workflows/transitions/(?P\d+)/delete/$', + name='setup_workflow_transition_delete', + view=SetupWorkflowTransitionDeleteView.as_view() ), + url( + regex=r'^workflows/transitions/(?P\d+)/edit/$', + name='setup_workflow_transition_edit', + view=SetupWorkflowTransitionEditView.as_view() + ), + url( + regex=r'^workflows/(?P\d+)/transitions/events/$', + name='setup_workflow_transition_events', + view=SetupWorkflowTransitionTriggerEventListView.as_view() + ), + + url( + regex=r'^workflow_instances/$', name='workflow_list', + view=WorkflowListView.as_view() + ), + url( + regex=r'^workflow_instances/(?P\d+)/documents/$', + name='setup_workflow_document_list', + view=WorkflowDocumentListView.as_view() + ), + url( + regex=r'^workflow_instances/(?P\d+)/documents/$', + name='workflow_document_list', + view=WorkflowDocumentListView.as_view() + ), + url( + regex=r'^workflow_instances/(?P\d+)/states/$', + name='workflow_state_list', view=WorkflowStateListView.as_view() + ), + url( + regex=r'^workflow_instances/states/(?P\d+)/documents/$', + name='workflow_state_document_list', + view=WorkflowStateDocumentListView.as_view() + ), + + url( + regex=r'^documents/(?P\d+)/workflows/$', + name='document_workflow_instance_list', + view=DocumentWorkflowInstanceListView.as_view() + ), + url( + regex=r'^documents/workflows/(?P\d+)/$', + name='workflow_instance_detail', + view=WorkflowInstanceDetailView.as_view() + ), + url( + regex=r'^documents/workflows/(?P\d+)/transition/$', + name='workflow_instance_transition', + view=WorkflowInstanceTransitionView.as_view() + ), + url( + regex=r'^tools/workflows/all/launch/$', + name='tool_launch_all_workflows', + view=ToolLaunchAllWorkflows.as_view() + ) ] api_urls = [ - url(r'^workflows/$', APIWorkflowListView.as_view(), name='workflow-list'), url( - r'^workflows/(?P[0-9]+)/$', APIWorkflowView.as_view(), - name='workflow-detail' + regex=r'^workflows/$', name='workflow-list', + view=APIWorkflowListView.as_view() ), url( - r'^workflows/(?P[0-9]+)/image/$', - APIWorkflowImageView.as_view(), name='workflow-image' + regex=r'^workflows/(?P\d+)/$', + name='workflow-detail', view=APIWorkflowView.as_view() ), url( - r'^workflows/(?P[0-9]+)/document_types/$', - APIWorkflowDocumentTypeList.as_view(), - name='workflow-document-type-list' + regex=r'^workflows/(?P\d+)/image/$', + name='workflow-image', view=APIWorkflowImageView.as_view() ), url( - r'^workflows/(?P[0-9]+)/document_types/(?P[0-9]+)/$', - APIWorkflowDocumentTypeView.as_view(), - name='workflow-document-type-detail' + regex=r'^workflows/(?P\d+)/document_types/$', + name='workflow-document-type-list', + view=APIWorkflowDocumentTypeList.as_view() ), url( - r'^workflows/(?P[0-9]+)/states/$', - APIWorkflowStateListView.as_view(), name='workflowstate-list' + regex=r'^workflows/(?P\d+)/document_types/(?P\d+)/$', + name='workflow-document-type-detail', + view=APIWorkflowDocumentTypeView.as_view() ), url( - r'^workflows/(?P[0-9]+)/states/(?P[0-9]+)/$', - APIWorkflowStateView.as_view(), name='workflowstate-detail' + regex=r'^workflows/(?P\d+)/states/$', + name='workflowstate-list', + view=APIWorkflowStateListView.as_view() ), url( - r'^workflows/(?P[0-9]+)/transitions/$', - APIWorkflowTransitionListView.as_view(), name='workflowtransition-list' + regex=r'^workflows/(?P\d+)/states/(?P\d+)/$', + name='workflowstate-detail', view=APIWorkflowStateView.as_view() ), url( - r'^workflows/(?P[0-9]+)/transitions/(?P[0-9]+)/$', - APIWorkflowTransitionView.as_view(), name='workflowtransition-detail' + regex=r'^workflows/(?P\d+)/transitions/$', + name='workflowtransition-list', + view=APIWorkflowTransitionListView.as_view() ), url( - r'^documents/(?P[0-9]+)/workflows/$', - APIWorkflowInstanceListView.as_view(), name='workflowinstance-list' + regex=r'^workflows/(?P\d+)/transitions/(?P\d+)/$', + name='workflowtransition-detail', + view=APIWorkflowTransitionView.as_view() ), url( - r'^documents/(?P[0-9]+)/workflows/(?P[0-9]+)/$', - APIWorkflowInstanceView.as_view(), name='workflowinstance-detail' + regex=r'^documents/(?P\d+)/workflows/$', + name='workflowinstance-list', + view=APIWorkflowInstanceListView.as_view() ), url( - r'^documents/(?P[0-9]+)/workflows/(?P[0-9]+)/log_entries/$', - APIWorkflowInstanceLogEntryListView.as_view(), - name='workflowinstancelogentry-list' + regex=r'^documents/(?P\d+)/workflows/(?P\d+)/$', + name='workflowinstance-detail', + view=APIWorkflowInstanceView.as_view() ), url( - r'^document_types/(?P[0-9]+)/workflows/$', - APIDocumentTypeWorkflowListView.as_view(), - name='documenttype-workflow-list' + regex=r'^documents/(?P\d+)/workflows/(?P\d+)/log_entries/$', + name='workflowinstancelogentry-list', + view=APIWorkflowInstanceLogEntryListView.as_view() ), + url( + regex=r'^document_types/(?P\d+)/workflows/$', + name='documenttype-workflow-list', + view=APIDocumentTypeWorkflowListView.as_view() + ) ] diff --git a/mayan/apps/document_states/views.py b/mayan/apps/document_states/views.py index a7f789109d..8e023f791b 100644 --- a/mayan/apps/document_states/views.py +++ b/mayan/apps/document_states/views.py @@ -58,7 +58,9 @@ class DocumentWorkflowInstanceListView(SingleObjectListView): ).dispatch(request, *args, **kwargs) def get_document(self): - return get_object_or_404(klass=Document, pk=self.kwargs['pk']) + return get_object_or_404( + klass=Document, pk=self.kwargs['document_id'] + ) def get_extra_context(self): return { @@ -114,7 +116,9 @@ class WorkflowInstanceDetailView(SingleObjectListView): return self.get_workflow_instance().log_entries.order_by('-datetime') def get_workflow_instance(self): - return get_object_or_404(klass=WorkflowInstance, pk=self.kwargs['pk']) + return get_object_or_404( + klass=WorkflowInstance, pk=self.kwargs['workflow_instance_id'] + ) class WorkflowInstanceTransitionView(FormView): @@ -127,11 +131,12 @@ class WorkflowInstanceTransitionView(FormView): transition=form.cleaned_data['transition'], user=self.request.user ) messages.success( - self.request, _( + message=_( 'Document "%s" transitioned successfully' - ) % self.get_workflow_instance().document + ) % self.get_workflow_instance().document, + request=self.request ) - return HttpResponseRedirect(self.get_success_url()) + return HttpResponseRedirect(redirect_to=self.get_success_url()) def get_extra_context(self): return { @@ -154,7 +159,9 @@ class WorkflowInstanceTransitionView(FormView): return self.get_workflow_instance().get_absolute_url() def get_workflow_instance(self): - return get_object_or_404(klass=WorkflowInstance, pk=self.kwargs['pk']) + return get_object_or_404( + klass=WorkflowInstance, pk=self.kwargs['workflow_instance_id'] + ) # Setup @@ -185,7 +192,7 @@ class SetupWorkflowListView(SingleObjectListView): class SetupWorkflowCreateView(SingleObjectCreateView): form_class = WorkflowForm model = Workflow - post_action_redirect = reverse_lazy('document_states:setup_workflow_list') + post_action_redirect = reverse_lazy(viewname='document_states:setup_workflow_list') view_permission = permission_workflow_create @@ -193,13 +200,15 @@ class SetupWorkflowEditView(SingleObjectEditView): form_class = WorkflowForm model = Workflow object_permission = permission_workflow_edit - post_action_redirect = reverse_lazy('document_states:setup_workflow_list') + pk_url_kwarg = 'workflow_id' + post_action_redirect = reverse_lazy(viewname='document_states:setup_workflow_list') class SetupWorkflowDeleteView(SingleObjectDeleteView): model = Workflow object_permission = permission_workflow_delete - post_action_redirect = reverse_lazy('document_states:setup_workflow_list') + pk_url_kwarg = 'workflow_id' + post_action_redirect = reverse_lazy(viewname='document_states:setup_workflow_list') class SetupWorkflowDocumentTypesView(AssignRemoveView): @@ -227,7 +236,9 @@ class SetupWorkflowDocumentTypesView(AssignRemoveView): } def get_object(self): - return get_object_or_404(klass=Workflow, pk=self.kwargs['pk']) + return get_object_or_404( + klass=Workflow, pk=self.kwargs['workflow_id'] + ) def left_list(self): return AssignRemoveView.generate_choices( @@ -288,18 +299,21 @@ class SetupWorkflowStateActionCreateView(SingleObjectDynamicFormCreateView): } def get_object(self): - return get_object_or_404(klass=WorkflowState, pk=self.kwargs['pk']) + return get_object_or_404( + klass=WorkflowState, pk=self.kwargs['workflow_state_id'] + ) def get_post_action_redirect(self): return reverse( - 'document_states:setup_workflow_state_action_list', - args=(self.get_object().pk,) + viewname='document_states:setup_workflow_state_action_list', + kwargs={'workflow_state_id': self.get_object().pk} ) class SetupWorkflowStateActionDeleteView(SingleObjectDeleteView): model = WorkflowStateAction object_permission = permission_workflow_edit + pk_url_kwarg = 'workflow_state_action_id' def get_extra_context(self): return { @@ -314,8 +328,8 @@ class SetupWorkflowStateActionDeleteView(SingleObjectDeleteView): def get_post_action_redirect(self): return reverse( - 'document_states:setup_workflow_state_action_list', - args=(self.get_object().state.pk,) + viewname='document_states:setup_workflow_state_action_list', + kwargs={'workflow_state_id': self.get_object().state.pk} ) @@ -323,6 +337,7 @@ class SetupWorkflowStateActionEditView(SingleObjectDynamicFormEditView): form_class = WorkflowStateActionDynamicForm model = WorkflowStateAction object_permission = permission_workflow_edit + pk_url_kwarg = 'workflow_state_action_id' def get_extra_context(self): return { @@ -348,8 +363,8 @@ class SetupWorkflowStateActionEditView(SingleObjectDynamicFormEditView): def get_post_action_redirect(self): return reverse( - 'document_states:setup_workflow_state_action_list', - args=(self.get_object().state.pk,) + viewname='document_states:setup_workflow_state_action_list', + kwargs={'workflow_state_id': self.get_object().state.pk} ) @@ -389,7 +404,9 @@ class SetupWorkflowStateActionListView(SingleObjectListView): return self.get_workflow_state().actions.all() def get_workflow_state(self): - return get_object_or_404(klass=WorkflowState, pk=self.kwargs['pk']) + return get_object_or_404( + klass=WorkflowState, pk=self.kwargs['workflow_state_id'] + ) class SetupWorkflowStateActionSelectionView(FormView): @@ -399,9 +416,12 @@ class SetupWorkflowStateActionSelectionView(FormView): def form_valid(self, form): klass = form.cleaned_data['klass'] return HttpResponseRedirect( - reverse( - 'document_states:setup_workflow_state_action_create', - args=(self.get_object().pk, klass,), + redirect_to=reverse( + viewname='document_states:setup_workflow_state_action_create', + kwargs={ + 'workflow_state_id': self.get_object().pk, + 'class_path': klass + } ) ) @@ -416,7 +436,9 @@ class SetupWorkflowStateActionSelectionView(FormView): } def get_object(self): - return get_object_or_404(klass=WorkflowState, pk=self.kwargs['pk']) + return get_object_or_404( + klass=WorkflowState, pk=self.kwargs['workflow_state_id'] + ) # Workflow states @@ -441,11 +463,12 @@ class SetupWorkflowStateCreateView(SingleObjectCreateView): def get_success_url(self): return reverse( - 'document_states:setup_workflow_state_list', args=(self.kwargs['pk'],) + viewname='document_states:setup_workflow_state_list', + kwargs={'workflow_id': self.kwargs['workflow_id']} ) def get_workflow(self): - workflow = get_object_or_404(klass=Workflow, pk=self.kwargs['pk']) + workflow = get_object_or_404(klass=Workflow, pk=self.kwargs['workflow_id']) AccessControlList.objects.check_access( permissions=(permission_workflow_edit,), obj=workflow, user=self.request.user @@ -456,6 +479,7 @@ class SetupWorkflowStateCreateView(SingleObjectCreateView): class SetupWorkflowStateDeleteView(SingleObjectDeleteView): model = WorkflowState object_permission = permission_workflow_edit + pk_url_kwarg = 'workflow_state_id' def get_extra_context(self): return { @@ -469,12 +493,12 @@ class SetupWorkflowStateDeleteView(SingleObjectDeleteView): def get_success_url(self): return reverse( - 'document_states:setup_workflow_state_list', - args=(self.get_object().workflow.pk,) + viewname='document_states:setup_workflow_state_list', + kwargs={'workflow_id': self.get_object().workflow.pk} ) def get_workflow(self): - workflow = get_object_or_404(klass=Workflow, pk=self.kwargs['pk']) + workflow = get_object_or_404(klass=Workflow, pk=self.kwargs['workflow_id']) AccessControlList.objects.check_access( permissions=(permission_workflow_edit,), obj=workflow, user=self.request.user @@ -486,6 +510,7 @@ class SetupWorkflowStateEditView(SingleObjectEditView): form_class = WorkflowStateForm model = WorkflowState object_permission = permission_workflow_edit + pk_url_kwarg = 'workflow_state_id' def get_extra_context(self): return { @@ -496,8 +521,8 @@ class SetupWorkflowStateEditView(SingleObjectEditView): def get_success_url(self): return reverse( - 'document_states:setup_workflow_state_list', - args=(self.get_object().workflow.pk,) + viewname='document_states:setup_workflow_state_list', + kwargs={'workflow_id': self.get_object().workflow.pk} ) @@ -537,7 +562,7 @@ class SetupWorkflowStateListView(SingleObjectListView): return self.get_workflow().states.all() def get_workflow(self): - return get_object_or_404(klass=Workflow, pk=self.kwargs['pk']) + return get_object_or_404(klass=Workflow, pk=self.kwargs['workflow_id']) # Transitions @@ -569,12 +594,12 @@ class SetupWorkflowTransitionCreateView(SingleObjectCreateView): def get_success_url(self): return reverse( - 'document_states:setup_workflow_transition_list', - args=(self.kwargs['pk'],) + viewname='document_states:setup_workflow_transition_list', + kwargs={'workflow_id': self.kwargs['workflow_id']} ) def get_workflow(self): - workflow = get_object_or_404(klass=Workflow, pk=self.kwargs['pk']) + workflow = get_object_or_404(klass=Workflow, pk=self.kwargs['workflow_id']) AccessControlList.objects.check_access( permissions=(permission_workflow_edit,), obj=workflow, user=self.request.user @@ -585,6 +610,7 @@ class SetupWorkflowTransitionCreateView(SingleObjectCreateView): class SetupWorkflowTransitionDeleteView(SingleObjectDeleteView): model = WorkflowTransition object_permission = permission_workflow_edit + pk_url_kwarg = 'workflow_transition_id' def get_extra_context(self): return { @@ -595,8 +621,8 @@ class SetupWorkflowTransitionDeleteView(SingleObjectDeleteView): def get_success_url(self): return reverse( - 'document_states:setup_workflow_transition_list', - args=(self.get_object().workflow.pk,) + viewname='document_states:setup_workflow_transition_list', + kwargs={'workflow_id': self.get_object().workflow.pk} ) @@ -604,6 +630,7 @@ class SetupWorkflowTransitionEditView(SingleObjectEditView): form_class = WorkflowTransitionForm model = WorkflowTransition object_permission = permission_workflow_edit + pk_url_kwarg = 'workflow_transition_id' def get_extra_context(self): return { @@ -621,8 +648,8 @@ class SetupWorkflowTransitionEditView(SingleObjectEditView): def get_success_url(self): return reverse( - 'document_states:setup_workflow_transition_list', - args=(self.get_object().workflow.pk,) + viewname='document_states:setup_workflow_transition_list', + kwargs={'workflow_id': self.get_object().workflow.pk} ) @@ -655,7 +682,9 @@ class SetupWorkflowTransitionListView(SingleObjectListView): return self.get_workflow().transitions.all() def get_workflow(self): - return get_object_or_404(klass=Workflow, pk=self.kwargs['pk']) + return get_object_or_404( + klass=Workflow, pk=self.kwargs['workflow_id'] + ) # Other @@ -750,7 +779,8 @@ class WorkflowStateDocumentListView(DocumentListView): def get_workflow_state(self): workflow_state = get_object_or_404( - klass=WorkflowStateRuntimeProxy, pk=self.kwargs['pk'] + klass=WorkflowStateRuntimeProxy, + pk=self.kwargs['workflow_state_id'] ) AccessControlList.objects.check_access( @@ -821,16 +851,16 @@ class SetupWorkflowTransitionTriggerEventListView(FormView): instance.save() except Exception as exception: messages.error( - self.request, - _( + message=_( 'Error updating workflow transition trigger events; %s' - ) % exception + ) % exception, request=self.request + ) else: messages.success( - self.request, _( + message=_( 'Workflow transition trigger events updated successfully' - ) + ), request=self.request ) return super( @@ -838,7 +868,9 @@ class SetupWorkflowTransitionTriggerEventListView(FormView): ).form_valid(form=form) def get_object(self): - return get_object_or_404(klass=WorkflowTransition, pk=self.kwargs['pk']) + return get_object_or_404( + klass=WorkflowTransition, pk=self.kwargs['worflow_transition_id'] + ) def get_extra_context(self): return { @@ -874,8 +906,8 @@ class SetupWorkflowTransitionTriggerEventListView(FormView): def get_post_action_redirect(self): return reverse( - 'document_states:setup_workflow_transition_list', - args=(self.get_object().workflow.pk,) + viewname='document_states:setup_workflow_transition_list', + kwargs={'workflow_id': self.get_object().workflow.pk} ) @@ -892,7 +924,8 @@ class ToolLaunchAllWorkflows(ConfirmView): def view_action(self): task_launch_all_workflows.apply_async() messages.success( - self.request, _('Workflow launch queued successfully.') + message=_('Workflow launch queued successfully.'), + request=self.request ) @@ -900,6 +933,7 @@ class WorkflowPreviewView(SingleObjectDetailView): form_class = WorkflowPreviewForm model = Workflow object_permission = permission_workflow_view + pk_url_kwarg = 'workflow_id' def get_extra_context(self): return { diff --git a/mayan/apps/document_states/workflow_actions.py b/mayan/apps/document_states/workflow_actions.py index 65ad0a3433..eb43adb4b2 100644 --- a/mayan/apps/document_states/workflow_actions.py +++ b/mayan/apps/document_states/workflow_actions.py @@ -158,7 +158,7 @@ class HTTPPostAction(WorkflowAction): logger.debug('payload template result: %s', result) try: - payload = json.loads(result, strict=False) + payload = json.loads(s=result, strict=False) except Exception as exception: raise WorkflowStateActionError( _('Payload JSON error: %s') % exception From c7bd2ee8f25801d6e71700cc586b6185ec107a2b Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Tue, 22 Jan 2019 03:19:30 -0400 Subject: [PATCH 036/209] Update document states app Change the app view namespace from 'document_states' to 'workflows'. Add missing icons. Improve view names. Split views into 3 modules: workflows views, runtime proxy views and instance views. Update views to comply with new MERCs 5 and 6. Fix failing tests. Signed-off-by: Roberto Rosario --- mayan/apps/document_states/apps.py | 91 ++- mayan/apps/document_states/icons.py | 33 +- mayan/apps/document_states/links.py | 272 +++++---- mayan/apps/document_states/models.py | 2 +- .../apps/document_states/tests/test_views.py | 175 +++--- mayan/apps/document_states/urls.py | 143 ++--- mayan/apps/document_states/views/__init__.py | 5 + .../views/workflow_instance_views.py | 142 +++++ .../views/workflow_proxy_views.py | 151 +++++ .../{views.py => views/workflow_views.py} | 566 +++++------------- 10 files changed, 829 insertions(+), 751 deletions(-) create mode 100644 mayan/apps/document_states/views/__init__.py create mode 100644 mayan/apps/document_states/views/workflow_instance_views.py create mode 100644 mayan/apps/document_states/views/workflow_proxy_views.py rename mayan/apps/document_states/{views.py => views/workflow_views.py} (54%) diff --git a/mayan/apps/document_states/apps.py b/mayan/apps/document_states/apps.py index 83744b2826..697ac051f8 100644 --- a/mayan/apps/document_states/apps.py +++ b/mayan/apps/document_states/apps.py @@ -22,28 +22,25 @@ from mayan.celery import app from .classes import WorkflowAction from .handlers import ( handler_index_document, handler_launch_workflow, - handler_trigger_transition, + handler_trigger_transition +) +from .links import ( + link_document_workflow_instance_list, link_tool_launch_all_workflows, + link_workflow_create, link_workflow_delete, link_workflow_document_types, + link_workflow_edit, link_workflow_instance_detail, + link_workflow_instance_transition, link_workflow_list, + link_workflow_preview, link_workflow_runtime_proxy_document_list, + link_workflow_runtime_proxy_list, + link_workflow_runtime_proxy_state_document_list, + link_workflow_runtime_proxy_state_list, link_workflow_state_action_delete, + link_workflow_state_action_edit, link_workflow_state_action_list, + link_workflow_state_action_selection, link_workflow_state_create, + link_workflow_state_delete, link_workflow_state_edit, + link_workflow_state_list, link_workflow_transition_create, + link_workflow_transition_delete, link_workflow_transition_edit, + link_workflow_transition_list, link_workflow_transition_triggers ) from .methods import method_get_workflow -from .links import ( - link_document_workflow_instance_list, link_setup_workflow_create, - link_setup_workflow_delete, link_setup_workflow_document_types, - link_setup_workflow_edit, link_setup_workflow_list, - link_setup_workflow_state_action_delete, - link_setup_workflow_state_action_edit, - link_setup_workflow_state_action_list, - link_setup_workflow_state_action_selection, - link_setup_workflow_state_create, link_setup_workflow_state_delete, - link_setup_workflow_state_edit, link_setup_workflow_states, - link_setup_workflow_transition_create, - link_setup_workflow_transition_delete, link_setup_workflow_transition_edit, - link_setup_workflow_transitions, link_tool_launch_all_workflows, - link_workflow_document_list, link_workflow_instance_detail, - link_workflow_instance_transition, - link_workflow_instance_transition_events, link_workflow_list, - link_workflow_preview, link_workflow_state_document_list, - link_workflow_state_list -) from .permissions import ( permission_workflow_delete, permission_workflow_edit, permission_workflow_transition, permission_workflow_view @@ -53,7 +50,7 @@ from .widgets import widget_transition_events class DocumentStatesApp(MayanAppConfig): - app_namespace = 'document_states' + app_namespace = 'workflows' app_url = 'workflows' has_rest_api = True has_tests = True @@ -239,30 +236,30 @@ class DocumentStatesApp(MayanAppConfig): ) menu_list_facet.bind_links( links=( - link_setup_workflow_document_types, - link_setup_workflow_states, link_setup_workflow_transitions, + link_workflow_document_types, + link_workflow_state_list, link_workflow_transition_list, link_workflow_preview, link_acl_list ), sources=(Workflow,) ) - menu_main.bind_links(links=(link_workflow_list,), position=10) + menu_main.bind_links(links=(link_workflow_runtime_proxy_list,), position=10) menu_object.bind_links( links=( - link_setup_workflow_edit, - link_setup_workflow_delete + link_workflow_edit, + link_workflow_delete ), sources=(Workflow,) ) menu_object.bind_links( links=( - link_setup_workflow_state_edit, - link_setup_workflow_state_action_list, - link_setup_workflow_state_delete + link_workflow_state_edit, + link_workflow_state_action_list, + link_workflow_state_delete ), sources=(WorkflowState,) ) menu_object.bind_links( links=( - link_setup_workflow_transition_edit, - link_workflow_instance_transition_events, link_acl_list, - link_setup_workflow_transition_delete + link_workflow_transition_edit, + link_workflow_transition_triggers, link_acl_list, + link_workflow_transition_delete ), sources=(WorkflowTransition,) ) menu_object.bind_links( @@ -273,60 +270,60 @@ class DocumentStatesApp(MayanAppConfig): ) menu_object.bind_links( links=( - link_workflow_document_list, link_workflow_state_list, + link_workflow_runtime_proxy_document_list, + link_workflow_runtime_proxy_state_list, ), sources=(WorkflowRuntimeProxy,) ) menu_object.bind_links( links=( - link_workflow_state_document_list, + link_workflow_runtime_proxy_state_document_list, ), sources=(WorkflowStateRuntimeProxy,) ) menu_object.bind_links( links=( - link_setup_workflow_state_action_edit, + link_workflow_state_action_edit, link_object_error_list, - link_setup_workflow_state_action_delete, + link_workflow_state_action_delete, ), sources=(WorkflowStateAction,) ) menu_secondary.bind_links( - links=(link_setup_workflow_list, link_setup_workflow_create), + links=(link_workflow_list, link_workflow_create), sources=( - Workflow, 'document_states:setup_workflow_create', - 'document_states:setup_workflow_list' + Workflow, 'document_states:workflow_create', + 'document_states:workflow_list' ) ) menu_secondary.bind_links( - links=(link_workflow_list,), + links=(link_workflow_runtime_proxy_list,), sources=( WorkflowRuntimeProxy, ) ) menu_secondary.bind_links( - links=(link_setup_workflow_state_action_selection,), + links=(link_workflow_state_action_selection,), sources=( WorkflowState, ) ) - menu_setup.bind_links(links=(link_setup_workflow_list,)) + menu_setup.bind_links(links=(link_workflow_list,)) menu_sidebar.bind_links( links=( - link_setup_workflow_transition_create, + link_workflow_transition_create, ), sources=( WorkflowTransition, - 'document_states:setup_workflow_transition_list', + 'document_states:workflow_transition_list', ) ) menu_sidebar.bind_links( links=( - link_setup_workflow_state_create, + link_workflow_state_create, ), sources=( WorkflowState, - 'document_states:setup_workflow_state_list', + 'document_states:workflow_state_list', ) ) menu_tools.bind_links(links=(link_tool_launch_all_workflows,)) - post_save.connect( dispatch_uid='workflows_handler_launch_workflow', receiver=handler_launch_workflow, sender=Document diff --git a/mayan/apps/document_states/icons.py b/mayan/apps/document_states/icons.py index 64db956c50..d9f305eebf 100644 --- a/mayan/apps/document_states/icons.py +++ b/mayan/apps/document_states/icons.py @@ -9,11 +9,42 @@ icon_setup_workflow_list = Icon(driver_name='fontawesome', symbol='sitemap') icon_tool_launch_all_workflows = Icon( driver_name='fontawesome', symbol='sitemap' ) -icon_workflow_create = Icon(driver_name='fontawesome', symbol='plus') +icon_workflow_create = Icon( + driver_name='fontawesome-dual', primary_symbol='sitemap', + secondary_symbol='plus' +) +icon_workflow_delete = Icon(driver_name='fontawesome', symbol='times') +icon_workflow_edit = Icon(driver_name='fontawesome', symbol='pencil-alt') icon_workflow_list = Icon(driver_name='fontawesome', symbol='sitemap') icon_workflow_preview = Icon(driver_name='fontawesome', symbol='eye') + +icon_workflow_state_action_delete = Icon( + driver_name='fontawesome', symbol='times' +) +icon_workflow_state_action_edit = Icon( + driver_name='fontawesome', symbol='pencil-alt' +) +icon_workflow_state_action_list = Icon( + driver_name='fontawesome', symbol='code' +) icon_workflow_state = Icon(driver_name='fontawesome', symbol='circle') +icon_workflow_state_create = Icon( + driver_name='fontawesome-dual', primary_symbol='circle', + secondary_symbol='plus' +) +icon_workflow_state_delete = Icon(driver_name='fontawesome', symbol='times') +icon_workflow_state_edit = Icon(driver_name='fontawesome', symbol='pencil-alt') icon_workflow_state_action = Icon(driver_name='fontawesome', symbol='code') +icon_workflow_state_action_selection = Icon( + driver_name='fontawesome-dual', primary_symbol='code', + secondary_symbol='plus' +) icon_workflow_transition = Icon( driver_name='fontawesome', symbol='arrows-alt-h' ) +icon_workflow_transition_create = Icon( + driver_name='fontawesome-dual', primary_symbol='arrows-alt-h', + secondary_symbol='plus' +) +icon_workflow_transition_delete = Icon(driver_name='fontawesome', symbol='times') +icon_workflow_transition_edit = Icon(driver_name='fontawesome', symbol='pencil-alt') diff --git a/mayan/apps/document_states/links.py b/mayan/apps/document_states/links.py index 27b1fd89c0..c80fa8c2e4 100644 --- a/mayan/apps/document_states/links.py +++ b/mayan/apps/document_states/links.py @@ -6,10 +6,15 @@ from mayan.apps.documents.icons import icon_document_type from mayan.apps.navigation import Link from .icons import ( - icon_document_workflow_instance_list, icon_setup_workflow_list, - icon_tool_launch_all_workflows, icon_workflow_create, icon_workflow_list, - icon_workflow_preview, icon_workflow_state, icon_workflow_state_action, - icon_workflow_transition + icon_document_workflow_instance_list, icon_tool_launch_all_workflows, + icon_workflow_create, icon_workflow_delete, icon_workflow_edit, + icon_workflow_list, icon_workflow_preview, icon_workflow_state, + icon_workflow_state_action, icon_workflow_state_action_delete, + icon_workflow_state_action_edit, icon_workflow_state_action_list, + icon_workflow_state_action_selection, icon_workflow_state_create, + icon_workflow_state_delete, icon_workflow_state_edit, + icon_workflow_transition, icon_workflow_transition_create, + icon_workflow_transition_delete, icon_workflow_transition_edit ) from .permissions import ( permission_workflow_create, permission_workflow_delete, @@ -18,129 +23,166 @@ from .permissions import ( ) link_document_workflow_instance_list = Link( - args='resolved_object.pk', icon_class=icon_document_workflow_instance_list, + icon_class=icon_document_workflow_instance_list, + kwargs={'document_id': 'resolved_object.pk'}, permissions=(permission_workflow_view,), text=_('Workflows'), - view='document_states:document_workflow_instance_list', -) -link_setup_workflow_create = Link( - icon_class=icon_workflow_create, permissions=(permission_workflow_create,), - text=_('Create workflow'), view='document_states:setup_workflow_create' -) -link_setup_workflow_delete = Link( - args='resolved_object.pk', permissions=(permission_workflow_delete,), - tags='dangerous', text=_('Delete'), - view='document_states:setup_workflow_delete', -) -link_setup_workflow_document_types = Link( - args='resolved_object.pk', icon_class=icon_document_type, - permissions=(permission_workflow_edit,), text=_('Document types'), - view='document_states:setup_workflow_document_types', -) -link_setup_workflow_edit = Link( - args='resolved_object.pk', permissions=(permission_workflow_edit,), - text=_('Edit'), view='document_states:setup_workflow_edit', -) -link_setup_workflow_list = Link( - icon_class=icon_setup_workflow_list, - permissions=(permission_workflow_view,), text=_('Workflows'), - view='document_states:setup_workflow_list' -) -link_setup_workflow_state_action_delete = Link( - args='resolved_object.pk', permissions=(permission_workflow_edit,), - tags='dangerous', text=_('Delete'), - view='document_states:setup_workflow_state_action_delete', -) -link_setup_workflow_state_action_edit = Link( - args='resolved_object.pk', permissions=(permission_workflow_edit,), - text=_('Edit'), view='document_states:setup_workflow_state_action_edit', -) -link_setup_workflow_state_action_list = Link( - args='resolved_object.pk', permissions=(permission_workflow_edit,), - text=_('Actions'), - view='document_states:setup_workflow_state_action_list', -) -link_setup_workflow_state_action_selection = Link( - args='resolved_object.pk', icon_class=icon_workflow_state_action, - permissions=(permission_workflow_edit,), text=_('Create action'), - view='document_states:setup_workflow_state_action_selection', -) -link_setup_workflow_state_create = Link( - args='resolved_object.pk', icon_class=icon_workflow_state, - permissions=(permission_workflow_edit,), text=_('Create state'), - view='document_states:setup_workflow_state_create', -) -link_setup_workflow_state_delete = Link( - args='object.pk', permissions=(permission_workflow_edit,), - tags='dangerous', text=_('Delete'), - view='document_states:setup_workflow_state_delete', -) -link_setup_workflow_state_edit = Link( - args='resolved_object.pk', permissions=(permission_workflow_edit,), - text=_('Edit'), view='document_states:setup_workflow_state_edit', -) -link_setup_workflow_states = Link( - args='resolved_object.pk', icon_class=icon_workflow_state, - permissions=(permission_workflow_view,), text=_('States'), - view='document_states:setup_workflow_state_list', -) -link_setup_workflow_transition_create = Link( - args='resolved_object.pk', icon_class=icon_workflow_transition, - permissions=(permission_workflow_edit,), text=_('Create transition'), - view='document_states:setup_workflow_transition_create', -) -link_setup_workflow_transition_delete = Link( - args='resolved_object.pk', permissions=(permission_workflow_edit,), - tags='dangerous', text=_('Delete'), - view='document_states:setup_workflow_transition_delete', -) -link_setup_workflow_transition_edit = Link( - args='resolved_object.pk', permissions=(permission_workflow_edit,), - text=_('Edit'), view='document_states:setup_workflow_transition_edit', -) -link_setup_workflow_transitions = Link( - args='resolved_object.pk', icon_class=icon_workflow_transition, - permissions=(permission_workflow_view,), text=_('Transitions'), - view='document_states:setup_workflow_transition_list', + view='workflows:document_workflow_instance_list' ) link_tool_launch_all_workflows = Link( icon_class=icon_tool_launch_all_workflows, permissions=(permission_workflow_tools,), text=_('Launch all workflows'), - view='document_states:tool_launch_all_workflows' + view='workflows:tool_launch_all_workflows' ) -link_workflow_instance_detail = Link( - args='resolved_object.pk', permissions=(permission_workflow_view,), - text=_('Detail'), view='document_states:workflow_instance_detail', +link_workflow_create = Link( + icon_class=icon_workflow_create, permissions=(permission_workflow_create,), + text=_('Create workflow'), view='workflows:workflow_create' ) -link_workflow_instance_transition = Link( - args='resolved_object.pk', text=_('Transition'), - view='document_states:workflow_instance_transition', +link_workflow_delete = Link( + icon_class=icon_workflow_delete, + kwargs={'workflow_id': 'resolved_object.pk'}, + permissions=(permission_workflow_delete,), tags='dangerous', + text=_('Delete'), view='workflows:workflow_delete' ) -link_workflow_document_list = Link( - args='resolved_object.pk', permissions=(permission_workflow_view,), - text=_('Workflow documents'), - view='document_states:workflow_document_list', +link_workflow_document_types = Link( + icon_class=icon_document_type, kwargs={'workflow_id': 'resolved_object.pk'}, + permissions=(permission_workflow_edit,), text=_('Document types'), + view='workflows:workflow_document_types' +) +link_workflow_edit = Link( + icon_class=icon_workflow_edit, + kwargs={'workflow_id': 'resolved_object.pk'}, + permissions=(permission_workflow_edit,), text=_('Edit'), + view='workflows:workflow_edit' ) link_workflow_list = Link( - icon_class=icon_workflow_list, permissions=(permission_workflow_view,), - text=_('Workflows'), view='document_states:workflow_list' -) -link_workflow_state_document_list = Link( - args='resolved_object.pk', permissions=(permission_workflow_view,), - text=_('State documents'), - view='document_states:workflow_state_document_list', -) -link_workflow_state_list = Link( - args='resolved_object.pk', permissions=(permission_workflow_view,), - text=_('States'), view='document_states:workflow_state_list', -) -link_workflow_instance_transition_events = Link( - args='resolved_object.pk', permissions=(permission_workflow_edit,), - text=_('Transition triggers'), - view='document_states:setup_workflow_transition_events' + icon_class=icon_workflow_list, + permissions=(permission_workflow_view,), text=_('Workflows'), + view='workflows:workflow_list' ) link_workflow_preview = Link( - args='resolved_object.pk', icon_class=icon_workflow_preview, + icon_class=icon_workflow_preview, + kwargs={'workflow_id': 'resolved_object.pk'}, permissions=(permission_workflow_view,), text=_('Preview'), - view='document_states:workflow_preview' + view='workflows:workflow_preview' +) + +# Workflow instances + +link_workflow_instance_detail = Link( + kwargs={'workflow_instance_id': 'resolved_object.pk'}, + permissions=(permission_workflow_view,), text=_('Detail'), + view='workflows:workflow_instance_detail' +) +link_workflow_instance_transition = Link( + kwargs={'workflow_instance_id': 'resolved_object.pk'}, text=_('Transition'), + view='workflows:workflow_instance_transition' +) + +# Workflow state actions + +link_workflow_state_action_delete = Link( + icon_class=icon_workflow_state_action_delete, + kwargs={'workflow_state_action_id': 'resolved_object.pk'}, + permissions=(permission_workflow_edit,), tags='dangerous', + text=_('Delete'), view='workflows:workflow_state_action_delete' +) +link_workflow_state_action_edit = Link( + icon_class=icon_workflow_state_action_edit, + kwargs={'workflow_state_action_id': 'resolved_object.pk'}, + permissions=(permission_workflow_edit,), text=_('Edit'), + view='workflows:workflow_state_action_edit' +) +link_workflow_state_action_list = Link( + icon_class=icon_workflow_state_action_list, + kwargs={'workflow_state_id': 'resolved_object.pk'}, + permissions=(permission_workflow_edit,), text=_('Actions'), + view='workflows:workflow_state_action_list' +) +link_workflow_state_action_selection = Link( + icon_class=icon_workflow_state_action_selection, + kwargs={'workflow_state_id': 'resolved_object.pk'}, + permissions=(permission_workflow_edit,), text=_('Create action'), + view='workflows:workflow_state_action_selection' +) + +# Workflow states + +link_workflow_state_create = Link( + icon_class=icon_workflow_state_create, + kwargs={'workflow_id': 'resolved_object.pk'}, + permissions=(permission_workflow_edit,), text=_('Create state'), + view='workflows:workflow_state_create' +) +link_workflow_state_delete = Link( + icon_class=icon_workflow_state_delete, + kwargs={'workflow_state_id': 'object.pk'}, + permissions=(permission_workflow_edit,), tags='dangerous', text=_('Delete'), + view='workflows:workflow_state_delete' +) +link_workflow_state_edit = Link( + icon_class=icon_workflow_state_edit, + kwargs={'workflow_state_id': 'resolved_object.pk'}, + permissions=(permission_workflow_edit,), text=_('Edit'), + view='workflows:workflow_state_edit' +) +link_workflow_state_list = Link( + icon_class=icon_workflow_state, + kwargs={'workflow_id': 'resolved_object.pk'}, + permissions=(permission_workflow_view,), text=_('States'), + view='workflows:workflow_state_list' +) + +# Workflow transitions + +link_workflow_transition_create = Link( + icon_class=icon_workflow_transition_create, + kwargs={'workflow_id': 'resolved_object.pk'}, + permissions=(permission_workflow_edit,), text=_('Create transition'), + view='workflows:workflow_transition_create' +) +link_workflow_transition_delete = Link( + icon_class=icon_workflow_transition_delete, + kwargs={'workflow_transition_id': 'resolved_object.pk'}, + permissions=(permission_workflow_edit,), tags='dangerous', text=_('Delete'), + view='workflows:workflow_transition_delete' +) +link_workflow_transition_edit = Link( + icon_class=icon_workflow_transition_edit, + kwargs={'workflow_transition_id': 'resolved_object.pk'}, + permissions=(permission_workflow_edit,), text=_('Edit'), + view='workflows:workflow_transition_edit' +) +link_workflow_transition_list = Link( + icon_class=icon_workflow_transition, + kwargs={'workflow_transition_id': 'resolved_object.pk'}, + permissions=(permission_workflow_view,), text=_('Transitions'), + view='workflows:workflow_transition_list' +) +link_workflow_transition_triggers = Link( + kwargs={'workflow_transition_id': 'resolved_object.pk'}, + permissions=(permission_workflow_edit,), text=_('Transition triggers'), + view='workflows:workflow_transition_triggers' +) + +# Workflow runtime proxies + +link_workflow_runtime_proxy_document_list = Link( + kwargs={'workflow_runtime_proxy_id': 'resolved_object.pk'}, + permissions=(permission_workflow_view,), text=_('Workflow documents'), + view='workflows:workflow_runtime_proxy_document_list' +) +link_workflow_runtime_proxy_list = Link( + icon_class=icon_workflow_list, permissions=(permission_workflow_view,), + text=_('Workflows'), view='workflows:workflow_runtime_proxy_list' +) +link_workflow_runtime_proxy_state_document_list = Link( + kwargs={'workflow_runtime_proxy_state_id': 'resolved_object.pk'}, + permissions=(permission_workflow_view,), text=_('State documents'), + view='workflows:workflow_runtime_proxy_state_document_list' +) +link_workflow_runtime_proxy_state_list = Link( + kwargs={'workflow_runtime_proxy_id': 'resolved_object.pk'}, + permissions=(permission_workflow_view,), text=_('States'), + view='workflows:workflow_runtime_proxy_state_list' ) diff --git a/mayan/apps/document_states/models.py b/mayan/apps/document_states/models.py index f8e179daf8..c131c2a02b 100644 --- a/mayan/apps/document_states/models.py +++ b/mayan/apps/document_states/models.py @@ -406,7 +406,7 @@ class WorkflowInstance(models.Model): def get_absolute_url(self): return reverse( - viewname='document_states:workflow_instance_detail', + viewname='workflows:workflow_instance_detail', kwargs={'workflow_instance_id': self.pk} ) diff --git a/mayan/apps/document_states/tests/test_views.py b/mayan/apps/document_states/tests/test_views.py index d25fa91971..f9de435c0f 100644 --- a/mayan/apps/document_states/tests/test_views.py +++ b/mayan/apps/document_states/tests/test_views.py @@ -22,14 +22,10 @@ from .literals import ( from .mixins import WorkflowTestMixin -class DocumentStateViewTestCase(WorkflowTestMixin, GenericViewTestCase): - def setUp(self): - super(DocumentStateViewTestCase, self).setUp() - self.login_user() - +class WorkflowViewTestCase(WorkflowTestMixin, GenericViewTestCase): def _request_workflow_create_view(self): return self.post( - viewname='document_states:setup_workflow_create', data={ + viewname='workflows:workflow_create', data={ 'label': TEST_WORKFLOW_LABEL, 'internal_name': TEST_WORKFLOW_INTERNAL_NAME, } @@ -37,38 +33,38 @@ class DocumentStateViewTestCase(WorkflowTestMixin, GenericViewTestCase): def test_workflow_create_view_no_permission(self): response = self._request_workflow_create_view() - self.assertEquals(response.status_code, 403) - self.assertEquals(Workflow.objects.count(), 0) + self.assertEqual(response.status_code, 403) + self.assertEqual(Workflow.objects.count(), 0) def test_workflow_create_view_with_permission(self): self.grant_permission(permission=permission_workflow_create) response = self._request_workflow_create_view() - self.assertEquals(response.status_code, 302) - self.assertEquals(Workflow.objects.count(), 1) - self.assertEquals(Workflow.objects.all()[0].label, TEST_WORKFLOW_LABEL) + self.assertEqual(response.status_code, 302) + self.assertEqual(Workflow.objects.count(), 1) + self.assertEqual(Workflow.objects.all()[0].label, TEST_WORKFLOW_LABEL) def _request_workflow_delete_view(self): return self.post( - viewname='document_states:setup_workflow_delete', + viewname='workflows:workflow_delete', kwargs={'workflow_id': self.workflow.pk} ) def test_workflow_delete_view_no_access(self): self._create_workflow() response = self._request_workflow_delete_view() - self.assertEquals(response.status_code, 403) + self.assertEqual(response.status_code, 404) self.assertTrue(self.workflow in Workflow.objects.all()) def test_workflow_delete_view_with_access(self): self._create_workflow() self.grant_access(permission=permission_workflow_delete, obj=self.workflow) response = self._request_workflow_delete_view() - self.assertEquals(response.status_code, 302) + self.assertEqual(response.status_code, 302) self.assertFalse(self.workflow in Workflow.objects.all()) def _request_workflow_edit_view(self): return self.post( - viewname='document_states:setup_workflow_edit', + viewname='workflows:workflow_edit', kwargs={'workflow_id': self.workflow.pk}, data={ 'label': TEST_WORKFLOW_LABEL_EDITED, @@ -79,7 +75,7 @@ class DocumentStateViewTestCase(WorkflowTestMixin, GenericViewTestCase): def test_workflow_edit_view_no_access(self): self._create_workflow() response = self._request_workflow_edit_view() - self.assertEquals(response.status_code, 404) + self.assertEqual(response.status_code, 404) self.workflow.refresh_from_db() self.assertEqual(self.workflow.label, TEST_WORKFLOW_LABEL) @@ -87,55 +83,52 @@ class DocumentStateViewTestCase(WorkflowTestMixin, GenericViewTestCase): self._create_workflow() self.grant_access(permission=permission_workflow_edit, obj=self.workflow) response = self._request_workflow_edit_view() - self.assertEquals(response.status_code, 302) + self.assertEqual(response.status_code, 302) self.workflow.refresh_from_db() self.assertEqual(self.workflow.label, TEST_WORKFLOW_LABEL_EDITED) def _request_workflow_list_view(self): return self.get( - viewname='document_states:setup_workflow_list', + viewname='workflows:workflow_list', ) def test_workflow_list_view_no_access(self): self._create_workflow() response = self._request_workflow_list_view() - self.assertEquals(response.status_code, 200) + self.assertEqual(response.status_code, 200) self.assertNotContains(response, text=self.workflow.label) def test_workflow_list_view_with_access(self): self._create_workflow() self.grant_access(permission=permission_workflow_view, obj=self.workflow) response = self._request_workflow_list_view() - self.assertEquals(response.status_code, 200) - self.assertContains(response, text=self.workflow.label) + self.assertContains( + response=response, text=self.workflow.label, status_code=200 + ) def _request_workflow_preview_view(self): return self.get( - viewname='document_states:workflow_preview', + viewname='workflows:workflow_preview', kwargs={'workflow_id': self.workflow.pk} ) def test_workflow_preview_view_no_access(self): self._create_workflow() response = self._request_workflow_preview_view() - self.assertEquals(response.status_code, 404) + self.assertEqual(response.status_code, 404) self.assertTrue(self.workflow in Workflow.objects.all()) def test_workflow_preview_view_with_access(self): self._create_workflow() self.grant_access(permission=permission_workflow_view, obj=self.workflow) response = self._request_workflow_preview_view() - self.assertEquals(response.status_code, 200) + self.assertEqual(response.status_code, 200) -class DocumentStateStateViewTestCase(WorkflowTestMixin, GenericViewTestCase): - def setUp(self): - super(DocumentStateStateViewTestCase, self).setUp() - self.login_user() - +class WorkflowStateViewTestCase(WorkflowTestMixin, GenericViewTestCase): def _request_workflow_state_create_view(self): return self.post( - viewname='document_states:setup_workflow_state_create', + viewname='workflows:workflow_state_create', kwargs={'workflow_id': self.workflow.pk}, data={ 'label': TEST_WORKFLOW_STATE_LABEL, @@ -146,8 +139,8 @@ class DocumentStateStateViewTestCase(WorkflowTestMixin, GenericViewTestCase): def test_create_workflow_state_no_access(self): self._create_workflow() response = self._request_workflow_state_create_view() - self.assertEquals(response.status_code, 403) - self.assertEquals(WorkflowState.objects.count(), 0) + self.assertEqual(response.status_code, 404) + self.assertEqual(WorkflowState.objects.count(), 0) def test_create_workflow_state_with_access(self): self._create_workflow() @@ -155,19 +148,19 @@ class DocumentStateStateViewTestCase(WorkflowTestMixin, GenericViewTestCase): permission=permission_workflow_edit, obj=self.workflow ) response = self._request_workflow_state_create_view() - self.assertEquals(response.status_code, 302) - self.assertEquals(WorkflowState.objects.count(), 1) - self.assertEquals( + self.assertEqual(response.status_code, 302) + self.assertEqual(WorkflowState.objects.count(), 1) + self.assertEqual( WorkflowState.objects.all()[0].label, TEST_WORKFLOW_STATE_LABEL ) - self.assertEquals( + self.assertEqual( WorkflowState.objects.all()[0].completion, TEST_WORKFLOW_STATE_COMPLETION ) def _request_workflow_state_delete_view(self): return self.post( - viewname='document_states:setup_workflow_state_delete', + viewname='workflows:workflow_state_delete', kwargs={'workflow_state_id': self.workflow_state.pk} ) @@ -175,20 +168,20 @@ class DocumentStateStateViewTestCase(WorkflowTestMixin, GenericViewTestCase): self._create_workflow() self._create_workflow_states() response = self._request_workflow_state_delete_view() - self.assertEquals(response.status_code, 404) - self.assertEquals(WorkflowState.objects.count(), 2) + self.assertEqual(response.status_code, 404) + self.assertEqual(WorkflowState.objects.count(), 2) def test_delete_workflow_state_with_access(self): self._create_workflow() self._create_workflow_states() self.grant_access(permission=permission_workflow_edit, obj=self.workflow) response = self._request_workflow_state_delete_view() - self.assertEquals(response.status_code, 302) - self.assertEquals(WorkflowState.objects.count(), 1) + self.assertEqual(response.status_code, 302) + self.assertEqual(WorkflowState.objects.count(), 1) def _request_workflow_state_edit_view(self): return self.post( - viewname='document_states:setup_workflow_state_edit', + viewname='workflows:workflow_state_edit', kwargs={'workflow_state_id': self.workflow_state.pk}, data={ 'label': TEST_WORKFLOW_STATE_LABEL_EDITED @@ -199,21 +192,21 @@ class DocumentStateStateViewTestCase(WorkflowTestMixin, GenericViewTestCase): self._create_workflow() self._create_workflow_states() response = self._request_workflow_state_edit_view() - self.assertEquals(response.status_code, 404) - self.assertEquals(self.workflow_state.label, TEST_WORKFLOW_STATE_LABEL) + self.assertEqual(response.status_code, 404) + self.assertEqual(self.workflow_state.label, TEST_WORKFLOW_STATE_LABEL) def test_edit_workflow_state_with_access(self): self._create_workflow() self._create_workflow_states() self.grant_access(permission=permission_workflow_edit, obj=self.workflow) response = self._request_workflow_state_edit_view() - self.assertEquals(response.status_code, 302) + self.assertEqual(response.status_code, 302) self.workflow_state.refresh_from_db() - self.assertEquals(self.workflow_state.label, TEST_WORKFLOW_STATE_LABEL_EDITED) + self.assertEqual(self.workflow_state.label, TEST_WORKFLOW_STATE_LABEL_EDITED) def _request_workflow_state_list_view(self): return self.get( - viewname='document_states:setup_workflow_state_list', + viewname='workflows:workflow_state_list', kwargs={'workflow_id': self.workflow.pk} ) @@ -221,22 +214,19 @@ class DocumentStateStateViewTestCase(WorkflowTestMixin, GenericViewTestCase): self._create_workflow() self._create_workflow_states() response = self._request_workflow_state_list_view() - self.assertEquals(response.status_code, 404) + self.assertEqual(response.status_code, 404) def test_workflow_state_list_with_access(self): self._create_workflow() self._create_workflow_states() self.grant_access(permission=permission_workflow_view, obj=self.workflow) response = self._request_workflow_state_list_view() - self.assertEquals(response.status_code, 200) - self.assertContains(response, text=self.workflow_state.label) + self.assertContains( + response=response, text=self.workflow_state.label, status_code=200 + ) -class DocumentStateToolViewTestCase(GenericDocumentViewTestCase): - def setUp(self): - super(DocumentStateToolViewTestCase, self).setUp() - self.login_user() - +class WorkflowToolViewTestCase(GenericDocumentViewTestCase): def _create_workflow(self): self.workflow = Workflow.objects.create(label=TEST_WORKFLOW_LABEL) self.workflow.document_types.add(self.document_type) @@ -262,7 +252,7 @@ class DocumentStateToolViewTestCase(GenericDocumentViewTestCase): def _request_workflow_launch_view(self): return self.post( - viewname='document_states:tool_launch_all_workflows', + viewname='workflows:tool_launch_all_workflows', ) def test_tool_launch_all_workflows_view_no_permission(self): @@ -283,11 +273,7 @@ class DocumentStateToolViewTestCase(GenericDocumentViewTestCase): ) -class DocumentStateTransitionViewTestCase(WorkflowTestMixin, GenericDocumentViewTestCase): - def setUp(self): - super(DocumentStateTransitionViewTestCase, self).setUp() - self.login_user() - +class WorkflowTransitionViewTestCase(WorkflowTestMixin, GenericDocumentViewTestCase): def _create_document(self): with open(TEST_SMALL_DOCUMENT_PATH, mode='rb') as file_object: self.document_2 = self.document_type.new_document( @@ -296,7 +282,7 @@ class DocumentStateTransitionViewTestCase(WorkflowTestMixin, GenericDocumentView def _request_workflow_transition_create_view(self): return self.post( - viewname='document_states:setup_workflow_transition_create', + viewname='workflows:workflow_transition_create', kwargs={'workflow_id': self.workflow.pk}, data={ 'label': TEST_WORKFLOW_TRANSITION_LABEL, 'origin_state': self.workflow_initial_state.pk, @@ -308,32 +294,32 @@ class DocumentStateTransitionViewTestCase(WorkflowTestMixin, GenericDocumentView self._create_workflow() self._create_workflow_states() response = self._request_workflow_transition_create_view() - self.assertEquals(response.status_code, 404) - self.assertEquals(WorkflowTransition.objects.count(), 0) + self.assertEqual(response.status_code, 404) + self.assertEqual(WorkflowTransition.objects.count(), 0) def test_create_workflow_transition_with_access(self): self._create_workflow() self._create_workflow_states() self.grant_access(permission=permission_workflow_edit, obj=self.workflow) response = self._request_workflow_transition_create_view() - self.assertEquals(response.status_code, 302) - self.assertEquals(WorkflowTransition.objects.count(), 1) - self.assertEquals( + self.assertEqual(response.status_code, 302) + self.assertEqual(WorkflowTransition.objects.count(), 1) + self.assertEqual( WorkflowTransition.objects.all()[0].label, TEST_WORKFLOW_TRANSITION_LABEL ) - self.assertEquals( + self.assertEqual( WorkflowTransition.objects.all()[0].origin_state, self.workflow_initial_state ) - self.assertEquals( + self.assertEqual( WorkflowTransition.objects.all()[0].destination_state, self.workflow_state ) def _request_workflow_transition_delete_view(self): return self.post( - viewname='document_states:setup_workflow_transition_delete', + viewname='workflows:workflow_transition_delete', kwargs={'workflow_transition_id': self.workflow_transition.pk} ) @@ -342,7 +328,7 @@ class DocumentStateTransitionViewTestCase(WorkflowTestMixin, GenericDocumentView self._create_workflow_states() self._create_workflow_transition() response = self._request_workflow_transition_delete_view() - self.assertEquals(response.status_code, 404) + self.assertEqual(response.status_code, 404) self.assertTrue(self.workflow_transition in WorkflowTransition.objects.all()) def test_delete_workflow_transition_with_access(self): @@ -351,12 +337,12 @@ class DocumentStateTransitionViewTestCase(WorkflowTestMixin, GenericDocumentView self._create_workflow_transition() self.grant_access(permission=permission_workflow_edit, obj=self.workflow) response = self._request_workflow_transition_delete_view() - self.assertEquals(response.status_code, 302) + self.assertEqual(response.status_code, 302) self.assertFalse(self.workflow_transition in WorkflowTransition.objects.all()) def _request_workflow_transition_edit_view(self): return self.post( - viewname='document_states:setup_workflow_transition_edit', + viewname='workflows:workflow_transition_edit', kwargs={'workflow_transition_id': self.workflow_transition.pk}, data={ 'label': TEST_WORKFLOW_TRANSITION_LABEL_EDITED, @@ -370,7 +356,7 @@ class DocumentStateTransitionViewTestCase(WorkflowTestMixin, GenericDocumentView self._create_workflow_states() self._create_workflow_transition() response = self._request_workflow_transition_edit_view() - self.assertEquals(response.status_code, 404) + self.assertEqual(response.status_code, 404) self.workflow_transition.refresh_from_db() self.assertEqual( self.workflow_transition.label, TEST_WORKFLOW_TRANSITION_LABEL @@ -382,7 +368,7 @@ class DocumentStateTransitionViewTestCase(WorkflowTestMixin, GenericDocumentView self._create_workflow_transition() self.grant_access(permission=permission_workflow_edit, obj=self.workflow) response = self._request_workflow_transition_edit_view() - self.assertEquals(response.status_code, 302) + self.assertEqual(response.status_code, 302) self.workflow_transition.refresh_from_db() self.assertEqual( self.workflow_transition.label, TEST_WORKFLOW_TRANSITION_LABEL_EDITED @@ -390,7 +376,7 @@ class DocumentStateTransitionViewTestCase(WorkflowTestMixin, GenericDocumentView def _request_workflow_transition_list_view(self): return self.get( - viewname='document_states:setup_workflow_transition_list', + viewname='workflows:workflow_transition_list', kwargs={'workflow_id': self.workflow.pk} ) @@ -399,8 +385,9 @@ class DocumentStateTransitionViewTestCase(WorkflowTestMixin, GenericDocumentView self._create_workflow_states() self._create_workflow_transition() response = self._request_workflow_transition_list_view() - self.assertEquals(response.status_code, 200) - self.assertNotContains(response, text=self.workflow_transition.label) + self.assertNotContains( + response, text=self.workflow_transition.label, status_code=404 + ) def test_workflow_transition_list_with_access(self): self._create_workflow() @@ -408,12 +395,14 @@ class DocumentStateTransitionViewTestCase(WorkflowTestMixin, GenericDocumentView self._create_workflow_transition() self.grant_access(permission=permission_workflow_view, obj=self.workflow) response = self._request_workflow_transition_list_view() - self.assertEquals(response.status_code, 200) - self.assertContains(response, text=self.workflow_transition.label) + self.assertContains( + response=response, text=self.workflow_transition.label, + status_code=200 + ) def _request_workflow_transition(self): return self.post( - viewname='document_states:workflow_instance_transition', + viewname='workflows:workflow_instance_transition', kwargs={'workflow_instance_id': self.workflow_instance.pk}, data={ 'transition': self.workflow_transition.pk, @@ -432,7 +421,7 @@ class DocumentStateTransitionViewTestCase(WorkflowTestMixin, GenericDocumentView self._create_document() self.workflow_instance = self.document_2.workflows.first() response = self._request_workflow_transition() - self.assertEqual(response.status_code, 200) + self.assertEqual(response.status_code, 404) # Workflow should remain in the same initial state self.assertEqual( self.workflow_instance.get_current_state(), self.workflow_initial_state @@ -477,28 +466,24 @@ class DocumentStateTransitionViewTestCase(WorkflowTestMixin, GenericDocumentView ) -class DocumentStateTransitionEventViewTestCase(WorkflowTestMixin, GenericDocumentViewTestCase): - def setUp(self): - super(DocumentStateTransitionEventViewTestCase, self).setUp() - self.login_user() - - def _request_workflow_transition_event_list_view(self): +class WorkflowTransitionTriggerViewTestCase(WorkflowTestMixin, GenericViewTestCase): + def _request_workflow_transition_triggers_list_view(self): return self.get( - viewname='document_states:setup_workflow_transition_events', + viewname='workflows:workflow_transition_triggers', kwargs={'workflow_transition_id': self.workflow_transition.pk} ) - def test_workflow_transition_event_list_no_access(self): + def test_workflow_transition_triggers_list_no_access(self): self._create_workflow() self._create_workflow_states() self._create_workflow_transition() - response = self._request_workflow_transition_event_list_view() - self.assertEquals(response.status_code, 404) + response = self._request_workflow_transition_triggers_list_view() + self.assertEqual(response.status_code, 404) - def test_workflow_transition_event_list_with_access(self): + def test_workflow_transition_triggers_list_with_access(self): self._create_workflow() self._create_workflow_states() self._create_workflow_transition() self.grant_access(permission=permission_workflow_edit, obj=self.workflow) - response = self._request_workflow_transition_event_list_view() - self.assertEquals(response.status_code, 200) + response = self._request_workflow_transition_triggers_list_view() + self.assertEqual(response.status_code, 200) diff --git a/mayan/apps/document_states/urls.py b/mayan/apps/document_states/urls.py index 48baa4d5e8..d7447a08f7 100644 --- a/mayan/apps/document_states/urls.py +++ b/mayan/apps/document_states/urls.py @@ -11,38 +11,37 @@ from .api_views import ( APIWorkflowTransitionView, APIWorkflowView ) from .views import ( - DocumentWorkflowInstanceListView, SetupWorkflowCreateView, - SetupWorkflowDeleteView, SetupWorkflowDocumentTypesView, - SetupWorkflowEditView, SetupWorkflowListView, - SetupWorkflowStateActionCreateView, SetupWorkflowStateActionDeleteView, - SetupWorkflowStateActionEditView, SetupWorkflowStateActionListView, - SetupWorkflowStateActionSelectionView, SetupWorkflowStateCreateView, - SetupWorkflowStateDeleteView, SetupWorkflowStateEditView, - SetupWorkflowStateListView, SetupWorkflowTransitionCreateView, - SetupWorkflowTransitionDeleteView, SetupWorkflowTransitionEditView, - SetupWorkflowTransitionListView, - SetupWorkflowTransitionTriggerEventListView, ToolLaunchAllWorkflows, - WorkflowDocumentListView, WorkflowInstanceDetailView, + DocumentWorkflowInstanceListView, ToolLaunchAllWorkflows, + WorkflowCreateView, WorkflowDeleteView, WorkflowDocumentTypesView, + WorkflowEditView, WorkflowInstanceDetailView, WorkflowInstanceTransitionView, WorkflowListView, WorkflowPreviewView, - WorkflowStateDocumentListView, WorkflowStateListView + WorkflowRuntimeProxyDocumentListView, WorkflowRuntimeProxyListView, + WorkflowRuntimeProxyStateDocumentListView, + WorkflowRuntimeProxyStateListView, WorkflowStateActionCreateView, + WorkflowStateActionDeleteView, WorkflowStateActionEditView, + WorkflowStateActionListView, WorkflowStateActionSelectionView, + WorkflowStateCreateView, WorkflowStateDeleteView, WorkflowStateEditView, + WorkflowStateListView, WorkflowTransitionCreateView, + WorkflowTransitionDeleteView, WorkflowTransitionEditView, + WorkflowTransitionListView, WorkflowTransitionTriggerEventListView ) urlpatterns = [ url( - regex=r'^workflows/$', name='setup_workflow_list', - view=SetupWorkflowListView.as_view() + regex=r'^workflows/$', name='workflow_list', + view=WorkflowListView.as_view() ), url( - regex=r'^workflows/create/$', name='setup_workflow_create', - view=SetupWorkflowCreateView.as_view() + regex=r'^workflows/create/$', name='workflow_create', + view=WorkflowCreateView.as_view() ), url( regex=r'^workflows/(?P\d+)/delete/$', - name='setup_workflow_delete', view=SetupWorkflowDeleteView.as_view() + name='workflow_delete', view=WorkflowDeleteView.as_view() ), url( regex=r'^workflows/(?P\d+)/edit/$', - name='setup_workflow_edit', view=SetupWorkflowEditView.as_view() + name='workflow_edit', view=WorkflowEditView.as_view() ), url( regex=r'^workflows/(?P\d+)/preview/$', @@ -50,106 +49,113 @@ urlpatterns = [ ), url( regex=r'^workflows/(?P\d+)/document_types/$', - name='setup_workflow_document_types', - view=SetupWorkflowDocumentTypesView.as_view() + name='workflow_document_types', + view=WorkflowDocumentTypesView.as_view() ), + + # Workflow states + url( regex=r'^workflows/(?P\d+)/states/$', - name='setup_workflow_state_list', - view=SetupWorkflowStateListView.as_view() + name='workflow_state_list', + view=WorkflowStateListView.as_view() ), url( regex=r'^workflows/(?P\d+)/states/create/$', - name='setup_workflow_state_create', - view=SetupWorkflowStateCreateView.as_view() + name='workflow_state_create', + view=WorkflowStateCreateView.as_view() ), url( regex=r'^workflows/states/(?P\d+)/delete/$', - name='setup_workflow_state_delete', - view=SetupWorkflowStateDeleteView.as_view() + name='workflow_state_delete', + view=WorkflowStateDeleteView.as_view() ), url( regex=r'^workflows/states/(?P\d+)/edit/$', - name='setup_workflow_state_edit', - view=SetupWorkflowStateEditView.as_view() + name='workflow_state_edit', + view=WorkflowStateEditView.as_view() ), + # Workflow states actions + url( regex=r'^workflows/states/(?P\d+)/actions/$', - name='setup_workflow_state_action_list', - view=SetupWorkflowStateActionListView.as_view() + name='workflow_state_action_list', + view=WorkflowStateActionListView.as_view() ), url( regex=r'^workflows/states/(?P\d+)/actions/selection/$', - name='setup_workflow_state_action_selection', - view=SetupWorkflowStateActionSelectionView.as_view(), + name='workflow_state_action_selection', + view=WorkflowStateActionSelectionView.as_view(), ), url( regex=r'^workflows/states/(?P\d+)/actions/(?P[a-zA-Z0-9_.]+)/create/$', - name='setup_workflow_state_action_create', - view=SetupWorkflowStateActionCreateView.as_view() + name='workflow_state_action_create', + view=WorkflowStateActionCreateView.as_view() ), url( regex=r'^workflows/states/actions/(?P\d+)/delete/$', - view=SetupWorkflowStateActionDeleteView.as_view(), - name='setup_workflow_state_action_delete' + view=WorkflowStateActionDeleteView.as_view(), + name='workflow_state_action_delete' ), url( regex=r'^workflows/states/actions/(?P\d+)/edit/$', - name='setup_workflow_state_action_edit', - view=SetupWorkflowStateActionEditView.as_view() + name='workflow_state_action_edit', + view=WorkflowStateActionEditView.as_view() ), + # Workflow transitions + url( regex=r'^workflows/(?P\d+)/transitions/$', - name='setup_workflow_transition_list', - view=SetupWorkflowTransitionListView.as_view() + name='workflow_transition_list', + view=WorkflowTransitionListView.as_view() ), url( regex=r'^workflows/(?P\d+)/transitions/create/$', - name='setup_workflow_transition_create', - view=SetupWorkflowTransitionCreateView.as_view() + name='workflow_transition_create', + view=WorkflowTransitionCreateView.as_view() ), url( - regex=r'^workflows/transitions/(?P\d+)/delete/$', - name='setup_workflow_transition_delete', - view=SetupWorkflowTransitionDeleteView.as_view() + regex=r'^workflows/transitions/(?P\d+)/delete/$', + name='workflow_transition_delete', + view=WorkflowTransitionDeleteView.as_view() ), url( - regex=r'^workflows/transitions/(?P\d+)/edit/$', - name='setup_workflow_transition_edit', - view=SetupWorkflowTransitionEditView.as_view() + regex=r'^workflows/transitions/(?P\d+)/edit/$', + name='workflow_transition_edit', + view=WorkflowTransitionEditView.as_view() ), url( - regex=r'^workflows/(?P\d+)/transitions/events/$', - name='setup_workflow_transition_events', - view=SetupWorkflowTransitionTriggerEventListView.as_view() + regex=r'^workflows/transitions/(?P\d+)/triggers/$', + name='workflow_transition_triggers', + view=WorkflowTransitionTriggerEventListView.as_view() ), + # Workflow runtime proxies + url( - regex=r'^workflow_instances/$', name='workflow_list', - view=WorkflowListView.as_view() + regex=r'^workflow_runtime_proxies/$', name='workflow_runtime_proxy_list', + view=WorkflowRuntimeProxyListView.as_view() ), url( - regex=r'^workflow_instances/(?P\d+)/documents/$', - name='setup_workflow_document_list', - view=WorkflowDocumentListView.as_view() + regex=r'^workflow_runtime_proxies/(?P\d+)/documents/$', + name='workflow_runtime_proxy_document_list', + view=WorkflowRuntimeProxyDocumentListView.as_view() ), url( - regex=r'^workflow_instances/(?P\d+)/documents/$', - name='workflow_document_list', - view=WorkflowDocumentListView.as_view() + regex=r'^workflow_runtime_proxies/(?P\d+)/states/$', + name='workflow_runtime_proxy_state_list', + view=WorkflowRuntimeProxyStateListView.as_view() ), url( - regex=r'^workflow_instances/(?P\d+)/states/$', - name='workflow_state_list', view=WorkflowStateListView.as_view() - ), - url( - regex=r'^workflow_instances/states/(?P\d+)/documents/$', - name='workflow_state_document_list', - view=WorkflowStateDocumentListView.as_view() + regex=r'^workflow_runtime_proxies/states/(?P\d+)/documents/$', + name='workflow_runtime_proxy_state_document_list', + view=WorkflowRuntimeProxyStateDocumentListView.as_view() ), + # Workflow instances + url( regex=r'^documents/(?P\d+)/workflows/$', name='document_workflow_instance_list', @@ -165,6 +171,9 @@ urlpatterns = [ name='workflow_instance_transition', view=WorkflowInstanceTransitionView.as_view() ), + + # Workflow tools + url( regex=r'^tools/workflows/all/launch/$', name='tool_launch_all_workflows', diff --git a/mayan/apps/document_states/views/__init__.py b/mayan/apps/document_states/views/__init__.py new file mode 100644 index 0000000000..b51fdb3cce --- /dev/null +++ b/mayan/apps/document_states/views/__init__.py @@ -0,0 +1,5 @@ +from __future__ import absolute_import, unicode_literals + +from .workflow_instance_views import * # NOQA +from .workflow_proxy_views import * # NOQA +from .workflow_views import * # NOQA diff --git a/mayan/apps/document_states/views/workflow_instance_views.py b/mayan/apps/document_states/views/workflow_instance_views.py new file mode 100644 index 0000000000..8633531fcc --- /dev/null +++ b/mayan/apps/document_states/views/workflow_instance_views.py @@ -0,0 +1,142 @@ +from __future__ import absolute_import, unicode_literals + +from django.contrib import messages +from django.http import HttpResponseRedirect +from django.shortcuts import get_object_or_404 +from django.utils.translation import ugettext_lazy as _ + +from mayan.apps.acls.models import AccessControlList +from mayan.apps.common.mixins import ExternalObjectMixin +from mayan.apps.common.views import FormView, SingleObjectListView +from mayan.apps.documents.models import Document + +from ..forms import WorkflowInstanceTransitionForm +from ..icons import icon_workflow_list +from ..models import WorkflowInstance +from ..permissions import ( + permission_workflow_transition, permission_workflow_view +) + +__all__ = ( + 'DocumentWorkflowInstanceListView', 'WorkflowInstanceDetailView', + 'WorkflowInstanceTransitionView' +) + + +class DocumentWorkflowInstanceListView(SingleObjectListView): + def dispatch(self, request, *args, **kwargs): + AccessControlList.objects.check_access( + permissions=permission_workflow_view, user=request.user, + obj=self.get_document() + ) + + return super( + DocumentWorkflowInstanceListView, self + ).dispatch(request, *args, **kwargs) + + def get_document(self): + return get_object_or_404( + klass=Document, pk=self.kwargs['document_id'] + ) + + def get_extra_context(self): + return { + 'hide_object': True, + 'no_results_icon': icon_workflow_list, + 'no_results_text': _( + 'Assign workflows to the document type of this document ' + 'to have this document execute those workflows. ' + ), + 'no_results_title': _( + 'There are no workflow for this document' + ), + 'object': self.get_document(), + 'title': _( + 'Workflows for document: %s' + ) % self.get_document(), + } + + def get_object_list(self): + return self.get_document().workflows.all() + + +class WorkflowInstanceDetailView(SingleObjectListView): + def dispatch(self, request, *args, **kwargs): + AccessControlList.objects.check_access( + permissions=permission_workflow_view, user=request.user, + obj=self.get_workflow_instance().document + ) + + return super( + WorkflowInstanceDetailView, self + ).dispatch(request, *args, **kwargs) + + def get_extra_context(self): + return { + 'hide_object': True, + 'navigation_object_list': ('object', 'workflow_instance'), + 'no_results_text': _( + 'This view will show the states changed as a workflow ' + 'instance is transitioned.' + ), + 'no_results_title': _( + 'There are no details for this workflow instance' + ), + 'object': self.get_workflow_instance().document, + 'title': _('Detail of workflow: %(workflow)s') % { + 'workflow': self.get_workflow_instance() + }, + 'workflow_instance': self.get_workflow_instance(), + } + + def get_object_list(self): + return self.get_workflow_instance().log_entries.order_by('-datetime') + + def get_workflow_instance(self): + return get_object_or_404( + klass=WorkflowInstance, pk=self.kwargs['workflow_instance_id'] + ) + + +class WorkflowInstanceTransitionView(ExternalObjectMixin, FormView): + external_object_class = WorkflowInstance + external_object_permission = permission_workflow_transition + external_object_pk_url_kwarg = 'workflow_instance_id' + form_class = WorkflowInstanceTransitionForm + template_name = 'appearance/generic_form.html' + + def form_valid(self, form): + self.get_workflow_instance().do_transition( + comment=form.cleaned_data['comment'], + transition=form.cleaned_data['transition'], user=self.request.user + ) + messages.success( + message=_( + 'Document "%s" transitioned successfully' + ) % self.get_workflow_instance().document, + request=self.request + ) + return HttpResponseRedirect(redirect_to=self.get_success_url()) + + def get_extra_context(self): + return { + 'navigation_object_list': ('object', 'workflow_instance'), + 'object': self.get_workflow_instance().document, + 'submit_label': _('Submit'), + 'title': _( + 'Do transition for workflow: %s' + ) % self.get_workflow_instance(), + 'workflow_instance': self.get_workflow_instance(), + } + + def get_form_extra_kwargs(self): + return { + 'user': self.request.user, + 'workflow_instance': self.get_workflow_instance() + } + + def get_success_url(self): + return self.get_workflow_instance().get_absolute_url() + + def get_workflow_instance(self): + return self.get_external_object() diff --git a/mayan/apps/document_states/views/workflow_proxy_views.py b/mayan/apps/document_states/views/workflow_proxy_views.py new file mode 100644 index 0000000000..aa9c90ede3 --- /dev/null +++ b/mayan/apps/document_states/views/workflow_proxy_views.py @@ -0,0 +1,151 @@ +from __future__ import absolute_import, unicode_literals + +from django.shortcuts import get_object_or_404 +from django.template import RequestContext +from django.utils.translation import ugettext_lazy as _ + +from mayan.apps.common.mixins import ExternalObjectMixin +from mayan.apps.common.views import SingleObjectListView +from mayan.apps.documents.models import Document +from mayan.apps.documents.views import DocumentListView + +from ..icons import icon_workflow_list +from ..links import link_workflow_create, link_workflow_state_create +from ..models import WorkflowRuntimeProxy, WorkflowStateRuntimeProxy +from ..permissions import permission_workflow_view + +__all__ = ( + 'WorkflowRuntimeProxyDocumentListView', 'WorkflowRuntimeProxyListView', + 'WorkflowRuntimeProxyStateDocumentListView', + 'WorkflowRuntimeProxyStateListView' +) + + +class WorkflowRuntimeProxyDocumentListView(ExternalObjectMixin, DocumentListView): + external_object_class = WorkflowRuntimeProxy + external_object_permission = permission_workflow_view + external_object_pk_url_kwarg = 'workflow_runtime_proxy_id' + + def get_document_queryset(self): + return Document.objects.filter(workflows__workflow=self.get_workflow()) + + def get_extra_context(self): + context = super(WorkflowRuntimeProxyDocumentListView, self).get_extra_context() + context.update( + { + 'no_results_text': _( + 'Associate a workflow with some document types and ' + 'documents of those types will be listed in this view.' + ), + 'no_results_title': _( + 'There are no documents executing this workflow' + ), + 'object': self.get_workflow(), + 'title': _('Documents with the workflow: %s') % self.get_workflow() + } + ) + return context + + def get_workflow(self): + return self.get_external_object() + + +class WorkflowRuntimeProxyListView(SingleObjectListView): + object_permission = permission_workflow_view + + def get_extra_context(self): + return { + 'hide_object': True, + 'no_results_icon': icon_workflow_list, + 'no_results_main_link': link_workflow_create.resolve( + context=RequestContext( + self.request, {} + ) + ), + 'no_results_text': _( + 'Create some workflows and associated them with a document ' + 'type. Active workflows will be shown here and the documents ' + 'for which they are executing.' + ), + 'no_results_title': _('There are no workflows'), + 'title': _('Workflows'), + } + + def get_object_list(self): + return WorkflowRuntimeProxy.objects.all() + + +class WorkflowRuntimeProxyStateDocumentListView(ExternalObjectMixin, DocumentListView): + external_object_class = WorkflowRuntimeProxy + external_object_permission = permission_workflow_view + external_object_pk_url_kwarg = 'workflow_runtime_proxy_id' + + def get_document_queryset(self): + return self.get_workflow_state().get_documents() + + def get_extra_context(self): + workflow_state = self.get_workflow_state() + context = super(WorkflowRuntimeProxyStateDocumentListView, self).get_extra_context() + context.update( + { + 'object': workflow_state, + 'navigation_object_list': ('object', 'workflow'), + 'no_results_title': _( + 'There are documents in this workflow state' + ), + 'title': _( + 'Documents in the workflow "%s", state "%s"' + ) % ( + workflow_state.workflow, workflow_state + ), + 'workflow': WorkflowRuntimeProxy.objects.get( + pk=workflow_state.workflow.pk + ), + } + ) + return context + + def get_workflow_state(self): + workflow_state = get_object_or_404( + klass=WorkflowRuntimeProxyStateDocumentListView, + pk=self.kwargs['workflow_instance_state_id'], + workflow_pk=self.get_workflow() + ) + + return workflow_state + + def get_workflow(self): + return self.get_external_object() + + +class WorkflowRuntimeProxyStateListView(ExternalObjectMixin, SingleObjectListView): + external_object_class = WorkflowRuntimeProxy + external_object_permission = permission_workflow_view + external_object_pk_url_kwarg = 'workflow_runtime_proxy_id' + + def get_extra_context(self): + return { + 'hide_columns': True, + 'hide_link': True, + 'no_results_main_link': link_workflow_state_create.resolve( + context=RequestContext( + self.request, {'object': self.get_workflow()} + ) + ), + 'no_results_text': _( + 'Create states and link them using transitions.' + ), + 'no_results_title': _( + 'This workflow doesn\'t have any state' + ), + 'object': self.get_workflow(), + 'title': _('States of workflow: %s') % self.get_workflow() + } + + def get_object_list(self): + return WorkflowStateRuntimeProxy.objects.filter( + workflow=self.get_workflow() + ) + + def get_workflow(self): + return self.get_external_object() diff --git a/mayan/apps/document_states/views.py b/mayan/apps/document_states/views/workflow_views.py similarity index 54% rename from mayan/apps/document_states/views.py rename to mayan/apps/document_states/views/workflow_views.py index 8e023f791b..d33af2624b 100644 --- a/mayan/apps/document_states/views.py +++ b/mayan/apps/document_states/views/workflow_views.py @@ -8,210 +8,90 @@ from django.template import RequestContext from django.urls import reverse, reverse_lazy from django.utils.translation import ugettext_lazy as _ -from mayan.apps.acls.models import AccessControlList +from mayan.apps.common.mixins import ExternalObjectMixin from mayan.apps.common.views import ( AssignRemoveView, ConfirmView, FormView, SingleObjectCreateView, SingleObjectDeleteView, SingleObjectDetailView, SingleObjectDynamicFormCreateView, SingleObjectDynamicFormEditView, SingleObjectEditView, SingleObjectListView ) -from mayan.apps.documents.models import Document -from mayan.apps.documents.views import DocumentListView from mayan.apps.events.classes import EventType from mayan.apps.events.models import StoredEventType -from .classes import WorkflowAction -from .forms import ( - WorkflowActionSelectionForm, WorkflowForm, WorkflowInstanceTransitionForm, - WorkflowPreviewForm, WorkflowStateActionDynamicForm, WorkflowStateForm, - WorkflowTransitionForm, WorkflowTransitionTriggerEventRelationshipFormSet +from ..classes import WorkflowAction +from ..forms import ( + WorkflowActionSelectionForm, WorkflowForm, WorkflowPreviewForm, + WorkflowStateActionDynamicForm, WorkflowStateForm, WorkflowTransitionForm, + WorkflowTransitionTriggerEventRelationshipFormSet ) -from .icons import ( +from ..icons import ( icon_workflow_list, icon_workflow_state, icon_workflow_state_action, icon_workflow_transition ) -from .links import ( - link_setup_workflow_create, link_setup_workflow_state_action_selection, - link_setup_workflow_state_create, link_setup_workflow_transition_create +from ..links import ( + link_workflow_create, link_workflow_state_action_selection, + link_workflow_state_create, link_workflow_transition_create ) -from .models import ( - Workflow, WorkflowInstance, WorkflowRuntimeProxy, WorkflowState, - WorkflowStateAction, WorkflowStateRuntimeProxy, WorkflowTransition +from ..models import ( + Workflow, WorkflowState, WorkflowStateAction, WorkflowTransition ) -from .permissions import ( +from ..permissions import ( permission_workflow_create, permission_workflow_delete, permission_workflow_edit, permission_workflow_tools, permission_workflow_view ) -from .tasks import task_launch_all_workflows +from ..tasks import task_launch_all_workflows + +__all__ = ( + 'ToolLaunchAllWorkflows', 'WorkflowCreateView', 'WorkflowDeleteView', + 'WorkflowDocumentTypesView', 'WorkflowEditView', 'WorkflowListView', + 'WorkflowPreviewView', 'WorkflowStateActionCreateView', + 'WorkflowStateActionDeleteView', 'WorkflowStateActionEditView', + 'WorkflowStateActionListView', 'WorkflowStateActionSelectionView', + 'WorkflowStateCreateView', 'WorkflowStateDeleteView', + 'WorkflowStateEditView', 'WorkflowStateListView', + 'WorkflowTransitionCreateView', 'WorkflowTransitionDeleteView', + 'WorkflowTransitionEditView', 'WorkflowTransitionListView', + 'WorkflowTransitionTriggerEventListView' +) -class DocumentWorkflowInstanceListView(SingleObjectListView): - def dispatch(self, request, *args, **kwargs): - AccessControlList.objects.check_access( - permissions=permission_workflow_view, user=request.user, - obj=self.get_document() +class ToolLaunchAllWorkflows(ConfirmView): + extra_context = { + 'title': _('Launch all workflows?'), + 'subtitle': _( + 'This will launch all workflows created after documents have ' + 'already been uploaded.' ) + } + view_permission = permission_workflow_tools - return super( - DocumentWorkflowInstanceListView, self - ).dispatch(request, *args, **kwargs) - - def get_document(self): - return get_object_or_404( - klass=Document, pk=self.kwargs['document_id'] - ) - - def get_extra_context(self): - return { - 'hide_object': True, - 'no_results_icon': icon_workflow_list, - 'no_results_text': _( - 'Assign workflows to the document type of this document ' - 'to have this document execute those workflows. ' - ), - 'no_results_title': _( - 'There are no workflow for this document' - ), - 'object': self.get_document(), - 'title': _( - 'Workflows for document: %s' - ) % self.get_document(), - } - - def get_object_list(self): - return self.get_document().workflows.all() - - -class WorkflowInstanceDetailView(SingleObjectListView): - def dispatch(self, request, *args, **kwargs): - AccessControlList.objects.check_access( - permissions=permission_workflow_view, user=request.user, - obj=self.get_workflow_instance().document - ) - - return super( - WorkflowInstanceDetailView, self - ).dispatch(request, *args, **kwargs) - - def get_extra_context(self): - return { - 'hide_object': True, - 'navigation_object_list': ('object', 'workflow_instance'), - 'no_results_text': _( - 'This view will show the states changed as a workflow ' - 'instance is transitioned.' - ), - 'no_results_title': _( - 'There are no details for this workflow instance' - ), - 'object': self.get_workflow_instance().document, - 'title': _('Detail of workflow: %(workflow)s') % { - 'workflow': self.get_workflow_instance() - }, - 'workflow_instance': self.get_workflow_instance(), - } - - def get_object_list(self): - return self.get_workflow_instance().log_entries.order_by('-datetime') - - def get_workflow_instance(self): - return get_object_or_404( - klass=WorkflowInstance, pk=self.kwargs['workflow_instance_id'] - ) - - -class WorkflowInstanceTransitionView(FormView): - form_class = WorkflowInstanceTransitionForm - template_name = 'appearance/generic_form.html' - - def form_valid(self, form): - self.get_workflow_instance().do_transition( - comment=form.cleaned_data['comment'], - transition=form.cleaned_data['transition'], user=self.request.user - ) + def view_action(self): + task_launch_all_workflows.apply_async() messages.success( - message=_( - 'Document "%s" transitioned successfully' - ) % self.get_workflow_instance().document, + message=_('Workflow launch queued successfully.'), request=self.request ) - return HttpResponseRedirect(redirect_to=self.get_success_url()) - - def get_extra_context(self): - return { - 'navigation_object_list': ('object', 'workflow_instance'), - 'object': self.get_workflow_instance().document, - 'submit_label': _('Submit'), - 'title': _( - 'Do transition for workflow: %s' - ) % self.get_workflow_instance(), - 'workflow_instance': self.get_workflow_instance(), - } - - def get_form_extra_kwargs(self): - return { - 'user': self.request.user, - 'workflow_instance': self.get_workflow_instance() - } - - def get_success_url(self): - return self.get_workflow_instance().get_absolute_url() - - def get_workflow_instance(self): - return get_object_or_404( - klass=WorkflowInstance, pk=self.kwargs['workflow_instance_id'] - ) -# Setup - -class SetupWorkflowListView(SingleObjectListView): - model = Workflow - object_permission = permission_workflow_view - - def get_extra_context(self): - return { - 'hide_object': True, - 'no_results_icon': icon_workflow_list, - 'no_results_main_link': link_setup_workflow_create.resolve( - context=RequestContext(request=self.request) - ), - 'no_results_text': _( - 'Workflows store a series of states and keep track of the ' - 'current state of a document. Transitions are used to change the ' - 'current state to a new one.' - ), - 'no_results_title': _( - 'No workflows have been defined' - ), - 'title': _('Workflows'), - } - - -class SetupWorkflowCreateView(SingleObjectCreateView): +class WorkflowCreateView(SingleObjectCreateView): + extra_context = { + 'title': _('Create new workflow'), + } form_class = WorkflowForm model = Workflow - post_action_redirect = reverse_lazy(viewname='document_states:setup_workflow_list') + post_action_redirect = reverse_lazy(viewname='workflows:workflow_list') view_permission = permission_workflow_create -class SetupWorkflowEditView(SingleObjectEditView): - form_class = WorkflowForm - model = Workflow - object_permission = permission_workflow_edit - pk_url_kwarg = 'workflow_id' - post_action_redirect = reverse_lazy(viewname='document_states:setup_workflow_list') - - -class SetupWorkflowDeleteView(SingleObjectDeleteView): +class WorkflowDeleteView(SingleObjectDeleteView): model = Workflow object_permission = permission_workflow_delete pk_url_kwarg = 'workflow_id' - post_action_redirect = reverse_lazy(viewname='document_states:setup_workflow_list') + post_action_redirect = reverse_lazy(viewname='workflows:workflow_list') -class SetupWorkflowDocumentTypesView(AssignRemoveView): +class WorkflowDocumentTypesView(AssignRemoveView): decode_content_type = True left_list_title = _('Available document types') object_permission = permission_workflow_edit @@ -258,10 +138,52 @@ class SetupWorkflowDocumentTypesView(AssignRemoveView): self.get_object().instances.filter(document__document_type=item).delete() -# Workflow state actions +class WorkflowEditView(SingleObjectEditView): + form_class = WorkflowForm + model = Workflow + object_permission = permission_workflow_edit + pk_url_kwarg = 'workflow_id' + post_action_redirect = reverse_lazy(viewname='workflows:workflow_list') -class SetupWorkflowStateActionCreateView(SingleObjectDynamicFormCreateView): +class WorkflowListView(SingleObjectListView): + model = Workflow + object_permission = permission_workflow_view + + def get_extra_context(self): + return { + 'hide_object': True, + 'no_results_icon': icon_workflow_list, + 'no_results_main_link': link_workflow_create.resolve( + context=RequestContext(request=self.request) + ), + 'no_results_text': _( + 'Workflows store a series of states and keep track of the ' + 'current state of a document. Transitions are used to change the ' + 'current state to a new one.' + ), + 'no_results_title': _( + 'No workflows have been defined' + ), + 'title': _('Workflows'), + } + + +class WorkflowPreviewView(SingleObjectDetailView): + form_class = WorkflowPreviewForm + model = Workflow + object_permission = permission_workflow_view + pk_url_kwarg = 'workflow_id' + + def get_extra_context(self): + return { + 'hide_labels': True, + 'object': self.get_object(), + 'title': _('Preview of: %s') % self.get_object() + } + + +class WorkflowStateActionCreateView(SingleObjectDynamicFormCreateView): form_class = WorkflowStateActionDynamicForm object_permission = permission_workflow_edit @@ -305,12 +227,12 @@ class SetupWorkflowStateActionCreateView(SingleObjectDynamicFormCreateView): def get_post_action_redirect(self): return reverse( - viewname='document_states:setup_workflow_state_action_list', + viewname='workflows:workflow_state_action_list', kwargs={'workflow_state_id': self.get_object().pk} ) -class SetupWorkflowStateActionDeleteView(SingleObjectDeleteView): +class WorkflowStateActionDeleteView(SingleObjectDeleteView): model = WorkflowStateAction object_permission = permission_workflow_edit pk_url_kwarg = 'workflow_state_action_id' @@ -328,12 +250,12 @@ class SetupWorkflowStateActionDeleteView(SingleObjectDeleteView): def get_post_action_redirect(self): return reverse( - viewname='document_states:setup_workflow_state_action_list', + viewname='workflows:workflow_state_action_list', kwargs={'workflow_state_id': self.get_object().state.pk} ) -class SetupWorkflowStateActionEditView(SingleObjectDynamicFormEditView): +class WorkflowStateActionEditView(SingleObjectDynamicFormEditView): form_class = WorkflowStateActionDynamicForm model = WorkflowStateAction object_permission = permission_workflow_edit @@ -363,12 +285,12 @@ class SetupWorkflowStateActionEditView(SingleObjectDynamicFormEditView): def get_post_action_redirect(self): return reverse( - viewname='document_states:setup_workflow_state_action_list', + viewname='workflows:workflow_state_action_list', kwargs={'workflow_state_id': self.get_object().state.pk} ) -class SetupWorkflowStateActionListView(SingleObjectListView): +class WorkflowStateActionListView(SingleObjectListView): object_permission = permission_workflow_edit def get_extra_context(self): @@ -376,7 +298,7 @@ class SetupWorkflowStateActionListView(SingleObjectListView): 'hide_object': True, 'navigation_object_list': ('object', 'workflow'), 'no_results_icon': icon_workflow_state_action, - 'no_results_main_link': link_setup_workflow_state_action_selection.resolve( + 'no_results_main_link': link_workflow_state_action_selection.resolve( context=RequestContext( request=self.request, dict_={ 'object': self.get_workflow_state() @@ -409,7 +331,7 @@ class SetupWorkflowStateActionListView(SingleObjectListView): ) -class SetupWorkflowStateActionSelectionView(FormView): +class WorkflowStateActionSelectionView(FormView): form_class = WorkflowActionSelectionForm view_permission = permission_workflow_edit @@ -417,7 +339,7 @@ class SetupWorkflowStateActionSelectionView(FormView): klass = form.cleaned_data['klass'] return HttpResponseRedirect( redirect_to=reverse( - viewname='document_states:setup_workflow_state_action_create', + viewname='workflows:workflow_state_action_create', kwargs={ 'workflow_state_id': self.get_object().pk, 'class_path': klass @@ -441,10 +363,10 @@ class SetupWorkflowStateActionSelectionView(FormView): ) -# Workflow states - - -class SetupWorkflowStateCreateView(SingleObjectCreateView): +class WorkflowStateCreateView(ExternalObjectMixin, SingleObjectCreateView): + external_object_class = Workflow + external_object_permission = permission_workflow_edit + external_object_pk_url_kwarg = 'workflow_id' form_class = WorkflowStateForm def get_extra_context(self): @@ -463,20 +385,15 @@ class SetupWorkflowStateCreateView(SingleObjectCreateView): def get_success_url(self): return reverse( - viewname='document_states:setup_workflow_state_list', + viewname='workflows:workflow_state_list', kwargs={'workflow_id': self.kwargs['workflow_id']} ) def get_workflow(self): - workflow = get_object_or_404(klass=Workflow, pk=self.kwargs['workflow_id']) - AccessControlList.objects.check_access( - permissions=(permission_workflow_edit,), obj=workflow, - user=self.request.user - ) - return workflow + return self.get_external_object() -class SetupWorkflowStateDeleteView(SingleObjectDeleteView): +class WorkflowStateDeleteView(SingleObjectDeleteView): model = WorkflowState object_permission = permission_workflow_edit pk_url_kwarg = 'workflow_state_id' @@ -488,25 +405,14 @@ class SetupWorkflowStateDeleteView(SingleObjectDeleteView): 'workflow_instance': self.get_object().workflow, } - def get_object_list(self): - return self.get_workflow().states.all() - def get_success_url(self): return reverse( - viewname='document_states:setup_workflow_state_list', + viewname='workflows:workflow_state_list', kwargs={'workflow_id': self.get_object().workflow.pk} ) - def get_workflow(self): - workflow = get_object_or_404(klass=Workflow, pk=self.kwargs['workflow_id']) - AccessControlList.objects.check_access( - permissions=(permission_workflow_edit,), obj=workflow, - user=self.request.user - ) - return workflow - -class SetupWorkflowStateEditView(SingleObjectEditView): +class WorkflowStateEditView(SingleObjectEditView): form_class = WorkflowStateForm model = WorkflowState object_permission = permission_workflow_edit @@ -521,29 +427,22 @@ class SetupWorkflowStateEditView(SingleObjectEditView): def get_success_url(self): return reverse( - viewname='document_states:setup_workflow_state_list', + viewname='workflows:workflow_state_list', kwargs={'workflow_id': self.get_object().workflow.pk} ) -class SetupWorkflowStateListView(SingleObjectListView): +class WorkflowStateListView(ExternalObjectMixin, SingleObjectListView): + external_object_class = Workflow + external_object_permission = permission_workflow_view + external_object_pk_url_kwarg = 'workflow_id' object_permission = permission_workflow_view - def dispatch(self, request, *args, **kwargs): - AccessControlList.objects.check_access( - permissions=permission_workflow_view, user=request.user, - obj=self.get_workflow() - ) - - return super( - SetupWorkflowStateListView, self - ).dispatch(request, *args, **kwargs) - def get_extra_context(self): return { 'hide_object': True, 'no_results_icon': icon_workflow_state, - 'no_results_main_link': link_setup_workflow_state_create.resolve( + 'no_results_main_link': link_workflow_state_create.resolve( context=RequestContext( self.request, {'object': self.get_workflow()} ) @@ -562,13 +461,13 @@ class SetupWorkflowStateListView(SingleObjectListView): return self.get_workflow().states.all() def get_workflow(self): - return get_object_or_404(klass=Workflow, pk=self.kwargs['workflow_id']) + return self.get_external_object() -# Transitions - - -class SetupWorkflowTransitionCreateView(SingleObjectCreateView): +class WorkflowTransitionCreateView(ExternalObjectMixin, SingleObjectCreateView): + external_object_class = Workflow + external_object_permission = permission_workflow_edit + external_object_pk_url_kwarg = 'workflow_id' form_class = WorkflowTransitionForm def get_extra_context(self): @@ -581,7 +480,7 @@ class SetupWorkflowTransitionCreateView(SingleObjectCreateView): def get_form_kwargs(self): kwargs = super( - SetupWorkflowTransitionCreateView, self + WorkflowTransitionCreateView, self ).get_form_kwargs() kwargs['workflow'] = self.get_workflow() return kwargs @@ -594,20 +493,15 @@ class SetupWorkflowTransitionCreateView(SingleObjectCreateView): def get_success_url(self): return reverse( - viewname='document_states:setup_workflow_transition_list', + viewname='workflows:workflow_transition_list', kwargs={'workflow_id': self.kwargs['workflow_id']} ) def get_workflow(self): - workflow = get_object_or_404(klass=Workflow, pk=self.kwargs['workflow_id']) - AccessControlList.objects.check_access( - permissions=(permission_workflow_edit,), obj=workflow, - user=self.request.user - ) - return workflow + return self.get_external_object() -class SetupWorkflowTransitionDeleteView(SingleObjectDeleteView): +class WorkflowTransitionDeleteView(SingleObjectDeleteView): model = WorkflowTransition object_permission = permission_workflow_edit pk_url_kwarg = 'workflow_transition_id' @@ -621,12 +515,12 @@ class SetupWorkflowTransitionDeleteView(SingleObjectDeleteView): def get_success_url(self): return reverse( - viewname='document_states:setup_workflow_transition_list', + viewname='workflows:workflow_transition_list', kwargs={'workflow_id': self.get_object().workflow.pk} ) -class SetupWorkflowTransitionEditView(SingleObjectEditView): +class WorkflowTransitionEditView(SingleObjectEditView): form_class = WorkflowTransitionForm model = WorkflowTransition object_permission = permission_workflow_edit @@ -641,26 +535,29 @@ class SetupWorkflowTransitionEditView(SingleObjectEditView): def get_form_kwargs(self): kwargs = super( - SetupWorkflowTransitionEditView, self + WorkflowTransitionEditView, self ).get_form_kwargs() kwargs['workflow'] = self.get_object().workflow return kwargs def get_success_url(self): return reverse( - viewname='document_states:setup_workflow_transition_list', + viewname='workflows:workflow_transition_list', kwargs={'workflow_id': self.get_object().workflow.pk} ) -class SetupWorkflowTransitionListView(SingleObjectListView): +class WorkflowTransitionListView(ExternalObjectMixin, SingleObjectListView): + external_object_class = Workflow + external_object_permission = permission_workflow_view + external_object_pk_url_kwarg = 'workflow_id' object_permission = permission_workflow_view def get_extra_context(self): return { 'hide_object': True, 'no_results_icon': icon_workflow_transition, - 'no_results_main_link': link_setup_workflow_transition_create.resolve( + 'no_results_main_link': link_workflow_transition_create.resolve( context=RequestContext( self.request, {'object': self.get_workflow()} ) @@ -682,167 +579,20 @@ class SetupWorkflowTransitionListView(SingleObjectListView): return self.get_workflow().transitions.all() def get_workflow(self): - return get_object_or_404( - klass=Workflow, pk=self.kwargs['workflow_id'] - ) + return self.get_external_object() -# Other - - -class WorkflowListView(SingleObjectListView): - object_permission = permission_workflow_view - - def get_extra_context(self): - return { - 'hide_object': True, - 'no_results_icon': icon_workflow_list, - 'no_results_main_link': link_setup_workflow_create.resolve( - context=RequestContext( - self.request, {} - ) - ), - 'no_results_text': _( - 'Create some workflows and associated them with a document ' - 'type. Active workflows will be shown here and the documents ' - 'for which they are executing.' - ), - 'no_results_title': _('There are no workflows'), - 'title': _('Workflows'), - } - - def get_object_list(self): - return WorkflowRuntimeProxy.objects.all() - - -class WorkflowDocumentListView(DocumentListView): - def dispatch(self, request, *args, **kwargs): - self.workflow = get_object_or_404( - klass=WorkflowRuntimeProxy, pk=self.kwargs['pk'] - ) - - AccessControlList.objects.check_access( - permissions=permission_workflow_view, user=request.user, - obj=self.workflow - ) - - return super( - WorkflowDocumentListView, self - ).dispatch(request, *args, **kwargs) - - def get_document_queryset(self): - return Document.objects.filter(workflows__workflow=self.workflow) - - def get_extra_context(self): - context = super(WorkflowDocumentListView, self).get_extra_context() - context.update( - { - 'no_results_text': _( - 'Associate a workflow with some document types and ' - 'documents of those types will be listed in this view.' - ), - 'no_results_title': _( - 'There are no documents executing this workflow' - ), - 'object': self.workflow, - 'title': _('Documents with the workflow: %s') % self.workflow - } - ) - return context - - -class WorkflowStateDocumentListView(DocumentListView): - def get_document_queryset(self): - return self.get_workflow_state().get_documents() - - def get_extra_context(self): - workflow_state = self.get_workflow_state() - context = super(WorkflowStateDocumentListView, self).get_extra_context() - context.update( - { - 'object': workflow_state, - 'navigation_object_list': ('object', 'workflow'), - 'no_results_title': _( - 'There are documents in this workflow state' - ), - 'title': _( - 'Documents in the workflow "%s", state "%s"' - ) % ( - workflow_state.workflow, workflow_state - ), - 'workflow': WorkflowRuntimeProxy.objects.get( - pk=workflow_state.workflow.pk - ), - } - ) - return context - - def get_workflow_state(self): - workflow_state = get_object_or_404( - klass=WorkflowStateRuntimeProxy, - pk=self.kwargs['workflow_state_id'] - ) - - AccessControlList.objects.check_access( - permissions=permission_workflow_view, user=self.request.user, - obj=workflow_state.workflow - ) - - return workflow_state - - -class WorkflowStateListView(SingleObjectListView): - def dispatch(self, request, *args, **kwargs): - AccessControlList.objects.check_access( - permissions=permission_workflow_view, user=request.user, - obj=self.get_workflow() - ) - - return super( - WorkflowStateListView, self - ).dispatch(request, *args, **kwargs) - - def get_extra_context(self): - return { - 'hide_columns': True, - 'hide_link': True, - 'no_results_main_link': link_setup_workflow_state_create.resolve( - context=RequestContext( - self.request, {'object': self.get_workflow()} - ) - ), - 'no_results_text': _( - 'Create states and link them using transitions.' - ), - 'no_results_title': _( - 'This workflow doesn\'t have any state' - ), - 'object': self.get_workflow(), - 'title': _('States of workflow: %s') % self.get_workflow() - } - - def get_object_list(self): - return WorkflowStateRuntimeProxy.objects.filter( - workflow=self.get_workflow() - ) - - def get_workflow(self): - return get_object_or_404(klass=WorkflowRuntimeProxy, pk=self.kwargs['pk']) - - -class SetupWorkflowTransitionTriggerEventListView(FormView): +class WorkflowTransitionTriggerEventListView(ExternalObjectMixin, FormView): + external_object_class = WorkflowTransition + external_object_permission = permission_workflow_edit + external_object_pk_url_kwarg = 'workflow_transition_id' form_class = WorkflowTransitionTriggerEventRelationshipFormSet submodel = StoredEventType def dispatch(self, *args, **kwargs): - AccessControlList.objects.check_access( - permissions=permission_workflow_edit, - user=self.request.user, obj=self.get_object().workflow - ) - EventType.refresh() return super( - SetupWorkflowTransitionTriggerEventListView, self + WorkflowTransitionTriggerEventListView, self ).dispatch(*args, **kwargs) def form_valid(self, form): @@ -864,13 +614,11 @@ class SetupWorkflowTransitionTriggerEventListView(FormView): ) return super( - SetupWorkflowTransitionTriggerEventListView, self + WorkflowTransitionTriggerEventListView, self ).form_valid(form=form) def get_object(self): - return get_object_or_404( - klass=WorkflowTransition, pk=self.kwargs['worflow_transition_id'] - ) + return self.get_external_object() def get_extra_context(self): return { @@ -906,38 +654,6 @@ class SetupWorkflowTransitionTriggerEventListView(FormView): def get_post_action_redirect(self): return reverse( - viewname='document_states:setup_workflow_transition_list', + viewname='workflows:workflow_transition_list', kwargs={'workflow_id': self.get_object().workflow.pk} ) - - -class ToolLaunchAllWorkflows(ConfirmView): - extra_context = { - 'title': _('Launch all workflows?'), - 'subtitle': _( - 'This will launch all workflows created after documents have ' - 'already been uploaded.' - ) - } - view_permission = permission_workflow_tools - - def view_action(self): - task_launch_all_workflows.apply_async() - messages.success( - message=_('Workflow launch queued successfully.'), - request=self.request - ) - - -class WorkflowPreviewView(SingleObjectDetailView): - form_class = WorkflowPreviewForm - model = Workflow - object_permission = permission_workflow_view - pk_url_kwarg = 'workflow_id' - - def get_extra_context(self): - return { - 'hide_labels': True, - 'object': self.get_object(), - 'title': _('Preview of: %s') % self.get_object() - } From 108c54630f9226f701d1190087aba205e9241239 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Tue, 22 Jan 2019 04:24:37 -0400 Subject: [PATCH 037/209] Update source app Sort arguments. Add keyword arguments. Update views regexes. Update URL parameters to use the "_id" form. Move setting literals to the literals.py module. Signed-off-by: Roberto Rosario --- mayan/apps/sources/apps.py | 66 ++--- mayan/apps/sources/icons.py | 2 +- mayan/apps/sources/links.py | 128 +++++---- mayan/apps/sources/literals.py | 7 + mayan/apps/sources/permissions.py | 18 +- mayan/apps/sources/queues.py | 22 +- mayan/apps/sources/settings.py | 16 +- mayan/apps/sources/tests/test_views.py | 60 ++-- mayan/apps/sources/urls.py | 96 +++---- mayan/apps/sources/utils.py | 30 +- mayan/apps/sources/views.py | 379 +++++++++++++------------ mayan/apps/sources/wizards.py | 28 +- 12 files changed, 429 insertions(+), 423 deletions(-) diff --git a/mayan/apps/sources/apps.py b/mayan/apps/sources/apps.py index ccfcc630d5..16510adc24 100644 --- a/mayan/apps/sources/apps.py +++ b/mayan/apps/sources/apps.py @@ -22,13 +22,13 @@ from .handlers import ( handler_create_default_document_source, handler_initialize_periodic_tasks ) from .links import ( - link_document_create_multiple, link_setup_source_check_now, - link_setup_source_create_imap_email, link_setup_source_create_pop3_email, - link_setup_source_create_sane_scanner, - link_setup_source_create_staging_folder, - link_setup_source_create_watch_folder, link_setup_source_create_webform, - link_setup_source_delete, link_setup_source_edit, link_setup_source_logs, - link_setup_sources, link_staging_file_delete, link_upload_version + link_document_create_multiple, link_source_check_now, + link_source_create_imap_email, link_source_create_pop3_email, + link_source_create_sane_scanner, + link_source_create_staging_folder, + link_source_create_watch_folder, link_source_create_webform, + link_source_delete, link_source_edit, link_source_logs, + link_source_list, link_staging_file_delete, link_upload_version ) from .queues import * # NOQA from .widgets import StagingFileThumbnailWidget @@ -45,24 +45,24 @@ class SourcesApp(MayanAppConfig): def ready(self): super(SourcesApp, self).ready() - POP3Email = self.get_model('POP3Email') - IMAPEmail = self.get_model('IMAPEmail') - Source = self.get_model('Source') - SourceLog = self.get_model('SourceLog') - SaneScanner = self.get_model('SaneScanner') - StagingFolderSource = self.get_model('StagingFolderSource') - WatchFolderSource = self.get_model('WatchFolderSource') - WebFormSource = self.get_model('WebFormSource') + POP3Email = self.get_model(model_name='POP3Email') + IMAPEmail = self.get_model(model_name='IMAPEmail') + Source = self.get_model(model_name='Source') + SourceLog = self.get_model(model_name='SourceLog') + SaneScanner = self.get_model(model_name='SaneScanner') + StagingFolderSource = self.get_model(model_name='StagingFolderSource') + WatchFolderSource = self.get_model(model_name='WatchFolderSource') + WebFormSource = self.get_model(model_name='WebFormSource') MissingItem( - label=_('Create a document source'), + condition=lambda: not Source.objects.exists(), description=_( 'Document sources are the way in which new documents are ' 'feed to Mayan EDMS, create at least a web form source to ' 'be able to upload documents from a browser.' ), - condition=lambda: not Source.objects.exists(), - view='sources:setup_source_list' + label=_('Create a document source'), + view='sources:source_list' ) SourceColumn( @@ -83,11 +83,11 @@ class SourcesApp(MayanAppConfig): ) html_widget = StagingFileThumbnailWidget() SourceColumn( - source=StagingFile, - label=_('Thumbnail'), func=lambda context: html_widget.render( instance=context['object'], - ) + ), + label=_('Thumbnail'), + source=StagingFile ) SourceColumn( @@ -134,7 +134,7 @@ class SourcesApp(MayanAppConfig): menu_list_facet.bind_links( links=( - link_setup_source_logs, link_transformation_list, + link_source_logs, link_transformation_list, ), sources=( POP3Email, IMAPEmail, SaneScanner, StagingFolderSource, WatchFolderSource, WebFormSource @@ -143,7 +143,7 @@ class SourcesApp(MayanAppConfig): menu_object.bind_links( links=( - link_setup_source_edit, link_setup_source_delete, + link_source_edit, link_source_delete, ), sources=( POP3Email, IMAPEmail, SaneScanner, StagingFolderSource, WatchFolderSource, WebFormSource @@ -153,24 +153,24 @@ class SourcesApp(MayanAppConfig): links=(link_staging_file_delete,), sources=(StagingFile,) ) menu_object.bind_links( - links=(link_setup_source_check_now,), + links=(link_source_check_now,), sources=(IMAPEmail, POP3Email, WatchFolderSource,) ) menu_secondary.bind_links( links=( - link_setup_sources, link_setup_source_create_webform, - link_setup_source_create_sane_scanner, - link_setup_source_create_staging_folder, - link_setup_source_create_pop3_email, - link_setup_source_create_imap_email, - link_setup_source_create_watch_folder + link_source_list, link_source_create_webform, + link_source_create_sane_scanner, + link_source_create_staging_folder, + link_source_create_pop3_email, + link_source_create_imap_email, + link_source_create_watch_folder ), sources=( POP3Email, IMAPEmail, StagingFolderSource, WatchFolderSource, - WebFormSource, 'sources:setup_source_list', - 'sources:setup_source_create' + WebFormSource, 'sources:source_list', + 'sources:source_create' ) ) - menu_setup.bind_links(links=(link_setup_sources,)) + menu_setup.bind_links(links=(link_source_list,)) menu_sidebar.bind_links( links=(link_upload_version,), sources=( diff --git a/mayan/apps/sources/icons.py b/mayan/apps/sources/icons.py index 087e65272f..0c2d77ee3f 100644 --- a/mayan/apps/sources/icons.py +++ b/mayan/apps/sources/icons.py @@ -6,8 +6,8 @@ icon_document_create_multiple = Icon( driver_name='fontawesome', symbol='upload' ) icon_log = Icon(driver_name='fontawesome', symbol='exclamation-triangle') -icon_setup_sources = Icon(driver_name='fontawesome', symbol='upload') icon_source_create = Icon(driver_name='fontawesome', symbol='plus') +icon_source_list = Icon(driver_name='fontawesome', symbol='upload') icon_staging_folder = Icon(driver_name='fontawesome', symbol='folder') icon_upload_view_link = Icon(driver_name='fontawesome', symbol='upload') icon_wizard_submit = Icon(driver_name='fontawesome', symbol='arrow-right') diff --git a/mayan/apps/sources/links.py b/mayan/apps/sources/links.py index 91170d22b4..66b71d2ac8 100644 --- a/mayan/apps/sources/links.py +++ b/mayan/apps/sources/links.py @@ -9,7 +9,7 @@ from mayan.apps.documents.permissions import ( from mayan.apps.navigation import Link from .icons import ( - icon_document_create_multiple, icon_log, icon_setup_sources, + icon_document_create_multiple, icon_log, icon_source_list, icon_source_create ) from .literals import ( @@ -18,8 +18,8 @@ from .literals import ( SOURCE_CHOICE_WEB_FORM ) from .permissions import ( - permission_sources_setup_create, permission_sources_setup_delete, - permission_sources_setup_edit, permission_sources_setup_view + permission_sources_create, permission_sources_delete, + permission_sources_edit, permission_sources_view ) @@ -31,13 +31,10 @@ def condition_check_document_creation_acls(context): app_label='documents', model_name='DocumentType' ) - queryset = AccessControlList.objects.filter_by_access( - permission=permission_document_create, user=context['user'], - queryset=DocumentType.objects.all() - ) - - if queryset: - return True + return AccessControlList.objects.restrict_queryset( + permission=permission_document_create, + queryset=DocumentType.objects.all(), user=context['user'] + ).exists() def document_new_version_not_blocked(context): @@ -53,72 +50,77 @@ link_document_create_multiple = Link( icon_class=icon_document_create_multiple, text=_('New document'), view='sources:document_create_multiple' ) -link_setup_sources = Link( - icon_class=icon_setup_sources, - permissions=(permission_sources_setup_view,), text=_('Sources'), - view='sources:setup_source_list' +link_source_check_now = Link( + kwargs={'source_id': 'resolved_object.pk'}, + permissions=(permission_sources_edit,), text=_('Check now'), + view='sources:source_check' ) -link_setup_source_create_imap_email = Link( - args='"%s"' % SOURCE_CHOICE_EMAIL_IMAP, icon_class=icon_source_create, - permissions=(permission_sources_setup_create,), - text=_('Add new IMAP email'), view='sources:setup_source_create', +link_source_create_imap_email = Link( + icon_class=icon_source_create, + kwargs={'source_type': '"%s"' % SOURCE_CHOICE_EMAIL_IMAP}, + permissions=(permission_sources_create,), + text=_('Add new IMAP email'), view='sources:source_create' ) -link_setup_source_create_pop3_email = Link( - args='"%s"' % SOURCE_CHOICE_EMAIL_POP3, icon_class=icon_source_create, - permissions=(permission_sources_setup_create,), - text=_('Add new POP3 email'), view='sources:setup_source_create', +link_source_create_pop3_email = Link( + icon_class=icon_source_create, + kwargs={'source_type': '"%s"' % SOURCE_CHOICE_EMAIL_POP3}, + permissions=(permission_sources_create,), + text=_('Add new POP3 email'), view='sources:source_create' ) -link_setup_source_create_staging_folder = Link( - args='"%s"' % SOURCE_CHOICE_STAGING, icon_class=icon_source_create, - permissions=(permission_sources_setup_create,), - text=_('Add new staging folder'), view='sources:setup_source_create', +link_source_create_staging_folder = Link( + icon_class=icon_source_create, + kwargs={'source_type': '"%s"' % SOURCE_CHOICE_STAGING}, + permissions=(permission_sources_create,), + text=_('Add new staging folder'), view='sources:source_create' ) -link_setup_source_create_watch_folder = Link( - args='"%s"' % SOURCE_CHOICE_WATCH, icon_class=icon_source_create, - permissions=(permission_sources_setup_create,), - text=_('Add new watch folder'), view='sources:setup_source_create', +link_source_create_watch_folder = Link( + icon_class=icon_source_create, + kwargs={'source_type': '"%s"' % SOURCE_CHOICE_WATCH}, + permissions=(permission_sources_create,), + text=_('Add new watch folder'), view='sources:source_create' ) -link_setup_source_create_webform = Link( - args='"%s"' % SOURCE_CHOICE_WEB_FORM, icon_class=icon_source_create, - permissions=(permission_sources_setup_create,), - text=_('Add new webform source'), view='sources:setup_source_create', +link_source_create_webform = Link( + icon_class=icon_source_create, + kwargs={'source_type': '"%s"' % SOURCE_CHOICE_WEB_FORM}, + permissions=(permission_sources_create,), + text=_('Add new webform source'), view='sources:source_create' ) -link_setup_source_create_sane_scanner = Link( - args='"%s"' % SOURCE_CHOICE_SANE_SCANNER, icon_class=icon_source_create, - permissions=(permission_sources_setup_create,), - text=_('Add new SANE scanner'), view='sources:setup_source_create', +link_source_create_sane_scanner = Link( + icon_class=icon_source_create, + kwargs={'source_type': '"%s"' % SOURCE_CHOICE_SANE_SCANNER}, + permissions=(permission_sources_create,), + text=_('Add new SANE scanner'), view='sources:source_create' ) -link_setup_source_delete = Link( - args=('resolved_object.pk',), - permissions=(permission_sources_setup_delete,), tags='dangerous', - text=_('Delete'), view='sources:setup_source_delete', +link_source_delete = Link( + kwargs={'source_id': 'resolved_object.pk'}, + permissions=(permission_sources_delete,), tags='dangerous', + text=_('Delete'), view='sources:source_delete' ) -link_setup_source_edit = Link( - args=('resolved_object.pk',), - permissions=(permission_sources_setup_edit,), text=_('Edit'), - view='sources:setup_source_edit', +link_source_edit = Link( + kwargs={'source_id': 'resolved_object.pk'}, + permissions=(permission_sources_edit,), text=_('Edit'), + view='sources:source_edit' ) link_source_list = Link( - permissions=(permission_sources_setup_view,), text=_('Document sources'), - view='sources:setup_web_form_list' + icon_class=icon_source_list, + permissions=(permission_sources_view,), text=_('Sources'), + view='sources:source_list' +) +link_source_logs = Link( + icon_class=icon_log, kwargs={'source_id': 'resolved_object.pk'}, + permissions=(permission_sources_view,), text=_('Logs'), + view='sources:source_logs' ) link_staging_file_delete = Link( - args=('source.pk', 'object.encoded_filename',), keep_query=True, - permissions=(permission_document_new_version, permission_document_create), - tags='dangerous', text=_('Delete'), view='sources:staging_file_delete', + keep_query=True, kwargs={ + 'staging_file_pk': 'source.pk', + 'encoded_filename': 'object.encoded_filename' + }, permissions=(permission_document_new_version, permission_document_create), + tags='dangerous', text=_('Delete'), view='sources:staging_file_delete' ) link_upload_version = Link( - args='resolved_object.pk', condition=document_new_version_not_blocked, + condition=document_new_version_not_blocked, + kwargs={'document_pk': 'resolved_object.pk'}, permissions=(permission_document_new_version,), - text=_('Upload new version'), view='sources:upload_version', -) -link_setup_source_logs = Link( - args=('resolved_object.pk',), icon_class=icon_log, - permissions=(permission_sources_setup_view,), text=_('Logs'), - view='sources:setup_source_logs', -) -link_setup_source_check_now = Link( - args=('resolved_object.pk',), - permissions=(permission_sources_setup_view,), text=_('Check now'), - view='sources:setup_source_check', + text=_('Upload new version'), view='sources:upload_version' ) diff --git a/mayan/apps/sources/literals.py b/mayan/apps/sources/literals.py index 40097ba537..039bb4c452 100644 --- a/mayan/apps/sources/literals.py +++ b/mayan/apps/sources/literals.py @@ -1,7 +1,9 @@ from __future__ import unicode_literals +import os import platform +from django.conf import settings from django.utils.translation import ugettext_lazy as _ if platform.system() == 'OpenBSD': @@ -16,6 +18,11 @@ DEFAULT_POP3_TIMEOUT = 60 DEFAULT_SOURCE_LOCK_EXPIRE = 600 DEFAULT_SOURCE_TASK_RETRY_DELAY = 10 +DEFAULT_STAGING_FILE_CACHE_STORAGE_BACKEND = 'SOURCES_STAGING_FILE_CACHE_STORAGE_BACKEND' +DEFAULT_STAGING_FILE_CACHE_STORAGE_BACKEND_ARGUMENTS = { + 'location': os.path.join(settings.MEDIA_ROOT, 'staging_file_cache') +} + SCANNER_SOURCE_FLATBED = 'flatbed' SCANNER_SOURCE_ADF = 'Automatic Document Feeder' diff --git a/mayan/apps/sources/permissions.py b/mayan/apps/sources/permissions.py index 94bb90e45b..1f85fba42f 100644 --- a/mayan/apps/sources/permissions.py +++ b/mayan/apps/sources/permissions.py @@ -6,18 +6,18 @@ from mayan.apps.permissions import PermissionNamespace namespace = PermissionNamespace(label=_('Sources setup'), name='source_setup') -permission_sources_setup_create = namespace.add_permission( - name='sources_setup_create', label=_('Create new document sources') +permission_sources_create = namespace.add_permission( + label=_('Create new document sources'), name='sources_setup_create' ) -permission_sources_setup_delete = namespace.add_permission( - name='sources_setup_delete', label=_('Delete document sources') +permission_sources_delete = namespace.add_permission( + label=_('Delete document sources'), name='sources_setup_delete' ) -permission_sources_setup_edit = namespace.add_permission( - name='sources_setup_edit', label=_('Edit document sources') +permission_sources_edit = namespace.add_permission( + label=_('Edit document sources'), name='sources_setup_edit' ) -permission_sources_setup_view = namespace.add_permission( - name='sources_setup_view', label=_('View existing document sources') +permission_sources_view = namespace.add_permission( + label=_('View existing document sources'), name='sources_setup_view' ) permission_staging_file_delete = namespace.add_permission( - name='sources_staging_file_delete', label=_('Delete staging files') + label=_('Delete staging files'), name='sources_staging_file_delete' ) diff --git a/mayan/apps/sources/queues.py b/mayan/apps/sources/queues.py index eebc52c0e4..8793292a97 100644 --- a/mayan/apps/sources/queues.py +++ b/mayan/apps/sources/queues.py @@ -5,28 +5,28 @@ from django.utils.translation import ugettext_lazy as _ from mayan.apps.task_manager.classes import CeleryQueue queue_sources = CeleryQueue( - name='sources', label=_('Sources') + label=_('Sources'), name='sources' ) queue_sources_periodic = CeleryQueue( - name='sources_periodic', label=_('Sources periodic'), transient=True + label=_('Sources periodic'), name='sources_periodic', transient=True ) queue_sources_fast = CeleryQueue( - name='sources_fast', label=_('Sources fast'), transient=True + label=_('Sources fast'), name='sources_fast', transient=True ) queue_sources_fast.add_task_type( - name='mayan.apps.sources.tasks.task_generate_staging_file_image', - label=_('Generate staging file image') + label=_('Generate staging file image'), + name='mayan.apps.sources.tasks.task_generate_staging_file_image' ) queue_sources_periodic.add_task_type( - name='mayan.apps.sources.tasks.task_check_interval_source', - label=_('Check interval source') + label=_('Check interval source'), + name='mayan.apps.sources.tasks.task_check_interval_source' ) queue_sources.add_task_type( - name='mayan.apps.sources.tasks.task_source_handle_upload', - label=_('Handle upload') + label=_('Handle upload'), + name='mayan.apps.sources.tasks.task_source_handle_upload' ) queue_sources.add_task_type( - name='mayan.apps.sources.tasks.task_upload_document', - label=_('Upload document') + label=_('Upload document'), + name='mayan.apps.sources.tasks.task_upload_document' ) diff --git a/mayan/apps/sources/settings.py b/mayan/apps/sources/settings.py index 4bf8778ac7..ae124f4959 100644 --- a/mayan/apps/sources/settings.py +++ b/mayan/apps/sources/settings.py @@ -1,15 +1,15 @@ from __future__ import unicode_literals -import os - -from django.conf import settings from django.utils.translation import ugettext_lazy as _ from mayan.apps.smart_settings import Namespace -from .literals import DEFAULT_SCANIMAGE_PATH +from .literals import ( + DEFAULT_SCANIMAGE_PATH, DEFAULT_STAGING_FILE_CACHE_STORAGE_BACKEND, + DEFAULT_STAGING_FILE_CACHE_STORAGE_BACKEND_ARGUMENTS +) -namespace = Namespace(name='sources', label=_('Sources')) +namespace = Namespace(label=_('Sources'), name='sources') setting_scanimage_path = namespace.add_setting( global_name='SOURCES_SCANIMAGE_PATH', default=DEFAULT_SCANIMAGE_PATH, @@ -19,7 +19,7 @@ setting_scanimage_path = namespace.add_setting( is_path=True ) setting_staging_file_image_cache_storage = namespace.add_setting( - global_name='SOURCES_STAGING_FILE_CACHE_STORAGE_BACKEND', + global_name=DEFAULT_STAGING_FILE_CACHE_STORAGE_BACKEND, default='django.core.files.storage.FileSystemStorage', help_text=_( 'Path to the Storage subclass to use when storing the cached ' 'staging_file image files.' @@ -27,9 +27,7 @@ setting_staging_file_image_cache_storage = namespace.add_setting( ) setting_staging_file_image_cache_storage_arguments = namespace.add_setting( global_name='SOURCES_STAGING_FILE_CACHE_STORAGE_BACKEND_ARGUMENTS', - default={ - 'location': os.path.join(settings.MEDIA_ROOT, 'staging_file_cache') - }, help_text=_( + default=DEFAULT_STAGING_FILE_CACHE_STORAGE_BACKEND_ARGUMENTS, help_text=_( 'Arguments to pass to the SOURCES_STAGING_FILE_CACHE_STORAGE_BACKEND.' ) ) diff --git a/mayan/apps/sources/tests/test_views.py b/mayan/apps/sources/tests/test_views.py index 45989d89da..fbaa09fc21 100644 --- a/mayan/apps/sources/tests/test_views.py +++ b/mayan/apps/sources/tests/test_views.py @@ -18,8 +18,8 @@ from ..links import link_upload_version from ..literals import SOURCE_CHOICE_WEB_FORM from ..models import StagingFolderSource, WebFormSource from ..permissions import ( - permission_sources_setup_create, permission_sources_setup_delete, - permission_sources_setup_view, permission_staging_file_delete + permission_sources_create, permission_sources_delete, + permission_sources_view, permission_staging_file_delete ) from .literals import ( @@ -33,7 +33,6 @@ class DocumentUploadTestCase(GenericDocumentViewTestCase): def setUp(self): super(DocumentUploadTestCase, self).setUp() self._create_source() - self.login_user() def _create_source(self): self.source = WebFormSource.objects.create( @@ -55,7 +54,7 @@ class DocumentUploadTestCase(GenericDocumentViewTestCase): def test_upload_wizard_without_permission(self): response = self._request_upload_wizard_view() - self.assertEqual(response.status_code, 403) + self.assertEqual(response.status_code, 404) self.assertEqual(Document.objects.count(), 0) def test_upload_wizard_with_permission(self): @@ -112,12 +111,12 @@ class DocumentUploadIssueTestCase(GenericDocumentViewTestCase): auto_upload_document = False def test_issue_25(self): - self.login_admin_user() + self.login_superuser() # Create new webform source self.post( - viewname='sources:setup_source_create', - args=(SOURCE_CHOICE_WEB_FORM,), + viewname='sources:source_create', + kwargs={'source_type': SOURCE_CHOICE_WEB_FORM}, data={'label': 'test', 'uncompress': 'n', 'enabled': True} ) self.assertEqual(WebFormSource.objects.count(), 1) @@ -145,7 +144,8 @@ class DocumentUploadIssueTestCase(GenericDocumentViewTestCase): # Test for issue 25 during editing self.post( - viewname='documents:document_edit', args=(document.pk,), data={ + viewname='documents:document_edit', + kargs={'document_id': document.pk}, data={ 'description': TEST_DOCUMENT_DESCRIPTION, 'language': document.language, 'label': document.label } @@ -171,7 +171,8 @@ class NewDocumentVersionViewTestCase(GenericDocumentViewTestCase): NewVersionBlock.objects.block(self.document) response = self.post( - viewname='sources:upload_version', args=(self.document.pk,), + viewname='sources:upload_version', + kwargs={'document_id': self.document.pk}, follow=True ) @@ -181,7 +182,8 @@ class NewDocumentVersionViewTestCase(GenericDocumentViewTestCase): ) response = self.get( - 'documents:document_version_list', args=(self.document.pk,), + viewname='documents:document_version_list', + kwargs={'document_id': self.document.pk}, follow=True ) @@ -204,13 +206,6 @@ class StagingFolderViewTestCase(GenericViewTestCase): fs_cleanup(self.temporary_directory) super(StagingFolderViewTestCase, self).tearDown() - def _request_staging_file_delete_view(self, staging_file): - return self.post( - viewname='sources:staging_file_delete', args=( - self.staging_folder.pk, staging_file.encoded_filename - ) - ) - def _create_staging_folder(self): self.staging_folder = StagingFolderSource.objects.create( label=TEST_SOURCE_LABEL, @@ -219,6 +214,15 @@ class StagingFolderViewTestCase(GenericViewTestCase): uncompress=TEST_SOURCE_UNCOMPRESS_N, ) + def _request_staging_file_delete_view(self, staging_file): + return self.post( + viewname='sources:staging_file_delete', + kwargs={ + 'staging_folder_id': self.staging_folder.pk, + 'encoded_filename': staging_file.encoded_filename + }, + ) + def test_staging_folder_delete_no_permission(self): self._create_staging_folder() @@ -259,7 +263,7 @@ class SourcesViewsTestCase(GenericViewTestCase): ) def _request_setup_source_list_view(self): - return self.get(viewname='sources:setup_source_list') + return self.get(viewname='sources:source_list') def test_source_list_view_no_permission(self): self._create_web_source() @@ -270,7 +274,7 @@ class SourcesViewsTestCase(GenericViewTestCase): def test_source_list_view_with_permission(self): self._create_web_source() - self.grant_permission(permission=permission_sources_setup_view) + self.grant_permission(permission=permission_sources_view) response = self._request_setup_source_list_view() self.assertContains( @@ -280,14 +284,14 @@ class SourcesViewsTestCase(GenericViewTestCase): def _request_setup_source_create_view(self): return self.post( args=(SOURCE_CHOICE_WEB_FORM,), - viewname='sources:setup_source_create', data={ + viewname='sources:source_create', data={ 'enabled': True, 'label': TEST_SOURCE_LABEL, 'uncompress': TEST_SOURCE_UNCOMPRESS_N } ) def test_source_create_view_no_permission(self): - self.grant_permission(permission=permission_sources_setup_view) + self.grant_permission(permission=permission_sources_view) response = self._request_setup_source_create_view() self.assertEqual(response.status_code, 403) @@ -295,8 +299,8 @@ class SourcesViewsTestCase(GenericViewTestCase): self.assertEqual(WebFormSource.objects.count(), 0) def test_source_create_view_with_permission(self): - self.grant_permission(permission=permission_sources_setup_create) - self.grant_permission(permission=permission_sources_setup_view) + self.grant_permission(permission=permission_sources_create) + self.grant_permission(permission=permission_sources_view) response = self._request_setup_source_create_view() self.assertEquals(response.status_code, 302) @@ -307,15 +311,15 @@ class SourcesViewsTestCase(GenericViewTestCase): def _request_setup_source_delete_view(self): return self.post( - args=(self.source.pk,), - viewname='sources:setup_source_delete' + viewname='sources:source_delete', + kwargs={'source_id': self.source.pk} ) def test_source_delete_view_with_permission(self): self._create_web_source() - self.grant_permission(permission=permission_sources_setup_delete) - self.grant_permission(permission=permission_sources_setup_view) + self.grant_permission(permission=permission_sources_delete) + self.grant_permission(permission=permission_sources_view) response = self._request_setup_source_delete_view() self.assertEqual(response.status_code, 302) @@ -324,7 +328,7 @@ class SourcesViewsTestCase(GenericViewTestCase): def test_source_delete_view_no_permission(self): self._create_web_source() - self.grant_permission(permission=permission_sources_setup_view) + self.grant_permission(permission=permission_sources_view) response = self._request_setup_source_delete_view() self.assertEqual(response.status_code, 403) diff --git a/mayan/apps/sources/urls.py b/mayan/apps/sources/urls.py index a34d9d77eb..da61d77be4 100644 --- a/mayan/apps/sources/urls.py +++ b/mayan/apps/sources/urls.py @@ -7,87 +7,81 @@ from .api_views import ( APIStagingSourceListView, APIStagingSourceView ) from .views import ( - SetupSourceCheckView, SetupSourceCreateView, SetupSourceDeleteView, - SetupSourceEditView, SetupSourceListView, SourceLogListView, + SourceCheckView, SourceCreateView, SourceDeleteView, + SourceEditView, SourceListView, SourceLogView, StagingFileDeleteView, UploadInteractiveVersionView, UploadInteractiveView ) from .wizards import DocumentCreateWizard urlpatterns = [ url( - r'^staging_file/(?P\d+)/(?P.+)/delete/$', - StagingFileDeleteView.as_view(), name='staging_file_delete' + regex=r'^sources/$', name='source_list', + view=SourceListView.as_view() + ), + url( + regex=r'^sources/(?P\w+)/create/$', + name='source_create', view=SourceCreateView.as_view() + ), + url( + regex=r'^sources/(?P\d+)/check/$', name='source_check', + view=SourceCheckView.as_view() + ), + url( + regex=r'^sources/(?P\d+)/delete/$', + name='source_delete', view=SourceDeleteView.as_view() + ), + url( + regex=r'^sources/(?P\d+)/edit/$', name='source_edit', + view=SourceEditView.as_view() + ), + url( + regex=r'^sources/(?P\d+)/logs/$', name='source_logs', + view=SourceLogView.as_view() ), url( - r'^upload/document/new/interactive/(?P\d+)/$', - UploadInteractiveView.as_view(), name='upload_interactive' + regex=r'^sources/(?P\d+)/document/upload/$', + name='upload_interactive', view=UploadInteractiveView.as_view() ), url( - r'^upload/document/new/interactive/$', UploadInteractiveView.as_view(), - name='upload_interactive' + regex=r'^sources/document/upload/$', name='upload_interactive', + view=UploadInteractiveView.as_view() ), url( - r'^upload/document/(?P\d+)/version/interactive/(?P\d+)/$', - UploadInteractiveVersionView.as_view(), name='upload_version' + regex=r'^sources/(?P\d+)/documents/(?P\d+)/versions/upload/$', + name='upload_version', view=UploadInteractiveVersionView.as_view() ), url( - r'^upload/document/(?P\d+)/version/interactive/$', - UploadInteractiveVersionView.as_view(), name='upload_version' - ), - - # Setup views - - url( - r'^setup/list/$', SetupSourceListView.as_view(), - name='setup_source_list' + regex=r'^sources/documents/(?P\d+)/version/upload/$', + name='upload_version', view=UploadInteractiveVersionView.as_view() ), url( - r'^setup/(?P\d+)/edit/$', SetupSourceEditView.as_view(), - name='setup_source_edit' + regex=r'^sources/wizard/$', name='document_create_multiple', + view=DocumentCreateWizard.as_view() ), url( - r'^setup/(?P\d+)/logs/$', SourceLogListView.as_view(), - name='setup_source_logs' - ), - url( - r'^setup/(?P\d+)/delete/$', SetupSourceDeleteView.as_view(), - name='setup_source_delete' - ), - url( - r'^setup/(?P\w+)/create/$', - SetupSourceCreateView.as_view(), name='setup_source_create' - ), - url( - r'^setup/(?P\d+)/check/$', SetupSourceCheckView.as_view(), - name='setup_source_check' - ), - - # Document create views - - url( - r'^create/from/local/multiple/$', DocumentCreateWizard.as_view(), - name='document_create_multiple' + regex=r'^staging_files/(?P\d+)/(?P.+)/delete/$', + name='staging_file_delete', view=StagingFileDeleteView.as_view() ), ] api_urls = [ url( - r'^staging_folders/file/(?P[0-9]+)/(?P.+)/image/$', - APIStagingSourceFileImageView.as_view(), - name='stagingfolderfile-image-view' + regex=r'^staging_folders/file/(?P\d+)/(?P.+)/image/$', + name='stagingfolderfile-image-view', + view=APIStagingSourceFileImageView.as_view() ), url( - r'^staging_folders/file/(?P[0-9]+)/(?P.+)/$', - APIStagingSourceFileView.as_view(), name='stagingfolderfile-detail' + regex=r'^staging_folders/file/(?P\d+)/(?P.+)/$', + name='stagingfolderfile-detail', view=APIStagingSourceFileView.as_view() ), url( - r'^staging_folders/$', APIStagingSourceListView.as_view(), - name='stagingfolder-list' + regex=r'^staging_folders/$', name='stagingfolder-list', + view=APIStagingSourceListView.as_view() ), url( - r'^staging_folders/(?P[0-9]+)/$', APIStagingSourceView.as_view(), - name='stagingfolder-detail' + regex=r'^staging_folders/(?P\d+)/$', + name='stagingfolder-detail', view=APIStagingSourceView.as_view() ) ] diff --git a/mayan/apps/sources/utils.py b/mayan/apps/sources/utils.py index 4bf36c41d2..0a20c9047d 100644 --- a/mayan/apps/sources/utils.py +++ b/mayan/apps/sources/utils.py @@ -14,21 +14,6 @@ from .models import ( ) -def get_class(source_type): - if source_type == SOURCE_CHOICE_WEB_FORM: - return WebFormSource - elif source_type == SOURCE_CHOICE_STAGING: - return StagingFolderSource - elif source_type == SOURCE_CHOICE_WATCH: - return WatchFolderSource - elif source_type == SOURCE_CHOICE_EMAIL_POP3: - return POP3Email - elif source_type == SOURCE_CHOICE_EMAIL_IMAP: - return IMAPEmail - elif source_type == SOURCE_CHOICE_SANE_SCANNER: - return SaneScanner - - def get_form_class(source_type): if source_type == SOURCE_CHOICE_WEB_FORM: return WebFormSetupForm @@ -44,6 +29,21 @@ def get_form_class(source_type): return SaneScannerSetupForm +def get_model(source_type): + if source_type == SOURCE_CHOICE_WEB_FORM: + return WebFormSource + elif source_type == SOURCE_CHOICE_STAGING: + return StagingFolderSource + elif source_type == SOURCE_CHOICE_WATCH: + return WatchFolderSource + elif source_type == SOURCE_CHOICE_EMAIL_POP3: + return POP3Email + elif source_type == SOURCE_CHOICE_EMAIL_IMAP: + return IMAPEmail + elif source_type == SOURCE_CHOICE_SANE_SCANNER: + return SaneScanner + + def get_upload_form_class(source_type): if source_type == SOURCE_CHOICE_WEB_FORM: return WebFormUploadForm diff --git a/mayan/apps/sources/views.py b/mayan/apps/sources/views.py index 54abe63029..a77ca7f398 100644 --- a/mayan/apps/sources/views.py +++ b/mayan/apps/sources/views.py @@ -15,13 +15,12 @@ from django.utils.translation import ugettext_lazy as _ from mayan.apps.acls.models import AccessControlList from mayan.apps.checkouts.models import NewVersionBlock from mayan.apps.common import menu_facet -from mayan.apps.common.mixins import ListModeMixin +from mayan.apps.common.mixins import ExternalObjectMixin, ListModeMixin from mayan.apps.common.models import SharedUploadedFile from mayan.apps.common.views import ( ConfirmView, MultiFormView, SingleObjectCreateView, SingleObjectDeleteView, SingleObjectEditView, SingleObjectListView ) -from mayan.apps.common.widgets import TwoStateWidget from mayan.apps.documents.models import Document, DocumentType from mayan.apps.documents.permissions import ( permission_document_create, permission_document_new_version @@ -34,29 +33,160 @@ from .forms import ( NewDocumentForm, NewVersionForm, WebFormUploadForm, WebFormUploadFormHTML5 ) from .icons import ( - icon_log, icon_setup_sources, icon_staging_folder, icon_upload_view_link + icon_log, icon_source_list, icon_staging_folder, icon_upload_view_link ) from .links import ( - link_setup_source_create_imap_email, link_setup_source_create_pop3_email, - link_setup_source_create_sane_scanner, - link_setup_source_create_staging_folder, - link_setup_source_create_watch_folder, link_setup_source_create_webform + link_source_create_imap_email, link_source_create_pop3_email, + link_source_create_sane_scanner, link_source_create_staging_folder, + link_source_create_watch_folder, link_source_create_webform ) from .literals import SOURCE_UNCOMPRESS_CHOICE_ASK, SOURCE_UNCOMPRESS_CHOICE_Y from .models import InteractiveSource, SaneScanner, Source, StagingFolderSource from .permissions import ( - permission_sources_setup_create, permission_sources_setup_delete, - permission_sources_setup_edit, permission_sources_setup_view, + permission_sources_create, permission_sources_delete, + permission_sources_edit, permission_sources_view, permission_staging_file_delete ) from .tasks import task_check_interval_source, task_source_handle_upload -from .utils import get_class, get_form_class, get_upload_form_class +from .utils import get_form_class, get_model, get_upload_form_class logger = logging.getLogger(__name__) -class SourceLogListView(SingleObjectListView): - view_permission = permission_sources_setup_view +class SourceCheckView(ExternalObjectMixin, ConfirmView): + """ + Trigger the task_check_interval_source task for a given source to + test/debug their configuration irrespective of the schedule task setup. + """ + external_object_pk_url_kwarg = 'source_id' + external_object_queryset = Source.objects.select_subclasses() + external_object_permission = permission_sources_edit + + def get_extra_context(self): + return { + 'object': self.get_object(), + 'subtitle': _( + 'This will execute the source check code even if the source ' + 'is not enabled. Sources that delete content after ' + 'downloading will not do so while being tested. Check the ' + 'source\'s error log for information during testing. A ' + 'successful test will clear the error log.' + ), 'title': _( + 'Trigger check for source "%s"?' + ) % self.get_object(), + } + + def get_object(self): + return self.get_external_object() + + def view_action(self): + task_check_interval_source.apply_async( + kwargs={ + 'source_id': self.get_object().pk, 'test': True + } + ) + + messages.success( + message=_('Source check queued.'), request=self.request + ) + + +class SourceCreateView(SingleObjectCreateView): + post_action_redirect = reverse_lazy(viewname='sources:source_list') + view_permission = permission_sources_create + + def get_form_class(self): + return get_form_class(self.kwargs['source_type']) + + def get_extra_context(self): + return { + 'object': get_model(self.kwargs['source_type']), + 'title': _( + 'Create new source of type: %s' + ) % get_model(self.kwargs['source_type']).class_fullname(), + } + + +class SourceDeleteView(SingleObjectDeleteView): + post_action_redirect = reverse_lazy(viewname='sources:source_list') + view_permission = permission_sources_delete + + def get_object(self): + return get_object_or_404( + klass=Source.objects.select_subclasses(), pk=self.kwargs['source_id'] + ) + + def get_form_class(self): + return get_form_class(self.get_object().source_type) + + def get_extra_context(self): + return { + 'object': self.get_object(), + 'title': _('Delete the source: %s?') % self.get_object(), + } + + +class SourceEditView(SingleObjectEditView): + post_action_redirect = reverse_lazy(viewname='sources:source_list') + view_permission = permission_sources_edit + + def get_object(self): + return get_object_or_404( + klass=Source.objects.select_subclasses(), pk=self.kwargs['source_id'] + ) + + def get_form_class(self): + return get_form_class(self.get_object().source_type) + + def get_extra_context(self): + return { + 'object': self.get_object(), + 'title': _('Edit source: %s') % self.get_object(), + } + + +class SourceListView(SingleObjectListView): + queryset = Source.objects.select_subclasses() + view_permission = permission_sources_view + + def get_extra_context(self): + return { + 'hide_link': True, + 'hide_object': True, + 'no_results_icon': icon_source_list, + 'no_results_secondary_links': [ + link_source_create_webform.resolve( + context=RequestContext(request=self.request) + ), + link_source_create_imap_email.resolve( + context=RequestContext(request=self.request) + ), + link_source_create_pop3_email.resolve( + context=RequestContext(request=self.request) + ), + link_source_create_sane_scanner.resolve( + context=RequestContext(request=self.request) + ), + link_source_create_staging_folder.resolve( + context=RequestContext(request=self.request) + ), + link_source_create_watch_folder.resolve( + context=RequestContext(request=self.request) + ), + ], + 'no_results_text': _( + 'Sources provide the means to upload documents. ' + 'Some sources like the webform, are interactive and require ' + 'user input to operate. Others like the email sources, are ' + 'automatic and run on the background without user intervention.' + ), + 'no_results_title': _('No sources available'), + 'title': _('Sources'), + } + + +class SourceLogView(SingleObjectListView): + view_permission = permission_sources_view def get_extra_context(self): return { @@ -76,7 +206,30 @@ class SourceLogListView(SingleObjectListView): def get_source(self): return get_object_or_404( - klass=Source.objects.select_subclasses(), pk=self.kwargs['pk'] + klass=Source.objects.select_subclasses(), pk=self.kwargs['source_id'] + ) + + +class StagingFileDeleteView(SingleObjectDeleteView): + object_permission = permission_staging_file_delete + object_permission_related = 'staging_folder' + + def get_extra_context(self): + return { + 'object': self.get_object(), + 'object_name': _('Staging file'), + 'source': self.get_source(), + } + + def get_object(self): + source = self.get_source() + return source.get_file( + encoded_filename=self.kwargs['encoded_filename'] + ) + + def get_source(self): + return get_object_or_404( + klass=StagingFolderSource, pk=self.kwargs['staging_folder_id'] ) @@ -122,13 +275,14 @@ class UploadBaseView(ListModeMixin, MultiFormView): if not InteractiveSource.objects.filter(enabled=True).exists(): messages.error( - request, - _( + message=_( 'No interactive document sources have been defined or ' 'none have been enabled, create one before proceeding.' - ) + ), request=request + ) + return HttpResponseRedirect( + redirect_to=reverse(viewname='sources:source_list') ) - return HttpResponseRedirect(reverse('sources:setup_source_list')) return super(UploadBaseView, self).dispatch(request, *args, **kwargs) @@ -142,7 +296,7 @@ class UploadBaseView(ListModeMixin, MultiFormView): try: staging_filelist = list(self.source.get_files()) except Exception as exception: - messages.error(self.request, exception) + messages.error(message=exception, request=self.request) staging_filelist = [] finally: subtemplates_list = [ @@ -243,7 +397,7 @@ class UploadInteractiveView(UploadBaseView): forms['source_form'].cleaned_data ) except SourceException as exception: - messages.error(self.request, exception) + messages.error(message=exception, request=self.request) else: shared_uploaded_file = SharedUploadedFile.objects.create( file=uploaded_file.file @@ -257,7 +411,7 @@ class UploadInteractiveView(UploadBaseView): try: self.source.clean_up_upload_file(uploaded_file) except Exception as exception: - messages.error(self.request, exception) + messages.error(message=exception, request=self.request) querystring = furl() querystring.args.update(self.request.GET) @@ -293,15 +447,14 @@ class UploadInteractiveView(UploadBaseView): raise type(exception)(message) else: messages.success( - self.request, - _( + message=_( 'New document queued for upload and will be available ' 'shortly.' - ) + ), request=self.request ) return HttpResponseRedirect( - '{}?{}'.format( + redirect_to='{}?{}'.format( reverse( self.request.resolver_match.view_name, kwargs=self.request.resolver_match.kwargs @@ -372,19 +525,19 @@ class UploadInteractiveVersionView(UploadBaseView): self.subtemplates_list = [] - self.document = get_object_or_404(klass=Document, pk=kwargs['document_pk']) + self.document = get_object_or_404(klass=Document, pk=kwargs['document_id']) # TODO: Try to remove this new version block check from here if NewVersionBlock.objects.is_blocked(self.document): messages.error( - self.request, - _( + message=_( 'Document "%s" is blocked from uploading new versions.' - ) % self.document + ) % self.document, request=self.request ) return HttpResponseRedirect( - reverse( - 'documents:document_version_list', args=(self.document.pk,) + redirect_to=reverse( + viewname='documents:document_version_list', + kwargs={'document_version_id': self.document.pk} ) ) @@ -405,7 +558,7 @@ class UploadInteractiveVersionView(UploadBaseView): forms['source_form'].cleaned_data ) except SourceException as exception: - messages.error(self.request, exception) + messages.error(message=exception, request=self.request) else: shared_uploaded_file = SharedUploadedFile.objects.create( file=uploaded_file.file @@ -414,7 +567,7 @@ class UploadInteractiveVersionView(UploadBaseView): try: self.source.clean_up_upload_file(uploaded_file) except Exception as exception: - messages.error(self.request, exception) + messages.error(message=exception, request=self.request) if not self.request.user.is_anonymous: user_id = self.request.user.pk @@ -429,16 +582,16 @@ class UploadInteractiveVersionView(UploadBaseView): )) messages.success( - self.request, - _( + message=_( 'New document version queued for upload and will be ' 'available shortly.' - ) + ), request=self.request ) return HttpResponseRedirect( - reverse( - 'documents:document_version_list', args=(self.document.pk,) + redirect_to=reverse( + viewname='documents:document_version_list', + kwargs={'document_id': self.document.pk} ) ) @@ -474,155 +627,3 @@ class UploadInteractiveVersionView(UploadBaseView): ) % self.source.label return context - - -class StagingFileDeleteView(SingleObjectDeleteView): - object_permission = permission_staging_file_delete - object_permission_related = 'staging_folder' - - def get_extra_context(self): - return { - 'object': self.get_object(), - 'object_name': _('Staging file'), - 'source': self.get_source(), - } - - def get_object(self): - source = self.get_source() - return source.get_file( - encoded_filename=self.kwargs['encoded_filename'] - ) - - def get_source(self): - return get_object_or_404( - klass=StagingFolderSource, pk=self.kwargs['pk'] - ) - - -# Setup views -class SetupSourceCheckView(ConfirmView): - """ - Trigger the task_check_interval_source task for a given source to - test/debug their configuration irrespective of the schedule task setup. - """ - view_permission = permission_sources_setup_create - - def get_extra_context(self): - return { - 'object': self.get_object(), - 'subtitle': _( - 'This will execute the source check code even if the source ' - 'is not enabled. Sources that delete content after ' - 'downloading will not do so while being tested. Check the ' - 'source\'s error log for information during testing. A ' - 'successful test will clear the error log.' - ), 'title': _( - 'Trigger check for source "%s"?' - ) % self.get_object(), - } - - def get_object(self): - return get_object_or_404(klass=Source.objects.select_subclasses(), pk=self.kwargs['pk']) - - def view_action(self): - task_check_interval_source.apply_async( - kwargs={ - 'source_id': self.get_object().pk, 'test': True - } - ) - - messages.success(self.request, _('Source check queued.')) - - -class SetupSourceCreateView(SingleObjectCreateView): - post_action_redirect = reverse_lazy('sources:setup_source_list') - view_permission = permission_sources_setup_create - - def get_form_class(self): - return get_form_class(self.kwargs['source_type']) - - def get_extra_context(self): - return { - 'object': self.kwargs['source_type'], - 'title': _( - 'Create new source of type: %s' - ) % get_class(self.kwargs['source_type']).class_fullname(), - } - - -class SetupSourceDeleteView(SingleObjectDeleteView): - post_action_redirect = reverse_lazy('sources:setup_source_list') - view_permission = permission_sources_setup_delete - - def get_object(self): - return get_object_or_404( - klass=Source.objects.select_subclasses(), pk=self.kwargs['pk'] - ) - - def get_form_class(self): - return get_form_class(self.get_object().source_type) - - def get_extra_context(self): - return { - 'object': self.get_object(), - 'title': _('Delete the source: %s?') % self.get_object(), - } - - -class SetupSourceEditView(SingleObjectEditView): - post_action_redirect = reverse_lazy('sources:setup_source_list') - view_permission = permission_sources_setup_edit - - def get_object(self): - return get_object_or_404( - klass=Source.objects.select_subclasses(), pk=self.kwargs['pk'] - ) - - def get_form_class(self): - return get_form_class(self.get_object().source_type) - - def get_extra_context(self): - return { - 'object': self.get_object(), - 'title': _('Edit source: %s') % self.get_object(), - } - - -class SetupSourceListView(SingleObjectListView): - queryset = Source.objects.select_subclasses() - view_permission = permission_sources_setup_view - - def get_extra_context(self): - return { - 'hide_link': True, - 'hide_object': True, - 'no_results_icon': icon_setup_sources, - 'no_results_secondary_links': [ - link_setup_source_create_webform.resolve( - context=RequestContext(request=self.request) - ), - link_setup_source_create_imap_email.resolve( - context=RequestContext(request=self.request) - ), - link_setup_source_create_pop3_email.resolve( - context=RequestContext(request=self.request) - ), - link_setup_source_create_sane_scanner.resolve( - context=RequestContext(request=self.request) - ), - link_setup_source_create_staging_folder.resolve( - context=RequestContext(request=self.request) - ), - link_setup_source_create_watch_folder.resolve( - context=RequestContext(request=self.request) - ), - ], - 'no_results_text': _( - 'Sources provide the means to upload documents. ' - 'Some sources like the webform, are interactive and require ' - 'user input to operate. Others like the email sources, are ' - 'automatic and run on the background without user intervention.' - ), - 'no_results_title': _('No sources available'), - 'title': _('Sources'), - } diff --git a/mayan/apps/sources/wizards.py b/mayan/apps/sources/wizards.py index 8b8f7549b8..b279b16029 100644 --- a/mayan/apps/sources/wizards.py +++ b/mayan/apps/sources/wizards.py @@ -150,12 +150,25 @@ class DocumentCreateWizard(SessionWizardView): 'none have been enabled, create one before proceeding.' ) ) - return HttpResponseRedirect(reverse('sources:setup_source_list')) + return HttpResponseRedirect(reverse(viewname='sources:setup_source_list')) return super( DocumentCreateWizard, self ).dispatch(request, *args, **kwargs) + def done(self, form_list, **kwargs): + query_dict = {} + + for step in WizardStep.get_all(): + query_dict.update(step.done(wizard=self) or {}) + + url = furl(reverse(viewname='sources:upload_interactive')) + # Use equal and not .update() to get the same result as using + # urlencode(doseq=True) + url.args = query_dict + + return HttpResponseRedirect(url) + def get_context_data(self, form, **kwargs): context = super( DocumentCreateWizard, self @@ -184,16 +197,3 @@ class DocumentCreateWizard(SessionWizardView): def get_form_kwargs(self, step): return WizardStep.get(name=step).get_form_kwargs(wizard=self) or {} - - def done(self, form_list, **kwargs): - query_dict = {} - - for step in WizardStep.get_all(): - query_dict.update(step.done(wizard=self) or {}) - - url = furl(reverse('sources:upload_interactive')) - # Use equal and not .update() to get the same result as using - # urlencode(doseq=True) - url.args = query_dict - - return HttpResponseRedirect(url) From 205ca594f5b76bcf08d6aa9347ee549dcad88e34 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Tue, 22 Jan 2019 22:16:56 -0400 Subject: [PATCH 038/209] Replace filter_by_access with restrict_queryset With the interface finalized, replace .filter_by_access() in the generic view mixins with restrict_queryset(). Signed-off-by: Roberto Rosario --- mayan/apps/common/mixins.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mayan/apps/common/mixins.py b/mayan/apps/common/mixins.py index 67132941e2..14ef15a44e 100644 --- a/mayan/apps/common/mixins.py +++ b/mayan/apps/common/mixins.py @@ -117,7 +117,7 @@ class ExternalObjectMixin(object): permission = self.get_external_object_permission() if permission: - queryset = AccessControlList.objects.filter_by_access( + queryset = AccessControlList.objects.restrict_queryset( permission=permission, queryset=queryset, user=self.request.user ) @@ -260,7 +260,7 @@ class MultipleObjectMixin(object): ) if self.object_permission: - return AccessControlList.objects.filter_by_access( + return AccessControlList.objects.restrict_queryset( permission=self.object_permission, queryset=queryset, user=self.request.user ) @@ -333,7 +333,7 @@ class ObjectListPermissionFilterMixin(object): queryset = super(ObjectListPermissionFilterMixin, self).get_queryset() if not self.access_object_retrieve_method and self.object_permission: - return AccessControlList.objects.filter_by_access( + return AccessControlList.objects.restrict_queryset( permission=self.object_permission, queryset=queryset, user=self.request.user ) From a769cc92e31b42d7b3fd0bfd49a7e49ccb809cbc Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Tue, 22 Jan 2019 22:21:49 -0400 Subject: [PATCH 039/209] Fix staging file delete view Signed-off-by: Roberto Rosario --- mayan/apps/sources/links.py | 7 ++++--- mayan/apps/sources/tests/test_views.py | 7 +------ mayan/apps/sources/views.py | 11 +++++------ 3 files changed, 10 insertions(+), 15 deletions(-) diff --git a/mayan/apps/sources/links.py b/mayan/apps/sources/links.py index 66b71d2ac8..bf206ec3b4 100644 --- a/mayan/apps/sources/links.py +++ b/mayan/apps/sources/links.py @@ -19,7 +19,8 @@ from .literals import ( ) from .permissions import ( permission_sources_create, permission_sources_delete, - permission_sources_edit, permission_sources_view + permission_sources_edit, permission_sources_view, + permission_staging_file_delete ) @@ -113,9 +114,9 @@ link_source_logs = Link( ) link_staging_file_delete = Link( keep_query=True, kwargs={ - 'staging_file_pk': 'source.pk', + 'staging_folder_id': 'source.pk', 'encoded_filename': 'object.encoded_filename' - }, permissions=(permission_document_new_version, permission_document_create), + }, permissions=(permission_staging_file_delete,), tags='dangerous', text=_('Delete'), view='sources:staging_file_delete' ) link_upload_version = Link( diff --git a/mayan/apps/sources/tests/test_views.py b/mayan/apps/sources/tests/test_views.py index fbaa09fc21..7d02866212 100644 --- a/mayan/apps/sources/tests/test_views.py +++ b/mayan/apps/sources/tests/test_views.py @@ -200,7 +200,6 @@ class StagingFolderViewTestCase(GenericViewTestCase): self.temporary_directory = mkdtemp() shutil.copy(TEST_SMALL_DOCUMENT_PATH, self.temporary_directory) self.filename = os.path.basename(TEST_SMALL_DOCUMENT_PATH) - self.login_user() def tearDown(self): fs_cleanup(self.temporary_directory) @@ -233,7 +232,7 @@ class StagingFolderViewTestCase(GenericViewTestCase): response = self._request_staging_file_delete_view( staging_file=staging_file ) - self.assertEqual(response.status_code, 403) + self.assertEqual(response.status_code, 404) self.assertEqual(len(list(self.staging_folder.get_files())), 1) def test_staging_folder_delete_with_permission(self): @@ -252,10 +251,6 @@ class StagingFolderViewTestCase(GenericViewTestCase): class SourcesViewsTestCase(GenericViewTestCase): - def setUp(self): - super(SourcesViewsTestCase, self).setUp() - self.login_user() - def _create_web_source(self): self.source = WebFormSource.objects.create( enabled=True, label=TEST_SOURCE_LABEL, diff --git a/mayan/apps/sources/views.py b/mayan/apps/sources/views.py index a77ca7f398..96778a0dbe 100644 --- a/mayan/apps/sources/views.py +++ b/mayan/apps/sources/views.py @@ -210,9 +210,10 @@ class SourceLogView(SingleObjectListView): ) -class StagingFileDeleteView(SingleObjectDeleteView): - object_permission = permission_staging_file_delete - object_permission_related = 'staging_folder' +class StagingFileDeleteView(ExternalObjectMixin, SingleObjectDeleteView): + external_object_class = StagingFolderSource + external_object_pk_url_kwarg = 'staging_folder_id' + external_object_permission = permission_staging_file_delete def get_extra_context(self): return { @@ -228,9 +229,7 @@ class StagingFileDeleteView(SingleObjectDeleteView): ) def get_source(self): - return get_object_or_404( - klass=StagingFolderSource, pk=self.kwargs['staging_folder_id'] - ) + return self.get_external_object() class UploadBaseView(ListModeMixin, MultiFormView): From 1d0ebbab64f7824cb208c03c05c70635a836b0a8 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Tue, 22 Jan 2019 22:35:37 -0400 Subject: [PATCH 040/209] FilteredSelectionFormOptions updates Fix displaying the name of the subclass when the queryset is missing. Add support for passing a new argument to specify if the field is required or not. Signed-off-by: Roberto Rosario --- mayan/apps/common/forms.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/mayan/apps/common/forms.py b/mayan/apps/common/forms.py index 4a331e01ab..97acc3405f 100644 --- a/mayan/apps/common/forms.py +++ b/mayan/apps/common/forms.py @@ -141,6 +141,7 @@ class FilteredSelectionFormOptions(object): {'model': None}, {'permission': None}, {'queryset': None}, + {'required': True}, {'user': None}, {'widget_class': None}, {'widget_attributes': {'size': '10'}}, @@ -191,7 +192,7 @@ class FilteredSelectionForm(forms.Form): raise ImproperlyConfigured( '{} requires a queryset or a model to be specified as ' 'a meta option or passed during initialization.'.format( - self.__class__ + self.__class__.__name__ ) ) @@ -221,7 +222,7 @@ class FilteredSelectionForm(forms.Form): self.fields[opts.field_name] = field_class( help_text=opts.help_text, label=opts.label, - queryset=queryset, required=True, + queryset=queryset, required=opts.required, widget=widget_class(attrs=opts.widget_attributes), **extra_kwargs ) From c059f1f021ffc124fda99c771d8ed4ad51dd599c Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Tue, 22 Jan 2019 22:37:24 -0400 Subject: [PATCH 041/209] Fix the cabinet wizard step Signed-off-by: Roberto Rosario --- mayan/apps/cabinets/forms.py | 10 ++++++---- mayan/apps/cabinets/wizard_steps.py | 2 ++ 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/mayan/apps/cabinets/forms.py b/mayan/apps/cabinets/forms.py index 82626055c9..e88c0d8973 100644 --- a/mayan/apps/cabinets/forms.py +++ b/mayan/apps/cabinets/forms.py @@ -6,7 +6,9 @@ from mayan.apps.common.forms import FilteredSelectionForm class CabinetListForm(FilteredSelectionForm): - _field_name = 'cabinets' - _label = _('Cabinets') - _widget_attributes = {'class': 'select2'} - _allow_multiple = True + class Meta: + allow_multiple = True + field_name = 'cabinets' + label = _('Cabinets') + required = False + widget_attributes = {'class': 'select2'} diff --git a/mayan/apps/cabinets/wizard_steps.py b/mayan/apps/cabinets/wizard_steps.py index 5f409480b4..4454efdcae 100644 --- a/mayan/apps/cabinets/wizard_steps.py +++ b/mayan/apps/cabinets/wizard_steps.py @@ -9,6 +9,7 @@ from django.utils.translation import ugettext_lazy as _ from mayan.apps.sources.wizards import WizardStep from .forms import CabinetListForm +from .models import Cabinet from .permissions import permission_cabinet_add_document @@ -41,6 +42,7 @@ class WizardStepCabinets(WizardStep): return { 'help_text': _('Cabinets to which the document will be added.'), 'permission': permission_cabinet_add_document, + 'queryset': Cabinet.objects.all(), 'user': wizard.request.user } From 3f48a5549e6c039227d25cac5e088f2d85789544 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Tue, 22 Jan 2019 22:38:37 -0400 Subject: [PATCH 042/209] Sort source form definitions Signed-off-by: Roberto Rosario --- mayan/apps/sources/forms.py | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/mayan/apps/sources/forms.py b/mayan/apps/sources/forms.py index c87dbda756..77cafb4d64 100644 --- a/mayan/apps/sources/forms.py +++ b/mayan/apps/sources/forms.py @@ -93,21 +93,6 @@ class SaneScannerSetupForm(forms.ModelForm): model = SaneScanner -class WebFormSetupForm(forms.ModelForm): - class Meta: - fields = ('label', 'enabled', 'uncompress') - model = WebFormSource - - -class StagingFolderSetupForm(forms.ModelForm): - class Meta: - fields = ( - 'label', 'enabled', 'folder_path', 'preview_width', - 'preview_height', 'uncompress', 'delete_after_upload' - ) - model = StagingFolderSource - - class EmailSetupBaseForm(forms.ModelForm): class Meta: fields = ( @@ -133,6 +118,15 @@ class POP3EmailSetupForm(EmailSetupBaseForm): model = POP3Email +class StagingFolderSetupForm(forms.ModelForm): + class Meta: + fields = ( + 'label', 'enabled', 'folder_path', 'preview_width', + 'preview_height', 'uncompress', 'delete_after_upload' + ) + model = StagingFolderSource + + class WatchFolderSetupForm(forms.ModelForm): class Meta: fields = ( @@ -140,3 +134,9 @@ class WatchFolderSetupForm(forms.ModelForm): 'folder_path', 'include_subdirectories' ) model = WatchFolderSource + + +class WebFormSetupForm(forms.ModelForm): + class Meta: + fields = ('label', 'enabled', 'uncompress') + model = WebFormSource From 8c085331f1599c0362d68d8042a09bf46becbc8a Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Wed, 23 Jan 2019 14:48:23 -0400 Subject: [PATCH 043/209] DetailForm: Use Meta class instead Instead of class attributes, make a generic reusable the FormOption class and update the DetailForm to use a Meta class for options. Signed-off-by: Roberto Rosario --- mayan/apps/common/forms.py | 121 ++++++++++++++++++++++++------------- mayan/apps/common/utils.py | 25 ++++++++ 2 files changed, 103 insertions(+), 43 deletions(-) diff --git a/mayan/apps/common/forms.py b/mayan/apps/common/forms.py index 97acc3405f..3310b997e6 100644 --- a/mayan/apps/common/forms.py +++ b/mayan/apps/common/forms.py @@ -4,6 +4,7 @@ import os from django import forms from django.conf import settings +from django.contrib.admin.utils import label_for_field from django.core.exceptions import ImproperlyConfigured from django.db import models from django.utils.module_loading import import_string @@ -13,7 +14,7 @@ from mayan.apps.acls.models import AccessControlList from .classes import Package from .models import UserLocaleProfile -from .utils import resolve_attribute +from .utils import introspect_attribute, resolve_attribute from .widgets import DisableableSelectWidget, PlainWidget, TextAreaDiv @@ -41,26 +42,89 @@ class ChoiceForm(forms.Form): selection = forms.MultipleChoiceField(widget=DisableableSelectWidget()) +class FormOptions(object): + def __init__(self, form, kwargs, options=None): + """ + Option definitions will be iterated. The option value will be + determined in the following order: as passed via keyword + arguments during form intialization, as form get_... method or + finally as static Meta options. This is to allow a form with + Meta options or method to be overrided at initialization + and increase the usability of a single class. + """ + for option_definition in self.option_definitions: + name = option_definition.keys()[0] + default_value = option_definition.values()[0] + + try: + # Check for a runtime value via kwargs + value = kwargs.pop(name) + except KeyError: + try: + # Check if there is a get_... method + value = getattr(self, 'get_{}'.format(name))() + except AttributeError: + try: + # Check the meta class options + value = getattr(options, name) + except AttributeError: + value = default_value + + setattr(self, name, value) + + +class DetailFormOption(FormOptions): + # Dictionary list of option names and default values + option_definitions = ( + {'extra_fields': []}, + ) + + class DetailForm(forms.ModelForm): def __init__(self, *args, **kwargs): - self.extra_fields = kwargs.pop('extra_fields', ()) + self.opts = DetailFormOption( + form=self, kwargs=kwargs, options=getattr(self, 'Meta', None) + ) super(DetailForm, self).__init__(*args, **kwargs) - for extra_field in self.extra_fields: - result = resolve_attribute(obj=self.instance, attribute=extra_field['field']) - label = 'label' in extra_field and extra_field['label'] or None + for extra_field in self.opts.extra_fields: + obj = extra_field.get('object', self.instance) + field = extra_field['field'] + + result = resolve_attribute( + attribute=field, obj=obj + ) + + label = extra_field.get('label', None) + + if not label: + attribute_name, obj = introspect_attribute( + attribute_name=field, obj=obj + ) + + if not obj: + label = _('None') + else: + try: + label = getattr( + getattr(obj, attribute_name), 'short_description' + ) + except AttributeError: + label = label_for_field( + name=attribute_name, model=obj + ) + # TODO: Add others result types <=> Field types if isinstance(result, models.query.QuerySet): - self.fields[extra_field['field']] = \ - forms.ModelMultipleChoiceField( - queryset=result, label=label) + self.fields[field] = forms.ModelMultipleChoiceField( + queryset=result, label=label + ) else: - self.fields[extra_field['field']] = forms.CharField( - label=extra_field['label'], + self.fields[field] = forms.CharField( initial=resolve_attribute( - obj=self.instance, - attribute=extra_field['field'] - ), + obj=obj, + attribute=field + ), label=label, widget=extra_field.get('widget', PlainWidget) ) @@ -131,7 +195,7 @@ class FileDisplayForm(forms.Form): self.fields['text'].initial = file_object.read() -class FilteredSelectionFormOptions(object): +class FilteredSelectionFormOptions(FormOptions): # Dictionary list of option names and default values option_definitions = ( {'allow_multiple': False}, @@ -147,35 +211,6 @@ class FilteredSelectionFormOptions(object): {'widget_attributes': {'size': '10'}}, ) - def __init__(self, form, kwargs, options=None): - """ - Option definitions will be iterated. The option value will be - determined in the following order: as passed via keyword - arguments during form intialization, as form get_... method or - finally as static Meta options. This is to allow a form with - Meta options or method to be overrided at initialization - and increase the usability of a single class. - """ - for option_definition in self.option_definitions: - name = option_definition.keys()[0] - default_value = option_definition.values()[0] - - try: - # Check for a runtime value via kwargs - value = kwargs.pop(name) - except KeyError: - try: - # Check if there is a get_... method - value = getattr(self, 'get_{}'.format(name))() - except AttributeError: - try: - # Check the meta class options - value = getattr(options, name) - except AttributeError: - value = default_value - - setattr(self, name, value) - class FilteredSelectionForm(forms.Form): """ diff --git a/mayan/apps/common/utils.py b/mayan/apps/common/utils.py index e5456b5a02..e88f7a2c4e 100644 --- a/mayan/apps/common/utils.py +++ b/mayan/apps/common/utils.py @@ -6,6 +6,7 @@ import shutil import tempfile from django.conf import settings +from django.core.exceptions import FieldDoesNotExist from django.db.models.constants import LOOKUP_SEP from django.urls import resolve as django_resolve from django.urls.base import get_script_prefix @@ -142,6 +143,30 @@ def get_storage_subclass(dotted_path): return StorageSubclass +def introspect_attribute(attribute_name, obj): + try: + # Try as a related field + obj._meta.get_field(field_name=attribute_name) + except (AttributeError, FieldDoesNotExist): + attribute_name = attribute_name.replace('__', '.') + + try: + # If there are separators in the attribute name, traverse them + # to the final attribute + attribute_part, attribute_remaining = attribute_name.split( + '.', 1 + ) + except ValueError: + return attribute_name, obj + else: + return introspect_attribute( + attribute_name=attribute_part, + obj=related_field.related_model, + ) + else: + return attribute_name, obj + + def TemporaryFile(*args, **kwargs): kwargs.update({'dir': setting_temporary_directory.value}) return tempfile.TemporaryFile(*args, **kwargs) From 75fd7647d4d0d38fe0e27ff0339bad54d21575ca Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Wed, 23 Jan 2019 14:49:59 -0400 Subject: [PATCH 044/209] Keys: Update use of DetailForm Fix absolute URL keyword argument. Move detail generation to the model. Signed-off-by: Roberto Rosario --- mayan/apps/django_gpg/forms.py | 41 +++++++++++---------------------- mayan/apps/django_gpg/models.py | 11 ++++++++- 2 files changed, 23 insertions(+), 29 deletions(-) diff --git a/mayan/apps/django_gpg/forms.py b/mayan/apps/django_gpg/forms.py index 8d7795b2d9..40710335ad 100644 --- a/mayan/apps/django_gpg/forms.py +++ b/mayan/apps/django_gpg/forms.py @@ -1,7 +1,6 @@ from __future__ import unicode_literals from django import forms -from django.utils.html import escape from django.utils.translation import ugettext_lazy as _ from mayan.apps.common.forms import DetailForm @@ -10,34 +9,20 @@ from .models import Key class KeyDetailForm(DetailForm): - def __init__(self, *args, **kwargs): - instance = kwargs['instance'] - - extra_fields = ( - {'label': _('Key ID'), 'field': 'key_id'}, - { - 'label': _('User ID'), - 'field': lambda x: escape(instance.user_id), - }, - { - 'label': _('Creation date'), 'field': 'creation_date', - 'widget': forms.widgets.DateInput - }, - { - 'label': _('Expiration date'), - 'field': lambda x: instance.expiration_date or _('None'), - 'widget': forms.widgets.DateInput - }, - {'label': _('Fingerprint'), 'field': 'fingerprint'}, - {'label': _('Length'), 'field': 'length'}, - {'label': _('Algorithm'), 'field': 'algorithm'}, - {'label': _('Type'), 'field': lambda x: instance.get_key_type_display()}, - ) - - kwargs['extra_fields'] = extra_fields - super(KeyDetailForm, self).__init__(*args, **kwargs) - class Meta: + extra_fields = ( + {'field': 'key_id'}, + {'field': 'get_escaped_user_id'}, + {'field': 'creation_date', 'widget': forms.widgets.DateInput}, + { + 'field': 'get_expiration_date_display', + 'widget': forms.widgets.DateInput + }, + {'field': 'fingerprint'}, + {'field': 'length'}, + {'field': 'algorithm'}, + {'label': _('Type'), 'field': 'get_key_type_display'}, + ) fields = () model = Key diff --git a/mayan/apps/django_gpg/models.py b/mayan/apps/django_gpg/models.py index c59748b63d..25885d9a5e 100644 --- a/mayan/apps/django_gpg/models.py +++ b/mayan/apps/django_gpg/models.py @@ -7,6 +7,7 @@ from django.core.exceptions import ValidationError from django.db import models from django.urls import reverse from django.utils.encoding import python_2_unicode_compatible +from django.utils.html import escape from django.utils.translation import ugettext_lazy as _ from .exceptions import NeedPassphrase, PassphraseError @@ -78,9 +79,17 @@ class Key(models.Model): def get_absolute_url(self): return reverse( - viewname='django_gpg:key_detail', kwargs={'key_pk': self.pk} + viewname='django_gpg:key_detail', kwargs={'key_id': self.pk} ) + def get_expiration_date_display(self): + return self.expiration_date or _('None') + get_expiration_date_display.short_description = _('Expiration date') + + def get_escaped_user_id(self): + return escape(self.user_id) + get_escaped_user_id.short_description = _('User ID') + @property def key_id(self): """ From daf79983aa2a6198d30678f79b505130bfc16076 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Fri, 25 Jan 2019 01:11:51 -0400 Subject: [PATCH 045/209] Update Link class interface Remove Link class support for multiple permissions. Accept only one permission for each link. Remove support for the permission related field. Signed-off-by: Roberto Rosario --- mayan/apps/navigation/classes.py | 370 ++++++++++---------- mayan/apps/navigation/tests/test_classes.py | 20 +- 2 files changed, 195 insertions(+), 195 deletions(-) diff --git a/mayan/apps/navigation/classes.py b/mayan/apps/navigation/classes.py index 2b91910579..b6ec8f9e50 100644 --- a/mayan/apps/navigation/classes.py +++ b/mayan/apps/navigation/classes.py @@ -31,49 +31,163 @@ from .utils import get_current_view_name logger = logging.getLogger(__name__) -class ResolvedLink(object): - def __init__(self, link, current_view_name): - self.current_view_name = current_view_name - self.disabled = False - self.link = link - self.url = '#' - self.context = None - self.request = None +class Link(object): + _registry = {} - @property - def active(self): - return self.link.view == self.current_view_name + @classmethod + def get(cls, name): + return cls._registry[name] - @property - def description(self): - return self.link.description + @classmethod + def remove(cls, name): + del cls._registry[name] - @property - def html_data(self): - return self.link.html_data + def __init__(self, text, view=None, args=None, condition=None, + conditional_disable=None, description=None, html_data=None, + html_extra_classes=None, icon=None, icon_class=None, + keep_query=False, kwargs=None, name=None, permission=None, + remove_from_query=None, tags=None, url=None): - @property - def html_extra_classes(self): - return self.link.html_extra_classes + self.args = args or [] + self.condition = condition + self.conditional_disable = conditional_disable + self.description = description + self.html_data = html_data + self.html_extra_classes = html_extra_classes + self.icon = icon + self.icon_class = icon_class + self.keep_query = keep_query + self.kwargs = kwargs or {} + self.name = name + self.permission = permission + self.remove_from_query = remove_from_query or [] + self.tags = tags + self.text = text + self.view = view + self.url = url - @property - def icon(self): - return self.link.icon + if name: + self.__class__._registry[name] = self - @property - def icon_class(self): - return self.link.icon_class + def resolve(self, context, resolved_object=None): + AccessControlList = apps.get_model( + app_label='acls', model_name='AccessControlList' + ) - @property - def tags(self): - return self.link.tags - - @property - def text(self): + # Try to get the request object the faster way and fallback to the + # slower method. try: - return self.link.text(context=self.context) - except TypeError: - return self.link.text + request = context.request + except AttributeError: + request = Variable('request').resolve(context) + + current_path = request.META['PATH_INFO'] + current_view_name = resolve(current_path).view_name + + # ACL is tested agains the resolved_object or just {{ object }} if not + if not resolved_object: + try: + resolved_object = Variable('object').resolve(context=context) + except VariableDoesNotExist: + pass + + # If this link has a required permission check that the user has it + # too + if self.permission: + if resolved_object: + try: + AccessControlList.objects.check_access( + obj=resolved_object, permission=self.permission, + user=request.user + ) + except PermissionDenied: + return None + else: + try: + Permission.check_user_permission( + permission=self.permission, user=request.user + ) + except PermissionDenied: + return None + + # Check to see if link has conditional display function and only + # display it if the result of the conditional display function is + # True + if self.condition: + if not self.condition(context): + return None + + resolved_link = ResolvedLink( + current_view_name=current_view_name, link=self + ) + + if self.view: + view_name = Variable('"{}"'.format(self.view)) + if isinstance(self.args, list) or isinstance(self.args, tuple): + # TODO: Don't check for instance check for iterable in try/except + # block. This update required changing all 'args' argument in + # links.py files to be iterables and not just strings. + args = [Variable(arg) for arg in self.args] + else: + args = [Variable(self.args)] + + # If we were passed an instance of the view context object we are + # resolving, inject it into the context. This help resolve links for + # object lists. + if resolved_object: + context['resolved_object'] = resolved_object + + try: + kwargs = self.kwargs(context) + except TypeError: + # Is not a callable + kwargs = self.kwargs + + kwargs = {key: Variable(value) for key, value in kwargs.items()} + + # Use Django's exact {% url %} code to resolve the link + node = URLNode( + view_name=view_name, args=args, kwargs=kwargs, asvar=None + ) + try: + resolved_link.url = node.render(context) + except Exception as exception: + logger.error( + 'Error resolving link "%s" URL; %s', self.text, exception + ) + elif self.url: + resolved_link.url = self.url + + # This is for links that should be displayed but that are not clickable + if self.conditional_disable: + resolved_link.disabled = self.conditional_disable(context) + else: + resolved_link.disabled = False + + # Lets a new link keep the same URL query string of the current URL + if self.keep_query: + # Sometimes we are required to remove a key from the URL QS + parsed_url = furl( + force_str( + request.get_full_path() or request.META.get( + 'HTTP_REFERER', resolve_url(settings.LOGIN_REDIRECT_URL) + ) + ) + ) + + for key in self.remove_from_query: + try: + parsed_url.query.remove(key) + except KeyError: + pass + + # Use the link's URL but with the previous URL querystring + new_url = furl(resolved_link.url) + new_url.args = parsed_url.querystr + resolved_link.url = new_url.url + + resolved_link.context = context + return resolved_link class Menu(object): @@ -295,165 +409,49 @@ class Menu(object): ) -class Link(object): - _registry = {} +class ResolvedLink(object): + def __init__(self, link, current_view_name): + self.current_view_name = current_view_name + self.disabled = False + self.link = link + self.url = '#' + self.context = None + self.request = None - @classmethod - def get(cls, name): - return cls._registry[name] + @property + def active(self): + return self.link.view == self.current_view_name - @classmethod - def remove(cls, name): - del cls._registry[name] + @property + def description(self): + return self.link.description - def __init__(self, text, view=None, args=None, condition=None, - conditional_disable=None, description=None, html_data=None, - html_extra_classes=None, icon=None, icon_class=None, - keep_query=False, kwargs=None, name=None, permissions=None, - permissions_related=None, remove_from_query=None, tags=None, - url=None): + @property + def html_data(self): + return self.link.html_data - self.args = args or [] - self.condition = condition - self.conditional_disable = conditional_disable - self.description = description - self.html_data = html_data - self.html_extra_classes = html_extra_classes - self.icon = icon - self.icon_class = icon_class - self.keep_query = keep_query - self.kwargs = kwargs or {} - self.name = name - self.permissions = permissions or [] - self.permissions_related = permissions_related - self.remove_from_query = remove_from_query or [] - self.tags = tags - self.text = text - self.view = view - self.url = url + @property + def html_extra_classes(self): + return self.link.html_extra_classes - if name: - self.__class__._registry[name] = self + @property + def icon(self): + return self.link.icon - def resolve(self, context, resolved_object=None): - AccessControlList = apps.get_model( - app_label='acls', model_name='AccessControlList' - ) + @property + def icon_class(self): + return self.link.icon_class - # Try to get the request object the faster way and fallback to the - # slower method. + @property + def tags(self): + return self.link.tags + + @property + def text(self): try: - request = context.request - except AttributeError: - request = Variable('request').resolve(context) - - current_path = request.META['PATH_INFO'] - current_view_name = resolve(current_path).view_name - - # ACL is tested agains the resolved_object or just {{ object }} if not - if not resolved_object: - try: - resolved_object = Variable('object').resolve(context=context) - except VariableDoesNotExist: - pass - - # If this link has a required permission check that the user has it - # too - if self.permissions: - if resolved_object: - try: - AccessControlList.objects.check_access( - permissions=self.permissions, user=request.user, - obj=resolved_object, related=self.permissions_related - ) - except PermissionDenied: - return None - else: - try: - Permission.check_permissions( - requester=request.user, permissions=self.permissions - ) - except PermissionDenied: - return None - - # Check to see if link has conditional display function and only - # display it if the result of the conditional display function is - # True - if self.condition: - if not self.condition(context): - return None - - resolved_link = ResolvedLink( - current_view_name=current_view_name, link=self - ) - - if self.view: - view_name = Variable('"{}"'.format(self.view)) - if isinstance(self.args, list) or isinstance(self.args, tuple): - # TODO: Don't check for instance check for iterable in try/except - # block. This update required changing all 'args' argument in - # links.py files to be iterables and not just strings. - args = [Variable(arg) for arg in self.args] - else: - args = [Variable(self.args)] - - # If we were passed an instance of the view context object we are - # resolving, inject it into the context. This help resolve links for - # object lists. - if resolved_object: - context['resolved_object'] = resolved_object - - try: - kwargs = self.kwargs(context) - except TypeError: - # Is not a callable - kwargs = self.kwargs - - kwargs = {key: Variable(value) for key, value in kwargs.items()} - - # Use Django's exact {% url %} code to resolve the link - node = URLNode( - view_name=view_name, args=args, kwargs=kwargs, asvar=None - ) - try: - resolved_link.url = node.render(context) - except Exception as exception: - logger.error( - 'Error resolving link "%s" URL; %s', self.text, exception - ) - elif self.url: - resolved_link.url = self.url - - # This is for links that should be displayed but that are not clickable - if self.conditional_disable: - resolved_link.disabled = self.conditional_disable(context) - else: - resolved_link.disabled = False - - # Lets a new link keep the same URL query string of the current URL - if self.keep_query: - # Sometimes we are required to remove a key from the URL QS - parsed_url = furl( - force_str( - request.get_full_path() or request.META.get( - 'HTTP_REFERER', resolve_url(settings.LOGIN_REDIRECT_URL) - ) - ) - ) - - for key in self.remove_from_query: - try: - parsed_url.query.remove(key) - except KeyError: - pass - - # Use the link's URL but with the previous URL querystring - new_url = furl(resolved_link.url) - new_url.args = parsed_url.querystr - resolved_link.url = new_url.url - - resolved_link.context = context - return resolved_link + return self.link.text(context=self.context) + except TypeError: + return self.link.text class Separator(Link): diff --git a/mayan/apps/navigation/tests/test_classes.py b/mayan/apps/navigation/tests/test_classes.py index d968e49b34..d14a82c3ab 100644 --- a/mayan/apps/navigation/tests/test_classes.py +++ b/mayan/apps/navigation/tests/test_classes.py @@ -24,10 +24,11 @@ class LinkClassTestCase(GenericViewTestCase): def setUp(self): super(LinkClassTestCase, self).setUp() - self.add_test_view(test_object=self.group) + self.add_test_view(test_object=self._test_case_group) self.namespace = PermissionNamespace( - label=TEST_PERMISSION_NAMESPACE_TEXT, name=TEST_PERMISSION_NAMESPACE_NAME + label=TEST_PERMISSION_NAMESPACE_TEXT, + name=TEST_PERMISSION_NAMESPACE_NAME ) self.permission = self.namespace.add_permission( @@ -49,7 +50,7 @@ class LinkClassTestCase(GenericViewTestCase): self.login_user() link = Link( - permissions=(self.permission,), text=TEST_LINK_TEXT, + permission=self.permission, text=TEST_LINK_TEXT, view=TEST_VIEW_NAME ) @@ -65,11 +66,11 @@ class LinkClassTestCase(GenericViewTestCase): self.login_user() link = Link( - permissions=(self.permission,), text=TEST_LINK_TEXT, + permission=self.permission, text=TEST_LINK_TEXT, view=TEST_VIEW_NAME ) - self.role.permissions.add(self.permission.stored_permission) + self._test_case_role.permissions.add(self.permission.stored_permission) response = self.get(TEST_VIEW_NAME) response.context.update({'request': response.wsgi_request}) @@ -84,12 +85,12 @@ class LinkClassTestCase(GenericViewTestCase): self.login_user() link = Link( - permissions=(self.permission,), text=TEST_LINK_TEXT, + permission=self.permission, text=TEST_LINK_TEXT, view=TEST_VIEW_NAME ) acl = AccessControlList.objects.create( - content_object=self.group, role=self.role + content_object=self._test_case_group, role=self._test_case_role ) acl.permissions.add(self.permission.stored_permission) @@ -156,10 +157,11 @@ class MenuClassTestCase(GenericViewTestCase): def setUp(self): super(MenuClassTestCase, self).setUp() - self.add_test_view(test_object=self.group) + self.add_test_view(test_object=self._test_case_group) self.namespace = PermissionNamespace( - label=TEST_PERMISSION_NAMESPACE_TEXT, name=TEST_PERMISSION_NAMESPACE_NAME + label=TEST_PERMISSION_NAMESPACE_TEXT, + name=TEST_PERMISSION_NAMESPACE_NAME ) self.permission = self.namespace.add_permission( From 92039772610b53c426d4c9825f256389d24e94ba Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Fri, 25 Jan 2019 01:13:53 -0400 Subject: [PATCH 046/209] Update all links to the new Link class interface Signed-off-by: Roberto Rosario --- mayan/apps/acls/links.py | 13 +- mayan/apps/cabinets/links.py | 18 +-- mayan/apps/checkouts/links.py | 17 +-- mayan/apps/common/links.py | 4 +- mayan/apps/converter/links.py | 8 +- mayan/apps/django_gpg/links.py | 21 ++- mayan/apps/document_comments/links.py | 6 +- mayan/apps/document_indexing/links.py | 10 +- mayan/apps/document_parsing/links.py | 14 +- mayan/apps/document_signatures/links.py | 78 +++++----- mayan/apps/document_states/links.py | 61 ++++---- mayan/apps/documents/links.py | 181 ++++++++++++------------ mayan/apps/events/links.py | 6 +- mayan/apps/file_metadata/links.py | 18 ++- mayan/apps/linking/links.py | 36 +++-- mayan/apps/mailer/links.py | 36 ++--- mayan/apps/mayan_statistics/links.py | 10 +- mayan/apps/metadata/links.py | 30 ++-- mayan/apps/motd/links.py | 6 +- mayan/apps/ocr/links.py | 22 +-- mayan/apps/smart_settings/links.py | 8 +- mayan/apps/sources/links.py | 32 ++--- mayan/apps/tags/links.py | 49 ++++--- mayan/apps/task_manager/links.py | 10 +- mayan/apps/user_management/links.py | 32 ++--- 25 files changed, 353 insertions(+), 373 deletions(-) diff --git a/mayan/apps/acls/links.py b/mayan/apps/acls/links.py index adebdea8fb..865fc822bb 100644 --- a/mayan/apps/acls/links.py +++ b/mayan/apps/acls/links.py @@ -30,21 +30,20 @@ def get_kwargs_factory(variable_name): link_acl_delete = Link( icon_class=icon_acl_delete, kwargs={'acl_id': 'resolved_object.pk'}, - permissions=(permission_acl_edit,), permissions_related='content_object', - tags='dangerous', text=_('Delete'), view='acls:acl_delete', + permission=permission_acl_edit, tags='dangerous', text=_('Delete'), + view='acls:acl_delete', ) link_acl_list = Link( icon_class=icon_acl_list, kwargs=get_kwargs_factory( variable_name='resolved_object' - ), permissions=(permission_acl_view,), text=_('ACLs'), view='acls:acl_list' + ), permission=permission_acl_view, text=_('ACLs'), view='acls:acl_list' ) link_acl_create = Link( icon_class=icon_acl_new, kwargs=get_kwargs_factory('resolved_object'), - permissions=(permission_acl_edit,), text=_('New ACL'), - view='acls:acl_create' + permission=permission_acl_edit, text=_('New ACL'), view='acls:acl_create' ) link_acl_permissions = Link( args='resolved_object.pk', icon_class=icon_permission, - permissions=(permission_acl_edit,), permissions_related='content_object', - text=_('Permissions'), view='acls:acl_permissions', + permission=permission_acl_edit, text=_('Permissions'), + view='acls:acl_permissions', ) diff --git a/mayan/apps/cabinets/links.py b/mayan/apps/cabinets/links.py index bdf0a25294..56f01aad3b 100644 --- a/mayan/apps/cabinets/links.py +++ b/mayan/apps/cabinets/links.py @@ -24,17 +24,17 @@ from .permissions import ( link_document_cabinet_list = Link( args='resolved_object.pk', icon_class=icon_cabinet_list, - permissions=(permission_document_view,), - text=_('Cabinets'), view='cabinets:document_cabinet_list', + permission=permission_document_view, text=_('Cabinets'), + view='cabinets:document_cabinet_list', ) link_document_cabinet_remove = Link( args='resolved_object.pk', icon_class=icon_document_cabinet_remove, - permissions=(permission_cabinet_remove_document,), + permission=permission_cabinet_remove_document, text=_('Remove from cabinets'), view='cabinets:document_cabinet_remove' ) link_document_cabinet_add = Link( args='object.pk', icon_class=icon_document_cabinet_add, - permissions=(permission_cabinet_add_document,), text=_('Add to cabinets'), + permission=permission_cabinet_add_document, text=_('Add to cabinets'), view='cabinets:document_cabinet_add', ) link_document_multiple_cabinet_add = Link( @@ -61,21 +61,21 @@ link_custom_acl_list.condition = cabinet_is_root link_cabinet_child_add = Link( args='object.pk', icon_class=icon_cabinet_child_add, - permissions=(permission_cabinet_create,), text=_('Add new level'), + permission=permission_cabinet_create, text=_('Add new level'), view='cabinets:cabinet_child_add' ) link_cabinet_create = Link( - icon_class=icon_cabinet_create, permissions=(permission_cabinet_create,), + icon_class=icon_cabinet_create, permission=permission_cabinet_create, text=_('Create cabinet'), view='cabinets:cabinet_create' ) link_cabinet_delete = Link( args='object.pk', icon_class=icon_cabinet_delete, - permissions=(permission_cabinet_delete,), tags='dangerous', + permission=permission_cabinet_delete, tags='dangerous', text=_('Delete'), view='cabinets:cabinet_delete' ) link_cabinet_edit = Link( args='object.pk', icon_class=icon_cabinet_edit, - permissions=(permission_cabinet_edit,), text=_('Edit'), + permission=permission_cabinet_edit, text=_('Edit'), view='cabinets:cabinet_edit' ) link_cabinet_list = Link( @@ -87,6 +87,6 @@ link_cabinet_list = Link( ) link_cabinet_view = Link( args='object.pk', icon_class=icon_cabinet_view, - permissions=(permission_cabinet_view,), text=_('Details'), + permission=permission_cabinet_view, text=_('Details'), view='cabinets:cabinet_view' ) diff --git a/mayan/apps/checkouts/links.py b/mayan/apps/checkouts/links.py index 1ed63fc666..b6ef58de91 100644 --- a/mayan/apps/checkouts/links.py +++ b/mayan/apps/checkouts/links.py @@ -9,7 +9,7 @@ from .icons import ( ) from .permissions import ( permission_document_checkin, permission_document_checkin_override, - permission_document_checkout + permission_document_checkout, permission_document_checkout_detail_view ) @@ -36,19 +36,16 @@ link_checkout_list = Link( link_checkout_document = Link( args='object.pk', condition=is_not_checked_out, icon_class=icon_checkout_document, - permissions=(permission_document_checkout,), text=_('Check out document'), + permission=permission_document_checkout, text=_('Check out document'), view='checkouts:checkout_document', ) link_checkin_document = Link( args='object.pk', condition=is_checked_out, - icon_class=icon_checkin_document, permissions=( - permission_document_checkin, permission_document_checkin_override - ), text=_('Check in document'), view='checkouts:checkin_document', - + icon_class=icon_checkin_document, permission=permission_document_checkin, + text=_('Check in document'), view='checkouts:checkin_document', ) link_checkout_info = Link( - args='resolved_object.pk', icon_class=icon_checkout_info, permissions=( - permission_document_checkin, permission_document_checkin_override, - permission_document_checkout - ), text=_('Check in/out'), view='checkouts:checkout_info', + args='resolved_object.pk', icon_class=icon_checkout_info, + permission=permission_document_checkout_detail_view, + text=_('Check in/out'), view='checkouts:checkout_info', ) diff --git a/mayan/apps/common/links.py b/mayan/apps/common/links.py index 0f58551f10..78fbe193c9 100644 --- a/mayan/apps/common/links.py +++ b/mayan/apps/common/links.py @@ -57,12 +57,12 @@ link_documentation = Link( link_object_error_list = Link( icon_class=icon_object_error_list, kwargs=get_kwargs_factory('resolved_object'), - permissions=(permission_error_log_view,), text=_('Errors'), + permission=permission_error_log_view, text=_('Errors'), view='common:object_error_list', ) link_object_error_list_clear = Link( kwargs=get_kwargs_factory('resolved_object'), - permissions=(permission_error_log_view,), text=_('Clear all'), + permission=permission_error_log_view, text=_('Clear all'), view='common:object_error_list_clear', ) link_forum = Link( diff --git a/mayan/apps/converter/links.py b/mayan/apps/converter/links.py index 85dc35778c..99ab3cef71 100644 --- a/mayan/apps/converter/links.py +++ b/mayan/apps/converter/links.py @@ -33,20 +33,20 @@ def get_kwargs_factory(variable_name): link_transformation_create = Link( icon_class=icon_transformation_create, kwargs=get_kwargs_factory('content_object'), - permissions=(permission_transformation_create,), + permission=permission_transformation_create, text=_('Create new transformation'), view='converter:transformation_create' ) link_transformation_delete = Link( - args='resolved_object.pk', permissions=(permission_transformation_delete,), + args='resolved_object.pk', permission=permission_transformation_delete, tags='dangerous', text=_('Delete'), view='converter:transformation_delete' ) link_transformation_edit = Link( - args='resolved_object.pk', permissions=(permission_transformation_edit,), + args='resolved_object.pk', permission=permission_transformation_edit, text=_('Edit'), view='converter:transformation_edit' ) link_transformation_list = Link( icon_class=icon_transformation, kwargs=get_kwargs_factory('resolved_object'), - permissions=(permission_transformation_view,), text=_('Transformations'), + permission=permission_transformation_view, text=_('Transformations'), view='converter:transformation_list' ) diff --git a/mayan/apps/django_gpg/links.py b/mayan/apps/django_gpg/links.py index 08264795d3..1a9d2811a1 100644 --- a/mayan/apps/django_gpg/links.py +++ b/mayan/apps/django_gpg/links.py @@ -12,41 +12,40 @@ from .permissions import ( link_key_delete = Link( kwargs={'key_id': 'resolved_object.pk'}, - permissions=(permission_key_delete,), tags='dangerous', text=_('Delete'), + permission=permission_key_delete, tags='dangerous', text=_('Delete'), view='django_gpg:key_delete' ) link_key_detail = Link( - kwargs={'key_id': 'resolved_object.pk'}, permissions=(permission_key_view,), + kwargs={'key_id': 'resolved_object.pk'}, permission=permission_key_view, text=_('Details'), view='django_gpg:key_detail' ) link_key_download = Link( kwargs={'key_id': 'resolved_object.pk'}, - permissions=(permission_key_download,), text=_('Download'), + permission=permission_key_download, text=_('Download'), view='django_gpg:key_download' ) link_key_query = Link( - icon_class=icon_keyserver_search, - permissions=(permission_keyserver_query,), text=_('Query keyservers'), - view='django_gpg:key_query' + icon_class=icon_keyserver_search, permission=permission_keyserver_query, + text=_('Query keyservers'), view='django_gpg:key_query' ) link_key_receive = Link( keep_query=True, kwargs={'key_id': 'object.key_id'}, - permissions=(permission_key_receive,), text=_('Import'), + permission=permission_key_receive, text=_('Import'), view='django_gpg:key_receive', ) link_key_setup = Link( - icon_class=icon_key_setup, permissions=(permission_key_view,), + icon_class=icon_key_setup, permission=permission_key_view, text=_('Key management'), view='django_gpg:key_public_list' ) link_key_upload = Link( - icon_class=icon_key_upload, permissions=(permission_key_upload,), + icon_class=icon_key_upload, permission=permission_key_upload, text=_('Upload key'), view='django_gpg:key_upload' ) link_private_keys = Link( - permissions=(permission_key_view,), text=_('Private keys'), + permission=permission_key_view, text=_('Private keys'), view='django_gpg:key_private_list' ) link_public_keys = Link( - permissions=(permission_key_view,), text=_('Public keys'), + permission=permission_key_view, text=_('Public keys'), view='django_gpg:key_public_list' ) diff --git a/mayan/apps/document_comments/links.py b/mayan/apps/document_comments/links.py index 58238f88e1..e344d55f6a 100644 --- a/mayan/apps/document_comments/links.py +++ b/mayan/apps/document_comments/links.py @@ -14,17 +14,17 @@ from .permissions import ( link_comment_add = Link( icon_class=icon_comment_add, kwargs={'document_id': 'object.pk'}, - permissions=(permission_comment_create,), text=_('Add comment'), + permission=permission_comment_create, text=_('Add comment'), view='comments:comment_add', ) link_comment_delete = Link( icon_class=icon_comment_delete, kwargs={'comment_id': 'object.pk'}, - permissions=(permission_comment_delete,), tags='dangerous', + permission=permission_comment_delete, tags='dangerous', text=_('Delete'), view='comments:comment_delete', ) link_comments_for_document = Link( icon_class=icon_comments_for_document, kwargs={'document_id': 'resolved_object.pk'}, - permissions=(permission_comment_view,), text=_('Comments'), + permission=permission_comment_view, text=_('Comments'), view='comments:comments_for_document', ) diff --git a/mayan/apps/document_indexing/links.py b/mayan/apps/document_indexing/links.py index 9427423150..cde0f1ab43 100644 --- a/mayan/apps/document_indexing/links.py +++ b/mayan/apps/document_indexing/links.py @@ -45,27 +45,27 @@ link_index_setup_list = Link( ) link_index_setup_create = Link( icon_class=icon_index_create, - permissions=(permission_document_indexing_create,), text=_('Create index'), + permission=permission_document_indexing_create, text=_('Create index'), view='indexing:index_setup_create' ) link_index_setup_edit = Link( args='resolved_object.pk', - permissions=(permission_document_indexing_edit,), text=_('Edit'), + permission=permission_document_indexing_edit, text=_('Edit'), view='indexing:index_setup_edit', ) link_index_setup_delete = Link( args='resolved_object.pk', - permissions=(permission_document_indexing_delete,), tags='dangerous', + permission=permission_document_indexing_delete, tags='dangerous', text=_('Delete'), view='indexing:index_setup_delete', ) link_index_setup_view = Link( args='resolved_object.pk', icon_class=icon_index_setup_view, - permissions=(permission_document_indexing_edit,), text=_('Tree template'), + permission=permission_document_indexing_edit, text=_('Tree template'), view='indexing:index_setup_view', ) link_index_setup_document_types = Link( args='resolved_object.pk', icon_class=icon_document_type, - permissions=(permission_document_indexing_edit,), text=_('Document types'), + permission=permission_document_indexing_edit, text=_('Document types'), view='indexing:index_setup_document_types', ) link_rebuild_index_instances = Link( diff --git a/mayan/apps/document_parsing/links.py b/mayan/apps/document_parsing/links.py index ac2cf20d7b..7d2100d2f1 100644 --- a/mayan/apps/document_parsing/links.py +++ b/mayan/apps/document_parsing/links.py @@ -18,25 +18,25 @@ from .permissions import ( link_document_content = Link( icon_class=icon_document_content, kwargs={'document_id': 'resolved_object.id'}, - permissions=(permission_content_view,), text=_('Content'), + permission=permission_content_view, text=_('Content'), view='document_parsing:document_content', ) link_document_page_content = Link( icon_class=icon_document_content, kwargs={'document_page_id': 'resolved_object.id'}, - permissions=(permission_content_view,), text=_('Content'), + permission=permission_content_view, text=_('Content'), view='document_parsing:document_page_content', ) link_document_parsing_errors_list = Link( icon_class=icon_document_parsing_errors_list, kwargs={'document_id': 'resolved_object.id'}, - permissions=(permission_content_view,), text=_('Parsing errors'), + permission=permission_content_view, text=_('Parsing errors'), view='document_parsing:document_parsing_error_list' ) link_document_content_download = Link( icon_class=icon_document_content_download, kwargs={'document_id': 'resolved_object.id'}, - permissions=(permission_content_view,), text=_('Download content'), + permission=permission_content_view, text=_('Download content'), view='document_parsing:document_content_download' ) link_document_multiple_submit = Link( @@ -46,13 +46,13 @@ link_document_multiple_submit = Link( link_document_submit = Link( icon_class=icon_document_submit, kwargs={'document_id': 'resolved_object.id'}, - permissions=(permission_parse_document,), + permission=permission_parse_document, text=_('Submit for parsing'), view='document_parsing:document_submit' ) link_document_type_parsing_settings = Link( icon_class=icon_document_type_parsing_settings, kwargs={'document_type_id': 'resolved_object.id'}, - permissions=(permission_document_type_parsing_setup,), + permission=permission_document_type_parsing_setup, text=_('Setup parsing'), view='document_parsing:document_type_parsing_settings', ) @@ -65,6 +65,6 @@ link_document_type_submit = Link( view='document_parsing:document_type_submit' ) link_error_list = Link( - icon_class=icon_link_error_list, permissions=(permission_content_view,), + icon_class=icon_link_error_list, permission=permission_content_view, text=_('Parsing errors'), view='document_parsing:error_list' ) diff --git a/mayan/apps/document_signatures/links.py b/mayan/apps/document_signatures/links.py index 60a2fcffd9..7ffdaaa12c 100644 --- a/mayan/apps/document_signatures/links.py +++ b/mayan/apps/document_signatures/links.py @@ -1,6 +1,5 @@ from __future__ import unicode_literals -from django.apps import apps from django.utils.translation import ugettext_lazy as _ from mayan.apps.navigation import Link @@ -9,8 +8,9 @@ from .icons import ( icon_all_document_version_signature_verify, icon_document_signature_list, link_document_version_signature_detached_create, icon_document_version_signature_embedded_create, - icon_document_version_signature_list + icon_document_version_signature_list, icon_document_version_signature_upload ) +from .literals import SIGNATURE_TYPE_DETACHED from .permissions import ( permission_document_version_sign_detached, permission_document_version_sign_embedded, @@ -23,69 +23,63 @@ from .permissions import ( def is_detached_signature(context): - SignatureBaseModel = apps.get_model( - app_label='document_signatures', model_name='SignatureBaseModel' - ) - - return SignatureBaseModel.objects.select_subclasses().get( - pk=context['object'].pk - ).is_detached + return context['object'].signature_type == SIGNATURE_TYPE_DETACHED link_all_document_version_signature_verify = Link( icon_class=icon_all_document_version_signature_verify, - permissions=(permission_document_version_signature_verify,), + permission=permission_document_version_signature_verify, text=_('Verify all documents'), - view='signatures:all_document_version_signature_verify', + view='signatures:all_document_version_signature_verify' ) link_document_signature_list = Link( - args='resolved_object.latest_version.pk', icon_class=icon_document_signature_list, - permissions=(permission_document_version_signature_view,), - text=_('Signatures'), view='signatures:document_version_signature_list', + kwargs={'document_version_id': 'resolved_object.latest_version.pk'}, + permission=permission_document_version_signature_view, + text=_('Signatures'), view='signatures:document_version_signature_list' ) link_document_version_signature_delete = Link( - args='resolved_object.pk', condition=is_detached_signature, - permissions=(permission_document_version_signature_delete,), - permissions_related='document_version.document', tags='dangerous', - text=_('Delete'), view='signatures:document_version_signature_delete', + condition=is_detached_signature, + kwargs={'signature_id': 'resolved_object.pk'}, + permission=permission_document_version_signature_delete, + tags='dangerous', text=_('Delete'), + view='signatures:document_version_signature_delete' ) link_document_version_signature_details = Link( - args='resolved_object.pk', - permissions=(permission_document_version_signature_view,), - permissions_related='document_version.document', text=_('Details'), - view='signatures:document_version_signature_details', + kwargs={'signature_id': 'resolved_object.pk'}, + permission=permission_document_version_signature_view, + text=_('Details'), view='signatures:document_version_signature_details' ) link_document_version_signature_list = Link( - args='resolved_object.pk', icon_class=icon_document_version_signature_list, - permissions=(permission_document_version_signature_view,), - permissions_related='document', text=_('Signatures'), - view='signatures:document_version_signature_list', + kwargs={'document_version_id': 'resolved_object.pk'}, + permission=permission_document_version_signature_view, + text=_('Signatures'), view='signatures:document_version_signature_list' ) link_document_version_signature_download = Link( - args='resolved_object.pk', condition=is_detached_signature, - permissions=(permission_document_version_signature_download,), - permissions_related='document_version.document', text=_('Download'), - view='signatures:document_version_signature_download', + condition=is_detached_signature, + kwargs={'signature_id': 'resolved_object.pk'}, + permission=permission_document_version_signature_download, + text=_('Download'), view='signatures:document_version_signature_download' ) link_document_version_signature_upload = Link( - args='resolved_object.pk', - permissions=(permission_document_version_signature_upload,), - permissions_related='document', text=_('Upload signature'), - view='signatures:document_version_signature_upload', + icon_class=icon_document_version_signature_upload, + kwargs={'document_version_id': 'resolved_object.pk'}, + permission=permission_document_version_signature_upload, + text=_('Upload signature'), + view='signatures:document_version_signature_upload' ) link_document_version_signature_detached_create = Link( - args='resolved_object.pk', icon_class=link_document_version_signature_detached_create, - permissions=(permission_document_version_sign_detached,), - permissions_related='document', text=_('Sign detached'), - view='signatures:document_version_signature_detached_create', + kwargs={'document_version_id': 'resolved_object.pk'}, + permission=permission_document_version_sign_detached, + text=_('Sign detached'), + view='signatures:document_version_signature_detached_create' ) link_document_version_signature_embedded_create = Link( - args='resolved_object.pk', icon_class=icon_document_version_signature_embedded_create, - permissions=(permission_document_version_sign_embedded,), - permissions_related='document', text=_('Sign embedded'), - view='signatures:document_version_signature_embedded_create', + kwargs={'document_version_id': 'resolved_object.pk'}, + permission=permission_document_version_sign_embedded, + text=_('Sign embedded'), + view='signatures:document_version_signature_embedded_create' ) diff --git a/mayan/apps/document_states/links.py b/mayan/apps/document_states/links.py index c80fa8c2e4..e51ccc6d5f 100644 --- a/mayan/apps/document_states/links.py +++ b/mayan/apps/document_states/links.py @@ -25,45 +25,42 @@ from .permissions import ( link_document_workflow_instance_list = Link( icon_class=icon_document_workflow_instance_list, kwargs={'document_id': 'resolved_object.pk'}, - permissions=(permission_workflow_view,), text=_('Workflows'), + permission=permission_workflow_view, text=_('Workflows'), view='workflows:document_workflow_instance_list' ) link_tool_launch_all_workflows = Link( icon_class=icon_tool_launch_all_workflows, - permissions=(permission_workflow_tools,), - text=_('Launch all workflows'), + permission=permission_workflow_tools, text=_('Launch all workflows'), view='workflows:tool_launch_all_workflows' ) link_workflow_create = Link( - icon_class=icon_workflow_create, permissions=(permission_workflow_create,), + icon_class=icon_workflow_create, permission=permission_workflow_create, text=_('Create workflow'), view='workflows:workflow_create' ) link_workflow_delete = Link( icon_class=icon_workflow_delete, kwargs={'workflow_id': 'resolved_object.pk'}, - permissions=(permission_workflow_delete,), tags='dangerous', - text=_('Delete'), view='workflows:workflow_delete' + permission=permission_workflow_delete, tags='dangerous', text=_('Delete'), + view='workflows:workflow_delete' ) link_workflow_document_types = Link( icon_class=icon_document_type, kwargs={'workflow_id': 'resolved_object.pk'}, - permissions=(permission_workflow_edit,), text=_('Document types'), + permission=permission_workflow_edit, text=_('Document types'), view='workflows:workflow_document_types' ) link_workflow_edit = Link( - icon_class=icon_workflow_edit, - kwargs={'workflow_id': 'resolved_object.pk'}, - permissions=(permission_workflow_edit,), text=_('Edit'), + icon_class=icon_workflow_edit, kwargs={'workflow_id': 'resolved_object.pk'}, + permission=permission_workflow_edit, text=_('Edit'), view='workflows:workflow_edit' ) link_workflow_list = Link( - icon_class=icon_workflow_list, - permissions=(permission_workflow_view,), text=_('Workflows'), - view='workflows:workflow_list' + icon_class=icon_workflow_list, permission=permission_workflow_view, + text=_('Workflows'), view='workflows:workflow_list' ) link_workflow_preview = Link( icon_class=icon_workflow_preview, kwargs={'workflow_id': 'resolved_object.pk'}, - permissions=(permission_workflow_view,), text=_('Preview'), + permission=permission_workflow_view, text=_('Preview'), view='workflows:workflow_preview' ) @@ -71,7 +68,7 @@ link_workflow_preview = Link( link_workflow_instance_detail = Link( kwargs={'workflow_instance_id': 'resolved_object.pk'}, - permissions=(permission_workflow_view,), text=_('Detail'), + permission=permission_workflow_view, text=_('Detail'), view='workflows:workflow_instance_detail' ) link_workflow_instance_transition = Link( @@ -84,25 +81,25 @@ link_workflow_instance_transition = Link( link_workflow_state_action_delete = Link( icon_class=icon_workflow_state_action_delete, kwargs={'workflow_state_action_id': 'resolved_object.pk'}, - permissions=(permission_workflow_edit,), tags='dangerous', + permission=permission_workflow_edit, tags='dangerous', text=_('Delete'), view='workflows:workflow_state_action_delete' ) link_workflow_state_action_edit = Link( icon_class=icon_workflow_state_action_edit, kwargs={'workflow_state_action_id': 'resolved_object.pk'}, - permissions=(permission_workflow_edit,), text=_('Edit'), + permission=permission_workflow_edit, text=_('Edit'), view='workflows:workflow_state_action_edit' ) link_workflow_state_action_list = Link( icon_class=icon_workflow_state_action_list, kwargs={'workflow_state_id': 'resolved_object.pk'}, - permissions=(permission_workflow_edit,), text=_('Actions'), + permission=permission_workflow_edit, text=_('Actions'), view='workflows:workflow_state_action_list' ) link_workflow_state_action_selection = Link( icon_class=icon_workflow_state_action_selection, kwargs={'workflow_state_id': 'resolved_object.pk'}, - permissions=(permission_workflow_edit,), text=_('Create action'), + permission=permission_workflow_edit, text=_('Create action'), view='workflows:workflow_state_action_selection' ) @@ -111,25 +108,25 @@ link_workflow_state_action_selection = Link( link_workflow_state_create = Link( icon_class=icon_workflow_state_create, kwargs={'workflow_id': 'resolved_object.pk'}, - permissions=(permission_workflow_edit,), text=_('Create state'), + permission=permission_workflow_edit, text=_('Create state'), view='workflows:workflow_state_create' ) link_workflow_state_delete = Link( icon_class=icon_workflow_state_delete, kwargs={'workflow_state_id': 'object.pk'}, - permissions=(permission_workflow_edit,), tags='dangerous', text=_('Delete'), + permission=permission_workflow_edit, tags='dangerous', text=_('Delete'), view='workflows:workflow_state_delete' ) link_workflow_state_edit = Link( icon_class=icon_workflow_state_edit, kwargs={'workflow_state_id': 'resolved_object.pk'}, - permissions=(permission_workflow_edit,), text=_('Edit'), + permission=permission_workflow_edit, text=_('Edit'), view='workflows:workflow_state_edit' ) link_workflow_state_list = Link( icon_class=icon_workflow_state, kwargs={'workflow_id': 'resolved_object.pk'}, - permissions=(permission_workflow_view,), text=_('States'), + permission=permission_workflow_view, text=_('States'), view='workflows:workflow_state_list' ) @@ -138,30 +135,30 @@ link_workflow_state_list = Link( link_workflow_transition_create = Link( icon_class=icon_workflow_transition_create, kwargs={'workflow_id': 'resolved_object.pk'}, - permissions=(permission_workflow_edit,), text=_('Create transition'), + permission=permission_workflow_edit, text=_('Create transition'), view='workflows:workflow_transition_create' ) link_workflow_transition_delete = Link( icon_class=icon_workflow_transition_delete, kwargs={'workflow_transition_id': 'resolved_object.pk'}, - permissions=(permission_workflow_edit,), tags='dangerous', text=_('Delete'), + permission=permission_workflow_edit, tags='dangerous', text=_('Delete'), view='workflows:workflow_transition_delete' ) link_workflow_transition_edit = Link( icon_class=icon_workflow_transition_edit, kwargs={'workflow_transition_id': 'resolved_object.pk'}, - permissions=(permission_workflow_edit,), text=_('Edit'), + permission=permission_workflow_edit, text=_('Edit'), view='workflows:workflow_transition_edit' ) link_workflow_transition_list = Link( icon_class=icon_workflow_transition, kwargs={'workflow_transition_id': 'resolved_object.pk'}, - permissions=(permission_workflow_view,), text=_('Transitions'), + permission=permission_workflow_view, text=_('Transitions'), view='workflows:workflow_transition_list' ) link_workflow_transition_triggers = Link( kwargs={'workflow_transition_id': 'resolved_object.pk'}, - permissions=(permission_workflow_edit,), text=_('Transition triggers'), + permission=permission_workflow_edit, text=_('Transition triggers'), view='workflows:workflow_transition_triggers' ) @@ -169,20 +166,20 @@ link_workflow_transition_triggers = Link( link_workflow_runtime_proxy_document_list = Link( kwargs={'workflow_runtime_proxy_id': 'resolved_object.pk'}, - permissions=(permission_workflow_view,), text=_('Workflow documents'), + permission=permission_workflow_view, text=_('Workflow documents'), view='workflows:workflow_runtime_proxy_document_list' ) link_workflow_runtime_proxy_list = Link( - icon_class=icon_workflow_list, permissions=(permission_workflow_view,), + icon_class=icon_workflow_list, permission=permission_workflow_view, text=_('Workflows'), view='workflows:workflow_runtime_proxy_list' ) link_workflow_runtime_proxy_state_document_list = Link( kwargs={'workflow_runtime_proxy_state_id': 'resolved_object.pk'}, - permissions=(permission_workflow_view,), text=_('State documents'), + permission=permission_workflow_view, text=_('State documents'), view='workflows:workflow_runtime_proxy_state_document_list' ) link_workflow_runtime_proxy_state_list = Link( kwargs={'workflow_runtime_proxy_id': 'resolved_object.pk'}, - permissions=(permission_workflow_view,), text=_('States'), + permission=permission_workflow_view, text=_('States'), view='workflows:workflow_runtime_proxy_state_list' ) diff --git a/mayan/apps/documents/links.py b/mayan/apps/documents/links.py index e8388d5212..8478b5b6c5 100644 --- a/mayan/apps/documents/links.py +++ b/mayan/apps/documents/links.py @@ -14,7 +14,7 @@ from .icons import ( icon_document_delete, icon_document_download, icon_document_edit, icon_document_favorites_add, icon_document_favorites_remove, icon_document_multiple_delete, - icon_document_multiepl_restore, icon_document_page_navigation_first, + icon_document_multiple_restore, icon_document_page_navigation_first, icon_document_page_navigation_last, icon_document_page_navigation_next, icon_document_page_navigation_previous, icon_document_page_return, icon_document_page_rotate_left, @@ -75,93 +75,92 @@ def is_min_zoom(context): # Facet link_document_preview = Link( args='resolved_object.id', icon_class=icon_document_preview, - permissions=(permission_document_view,), - text=_('Preview'), view='documents:document_preview', + permission=permission_document_view, text=_('Preview'), + view='documents:document_preview' ) link_document_properties = Link( args='resolved_object.id', icon_class=icon_document_properties, - permissions=(permission_document_view,), - text=_('Properties'), view='documents:document_properties', + permission=permission_document_view, text=_('Properties'), + view='documents:document_properties' ) link_document_version_list = Link( args='resolved_object.pk', icon_class=icon_document_version_list, - permissions=(permission_document_version_view,), - text=_('Versions'), view='documents:document_version_list', + permission=permission_document_version_view, text=_('Versions'), + view='documents:document_version_list' ) link_document_pages = Link( args='resolved_object.pk', icon_class=icon_document_pages, - permissions=(permission_document_view,), text=_('Pages'), - view='documents:document_pages', + permission=permission_document_view, text=_('Pages'), + view='documents:document_pages' ) # Actions link_document_clear_transformations = Link( - args='resolved_object.id', - permissions=(permission_transformation_delete,), + args='resolved_object.id', permission=permission_transformation_delete, text=_('Clear transformations'), - view='documents:document_clear_transformations', + view='documents:document_clear_transformations' ) link_document_clone_transformations = Link( - args='resolved_object.id', permissions=(permission_transformation_edit,), + args='resolved_object.id', permission=permission_transformation_edit, text=_('Clone transformations'), - view='documents:document_clone_transformations', + view='documents:document_clone_transformations' ) link_document_delete = Link( args='resolved_object.id', icon_class=icon_document_delete, - permissions=(permission_document_delete,), tags='dangerous', - text=_('Delete'), view='documents:document_delete', + permission=permission_document_delete, tags='dangerous', + text=_('Delete'), view='documents:document_delete' ) link_document_favorites_add = Link( args='resolved_object.id', icon_class=icon_document_favorites_add, - permissions=(permission_document_view,), text=_('Add to favorites'), - view='documents:document_add_to_favorites', + permission=permission_document_view, text=_('Add to favorites'), + view='documents:document_add_to_favorites' ) link_document_favorites_remove = Link( args='resolved_object.id', icon_class=icon_document_favorites_remove, - permissions=(permission_document_view,), text=_('Remove from favorites'), - view='documents:document_remove_from_favorites', + permission=permission_document_view, text=_('Remove from favorites'), + view='documents:document_remove_from_favorites' ) link_document_trash = Link( args='resolved_object.id', icon_class=icon_document_trash, - permissions=(permission_document_trash,), tags='dangerous', - text=_('Move to trash'), view='documents:document_trash', + permission=permission_document_trash, tags='dangerous', + text=_('Move to trash'), view='documents:document_trash' ) link_document_edit = Link( args='resolved_object.id', icon_class=icon_document_edit, - permissions=(permission_document_properties_edit,), - text=_('Edit properties'), view='documents:document_edit', + permission=permission_document_properties_edit, + text=_('Edit properties'), view='documents:document_edit' ) link_document_document_type_edit = Link( args='resolved_object.id', - permissions=(permission_document_properties_edit,), text=_('Change type'), - view='documents:document_document_type_edit', + permission=permission_document_properties_edit, text=_('Change type'), + view='documents:document_document_type_edit' ) link_document_download = Link( args='resolved_object.id', icon_class=icon_document_download, - permissions=(permission_document_download,), text=_('Advanced download'), - view='documents:document_download_form', + permission=permission_document_download, text=_('Advanced download'), + view='documents:document_download_form' ) link_document_print = Link( args='resolved_object.id', icon_class=icon_document_print, - permissions=(permission_document_print,), - text=_('Print'), view='documents:document_print', + permission=permission_document_print, text=_('Print'), + view='documents:document_print' ) link_document_quick_download = Link( - args='resolved_object.id', permissions=(permission_document_download,), - text=_('Quick download'), view='documents:document_download', + args='resolved_object.id', permission=permission_document_download, + text=_('Quick download'), view='documents:document_download' ) link_document_update_page_count = Link( - args='resolved_object.pk', permissions=(permission_document_tools,), + args='resolved_object.pk', permission=permission_document_tools, text=_('Recalculate page count'), view='documents:document_update_page_count' ) link_document_restore = Link( - icon_class=icon_document_restore, - permissions=(permission_document_restore,), text=_('Restore'), - view='documents:document_restore', args='object.pk' + args='object.pk', icon_class=icon_document_restore, + permission=permission_document_restore, text=_('Restore'), + view='documents:document_restore' ) link_document_multiple_clear_transformations = Link( - permissions=(permission_transformation_delete,), + permission=permission_transformation_delete, text=_('Clear transformations'), view='documents:document_multiple_clear_transformations' ) @@ -175,49 +174,50 @@ link_document_multiple_delete = Link( ) link_document_multiple_favorites_add = Link( text=_('Add to favorites'), - view='documents:document_multiple_add_to_favorites', + view='documents:document_multiple_add_to_favorites' ) link_document_multiple_favorites_remove = Link( text=_('Remove from favorites'), - view='documents:document_multiple_remove_from_favorites', + view='documents:document_multiple_remove_from_favorites' ) link_document_multiple_document_type_edit = Link( text=_('Change type'), view='documents:document_multiple_document_type_edit' ) link_document_multiple_download = Link( - text=_('Advanced download'), view='documents:document_multiple_download_form' + text=_('Advanced download'), + view='documents:document_multiple_download_form' ) link_document_multiple_update_page_count = Link( text=_('Recalculate page count'), view='documents:document_multiple_update_page_count' ) link_document_multiple_restore = Link( - icon_class=icon_document_multiepl_restore, text=_('Restore'), + icon_class=icon_document_multiple_restore, text=_('Restore'), view='documents:document_multiple_restore' ) # Versions link_document_version_download = Link( args='resolved_object.pk', icon_class=icon_document_version_download, - permissions=(permission_document_download,), - text=_('Download version'), view='documents:document_version_download_form' + permission=permission_document_download, text=_('Download version'), + view='documents:document_version_download_form' ) link_document_version_return_document = Link( args='resolved_object.document.pk', icon_class=icon_document_version_return_document, - permissions=(permission_document_view,), text=_('Document'), - view='documents:document_preview', + permission=permission_document_view, text=_('Document'), + view='documents:document_preview' ) link_document_version_return_list = Link( args='resolved_object.document.pk', icon_class=icon_document_version_return_list, - permissions=(permission_document_version_view,), text=_('Versions'), - view='documents:document_version_list', + permission=permission_document_version_view, text=_('Versions'), + view='documents:document_version_list' ) link_document_version_view = Link( args='resolved_object.pk', icon_class=icon_document_version_view, - permissions=(permission_document_version_view,), text=_('Preview'), + permission=permission_document_version_view, text=_('Preview'), view='documents:document_version_view' ) @@ -249,12 +249,12 @@ link_clear_image_cache = Link( description=_( 'Clear the graphics representations used to speed up the documents\' ' 'display and interactive transformations results.' - ), permissions=(permission_document_tools,), + ), permission=permission_document_tools, text=_('Clear document image cache'), view='documents:document_clear_image_cache' ) link_trash_can_empty = Link( - icon_class=icon_trash_can_empty, permissions=(permission_empty_trash,), + icon_class=icon_trash_can_empty, permission=permission_empty_trash, text=_('Empty trash'), view='documents:trash_can_empty' ) @@ -262,119 +262,119 @@ link_trash_can_empty = Link( link_document_page_navigation_first = Link( args='resolved_object.pk', conditional_disable=is_first_page, icon_class=icon_document_page_navigation_first, - keep_query=True, permissions=(permission_document_view,), - text=_('First page'), view='documents:document_page_navigation_first', + keep_query=True, permission=permission_document_view, + text=_('First page'), view='documents:document_page_navigation_first' ) link_document_page_navigation_last = Link( args='resolved_object.pk', conditional_disable=is_last_page, icon_class=icon_document_page_navigation_last, keep_query=True, text=_('Last page'), - permissions=(permission_document_view,), - view='documents:document_page_navigation_last', + permission=permission_document_view, + view='documents:document_page_navigation_last' ) link_document_page_navigation_previous = Link( args='resolved_object.pk', conditional_disable=is_first_page, icon_class=icon_document_page_navigation_previous, - keep_query=True, permissions=(permission_document_view,), + keep_query=True, permission=permission_document_view, text=_('Previous page'), - view='documents:document_page_navigation_previous', + view='documents:document_page_navigation_previous' ) link_document_page_navigation_next = Link( args='resolved_object.pk', conditional_disable=is_last_page, icon_class=icon_document_page_navigation_next, keep_query=True, text=_('Next page'), - permissions=(permission_document_view,), - view='documents:document_page_navigation_next', + permission=permission_document_view, + view='documents:document_page_navigation_next' ) link_document_page_return = Link( args='resolved_object.document.pk', icon_class=icon_document_page_return, - permissions=(permission_document_view,), text=_('Document'), - view='documents:document_preview', + permission=permission_document_view, text=_('Document'), + view='documents:document_preview' ) link_document_page_rotate_left = Link( args='resolved_object.pk', icon_class=icon_document_page_rotate_left, - keep_query=True, permissions=(permission_document_view,), - text=_('Rotate left'), view='documents:document_page_rotate_left', + keep_query=True, permission=permission_document_view, + text=_('Rotate left'), view='documents:document_page_rotate_left' ) link_document_page_rotate_right = Link( args='resolved_object.pk', icon_class=icon_document_page_rotate_right, - keep_query=True, permissions=(permission_document_view,), - text=_('Rotate right'), view='documents:document_page_rotate_right', + keep_query=True, permission=permission_document_view, + text=_('Rotate right'), view='documents:document_page_rotate_right' ) link_document_page_view = Link( icon_class=icon_document_page_view, - permissions=(permission_document_view,), text=_('Page image'), + permission=permission_document_view, text=_('Page image'), view='documents:document_page_view', args='resolved_object.pk' ) link_document_page_view_reset = Link( icon_class=icon_document_page_view_reset, - permissions=(permission_document_view,), text=_('Reset view'), + permission=permission_document_view, text=_('Reset view'), view='documents:document_page_view_reset', args='resolved_object.pk' ) link_document_page_zoom_in = Link( args='resolved_object.pk', conditional_disable=is_max_zoom, icon_class=icon_document_page_zoom_in, keep_query=True, - permissions=(permission_document_view,), text=_('Zoom in'), - view='documents:document_page_zoom_in', + permission=permission_document_view, text=_('Zoom in'), + view='documents:document_page_zoom_in' ) link_document_page_zoom_out = Link( args='resolved_object.pk', conditional_disable=is_min_zoom, icon_class=icon_document_page_zoom_out, keep_query=True, - permissions=(permission_document_view,), text=_('Zoom out'), - view='documents:document_page_zoom_out', + permission=permission_document_view, text=_('Zoom out'), + view='documents:document_page_zoom_out' ) # Document versions link_document_version_revert = Link( args='object.pk', condition=is_not_current_version, - permissions=(permission_document_version_revert,), tags='dangerous', - text=_('Revert'), view='documents:document_version_revert', + permission=permission_document_version_revert, tags='dangerous', + text=_('Revert'), view='documents:document_version_revert' ) # Document type related links link_document_type_create = Link( icon_class=icon_document_type_create, - permissions=(permission_document_type_create,), + permission=permission_document_type_create, text=_('Create document type'), view='documents:document_type_create' ) link_document_type_delete = Link( args='resolved_object.id', icon_class=icon_document_type_delete, - permissions=(permission_document_type_delete,), tags='dangerous', - text=_('Delete'), view='documents:document_type_delete', + permission=permission_document_type_delete, tags='dangerous', + text=_('Delete'), view='documents:document_type_delete' ) link_document_type_edit = Link( args='resolved_object.id', icon_class=icon_document_type_edit, - permissions=(permission_document_type_edit,), text=_('Edit'), - view='documents:document_type_edit', + permission=permission_document_type_edit, text=_('Edit'), + view='documents:document_type_edit' ) link_document_type_filename_create = Link( args='document_type.id', icon_class=icon_document_type_filename_create, - permissions=(permission_document_type_edit,), + permission=permission_document_type_edit, text=_('Add quick label to document type'), - view='documents:document_type_filename_create', + view='documents:document_type_filename_create' ) link_document_type_filename_delete = Link( - args='resolved_object.id', permissions=(permission_document_type_edit,), + args='resolved_object.id', permission=permission_document_type_edit, tags='dangerous', text=_('Delete'), - view='documents:document_type_filename_delete', + view='documents:document_type_filename_delete' ) link_document_type_filename_edit = Link( - args='resolved_object.id', permissions=(permission_document_type_edit,), - text=_('Edit'), view='documents:document_type_filename_edit', + args='resolved_object.id', permission=permission_document_type_edit, + text=_('Edit'), view='documents:document_type_filename_edit' ) link_document_type_filename_list = Link( args='resolved_object.id', icon_class=icon_document_type_filename, - permissions=(permission_document_type_view,), text=_('Quick labels'), - view='documents:document_type_filename_list', + permission=permission_document_type_view, text=_('Quick labels'), + view='documents:document_type_filename_list' ) link_document_type_list = Link( icon_class=icon_document_type_list, - permissions=(permission_document_type_view,), text=_('Document types'), + permission=permission_document_type_view, text=_('Document types'), view='documents:document_type_list' ) link_document_type_setup = Link( icon_class=icon_document_type_setup, - permissions=(permission_document_type_view,), text=_('Document types'), + permission=permission_document_type_view, text=_('Document types'), view='documents:document_type_list' ) link_duplicated_document_list = Link( @@ -383,12 +383,11 @@ link_duplicated_document_list = Link( ) link_document_duplicates_list = Link( args='resolved_object.id', icon_class=icon_document_duplicates_list, - permissions=(permission_document_view,), text=_('Duplicates'), - view='documents:document_duplicates_list', + permission=permission_document_view, text=_('Duplicates'), + view='documents:document_duplicates_list' ) link_duplicated_document_scan = Link( icon_class=icon_duplicated_document_scan, - permissions=(permission_document_tools,), - text=_('Duplicated document scan'), + permission=permission_document_tools, text=_('Duplicated document scan'), view='documents:duplicated_document_scan' ) diff --git a/mayan/apps/events/links.py b/mayan/apps/events/links.py index 0a2b8c6fd5..e020df2af8 100644 --- a/mayan/apps/events/links.py +++ b/mayan/apps/events/links.py @@ -41,11 +41,11 @@ link_events_details = Link( link_events_for_object = Link( icon_class=icon_events_for_object, kwargs=get_kwargs_factory('resolved_object'), - permissions=(permission_events_view,), text=_('Events'), + permission=permission_events_view, text=_('Events'), view='events:events_for_object', ) link_events_list = Link( - icon_class=icon_events_list, permissions=(permission_events_view,), + icon_class=icon_events_list, permission=permission_events_view, text=_('Events'), view='events:events_list' ) link_event_types_subscriptions_list = Link( @@ -63,7 +63,7 @@ link_notification_mark_read_all = Link( link_object_event_types_user_subcriptions_list = Link( icon_class=icon_object_event_types_user_subcriptions_list, kwargs=get_kwargs_factory('resolved_object'), - permissions=(permission_events_view,), text=_('Subscriptions'), + permission=permission_events_view, text=_('Subscriptions'), view='events:object_event_types_user_subcriptions_list', ) link_user_notifications_list = Link( diff --git a/mayan/apps/file_metadata/links.py b/mayan/apps/file_metadata/links.py index ef884470c7..99f827ef39 100644 --- a/mayan/apps/file_metadata/links.py +++ b/mayan/apps/file_metadata/links.py @@ -15,19 +15,19 @@ from .permissions import ( link_document_driver_list = Link( icon_class=icon_file_metadata, kwargs={'document_id': 'resolved_object.id'}, - permissions=(permission_file_metadata_view,), text=_('File metadata'), - view='file_metadata:document_driver_list', + permission=permission_file_metadata_view, text=_('File metadata'), + view='file_metadata:document_driver_list' ) link_document_file_metadata_list = Link( icon_class=icon_file_metadata, kwargs={'document_version_driver_id': 'resolved_object.id'}, - permissions=(permission_file_metadata_view,), text=_('Attributes'), - view='file_metadata:document_version_driver_file_metadata_list', + permission=permission_file_metadata_view, text=_('Attributes'), + view='file_metadata:document_version_driver_file_metadata_list' ) link_document_submit = Link( icon_class=icon_document_submit, kwargs={'document_id': 'resolved_object.id'}, - permissions=(permission_file_metadata_submit,), + permission=permission_file_metadata_submit, text=_('Submit for file metadata'), view='file_metadata:document_submit' ) link_document_multiple_submit = Link( @@ -37,13 +37,11 @@ link_document_multiple_submit = Link( link_document_type_file_metadata_settings = Link( icon_class=icon_file_metadata, kwargs={'document_type_id': 'resolved_object.id'}, - permissions=(permission_document_type_file_metadata_setup,), - text=_('Setup file metadata'), - view='file_metadata:document_type_settings', + permission=permission_document_type_file_metadata_setup, + text=_('Setup file metadata'), view='file_metadata:document_type_settings' ) link_document_type_submit = Link( - icon_class=icon_file_metadata, - permissions=(permission_file_metadata_submit,), + icon_class=icon_file_metadata, permission=permission_file_metadata_submit, text=_('File metadata processing per type'), view='file_metadata:document_type_submit' ) diff --git a/mayan/apps/linking/links.py b/mayan/apps/linking/links.py index 911a6c47df..c81392917c 100644 --- a/mayan/apps/linking/links.py +++ b/mayan/apps/linking/links.py @@ -19,61 +19,59 @@ from .permissions import ( link_smart_link_condition_create = Link( icon_class=icon_smart_link_condition_create, kwargs={'smart_link_id': 'object.pk'}, - permissions=(permission_smart_link_edit,), text=_('Create condition'), + permission=permission_smart_link_edit, text=_('Create condition'), view='linking:smart_link_condition_create' ) link_smart_link_condition_delete = Link( kwargs={'smart_link_condition_id': 'resolved_object.pk'}, - permissions=(permission_smart_link_edit,), tags='dangerous', + permission=permission_smart_link_edit, tags='dangerous', text=_('Delete'), view='linking:smart_link_condition_delete' ) link_smart_link_condition_edit = Link( kwargs={'smart_link_condition_id': 'resolved_object.pk'}, - permissions=(permission_smart_link_edit,), text=_('Edit'), + permission=permission_smart_link_edit, text=_('Edit'), view='linking:smart_link_condition_edit' ) link_smart_link_condition_list = Link( icon_class=icon_smart_link_condition, kwargs={'smart_link_id': 'object.pk'}, - permissions=(permission_smart_link_edit,), text=_('Conditions'), + permission=permission_smart_link_edit, text=_('Conditions'), view='linking:smart_link_condition_list' ) link_smart_link_create = Link( - icon_class=icon_smart_link_create, - permissions=(permission_smart_link_create,), + icon_class=icon_smart_link_create, permission=permission_smart_link_create, text=_('Create new smart link'), view='linking:smart_link_create' ) link_smart_link_delete = Link( kwargs={'smart_link_id': 'object.pk'}, - permissions=(permission_smart_link_delete,), - tags='dangerous', text=_('Delete'), view='linking:smart_link_delete' + permission=permission_smart_link_delete, tags='dangerous', + text=_('Delete'), view='linking:smart_link_delete' ) link_smart_link_document_types = Link( icon_class=icon_document_type, kwargs={'document_type_id': 'object.pk'}, - permissions=(permission_smart_link_edit,), text=_('Document types'), - view='linking:smart_link_document_types', + permission=permission_smart_link_edit, text=_('Document types'), + view='linking:smart_link_document_types' ) link_smart_link_edit = Link( kwargs={'smart_link_id': 'object.pk'}, - permissions=(permission_smart_link_edit,), - text=_('Edit'), view='linking:smart_link_edit', + permission=permission_smart_link_edit, text=_('Edit'), + view='linking:smart_link_edit' ) link_smart_link_instance_view = Link( kwargs={'document_id': 'document.pk', 'smart_link_id': 'object.pk'}, - permissions=(permission_smart_link_view,), text=_('Documents'), + permission=permission_smart_link_view, text=_('Documents'), view='linking:resolved_smart_link_details' ) link_smart_link_instances_for_document = Link( icon_class=icon_smart_link_instances_for_document, kwargs={'document_id': 'resolved_object.pk'}, - permissions=(permission_document_view,), text=_('Smart links'), - view='linking:resolved_smart_links_for_document', + permission=permission_document_view, text=_('Smart links'), + view='linking:resolved_smart_links_for_document' ) link_smart_link_list = Link( - permissions=(permission_smart_link_view,), text=_('Smart links'), + permission=permission_smart_link_view, text=_('Smart links'), view='linking:smart_link_list' ) link_smart_link_setup = Link( - icon_class=icon_smart_link_setup, - permissions=(permission_smart_link_view,), text=_('Smart links'), - view='linking:smart_link_list' + icon_class=icon_smart_link_setup, permission=permission_smart_link_view, + text=_('Smart links'), view='linking:smart_link_list' ) diff --git a/mayan/apps/mailer/links.py b/mayan/apps/mailer/links.py index aa25965cfc..e9acd5831f 100644 --- a/mayan/apps/mailer/links.py +++ b/mayan/apps/mailer/links.py @@ -19,12 +19,12 @@ from .permissions import ( link_document_send = Link( args='resolved_object.pk', icon_class=icon_document_send, - permissions=(permission_mailing_send_document,), text=_('Email document'), + permission=permission_mailing_send_document, text=_('Email document'), view='mailer:document_send' ) link_document_send_link = Link( args='resolved_object.pk', icon_class=icon_document_send_link, - permissions=(permission_mailing_link,), text=_('Email link'), + permission=permission_mailing_link, text=_('Email link'), view='mailer:document_send_link' ) link_document_multiple_send = Link( @@ -37,40 +37,40 @@ link_document_multiple_send_link = Link( ) link_system_mailer_error_log = Link( icon_class=icon_system_mailer_error_log, - permissions=(permission_view_error_log,), - text=_('System mailer error log'), view='mailer:system_mailer_error_log', + permission=permission_view_error_log, + text=_('System mailer error log'), view='mailer:system_mailer_error_log' ) link_user_mailer_create = Link( icon_class=icon_user_mailer_create, - permissions=(permission_user_mailer_create,), - text=_('User mailer create'), view='mailer:user_mailer_backend_selection', + permission=permission_user_mailer_create, text=_('User mailer create'), + view='mailer:user_mailer_backend_selection' ) link_user_mailer_delete = Link( args='resolved_object.pk', icon_class=icon_user_mailer_delete, - permissions=(permission_user_mailer_delete,), tags='dangerous', - text=_('Delete'), view='mailer:user_mailer_delete', + permission=permission_user_mailer_delete, tags='dangerous', + text=_('Delete'), view='mailer:user_mailer_delete' ) link_user_mailer_edit = Link( args='object.pk', icon_class=icon_user_mailer_edit, - permissions=(permission_user_mailer_edit,), text=_('Edit'), - view='mailer:user_mailer_edit', + permission=permission_user_mailer_edit, text=_('Edit'), + view='mailer:user_mailer_edit' ) link_user_mailer_log_list = Link( - args='object.pk', permissions=(permission_user_mailer_view,), - text=_('Log'), view='mailer:user_mailer_log', + args='object.pk', permission=permission_user_mailer_view, + text=_('Log'), view='mailer:user_mailer_log' ) link_user_mailer_list = Link( icon_class=icon_user_mailer_list, - permissions=(permission_user_mailer_view,), - text=_('Mailing profiles list'), view='mailer:user_mailer_list', + permission=permission_user_mailer_view, + text=_('Mailing profiles list'), view='mailer:user_mailer_list' ) link_user_mailer_setup = Link( icon_class=icon_user_mailer_setup, - permissions=(permission_user_mailer_view,), - text=_('Mailing profiles'), view='mailer:user_mailer_list', + permission=permission_user_mailer_view, + text=_('Mailing profiles'), view='mailer:user_mailer_list' ) link_user_mailer_test = Link( args='object.pk', icon_class=icon_user_mailer_test, - permissions=(permission_user_mailer_use,), text=_('Test'), - view='mailer:user_mailer_test', + permission=permission_user_mailer_use, text=_('Test'), + view='mailer:user_mailer_test' ) diff --git a/mayan/apps/mayan_statistics/links.py b/mayan/apps/mayan_statistics/links.py index 76a7e70e56..c06983e786 100644 --- a/mayan/apps/mayan_statistics/links.py +++ b/mayan/apps/mayan_statistics/links.py @@ -9,22 +9,22 @@ from .permissions import permission_statistics_view # Translators: 'Queue' here is the verb, to queue a statistic to update link_execute = Link( - permissions=(permission_statistics_view,), text=_('Queue'), + permission=permission_statistics_view, text=_('Queue'), view='statistics:statistic_queue', args='resolved_object.slug' ) link_view = Link( - permissions=(permission_statistics_view,), text=_('View'), + permission=permission_statistics_view, text=_('View'), view='statistics:statistic_detail', args='resolved_object.slug' ) link_namespace_details = Link( - permissions=(permission_statistics_view,), text=_('Namespace details'), + permission=permission_statistics_view, text=_('Namespace details'), view='statistics:namespace_details', args='resolved_object.slug' ) link_namespace_list = Link( - permissions=(permission_statistics_view,), text=_('Namespace list'), + permission=permission_statistics_view, text=_('Namespace list'), view='statistics:namespace_list' ) link_statistics = Link( - icon_class=icon_statistics, permissions=(permission_statistics_view,), + icon_class=icon_statistics, permission=permission_statistics_view, text=_('Statistics'), view='statistics:namespace_list' ) diff --git a/mayan/apps/metadata/links.py b/mayan/apps/metadata/links.py index 75869e0525..b2b187578e 100644 --- a/mayan/apps/metadata/links.py +++ b/mayan/apps/metadata/links.py @@ -23,25 +23,25 @@ from .permissions import ( link_document_metadata_add = Link( icon_class=icon_document_metadata_add, kwargs={'document_id': 'object.pk'}, - permissions=(permission_document_metadata_add,), text=_('Add metadata'), + permission=permission_document_metadata_add, text=_('Add metadata'), view='metadata:document_metadata_add', ) link_document_metadata_edit = Link( icon_class=icon_document_metadata_edit, kwargs={'document_id': 'object.pk'}, - permissions=(permission_document_metadata_edit,), text=_('Edit metadata'), + permission=permission_document_metadata_edit, text=_('Edit metadata'), view='metadata:document_metadata_edit' ) link_document_metadata_remove = Link( icon_class=icon_document_metadata_remove, kwargs={'document_id': 'object.pk'}, - permissions=(permission_document_metadata_remove,), - text=_('Remove metadata'), view='metadata:document_metadata_remove', + permission=permission_document_metadata_remove, + text=_('Remove metadata'), view='metadata:document_metadata_remove' ) link_document_metadata_view = Link( icon_class=icon_document_metadata_view, kwargs={'document_id': 'resolved_object.pk'}, - permissions=(permission_document_metadata_view,), text=_('Metadata'), - view='metadata:document_metadata_view', + permission=permission_document_metadata_view, text=_('Metadata'), + view='metadata:document_metadata_view' ) link_document_multiple_metadata_add = Link( icon_class=icon_document_multiple_metadata_add, text=_('Add metadata'), @@ -59,34 +59,34 @@ link_document_multiple_metadata_remove = Link( link_document_type_metadata_types = Link( icon_class=icon_document_type_metadata_types, kwargs={'document_type_id': 'resolved_object.pk'}, - permissions=(permission_document_type_edit,), text=_('Metadata types'), - view='metadata:document_type_metadata_types', + permission=permission_document_type_edit, text=_('Metadata types'), + view='metadata:document_type_metadata_types' ) link_metadata_type_document_types = Link( icon_class=icon_document_type, kwargs={'metadata_type_id': 'resolved_object.pk'}, - permissions=(permission_document_type_edit,), text=_('Document types'), - view='metadata:metadata_type_document_types', + permission=permission_document_type_edit, text=_('Document types'), + view='metadata:metadata_type_document_types' ) link_metadata_type_create = Link( icon_class=icon_metadata_type_create, - permissions=(permission_metadata_type_create,), text=_('Create new'), + permission=permission_metadata_type_create, text=_('Create new'), view='metadata:metadata_type_create' ) link_metadata_type_delete = Link( icon_class=icon_metadata_type_delete, kwargs={'metadata_type_id': 'object.pk'}, - permissions=(permission_metadata_type_delete,), tags='dangerous', - text=_('Delete'), view='metadata:metadata_type_delete', + permission=permission_metadata_type_delete, tags='dangerous', + text=_('Delete'), view='metadata:metadata_type_delete' ) link_metadata_type_edit = Link( icon_class=icon_metadata_type_edit, kwargs={'metadata_type_id': 'object.pk'}, - permissions=(permission_metadata_type_edit,), text=_('Edit'), + permission=permission_metadata_type_edit, text=_('Edit'), view='metadata:metadata_type_edit' ) link_metadata_type_list = Link( icon_class=icon_metadata_type_list, - permissions=(permission_metadata_type_view,), text=_('Metadata types'), + permission=permission_metadata_type_view, text=_('Metadata types'), view='metadata:metadata_type_list' ) diff --git a/mayan/apps/motd/links.py b/mayan/apps/motd/links.py index b5fd4ca109..d29b1547fc 100644 --- a/mayan/apps/motd/links.py +++ b/mayan/apps/motd/links.py @@ -14,17 +14,17 @@ from .permissions import ( ) link_message_create = Link( - icon_class=icon_message_create, permissions=(permission_message_create,), + icon_class=icon_message_create, permission=permission_message_create, text=_('Create message'), view='motd:message_create' ) link_message_delete = Link( icon_class=icon_message_delete, kwargs={'message_id': 'object.pk'}, - permissions=(permission_message_delete,), tags='dangerous', + permission=permission_message_delete, tags='dangerous', text=_('Delete'), view='motd:message_delete' ) link_message_edit = Link( icon_class=icon_message_edit, kwargs={'message_id': 'object.pk'}, - permissions=(permission_message_edit,), text=_('Edit'), + permission=permission_message_edit, text=_('Edit'), view='motd:message_edit' ) link_message_list = Link( diff --git a/mayan/apps/ocr/links.py b/mayan/apps/ocr/links.py index cf67bdaf50..a11ad24899 100644 --- a/mayan/apps/ocr/links.py +++ b/mayan/apps/ocr/links.py @@ -18,19 +18,19 @@ from .permissions import ( link_document_page_ocr_content = Link( icon_class=icon_document_content, kwargs={'document_page_id': 'resolved_object.id'}, - permissions=(permission_ocr_content_view,), text=_('OCR'), - view='ocr:document_page_content', + permission=permission_ocr_content_view, text=_('OCR'), + view='ocr:document_page_content' ) link_document_ocr_content = Link( icon_class=icon_document_content, kwargs={'document_id': 'resolved_object.id'}, - permissions=(permission_ocr_content_view,), text=_('OCR'), - view='ocr:document_content', + permission=permission_ocr_content_view, text=_('OCR'), + view='ocr:document_content' ) link_document_submit = Link( icon_class=icon_document_submit, kwargs={'document_id': 'resolved_object.id'}, - permissions=(permission_ocr_document,), text=_('Submit for OCR'), + permission=permission_ocr_document, text=_('Submit for OCR'), view='ocr:document_submit' ) link_document_multiple_submit = Link( @@ -40,27 +40,27 @@ link_document_multiple_submit = Link( link_document_type_ocr_settings = Link( icon_class=icon_document_type_ocr_settings, kwargs={'document_type_id': 'resolved_object.id'}, - permissions=(permission_document_type_ocr_setup,), text=_('Setup OCR'), - view='ocr:document_type_settings', + permission=permission_document_type_ocr_setup, text=_('Setup OCR'), + view='ocr:document_type_settings' ) link_document_type_submit = Link( icon_class=icon_document_type_submit, - permissions=(permission_ocr_document,), text=_('OCR documents per type'), + permission=permission_ocr_document, text=_('OCR documents per type'), view='ocr:document_type_submit' ) link_entry_list = Link( - icon_class=icon_entry_list, permissions=(permission_ocr_document,), + icon_class=icon_entry_list, permission=permission_ocr_document, text=_('OCR errors'), view='ocr:entry_list' ) link_document_ocr_errors_list = Link( icon_class=icon_document_ocr_errors_list, kwargs={'document_id': 'resolved_object.id'}, - permissions=(permission_ocr_content_view,), text=_('OCR errors'), + permission=permission_ocr_content_view, text=_('OCR errors'), view='ocr:document_error_list' ) link_document_ocr_download = Link( icon_class=icon_document_ocr_download, kwargs={'document_id': 'resolved_object.id'}, - permissions=(permission_ocr_content_view,), text=_('Download OCR text'), + permission=permission_ocr_content_view, text=_('Download OCR text'), view='ocr:document_download' ) diff --git a/mayan/apps/smart_settings/links.py b/mayan/apps/smart_settings/links.py index 67c66e53f3..8068ad2309 100644 --- a/mayan/apps/smart_settings/links.py +++ b/mayan/apps/smart_settings/links.py @@ -8,21 +8,21 @@ from .icons import icon_namespace_list from .permissions import permission_settings_edit, permission_settings_view link_namespace_list = Link( - icon_class=icon_namespace_list, permissions=(permission_settings_view,), + icon_class=icon_namespace_list, permission=permission_settings_view, text=_('Settings'), view='settings:namespace_list' ) link_namespace_detail = Link( kwargs={'namespace_name': 'resolved_object.name'}, - permissions=(permission_settings_view,), text=_('Settings'), + permission=permission_settings_view, text=_('Settings'), view='settings:namespace_detail' ) # Duplicate the link to use a different name link_namespace_root_list = Link( - icon_class=icon_namespace_list, permissions=(permission_settings_view,), + icon_class=icon_namespace_list, permission=permission_settings_view, text=_('Namespaces'), view='settings:namespace_list' ) link_setting_edit = Link( kwargs={'setting_global_name': 'resolved_object.global_name'}, - permissions=(permission_settings_edit,), text=_('Edit'), + permission=permission_settings_edit, text=_('Edit'), view='settings:setting_edit_view' ) diff --git a/mayan/apps/sources/links.py b/mayan/apps/sources/links.py index bf206ec3b4..a35c6ca796 100644 --- a/mayan/apps/sources/links.py +++ b/mayan/apps/sources/links.py @@ -53,75 +53,75 @@ link_document_create_multiple = Link( ) link_source_check_now = Link( kwargs={'source_id': 'resolved_object.pk'}, - permissions=(permission_sources_edit,), text=_('Check now'), + permission=permission_sources_edit, text=_('Check now'), view='sources:source_check' ) link_source_create_imap_email = Link( icon_class=icon_source_create, kwargs={'source_type': '"%s"' % SOURCE_CHOICE_EMAIL_IMAP}, - permissions=(permission_sources_create,), - text=_('Add new IMAP email'), view='sources:source_create' + permission=permission_sources_create, text=_('Add new IMAP email'), + view='sources:source_create' ) link_source_create_pop3_email = Link( icon_class=icon_source_create, kwargs={'source_type': '"%s"' % SOURCE_CHOICE_EMAIL_POP3}, - permissions=(permission_sources_create,), - text=_('Add new POP3 email'), view='sources:source_create' + permission=permission_sources_create, text=_('Add new POP3 email'), + view='sources:source_create' ) link_source_create_staging_folder = Link( icon_class=icon_source_create, kwargs={'source_type': '"%s"' % SOURCE_CHOICE_STAGING}, - permissions=(permission_sources_create,), + permission=permission_sources_create, text=_('Add new staging folder'), view='sources:source_create' ) link_source_create_watch_folder = Link( icon_class=icon_source_create, kwargs={'source_type': '"%s"' % SOURCE_CHOICE_WATCH}, - permissions=(permission_sources_create,), + permission=permission_sources_create, text=_('Add new watch folder'), view='sources:source_create' ) link_source_create_webform = Link( icon_class=icon_source_create, kwargs={'source_type': '"%s"' % SOURCE_CHOICE_WEB_FORM}, - permissions=(permission_sources_create,), + permission=permission_sources_create, text=_('Add new webform source'), view='sources:source_create' ) link_source_create_sane_scanner = Link( icon_class=icon_source_create, kwargs={'source_type': '"%s"' % SOURCE_CHOICE_SANE_SCANNER}, - permissions=(permission_sources_create,), + permission=permission_sources_create, text=_('Add new SANE scanner'), view='sources:source_create' ) link_source_delete = Link( kwargs={'source_id': 'resolved_object.pk'}, - permissions=(permission_sources_delete,), tags='dangerous', + permission=permission_sources_delete, tags='dangerous', text=_('Delete'), view='sources:source_delete' ) link_source_edit = Link( kwargs={'source_id': 'resolved_object.pk'}, - permissions=(permission_sources_edit,), text=_('Edit'), + permission=permission_sources_edit, text=_('Edit'), view='sources:source_edit' ) link_source_list = Link( icon_class=icon_source_list, - permissions=(permission_sources_view,), text=_('Sources'), + permission=permission_sources_view, text=_('Sources'), view='sources:source_list' ) link_source_logs = Link( icon_class=icon_log, kwargs={'source_id': 'resolved_object.pk'}, - permissions=(permission_sources_view,), text=_('Logs'), + permission=permission_sources_view, text=_('Logs'), view='sources:source_logs' ) link_staging_file_delete = Link( keep_query=True, kwargs={ 'staging_folder_id': 'source.pk', 'encoded_filename': 'object.encoded_filename' - }, permissions=(permission_staging_file_delete,), + }, permission=permission_staging_file_delete, tags='dangerous', text=_('Delete'), view='sources:staging_file_delete' ) link_upload_version = Link( condition=document_new_version_not_blocked, kwargs={'document_pk': 'resolved_object.pk'}, - permissions=(permission_document_new_version,), - text=_('Upload new version'), view='sources:upload_version' + permission=permission_document_new_version, text=_('Upload new version'), + view='sources:upload_version' ) diff --git a/mayan/apps/tags/links.py b/mayan/apps/tags/links.py index 17ce2eee4c..062a44f425 100644 --- a/mayan/apps/tags/links.py +++ b/mayan/apps/tags/links.py @@ -6,10 +6,10 @@ from mayan.apps.documents.icons import icon_document_list from mayan.apps.navigation import Link, get_cascade_condition from .icons import ( - icon_multiple_documents_tag_attach, icon_multiple_documents_tag_remove, - icon_tag_attach, icon_tag_create, icon_tag_delete, icon_tag_edit, + icon_document_multiple_tag_multiple_remove, icon_document_multiple_tag_multiple_remove, + icon_document_tag_multiple_attach, icon_tag_create, icon_tag_delete, icon_tag_edit, icon_tag_document_list, icon_tag_list, icon_tag_multiple_delete, - icon_tag_remove + icon_document_tag_multiple_remove ) from .permissions import ( permission_tag_attach, permission_tag_create, permission_tag_delete, @@ -19,39 +19,38 @@ from .permissions import ( link_document_tag_list = Link( args='resolved_object.pk', icon_class=icon_tag_document_list, - permissions=(permission_tag_view,), text=_('Tags'), - view='tags:document_tags', + permission=permission_tag_view, text=_('Tags'), view='tags:document_tags' ) -link_multiple_documents_tag_remove = Link( - icon_class=icon_multiple_documents_tag_remove, text=_('Remove tag'), - view='tags:multiple_documents_selection_tag_remove' +link_document_multiple_tag_multiple_attach = Link( + icon_class=icon_document_multiple_tag_multiple_remove, text=_('Attach tags'), + view='tags:document_multiple_tag_multiple_attach' ) -link_multiple_documents_attach_tag = Link( - icon_class=icon_multiple_documents_tag_attach, text=_('Attach tags'), - view='tags:multiple_documents_tag_attach' +link_document_multiple_tag_multiple_remove = Link( + icon_class=icon_document_multiple_tag_multiple_remove, text=_('Remove tag'), + view='tags:document_multiple_tag_multiple_remove' ) -link_single_document_multiple_tag_remove = Link( - args='object.id', icon_class=icon_tag_remove, - permissions=(permission_tag_remove,), text=_('Remove tags'), - view='tags:single_document_multiple_tag_remove', +link_document_tag_multiple_attach = Link( + args='object.pk', icon_class=icon_document_tag_multiple_attach, + permission=permission_tag_attach, text=_('Attach tags'), + view='tags:document_tag_multiple_attach' ) -link_tag_attach = Link( - args='object.pk', icon_class=icon_tag_attach, - permissions=(permission_tag_attach,), text=_('Attach tags'), - view='tags:tag_attach', +link_document_tag_multiple_remove = Link( + args='object.id', icon_class=icon_document_tag_multiple_remove, + permission=permission_tag_remove, text=_('Remove tags'), + view='tags:document_tag_multiple_remove' ) link_tag_create = Link( - icon_class=icon_tag_create, permissions=(permission_tag_create,), + icon_class=icon_tag_create, permission=permission_tag_create, text=_('Create new tag'), view='tags:tag_create' ) link_tag_delete = Link( args='object.id', icon_class=icon_tag_delete, - permissions=(permission_tag_delete,), tags='dangerous', text=_('Delete'), - view='tags:tag_delete', + permission=permission_tag_delete, tags='dangerous', text=_('Delete'), + view='tags:tag_delete' ) link_tag_edit = Link( args='object.id', icon_class=icon_tag_edit, - permissions=(permission_tag_edit,), text=_('Edit'), view='tags:tag_edit', + permission=permission_tag_edit, text=_('Edit'), view='tags:tag_edit' ) link_tag_list = Link( condition=get_cascade_condition( @@ -60,10 +59,10 @@ link_tag_list = Link( ), icon_class=icon_tag_list, text=_('All'), view='tags:tag_list' ) link_tag_multiple_delete = Link( - icon_class=icon_tag_multiple_delete, permissions=(permission_tag_delete,), + icon_class=icon_tag_multiple_delete, permission=permission_tag_delete, text=_('Delete'), view='tags:tag_multiple_delete' ) link_tag_tagged_item_list = Link( args='object.id', icon_class=icon_document_list, text=('Documents'), - view='tags:tag_tagged_item_list', + view='tags:tag_tagged_item_list' ) diff --git a/mayan/apps/task_manager/links.py b/mayan/apps/task_manager/links.py index 1e88569078..d7f76a8261 100644 --- a/mayan/apps/task_manager/links.py +++ b/mayan/apps/task_manager/links.py @@ -8,22 +8,22 @@ from .icons import icon_task_manager, icon_queue_list from .permissions import permission_task_view link_task_manager = Link( - icon_class=icon_task_manager, permissions=(permission_task_view,), + icon_class=icon_task_manager, permission=permission_task_view, text=_('Task manager'), view='task_manager:queue_list' ) link_queue_list = Link( - icon_class=icon_queue_list, permissions=(permission_task_view,), + icon_class=icon_queue_list, permission=permission_task_view, text=_('Background task queues'), view='task_manager:queue_list' ) link_queue_active_task_list = Link( - args='resolved_object.name', permissions=(permission_task_view,), + args='resolved_object.name', permission=permission_task_view, text=_('Active tasks'), view='task_manager:queue_active_task_list' ) link_queue_reserved_task_list = Link( - args='resolved_object.name', permissions=(permission_task_view,), + args='resolved_object.name', permission=permission_task_view, text=_('Reserved tasks'), view='task_manager:queue_reserved_task_list' ) link_queue_scheduled_task_list = Link( - args='resolved_object.name', permissions=(permission_task_view,), + args='resolved_object.name', permission=permission_task_view, text=_('Scheduled tasks'), view='task_manager:queue_scheduled_task_list' ) diff --git a/mayan/apps/user_management/links.py b/mayan/apps/user_management/links.py index 2eb580649e..ed75f17ab8 100644 --- a/mayan/apps/user_management/links.py +++ b/mayan/apps/user_management/links.py @@ -34,78 +34,78 @@ link_current_user_edit = Link( view='user_management:current_user_edit' ) link_group_create = Link( - icon_class=icon_group_create, permissions=(permission_group_create,), + icon_class=icon_group_create, permission=permission_group_create, text=_('Create new group'), view='user_management:group_create' ) link_group_delete = Link( args='object.id', icon_class=icon_group_delete, - permissions=(permission_group_delete,), tags='dangerous', + permission=permission_group_delete, tags='dangerous', text=_('Delete'), view='user_management:group_delete', ) link_group_edit = Link( args='object.id', icon_class=icon_group_edit, - permissions=(permission_group_edit,), text=_('Edit'), + permission=permission_group_edit, text=_('Edit'), view='user_management:group_edit', ) link_group_list = Link( - icon_class=icon_group_list, permissions=(permission_group_view,), + icon_class=icon_group_list, permission=permission_group_view, text=_('Groups'), view='user_management:group_list' ) link_group_members = Link( args='object.id', icon_class=icon_group_members, - permissions=(permission_group_edit,), text=_('Users'), + permission=permission_group_edit, text=_('Users'), view='user_management:group_members', ) link_group_setup = Link( - icon_class=icon_group_setup, permissions=(permission_group_view,), + icon_class=icon_group_setup, permission=permission_group_view, text=_('Groups'), view='user_management:group_list' ) link_user_create = Link( - icon_class=icon_user_create, permissions=(permission_user_create,), + icon_class=icon_user_create, permission=permission_user_create, text=_('Create new user'), view='user_management:user_create' ) link_user_delete = Link( args='object.id', icon_class=icon_user_delete, - permissions=(permission_user_delete,), tags='dangerous', text=_('Delete'), + permission=permission_user_delete, tags='dangerous', text=_('Delete'), view='user_management:user_delete', ) link_user_edit = Link( args='object.id', icon_class=icon_user_edit, - permissions=(permission_user_edit,), text=_('Edit'), + permission=permission_user_edit, text=_('Edit'), view='user_management:user_edit', ) link_user_groups = Link( args='object.id', condition=condition_is_not_superuser, - icon_class=icon_group, permissions=(permission_user_edit,), + icon_class=icon_group, permission=permission_user_edit, text=_('Groups'), view='user_management:user_groups', ) link_user_list = Link( - icon_class=icon_user_list, permissions=(permission_user_view,), + icon_class=icon_user_list, permission=permission_user_view, text=_('Users'), view='user_management:user_list' ) link_user_multiple_delete = Link( icon_class=icon_user_multiple_delete, - permissions=(permission_user_delete,), tags='dangerous', text=_('Delete'), + permission=permission_user_delete, tags='dangerous', text=_('Delete'), view='user_management:user_multiple_delete' ) link_user_multiple_set_password = Link( icon_class=icon_user_multiple_set_password, - permissions=(permission_user_edit,), text=_('Set password'), + permission=permission_user_edit, text=_('Set password'), view='user_management:user_multiple_set_password' ) link_user_set_options = Link( args='object.id', icon_class=icon_user_set_options, - permissions=(permission_user_edit,), text=_('User options'), + permission=permission_user_edit, text=_('User options'), view='user_management:user_options', ) link_user_set_password = Link( args='object.id', icon_class=icon_user_set_password, - permissions=(permission_user_edit,), text=_('Set password'), + permission=permission_user_edit, text=_('Set password'), view='user_management:user_set_password', ) link_user_setup = Link( - icon_class=icon_user_setup, permissions=(permission_user_view,), + icon_class=icon_user_setup, permission=permission_user_view, text=_('Users'), view='user_management:user_list' ) From c5ce20bbea8e51152aa81fd17067f65e5aa733e7 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Fri, 25 Jan 2019 01:16:48 -0400 Subject: [PATCH 047/209] Remove role permission grant revoke permissions Signed-off-by: Roberto Rosario --- HISTORY.rst | 2 ++ mayan/apps/permissions/apps.py | 2 -- mayan/apps/permissions/links.py | 21 ++++++++++----------- mayan/apps/permissions/permissions.py | 6 ------ mayan/apps/permissions/tests/test_views.py | 15 +++------------ mayan/apps/permissions/views.py | 15 +-------------- 6 files changed, 16 insertions(+), 45 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index ba2790de20..6645e10ed4 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -222,6 +222,8 @@ filtering in Python. The refactor added cascading access checking in preparation for nested cabinet access control and the removal of the permission proxy support which is now redundant. +- Remove the permissions to grant or revoke a permission to a role. + The instead the role edit permission is used. 3.1.9 (2018-11-01) ================== diff --git a/mayan/apps/permissions/apps.py b/mayan/apps/permissions/apps.py index 00612df967..34ca946cbd 100644 --- a/mayan/apps/permissions/apps.py +++ b/mayan/apps/permissions/apps.py @@ -22,7 +22,6 @@ from .links import ( link_role_list, link_role_permissions ) from .permissions import ( - permission_permission_grant, permission_permission_revoke, permission_role_delete, permission_role_edit, permission_role_view ) from .search import * # NOQA @@ -45,7 +44,6 @@ class PermissionsApp(MayanAppConfig): ModelPermission.register( model=Role, permissions=( permission_acl_edit, permission_acl_view, - permission_permission_grant, permission_permission_revoke, permission_role_delete, permission_role_edit, permission_role_view ) diff --git a/mayan/apps/permissions/links.py b/mayan/apps/permissions/links.py index c883137b79..1ee11c45b1 100644 --- a/mayan/apps/permissions/links.py +++ b/mayan/apps/permissions/links.py @@ -10,49 +10,48 @@ from .icons import ( icon_role_list, icon_role_permissions ) from .permissions import ( - permission_permission_grant, permission_permission_revoke, permission_role_create, permission_role_delete, permission_role_edit, permission_role_view ) link_group_roles = Link( icon_class=icon_role_list, kwargs={'group_id': 'object.id'}, - permissions=(permission_group_edit,), text=_('Roles'), + permission=permission_group_edit, text=_('Roles'), view='permissions:group_roles', ) link_permission_grant = Link( - permissions=(permission_permission_grant,), text=_('Grant'), + permission=permission_role_edit, text=_('Grant'), view='permissions:permission_multiple_grant' ) link_permission_revoke = Link( - permissions=(permission_permission_revoke,), text=_('Revoke'), + permission=permission_role_edit, text=_('Revoke'), view='permissions:permission_multiple_revoke' ) link_role_create = Link( - icon_class=icon_role_create, permissions=(permission_role_create,), + icon_class=icon_role_create, permission=permission_role_create, text=_('Create new role'), view='permissions:role_create' ) link_role_delete = Link( icon_class=icon_role_delete, kwargs={'role_id': 'object.id'}, - permissions=(permission_role_delete,), tags='dangerous', text=_('Delete'), + permission=permission_role_delete, tags='dangerous', text=_('Delete'), view='permissions:role_delete', ) link_role_edit = Link( icon_class=icon_role_edit, kwargs={'role_id': 'object.id'}, - permissions=(permission_role_edit,), text=_('Edit'), + permission=permission_role_edit, text=_('Edit'), view='permissions:role_edit', ) link_role_list = Link( - icon_class=icon_role_list, permissions=(permission_role_view,), + icon_class=icon_role_list, permission=permission_role_view, text=_('Roles'), view='permissions:role_list' ) link_role_groups = Link( icon_class=icon_role_groups, kwargs={'role_id': 'object.id'}, - permissions=(permission_role_edit,), text=_('Groups'), + permission=permission_role_edit, text=_('Groups'), view='permissions:role_groups', ) link_role_permissions = Link( icon_class=icon_role_permissions, kwargs={'role_id': 'object.id'}, - permissions=(permission_permission_grant, permission_permission_revoke), - text=_('Role permissions'), view='permissions:role_permissions', + permission=permission_role_edit, text=_('Role permissions'), + view='permissions:role_permissions', ) diff --git a/mayan/apps/permissions/permissions.py b/mayan/apps/permissions/permissions.py index 4411ca53ad..986b5b5485 100644 --- a/mayan/apps/permissions/permissions.py +++ b/mayan/apps/permissions/permissions.py @@ -6,12 +6,6 @@ from . import PermissionNamespace namespace = PermissionNamespace(label=_('Permissions'), name='permissions') -permission_permission_grant = namespace.add_permission( - label=_('Grant permissions'), name='permission_grant' -) -permission_permission_revoke = namespace.add_permission( - label=_('Revoke permissions'), name='permission_revoke' -) permission_role_create = namespace.add_permission( label=_('Create roles'), name='role_create' ) diff --git a/mayan/apps/permissions/tests/test_views.py b/mayan/apps/permissions/tests/test_views.py index c4c28c7fd0..2ec8ecfe2a 100644 --- a/mayan/apps/permissions/tests/test_views.py +++ b/mayan/apps/permissions/tests/test_views.py @@ -6,7 +6,6 @@ from mayan.apps.user_management.tests import GroupTestMixin from ..models import Role from ..permissions import ( - permission_permission_grant, permission_permission_revoke, permission_role_create, permission_role_delete, permission_role_edit, permission_role_view ) @@ -123,23 +122,15 @@ class PermissionsViewsTestCase(GroupTestMixin, RoleTestMixin, GenericViewTestCas kwargs={'role_id': self.test_role.pk} ) - def test_role_permissions_view_no_access(self): + def test_role_permissions_view_no_permission(self): self._create_test_role() response = self._request_role_permissions_view() self.assertEqual(response.status_code, 403) - def test_role_permissions_view_with_permission_grant(self): + def test_role_permissions_view_with_access(self): self._create_test_role() self.grant_access( - permission=permission_permission_grant, obj=self.test_role - ) - response = self._request_role_permissions_view() - self.assertEqual(response.status_code, 200) - - def test_role_permissions_view_with_permission_revoke(self): - self._create_test_role() - self.grant_access( - permission=permission_permission_revoke, obj=self.test_role + permission=permission_permission_view, obj=self.test_role ) response = self._request_role_permissions_view() self.assertEqual(response.status_code, 200) diff --git a/mayan/apps/permissions/views.py b/mayan/apps/permissions/views.py index 127312fe9a..db89b01d23 100644 --- a/mayan/apps/permissions/views.py +++ b/mayan/apps/permissions/views.py @@ -21,7 +21,6 @@ from .icons import icon_role_list from .links import link_role_create from .models import Role, StoredPermission from .permissions import ( - permission_permission_grant, permission_permission_revoke, permission_role_create, permission_role_delete, permission_role_edit, permission_role_view ) @@ -147,6 +146,7 @@ class RoleListView(SingleObjectListView): class RolePermissionsView(AssignRemoveView): grouped = True left_list_title = _('Available permissions') + object_permission = permission_role_edit right_list_title = _('Granted permissions') @staticmethod @@ -171,19 +171,9 @@ class RolePermissionsView(AssignRemoveView): return results def add(self, item): - Permission.check_permissions( - self.request.user, permissions=(permission_permission_grant,) - ) permission = get_object_or_404(klass=StoredPermission, pk=item) self.get_object().permissions.add(permission) - def dispatch(self, request, *args, **kwargs): - AccessControlList.objects.check_access( - permissions=(permission_permission_grant, permission_permission_revoke), - user=self.request.user, obj=self.get_object() - ) - return super(RolePermissionsView, self).dispatch(request, *args, **kwargs) - def get_extra_context(self): return { 'object': self.get_object(), @@ -207,9 +197,6 @@ class RolePermissionsView(AssignRemoveView): ) def remove(self, item): - Permission.check_permissions( - self.request.user, permissions=(permission_permission_revoke,) - ) permission = get_object_or_404(klass=StoredPermission, pk=item) self.get_object().permissions.remove(permission) From f076a49d2d1ab3ca5bb1aaff403154b79f38ccb2 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Fri, 25 Jan 2019 01:17:21 -0400 Subject: [PATCH 048/209] Deprecate the check_permissions method Signed-off-by: Roberto Rosario --- mayan/apps/permissions/classes.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/mayan/apps/permissions/classes.py b/mayan/apps/permissions/classes.py index 64a370b9ef..4481ed38b0 100644 --- a/mayan/apps/permissions/classes.py +++ b/mayan/apps/permissions/classes.py @@ -2,12 +2,15 @@ from __future__ import unicode_literals import itertools import logging +import warnings from django.apps import apps from django.core.exceptions import PermissionDenied from django.utils.encoding import force_text, python_2_unicode_compatible from django.utils.translation import ugettext_lazy as _ +from mayan.apps.common.warnings import InterfaceWarning + from .exceptions import InvalidNamespace logger = logging.getLogger(__name__) @@ -75,6 +78,11 @@ class Permission(object): # Deprecated method @classmethod def check_permissions(cls, permissions, requester): + warnings.warn( + 'The method .check_permissions() is deprecated. Use ' + '.check_user_permission() instead.', InterfaceWarning + ) + try: for permission in permissions: if permission.stored_permission.user_has_this(user=requester): From 382995ae40950d598575cf98720b2755f3216f23 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Fri, 25 Jan 2019 01:18:44 -0400 Subject: [PATCH 049/209] Update ACLs app Remove support for passing a related field argument when checking for access for restricting a queryset. Remove a duplicate permission check. Fix bug when filtering the direct ACL for an object, the ACL query was filtering by the ACL ID instead of the object ID. Signed-off-by: Roberto Rosario --- mayan/apps/acls/managers.py | 40 +++++++--------------------- mayan/apps/acls/tests/test_models.py | 18 ++++++------- 2 files changed, 18 insertions(+), 40 deletions(-) diff --git a/mayan/apps/acls/managers.py b/mayan/apps/acls/managers.py index 281c136626..b60e301d79 100644 --- a/mayan/apps/acls/managers.py +++ b/mayan/apps/acls/managers.py @@ -111,8 +111,9 @@ class AccessControlListManager(models.Manager): content_type = ContentType.objects.get_for_model(model=queryset.model) field_lookup = 'id__in' acl_filter = self.filter( - permissions=stored_permission, role__groups__user=user - ).values('id') + content_type=content_type, permissions=stored_permission, + role__groups__user=user + ).values('object_id') result.append(Q(**{field_lookup: acl_filter})) # Case 4: Original model, has an inherited related field @@ -125,41 +126,22 @@ class AccessControlListManager(models.Manager): else: inherited_acl_queries = self._get_acl_filters( queryset=queryset, stored_permission=stored_permission, - user=user, related_field_name=related_field_name + related_field_name=related_field_name, user=user ) result.extend(inherited_acl_queries) return result - def check_access(self, permissions, user, obj, related=None, raise_404=False): + def check_access(self, obj, permission, user, raise_404=False): warnings.warn( 'check_access() is deprecated, use restrict_queryset() to ' 'produce a queryset from which to .get() the corresponding ' 'object in the local code.', InterfaceWarning ) - try: - # permissions can be a single permission or a list of permissions - permission = permissions[0] - except TypeError: - permission = permissions - else: - warnings.warn( - 'Passing multiple permissions via the `permissions` argument ' - 'is deprecated. Pass a single permission. Use multiple call ' - 'to check against multiple permissions.', InterfaceWarning - ) - - if related: - warnings.warn( - 'Passing a related field name to check_access() is ' - 'deprecated. Register the related field using ' - 'common.classes.ModelPermission.', InterfaceWarning - ) - queryset = self.restrict_queryset( permission=permission, queryset=obj._meta.default_manager.all(), - user=user, related_field_name=related + user=user ) if queryset.filter(pk=obj.pk).exists(): @@ -168,7 +150,7 @@ class AccessControlListManager(models.Manager): if raise_404: raise Http404 else: - return PermissionDenied + raise PermissionDenied def get_inherited_permissions(self, role, obj): try: @@ -229,7 +211,7 @@ class AccessControlListManager(models.Manager): permission=permission, queryset=queryset, user=user ) - def restrict_queryset(self, permission, queryset, user, related_field_name=None): + def restrict_queryset(self, permission, queryset, user): # `related_field_name` is left only for compatibility with check_access # once check_access() is removed the `related_field_name` argument # will be removed too. @@ -237,13 +219,9 @@ class AccessControlListManager(models.Manager): # Check directly granted permission via a role try: Permission.check_user_permission(permission=permission, user=user) - - Permission.check_permissions( - requester=user, permissions=(permission,) - ) except PermissionDenied: acl_filters = self._get_acl_filters( - queryset=queryset, related_field_name=related_field_name, + queryset=queryset, stored_permission=permission.stored_permission, user=user ) diff --git a/mayan/apps/acls/tests/test_models.py b/mayan/apps/acls/tests/test_models.py index f113a4ff43..ec0c600925 100644 --- a/mayan/apps/acls/tests/test_models.py +++ b/mayan/apps/acls/tests/test_models.py @@ -38,7 +38,7 @@ class PermissionTestCase(DocumentTestMixin, BaseTestCase): def test_check_access_without_permissions(self): with self.assertRaises(PermissionDenied): AccessControlList.objects.check_access( - obj=self.test_document_1, permissions=(permission_document_view,), + obj=self.test_document_1, permission=permission_document_view, user=self._test_case_user ) @@ -58,7 +58,7 @@ class PermissionTestCase(DocumentTestMixin, BaseTestCase): try: AccessControlList.objects.check_access( - obj=self.test_document_1, permissions=(permission_document_view,), + obj=self.test_document_1, permission=permission_document_view, user=self._test_case_user ) except PermissionDenied: @@ -85,26 +85,26 @@ class PermissionTestCase(DocumentTestMixin, BaseTestCase): try: AccessControlList.objects.check_access( - obj=self.test_document_1, permissions=(permission_document_view,), + obj=self.test_document_1, permission=permission_document_view, user=self._test_case_user ) except PermissionDenied: self.fail('PermissionDenied exception was not expected.') - def test_check_access_with_inherited_acl_and_local_acl(self): - acl = AccessControlList.objects.create( + def test_check_access_with_inherited_acl_and_direct_acl(self): + test_acl_1 = AccessControlList.objects.create( content_object=self.test_document_type_1, role=self._test_case_role ) - acl.permissions.add(permission_document_view.stored_permission) + test_acl_1.permissions.add(permission_document_view.stored_permission) - acl = AccessControlList.objects.create( + test_acl_2 = AccessControlList.objects.create( content_object=self.test_document_3, role=self._test_case_role ) - acl.permissions.add(permission_document_view.stored_permission) + test_acl_2.permissions.add(permission_document_view.stored_permission) try: AccessControlList.objects.check_access( - obj=self.test_document_3, permissions=(permission_document_view,), + obj=self.test_document_3, permission=permission_document_view, user=self._test_case_user ) except PermissionDenied: From c5d4054fb68be3faf2a3a2e63ff3b7c99c9b84b1 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Fri, 25 Jan 2019 01:22:02 -0400 Subject: [PATCH 050/209] Add test mixin to generate random primary keys Add a new mixin to monkey patch the Model class to force each newly created model instance to use a randomly generated primary key. Signed-off-by: Roberto Rosario --- HISTORY.rst | 1 + mayan/apps/common/tests/base.py | 7 ++--- mayan/apps/common/tests/mixins.py | 49 +++++++++++++++++++++++++++++++ 3 files changed, 53 insertions(+), 4 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 6645e10ed4..3098b6f852 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -224,6 +224,7 @@ of the permission proxy support which is now redundant. - Remove the permissions to grant or revoke a permission to a role. The instead the role edit permission is used. +- Add a test mixin to generate random model primary keys. 3.1.9 (2018-11-01) ================== diff --git a/mayan/apps/common/tests/base.py b/mayan/apps/common/tests/base.py index 82f9f19b21..2fc36e4599 100644 --- a/mayan/apps/common/tests/base.py +++ b/mayan/apps/common/tests/base.py @@ -1,7 +1,6 @@ from __future__ import absolute_import, unicode_literals from django.test import TestCase -from django.urls import reverse from django_downloadview import assert_download_response from mayan.apps.acls.tests.mixins import ACLTestCaseMixin @@ -10,12 +9,12 @@ from mayan.apps.smart_settings.classes import Namespace from .mixins import ( ClientMethodsTestCaseMixin, ContentTypeCheckMixin, DatabaseConversionMixin, - OpenFileCheckTestCaseMixin, TempfileCheckTestCaseMixin, - TestViewTestCaseMixin + OpenFileCheckTestCaseMixin, RandomPrimaryKeyModelMonkeyPatchMixin, + TempfileCheckTestCaseMixin, TestViewTestCaseMixin ) -class BaseTestCase(DatabaseConversionMixin, ACLTestCaseMixin, ContentTypeCheckMixin, OpenFileCheckTestCaseMixin, TempfileCheckTestCaseMixin, TestCase): +class BaseTestCase(RandomPrimaryKeyModelMonkeyPatchMixin, DatabaseConversionMixin, ACLTestCaseMixin, ContentTypeCheckMixin, OpenFileCheckTestCaseMixin, TempfileCheckTestCaseMixin, TestCase): """ This is the most basic test case class any test in the project should use. """ diff --git a/mayan/apps/common/tests/mixins.py b/mayan/apps/common/tests/mixins.py index 621353bd68..9f3fa318f7 100644 --- a/mayan/apps/common/tests/mixins.py +++ b/mayan/apps/common/tests/mixins.py @@ -2,10 +2,12 @@ from __future__ import unicode_literals import glob import os +import random from django.conf import settings from django.conf.urls import url from django.core import management +from django.db import models from django.http import HttpResponse from django.template import Context, Template from django.test.utils import ContextList @@ -138,6 +140,53 @@ class OpenFileCheckTestCaseMixin(object): super(OpenFileCheckTestCaseMixin, self).tearDown() +class RandomPrimaryKeyModelMonkeyPatchMixin(object): + random_primary_key_random_floor = 100 + random_primary_key_random_ceiling = 10000 + random_primary_key_maximum_attempts = 100 + + @staticmethod + def get_unique_primary_key(model): + pk_list = model._meta.default_manager.values_list('pk', flat=True) + + attempts = 0 + while True: + primary_key = random.randint( + RandomPrimaryKeyModelMonkeyPatchMixin.random_primary_key_random_floor, + RandomPrimaryKeyModelMonkeyPatchMixin.random_primary_key_random_ceiling + ) + + if primary_key not in pk_list: + break + + attempts = attempts + 1 + + if attempts > RandomPrimaryKeyModelMonkeyPatchMixin.random_primary_key_maximum_attempts: + raise Exception( + 'Maximum number of retries for an unique random primary ' + 'key reached.' + ) + + return primary_key + + def setUp(self): + original_save = models.Model.save + + def new_save(self, *args, **kwargs): + if self.pk: + return original_save(self, *args, **kwargs) + else: + self.pk = RandomPrimaryKeyModelMonkeyPatchMixin.get_unique_primary_key( + model=self._meta.model + ) + self.id = self.pk + + return self.save_base(force_insert=True) + + setattr(models.Model, 'save', new_save) + super(RandomPrimaryKeyModelMonkeyPatchMixin, self).setUp() + + class TempfileCheckTestCaseMixin(object): # Ignore the jvmstat instrumentation and GitLab's CI .config files # Ignore LibreOffice fontconfig cache dir From 2ed7858acbd51f66a1b53ddb3bbc549db797a827 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Fri, 25 Jan 2019 01:24:19 -0400 Subject: [PATCH 051/209] Move filterted from initialization Signed-off-by: Roberto Rosario --- mayan/apps/common/forms.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mayan/apps/common/forms.py b/mayan/apps/common/forms.py index 3310b997e6..51a9ba685b 100644 --- a/mayan/apps/common/forms.py +++ b/mayan/apps/common/forms.py @@ -247,14 +247,14 @@ class FilteredSelectionForm(forms.Form): else: widget_class = opts.widget_class - super(FilteredSelectionForm, self).__init__(*args, **kwargs) - if opts.permission: queryset = AccessControlList.objects.filter_by_access( permission=opts.permission, queryset=queryset, user=opts.user ) + super(FilteredSelectionForm, self).__init__(*args, **kwargs) + self.fields[opts.field_name] = field_class( help_text=opts.help_text, label=opts.label, queryset=queryset, required=opts.required, From 319b74c85faec65983935019dfe42b6b6951791b Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Fri, 25 Jan 2019 01:24:54 -0400 Subject: [PATCH 052/209] Force use of get_object_list method Update the SingleObject Delete, Detail and Download views to force use of a get_object_list method instead of allowing subclasses to override the get_queryset method and bypass the object permission checks. Signed-off-by: Roberto Rosario --- mayan/apps/common/generics.py | 68 ++++++++++++++++++++++++++++++++--- 1 file changed, 64 insertions(+), 4 deletions(-) diff --git a/mayan/apps/common/generics.py b/mayan/apps/common/generics.py index 840011d97e..f7afb7f1f8 100644 --- a/mayan/apps/common/generics.py +++ b/mayan/apps/common/generics.py @@ -421,10 +421,18 @@ class SingleObjectDynamicFormCreateView(DynamicFormViewMixin, SingleObjectCreate class SingleObjectDeleteView(ObjectNameMixin, DeleteExtraDataMixin, 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 __init__(self, *args, **kwargs): + result = super(SingleObjectDeleteView, self).__init__(*args, **kwargs) + + if self.__class__.mro()[0].get_queryset != SingleObjectDeleteView.get_queryset: + raise ImproperlyConfigured( + '%(cls)s is overloading the get_queryset method. Subclasses ' + 'should implement the get_object_list method instead. ' % { + 'cls': self.__class__.__name__ + } + ) + + return result def delete(self, request, *args, **kwargs): self.object = self.get_object() @@ -453,20 +461,72 @@ class SingleObjectDeleteView(ObjectNameMixin, DeleteExtraDataMixin, ViewPermissi return result + def get_context_data(self, **kwargs): + context = super(SingleObjectDeleteView, self).get_context_data(**kwargs) + context.update({'delete_view': True}) + return context + + def get_queryset(self): + try: + return super(SingleObjectDeleteView, self).get_queryset() + except ImproperlyConfigured: + self.queryset = self.get_object_list() + return super(SingleObjectDeleteView, self).get_queryset() + class SingleObjectDetailView(ViewPermissionCheckMixin, ObjectPermissionCheckMixin, FormExtraKwargsMixin, ExtraContextMixin, ModelFormMixin, DetailView): template_name = 'appearance/generic_form.html' + def __init__(self, *args, **kwargs): + result = super(SingleObjectDetailView, self).__init__(*args, **kwargs) + + if self.__class__.mro()[0].get_queryset != SingleObjectDetailView.get_queryset: + raise ImproperlyConfigured( + '%(cls)s is overloading the get_queryset method. Subclasses ' + 'should implement the get_object_list method instead. ' % { + 'cls': self.__class__.__name__ + } + ) + + return result + def get_context_data(self, **kwargs): context = super(SingleObjectDetailView, self).get_context_data(**kwargs) context.update({'read_only': True, 'form': self.get_form()}) return context + def get_queryset(self): + try: + return super(SingleObjectDetailView, self).get_queryset() + except ImproperlyConfigured: + self.queryset = self.get_object_list() + return super(SingleObjectDetailView, self).get_queryset() + class SingleObjectDownloadView(ViewPermissionCheckMixin, ObjectPermissionCheckMixin, VirtualDownloadView, SingleObjectMixin): TextIteratorIO = TextIteratorIO VirtualFile = VirtualFile + def __init__(self, *args, **kwargs): + result = super(SingleObjectDownloadView, self).__init__(*args, **kwargs) + + if self.__class__.mro()[0].get_queryset != SingleObjectDownloadView.get_queryset: + raise ImproperlyConfigured( + '%(cls)s is overloading the get_queryset method. Subclasses ' + 'should implement the get_object_list method instead. ' % { + 'cls': self.__class__.__name__ + } + ) + + return result + + def get_queryset(self): + try: + return super(SingleObjectDownloadView, self).get_queryset() + except ImproperlyConfigured: + self.queryset = self.get_object_list() + return super(SingleObjectDownloadView, self).get_queryset() + class SingleObjectEditView(ObjectNameMixin, ViewPermissionCheckMixin, ObjectPermissionCheckMixin, ExtraContextMixin, FormExtraKwargsMixin, RedirectionMixin, UpdateView): template_name = 'appearance/generic_form.html' From 746f40dda01cd95cd743d3d1252531916913eba8 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Fri, 25 Jan 2019 01:26:21 -0400 Subject: [PATCH 053/209] Add missing line in introspect_attribute Signed-off-by: Roberto Rosario --- mayan/apps/common/utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mayan/apps/common/utils.py b/mayan/apps/common/utils.py index e88f7a2c4e..fb3df43336 100644 --- a/mayan/apps/common/utils.py +++ b/mayan/apps/common/utils.py @@ -159,6 +159,7 @@ def introspect_attribute(attribute_name, obj): except ValueError: return attribute_name, obj else: + related_field = obj._meta.get_field(field_name=attribute_part) return introspect_attribute( attribute_name=attribute_part, obj=related_field.related_model, From 9ce930367d4dc5ad052cd7630fd0cac83cbc7fc4 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Fri, 25 Jan 2019 01:29:23 -0400 Subject: [PATCH 054/209] Remove use of object_related view attribute Signed-off-by: Roberto Rosario --- mayan/apps/document_indexing/views.py | 3 --- mayan/apps/documents/views/document_version_views.py | 1 - 2 files changed, 4 deletions(-) diff --git a/mayan/apps/document_indexing/views.py b/mayan/apps/document_indexing/views.py index 96a58983e0..8b8ee0bb15 100644 --- a/mayan/apps/document_indexing/views.py +++ b/mayan/apps/document_indexing/views.py @@ -195,7 +195,6 @@ class TemplateNodeCreateView(SingleObjectCreateView): class TemplateNodeDeleteView(SingleObjectDeleteView): model = IndexTemplateNode object_permission = permission_document_indexing_edit - object_permission_related = 'index' def get_extra_context(self): return { @@ -218,7 +217,6 @@ class TemplateNodeEditView(SingleObjectEditView): form_class = IndexTemplateNodeForm model = IndexTemplateNode object_permission = permission_document_indexing_edit - object_permission_related = 'index' def get_extra_context(self): return { @@ -338,7 +336,6 @@ class DocumentIndexNodeListView(SingleObjectListView): Show a list of indexes where the current document can be found """ object_permission = permission_document_indexing_instance_view - object_permission_related = 'index' def dispatch(self, request, *args, **kwargs): AccessControlList.objects.check_access( diff --git a/mayan/apps/documents/views/document_version_views.py b/mayan/apps/documents/views/document_version_views.py index 4473ec41be..2e999435bd 100644 --- a/mayan/apps/documents/views/document_version_views.py +++ b/mayan/apps/documents/views/document_version_views.py @@ -55,7 +55,6 @@ class DocumentVersionListView(SingleObjectListView): class DocumentVersionRevertView(ConfirmView): object_permission = permission_document_version_revert - object_permission_related = 'document' def get_extra_context(self): return { From 890f8726813db5001374e85adce3730e297bed09 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Fri, 25 Jan 2019 01:30:05 -0400 Subject: [PATCH 055/209] Add keyword argument Signed-off-by: Roberto Rosario --- mayan/apps/mayan_statistics/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mayan/apps/mayan_statistics/models.py b/mayan/apps/mayan_statistics/models.py index 92b913efad..c6090e691e 100644 --- a/mayan/apps/mayan_statistics/models.py +++ b/mayan/apps/mayan_statistics/models.py @@ -25,7 +25,7 @@ class StatisticResult(models.Model): return self.slug def get_data(self): - return json.loads(self.serialize_data) + return json.loads(s=self.serialize_data) def store_data(self, data): self.serialize_data = json.dumps(data) From 4937d8b776dca9fa5774a98ff3e3a4c380d79a2d Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Fri, 25 Jan 2019 01:32:05 -0400 Subject: [PATCH 056/209] Update document signatures app Add keyword arguments. Remove source column functions and move their code to the model. Use the FilteredSelectionForm for the key selection in the document version signing view. Update the field definition of the DetailForm subclasses to use the new internface. Update URL parameters to use the "_id" form. Update views filtering to comply with MERC 6. Move repeated test code to its own test mixin. Update links to work with the new Link class interface. Modernize tests. Signed-off-by: Roberto Rosario --- mayan/apps/document_signatures/apps.py | 29 +- mayan/apps/document_signatures/forms.py | 94 ++--- mayan/apps/document_signatures/icons.py | 3 + mayan/apps/document_signatures/literals.py | 9 +- mayan/apps/document_signatures/models.py | 53 ++- mayan/apps/document_signatures/storages.py | 2 - .../apps/document_signatures/tests/mixins.py | 22 ++ .../document_signatures/tests/test_links.py | 83 ++--- .../document_signatures/tests/test_models.py | 223 ++++------- .../document_signatures/tests/test_views.py | 349 ++++++------------ mayan/apps/document_signatures/urls.py | 50 +-- mayan/apps/document_signatures/views.py | 174 ++++----- 12 files changed, 450 insertions(+), 641 deletions(-) create mode 100644 mayan/apps/document_signatures/tests/mixins.py diff --git a/mayan/apps/document_signatures/apps.py b/mayan/apps/document_signatures/apps.py index ae6feff669..55dd1dfd57 100644 --- a/mayan/apps/document_signatures/apps.py +++ b/mayan/apps/document_signatures/apps.py @@ -63,15 +63,15 @@ class DocumentSignaturesApp(MayanAppConfig): app_label='django_gpg', model_name='Key' ) - EmbeddedSignature = self.get_model('EmbeddedSignature') + EmbeddedSignature = self.get_model(model_name='EmbeddedSignature') - SignatureBaseModel = self.get_model('SignatureBaseModel') + SignatureBaseModel = self.get_model(model_name='SignatureBaseModel') DocumentVersion.register_post_save_hook( - order=1, func=EmbeddedSignature.objects.create + func=EmbeddedSignature.objects.create, order=1 ) DocumentVersion.register_pre_open_hook( - order=1, func=EmbeddedSignature.objects.open_signed + func=EmbeddedSignature.objects.open_signed, order=1 ) ModelPermission.register( @@ -85,22 +85,15 @@ class DocumentSignaturesApp(MayanAppConfig): ) ) - SourceColumn( - source=SignatureBaseModel, label=_('Date'), attribute='date' + ModelPermission.register_inheritance( + model=SignatureBaseModel, related='document_version' ) + + SourceColumn(attribute='date', source=SignatureBaseModel) + SourceColumn(attribute='get_key_id', source=SignatureBaseModel) + SourceColumn(attribute='get_signature_id', source=SignatureBaseModel) SourceColumn( - source=SignatureBaseModel, label=_('Key ID'), - attribute='get_key_id' - ) - SourceColumn( - source=SignatureBaseModel, label=_('Signature ID'), - func=lambda context: context['object'].signature_id or _('None') - ) - SourceColumn( - source=SignatureBaseModel, label=_('Type'), - func=lambda context: SignatureBaseModel.objects.get_subclass( - pk=context['object'].pk - ).get_signature_type_display() + attribute='get_signature_type_display', source=SignatureBaseModel ) app.conf.task_queues.append( diff --git a/mayan/apps/document_signatures/forms.py b/mayan/apps/document_signatures/forms.py index e0403a23c9..dfcfb08858 100644 --- a/mayan/apps/document_signatures/forms.py +++ b/mayan/apps/document_signatures/forms.py @@ -5,8 +5,7 @@ import logging from django import forms from django.utils.translation import ugettext_lazy as _ -from mayan.apps.acls.models import AccessControlList -from mayan.apps.common.forms import DetailForm +from mayan.apps.common.forms import DetailForm, FilteredSelectionForm from mayan.apps.django_gpg.models import Key from mayan.apps.django_gpg.permissions import permission_key_sign @@ -15,44 +14,39 @@ from .models import SignatureBaseModel logger = logging.getLogger(__name__) -class DocumentVersionSignatureCreateForm(forms.Form): +class DocumentVersionSignatureCreateForm(FilteredSelectionForm): key = forms.ModelChoiceField( label=_('Key'), queryset=Key.objects.none() ) passphrase = forms.CharField( - label=_('Passphrase'), required=False, + help_text=_( + 'The passphrase to unlock the key and allow it to be used to ' + 'sign the document version.' + ), label=_('Passphrase'), required=False, widget=forms.widgets.PasswordInput ) - def __init__(self, *args, **kwargs): - user = kwargs.pop('user', None) - logger.debug('user: %s', user) - super( - DocumentVersionSignatureCreateForm, self - ).__init__(*args, **kwargs) - - queryset = AccessControlList.objects.filter_by_access( - permission_key_sign, user, queryset=Key.objects.private_keys() + class Meta: + allow_multiple = False + field_name = 'key' + label = _('Key') + help_text = _( + 'Private key that will be used to sign this document version.' ) - - self.fields['key'].queryset = queryset + permission = permission_key_sign + queryset = Key.objects.private_keys() + required = True + widget_attributes = {'class': 'select2'} class DocumentVersionSignatureDetailForm(DetailForm): def __init__(self, *args, **kwargs): - extra_fields = ( - {'label': _('Signature is embedded?'), 'field': 'is_embedded'}, - { - 'label': _('Signature date'), 'field': 'date', - 'widget': forms.widgets.DateInput - }, - {'label': _('Signature key ID'), 'field': 'key_id'}, - { - 'label': _('Signature key present?'), - 'field': lambda x: x.public_key_fingerprint is not None - }, - ) + super( + DocumentVersionSignatureDetailForm, self + ).__init__(*args, **kwargs) + + extra_fields = self.Meta.extra_fields if kwargs['instance'].public_key_fingerprint: key = Key.objects.get( @@ -60,44 +54,52 @@ class DocumentVersionSignatureDetailForm(DetailForm): ) extra_fields += ( - {'label': _('Signature ID'), 'field': 'signature_id'}, + {'field': 'signature_id'}, { - 'label': _('Key fingerprint'), - 'field': lambda x: key.fingerprint + 'field': 'fingerprint', + 'object': key }, { - 'label': _('Key creation date'), - 'field': lambda x: key.creation_date, + 'field': 'creation_date', + 'object': key, 'widget': forms.widgets.DateInput }, { - 'label': _('Key expiration date'), - 'field': lambda x: key.expiration_date or _('None'), + 'field': 'get_expiration_date_display', + 'object': key, 'widget': forms.widgets.DateInput }, { - 'label': _('Key length'), - 'field': lambda x: key.length + 'field': 'length', + 'object': key }, { - 'label': _('Key algorithm'), - 'field': lambda x: key.algorithm + 'field': 'algorithm', + 'object': key }, { - 'label': _('Key user ID'), - 'field': lambda x: key.user_id + 'field': 'get_escaped_user_id', + 'object': key }, { - 'label': _('Key type'), - 'field': lambda x: key.get_key_type_display() + 'field': 'get_key_type_display', + 'object': key }, ) - kwargs['extra_fields'] = extra_fields - super( - DocumentVersionSignatureDetailForm, self - ).__init__(*args, **kwargs) + self.Meta.extra_fields = extra_fields class Meta: + extra_fields = ( + {'field': 'get_signature_type_display'}, + { + 'field': 'date', + 'widget': forms.widgets.DateInput + }, + {'field': 'key_id'}, + { + 'field': 'get_key_available_display' + }, + ) fields = () model = SignatureBaseModel diff --git a/mayan/apps/document_signatures/icons.py b/mayan/apps/document_signatures/icons.py index cb7ea37e33..63803faeb6 100644 --- a/mayan/apps/document_signatures/icons.py +++ b/mayan/apps/document_signatures/icons.py @@ -17,3 +17,6 @@ icon_document_version_signature_embedded_create = Icon( icon_document_version_signature_list = Icon( driver_name='fontawesome', symbol='certificate' ) +icon_document_version_signature_upload = Icon( + driver_name='fontawesome', symbol='upload' +) diff --git a/mayan/apps/document_signatures/literals.py b/mayan/apps/document_signatures/literals.py index f27ca1f9d8..d8d11c4694 100644 --- a/mayan/apps/document_signatures/literals.py +++ b/mayan/apps/document_signatures/literals.py @@ -1,3 +1,10 @@ from __future__ import unicode_literals -RETRY_DELAY = 10 +from django.utils.translation import ugettext_lazy as _ + +SIGNATURE_TYPE_DETACHED = 1 +SIGNATURE_TYPE_EMBEDDED = 2 +SIGNATURE_TYPE_CHOICES = ( + (SIGNATURE_TYPE_DETACHED, _('Detached')), + (SIGNATURE_TYPE_EMBEDDED, _('Embedded')), +) diff --git a/mayan/apps/document_signatures/models.py b/mayan/apps/document_signatures/models.py index 47e226f5ed..17ea367035 100644 --- a/mayan/apps/document_signatures/models.py +++ b/mayan/apps/document_signatures/models.py @@ -14,12 +14,16 @@ from mayan.apps.django_gpg.exceptions import VerificationError from mayan.apps.django_gpg.models import Key from mayan.apps.documents.models import DocumentVersion +from .literals import ( + SIGNATURE_TYPE_CHOICES, SIGNATURE_TYPE_DETACHED, SIGNATURE_TYPE_EMBEDDED +) from .managers import EmbeddedSignatureManager from .storages import storage_detachedsignature logger = logging.getLogger(__name__) +# TODO: Move to an utils module or as a static class of DetachedSignature def upload_to(*args, **kwargs): return force_text(uuid.uuid4()) @@ -65,8 +69,8 @@ class SignatureBaseModel(models.Model): def get_absolute_url(self): return reverse( - 'document_signatures:document_version_signature_detail', - args=(self.pk,) + viewname='document_signatures:document_version_signature_detail', + kwargs={'document_version_id': self.pk} ) def get_key_id(self): @@ -74,20 +78,41 @@ class SignatureBaseModel(models.Model): return self.public_key_fingerprint[-16:] else: return self.key_id + get_key_id.short_description = _('Key ID') + + def get_signature_id(self): + return self.signature_id or _('None') + get_signature_id.short_description = _('Signature ID') def get_signature_type_display(self): - if self.is_detached: - return _('Detached') + if hasattr(self, 'signaturebasemodel_ptr'): + model = self else: - return _('Embedded') + model = self._meta.default_manager.get_subclass(pk=self.pk) + + return dict(SIGNATURE_TYPE_CHOICES).get( + model.signature_type, _('Unknown') + ) + get_signature_type_display.short_description = _('Type') + + def is_key_available(self): + return self.public_key_fingerprint is not None + + def get_key_available_display(self): + if self.is_key_available(): + return _('Yes') + else: + return _('No') + get_key_available_display.short_description = _('Signature key present?') @property - def is_detached(self): - return hasattr(self, 'signature_file') + def signature_type(self): + if hasattr(self, 'signaturebasemodel_ptr'): + model = self + else: + model = self._meta.default_manager.get_subclass(pk=self.pk) - @property - def is_embedded(self): - return not hasattr(self, 'signature_file') + return model._signature_type class EmbeddedSignature(SignatureBaseModel): @@ -97,6 +122,10 @@ class EmbeddedSignature(SignatureBaseModel): verbose_name = _('Document version embedded signature') verbose_name_plural = _('Document version embedded signatures') + @property + def _signature_type(self): + return SIGNATURE_TYPE_EMBEDDED + def save(self, *args, **kwargs): logger.debug('checking for embedded signature') @@ -141,6 +170,10 @@ class DetachedSignature(SignatureBaseModel): def __str__(self): return '{}-{}'.format(self.document_version, _('signature')) + @property + def _signature_type(self): + return SIGNATURE_TYPE_DETACHED + def delete(self, *args, **kwargs): if self.signature_file.name: self.signature_file.storage.delete(name=self.signature_file.name) diff --git a/mayan/apps/document_signatures/storages.py b/mayan/apps/document_signatures/storages.py index 03c959df5a..1b88c818ca 100644 --- a/mayan/apps/document_signatures/storages.py +++ b/mayan/apps/document_signatures/storages.py @@ -1,7 +1,5 @@ from __future__ import unicode_literals -from django.utils.module_loading import import_string - from mayan.apps.common.utils import get_storage_subclass from .settings import ( diff --git a/mayan/apps/document_signatures/tests/mixins.py b/mayan/apps/document_signatures/tests/mixins.py new file mode 100644 index 0000000000..4b057c395c --- /dev/null +++ b/mayan/apps/document_signatures/tests/mixins.py @@ -0,0 +1,22 @@ +from __future__ import absolute_import, unicode_literals + +from django.core.files import File + +from mayan.apps.django_gpg.models import Key + +from ..models import DetachedSignature + +from .literals import TEST_KEY_FILE, TEST_SIGNATURE_FILE_PATH + + +class SignaturesTestMixin(object): + def _create_test_key(self): + with open(TEST_KEY_FILE, mode='rb') as file_object: + self.test_key = Key.objects.create(key_data=file_object.read()) + + def _upload_test_signature(self): + with open(TEST_SIGNATURE_FILE_PATH, mode='rb') as file_object: + self.test_signature = DetachedSignature.objects.create( + document_version=self.test_document.latest_version, + signature_file=File(file_object) + ) diff --git a/mayan/apps/document_signatures/tests/test_links.py b/mayan/apps/document_signatures/tests/test_links.py index e11e5fe157..13a659b6e6 100644 --- a/mayan/apps/document_signatures/tests/test_links.py +++ b/mayan/apps/document_signatures/tests/test_links.py @@ -1,6 +1,5 @@ from __future__ import unicode_literals -from django.core.files import File from django.urls import reverse from mayan.apps.documents.tests import ( @@ -11,28 +10,24 @@ from ..links import ( link_document_version_signature_delete, link_document_version_signature_details ) -from ..models import DetachedSignature from ..permissions import ( permission_document_version_signature_delete, permission_document_version_signature_view ) -from .literals import TEST_SIGNATURE_FILE_PATH, TEST_SIGNED_DOCUMENT_PATH +from .literals import TEST_SIGNED_DOCUMENT_PATH +from .mixins import SignaturesTestMixin -class DocumentSignatureLinksTestCase(GenericDocumentViewTestCase): - def setUp(self): - super(DocumentSignatureLinksTestCase, self).setUp() - self.login_user() +class DocumentSignatureLinksTestCase(SignaturesTestMixin, GenericDocumentViewTestCase): + auto_upload_document = False def test_document_version_signature_detail_link_no_permission(self): - with open(TEST_SIGNED_DOCUMENT_PATH, mode='rb') as file_object: - document = self.document_type.new_document( - file_object=file_object - ) + self.test_document_path = TEST_SIGNED_DOCUMENT_PATH + self.test_document = self.upload_document() self.add_test_view( - test_object=document.latest_version.signatures.first() + test_object=self.test_document.latest_version.signatures.first() ) context = self.get_test_view() resolved_link = link_document_version_signature_details.resolve( @@ -41,18 +36,17 @@ class DocumentSignatureLinksTestCase(GenericDocumentViewTestCase): self.assertEqual(resolved_link, None) - def test_document_version_signature_detail_link_with_permission(self): - with open(TEST_SIGNED_DOCUMENT_PATH, mode='rb') as file_object: - document = self.document_type.new_document( - file_object=file_object - ) + def test_document_version_signature_detail_link_with_access(self): + self.test_document_path = TEST_SIGNED_DOCUMENT_PATH + self.test_document = self.upload_document() - self.role.permissions.add( - permission_document_version_signature_view.stored_permission + self.grant_access( + obj=self.test_document, + permission=permission_document_version_signature_view ) self.add_test_view( - test_object=document.latest_version.signatures.first() + test_object=self.test_document.latest_version.signatures.first() ) context = self.get_test_view() resolved_link = link_document_version_signature_details.resolve( @@ -63,25 +57,20 @@ class DocumentSignatureLinksTestCase(GenericDocumentViewTestCase): self.assertEqual( resolved_link.url, reverse( - 'signatures:document_version_signature_details', - args=(document.latest_version.signatures.first().pk,) + viewname='signatures:document_version_signature_details', + kwargs={ + 'signature_id': self.test_document.latest_version.signatures.first().pk + } ) ) def test_document_version_signature_delete_link_no_permission(self): - with open(TEST_DOCUMENT_PATH, mode='rb') as file_object: - document = self.document_type.new_document( - file_object=file_object - ) - - with open(TEST_SIGNATURE_FILE_PATH, mode='rb') as file_object: - DetachedSignature.objects.create( - document_version=document.latest_version, - signature_file=File(file_object) - ) + self.test_document_path = TEST_DOCUMENT_PATH + self.test_document = self.upload_document() + self._upload_test_signature() self.add_test_view( - test_object=document.latest_version.signatures.first() + test_object=self.test_document.latest_version.signatures.first() ) context = self.get_test_view() resolved_link = link_document_version_signature_delete.resolve( @@ -90,24 +79,18 @@ class DocumentSignatureLinksTestCase(GenericDocumentViewTestCase): self.assertEqual(resolved_link, None) - def test_document_version_signature_delete_link_with_permission(self): - with open(TEST_DOCUMENT_PATH, mode='rb') as file_object: - document = self.document_type.new_document( - file_object=file_object - ) + def test_document_version_signature_delete_link_with_access(self): + self.test_document_path = TEST_DOCUMENT_PATH + self.test_document = self.upload_document() + self._upload_test_signature() - with open(TEST_SIGNATURE_FILE_PATH, mode='rb') as file_object: - DetachedSignature.objects.create( - document_version=document.latest_version, - signature_file=File(file_object) - ) - - self.role.permissions.add( - permission_document_version_signature_delete.stored_permission + self.grant_access( + obj=self.test_document, + permission=permission_document_version_signature_delete ) self.add_test_view( - test_object=document.latest_version.signatures.first() + test_object=self.test_document.latest_version.signatures.first() ) context = self.get_test_view() resolved_link = link_document_version_signature_delete.resolve( @@ -118,7 +101,9 @@ class DocumentSignatureLinksTestCase(GenericDocumentViewTestCase): self.assertEqual( resolved_link.url, reverse( - 'signatures:document_version_signature_delete', - args=(document.latest_version.signatures.first().pk,) + viewname='signatures:document_version_signature_delete', + kwargs={ + 'signature_id': self.test_document.latest_version.signatures.first().pk + } ) ) diff --git a/mayan/apps/document_signatures/tests/test_models.py b/mayan/apps/document_signatures/tests/test_models.py index 87bce78057..09a287297e 100644 --- a/mayan/apps/document_signatures/tests/test_models.py +++ b/mayan/apps/document_signatures/tests/test_models.py @@ -4,110 +4,86 @@ import hashlib import logging import time -from django.core.files import File - -from mayan.apps.common.tests import BaseTestCase from mayan.apps.django_gpg.models import Key from mayan.apps.django_gpg.tests.literals import ( TEST_KEY_DATA, TEST_KEY_PASSPHRASE ) -from mayan.apps.documents.models import DocumentType, DocumentVersion +from mayan.apps.documents.models import DocumentVersion from mayan.apps.documents.tests import ( - TEST_DOCUMENT_PATH, TEST_DOCUMENT_TYPE_LABEL + GenericDocumentTestCase, TEST_DOCUMENT_PATH, ) from ..models import DetachedSignature, EmbeddedSignature from ..tasks import task_verify_missing_embedded_signature -from .literals import ( - TEST_KEY_FILE, TEST_KEY_ID, TEST_SIGNATURE_FILE_PATH, TEST_SIGNATURE_ID, - TEST_SIGNED_DOCUMENT_PATH -) +from .literals import TEST_KEY_ID, TEST_SIGNATURE_ID, TEST_SIGNED_DOCUMENT_PATH +from .mixins import SignaturesTestMixin -class DocumentSignaturesTestCase(BaseTestCase): - def setUp(self): - super(DocumentSignaturesTestCase, self).setUp() - self.document_type = DocumentType.objects.create( - label=TEST_DOCUMENT_TYPE_LABEL - ) - - def tearDown(self): - self.document_type.delete() - super(DocumentSignaturesTestCase, self).tearDown() +class DocumentSignaturesTestCase(SignaturesTestMixin, GenericDocumentTestCase): + auto_upload_document = False def test_embedded_signature_no_key(self): - with open(TEST_SIGNED_DOCUMENT_PATH, mode='rb') as file_object: - signed_document = self.document_type.new_document( - file_object=file_object - ) + self.test_document_path = TEST_SIGNED_DOCUMENT_PATH + self.test_signed_document = self.upload_document() self.assertEqual(EmbeddedSignature.objects.count(), 1) signature = EmbeddedSignature.objects.first() self.assertEqual( - signature.document_version, signed_document.latest_version + signature.document_version, self.test_signed_document.latest_version ) self.assertEqual(signature.key_id, TEST_KEY_ID) self.assertEqual(signature.signature_id, None) def test_embedded_signature_post_key_verify(self): - with open(TEST_SIGNED_DOCUMENT_PATH, mode='rb') as file_object: - signed_document = self.document_type.new_document( - file_object=file_object - ) + self.test_document_path = TEST_SIGNED_DOCUMENT_PATH + self.test_signed_document = self.upload_document() self.assertEqual(EmbeddedSignature.objects.count(), 1) signature = EmbeddedSignature.objects.first() self.assertEqual( - signature.document_version, signed_document.latest_version + signature.document_version, self.test_signed_document.latest_version ) self.assertEqual(signature.key_id, TEST_KEY_ID) self.assertEqual(signature.signature_id, None) - with open(TEST_KEY_FILE, mode='rb') as file_object: - Key.objects.create(key_data=file_object.read()) + self._create_test_key() signature = EmbeddedSignature.objects.first() self.assertEqual(signature.signature_id, TEST_SIGNATURE_ID) def test_embedded_signature_post_no_key_verify(self): - with open(TEST_KEY_FILE, mode='rb') as file_object: - key = Key.objects.create(key_data=file_object.read()) + self._create_test_key() - with open(TEST_SIGNED_DOCUMENT_PATH, mode='rb') as file_object: - signed_document = self.document_type.new_document( - file_object=file_object - ) + self.test_document_path = TEST_SIGNED_DOCUMENT_PATH + self.test_signed_document = self.upload_document() self.assertEqual(EmbeddedSignature.objects.count(), 1) signature = EmbeddedSignature.objects.first() self.assertEqual( - signature.document_version, signed_document.latest_version + signature.document_version, self.test_signed_document.latest_version ) self.assertEqual(signature.key_id, TEST_KEY_ID) self.assertEqual(signature.signature_id, TEST_SIGNATURE_ID) - key.delete() + self.test_key.delete() signature = EmbeddedSignature.objects.first() self.assertEqual(signature.signature_id, None) def test_embedded_signature_with_key(self): - with open(TEST_KEY_FILE, mode='rb') as file_object: - key = Key.objects.create(key_data=file_object.read()) + self._create_test_key() - with open(TEST_SIGNED_DOCUMENT_PATH, mode='rb') as file_object: - self.signed_document = self.document_type.new_document( - file_object=file_object - ) + self.test_document_path = TEST_SIGNED_DOCUMENT_PATH + self.test_signed_document = self.upload_document() self.assertEqual(EmbeddedSignature.objects.count(), 1) @@ -115,127 +91,109 @@ class DocumentSignaturesTestCase(BaseTestCase): self.assertEqual( signature.document_version, - self.signed_document.latest_version + self.test_signed_document.latest_version ) self.assertEqual(signature.key_id, TEST_KEY_ID) - self.assertEqual(signature.public_key_fingerprint, key.fingerprint) + self.assertEqual( + signature.public_key_fingerprint, self.test_key.fingerprint + ) self.assertEqual(signature.signature_id, TEST_SIGNATURE_ID) def test_detached_signature_no_key(self): - with open(TEST_DOCUMENT_PATH, mode='rb') as file_object: - document = self.document_type.new_document( - file_object=file_object - ) + self.test_document_path = TEST_DOCUMENT_PATH + self.test_document = self.upload_document() - with open(TEST_SIGNATURE_FILE_PATH, mode='rb') as file_object: - DetachedSignature.objects.create( - document_version=document.latest_version, - signature_file=File(file_object) - ) + self._upload_test_signature() self.assertEqual(DetachedSignature.objects.count(), 1) signature = DetachedSignature.objects.first() - self.assertEqual(signature.document_version, document.latest_version) + self.assertEqual( + signature.document_version, self.test_document.latest_version + ) self.assertEqual(signature.key_id, TEST_KEY_ID) self.assertEqual(signature.public_key_fingerprint, None) def test_detached_signature_with_key(self): - with open(TEST_KEY_FILE, mode='rb') as file_object: - key = Key.objects.create(key_data=file_object.read()) + self._create_test_key() - with open(TEST_DOCUMENT_PATH, mode='rb') as file_object: - document = self.document_type.new_document( - file_object=file_object - ) - - with open(TEST_SIGNATURE_FILE_PATH, mode='rb') as file_object: - DetachedSignature.objects.create( - document_version=document.latest_version, - signature_file=File(file_object) - ) + self.test_document_path = TEST_DOCUMENT_PATH + self.test_document = self.upload_document() + self._upload_test_signature() self.assertEqual(DetachedSignature.objects.count(), 1) signature = DetachedSignature.objects.first() - self.assertEqual(signature.document_version, document.latest_version) + self.assertEqual( + signature.document_version, self.test_document.latest_version + ) self.assertEqual(signature.key_id, TEST_KEY_ID) - self.assertEqual(signature.public_key_fingerprint, key.fingerprint) + self.assertEqual( + signature.public_key_fingerprint, self.test_key.fingerprint + ) def test_detached_signature_post_key_verify(self): - with open(TEST_DOCUMENT_PATH, mode='rb') as file_object: - document = self.document_type.new_document( - file_object=file_object - ) - - with open(TEST_SIGNATURE_FILE_PATH, mode='rb') as file_object: - DetachedSignature.objects.create( - document_version=document.latest_version, - signature_file=File(file_object) - ) + self.test_document_path = TEST_DOCUMENT_PATH + self.test_document = self.upload_document() + self._upload_test_signature() self.assertEqual(DetachedSignature.objects.count(), 1) signature = DetachedSignature.objects.first() - self.assertEqual(signature.document_version, document.latest_version) + self.assertEqual( + signature.document_version, self.test_document.latest_version + ) self.assertEqual(signature.key_id, TEST_KEY_ID) self.assertEqual(signature.public_key_fingerprint, None) - with open(TEST_KEY_FILE, mode='rb') as file_object: - key = Key.objects.create(key_data=file_object.read()) + self._create_test_key() signature = DetachedSignature.objects.first() - self.assertEqual(signature.public_key_fingerprint, key.fingerprint) + self.assertEqual( + signature.public_key_fingerprint, self.test_key.fingerprint + ) def test_detached_signature_post_no_key_verify(self): - with open(TEST_KEY_FILE, mode='rb') as file_object: - key = Key.objects.create(key_data=file_object.read()) + self._create_test_key() - with open(TEST_DOCUMENT_PATH, mode='rb') as file_object: - document = self.document_type.new_document( - file_object=file_object - ) - - with open(TEST_SIGNATURE_FILE_PATH, mode='rb') as file_object: - DetachedSignature.objects.create( - document_version=document.latest_version, - signature_file=File(file_object) - ) + self.test_document_path = TEST_DOCUMENT_PATH + self.test_document = self.upload_document() + self._upload_test_signature() self.assertEqual(DetachedSignature.objects.count(), 1) signature = DetachedSignature.objects.first() - self.assertEqual(signature.document_version, document.latest_version) + self.assertEqual( + signature.document_version, self.test_document.latest_version + ) self.assertEqual(signature.key_id, TEST_KEY_ID) - self.assertEqual(signature.public_key_fingerprint, key.fingerprint) + self.assertEqual( + signature.public_key_fingerprint, self.test_key.fingerprint + ) - key.delete() + self.test_key.delete() signature = DetachedSignature.objects.first() self.assertEqual(signature.public_key_fingerprint, None) def test_document_no_signature(self): - with open(TEST_DOCUMENT_PATH, mode='rb') as file_object: - self.document_type.new_document( - file_object=file_object - ) + self.test_document_path = TEST_DOCUMENT_PATH + self.upload_document() self.assertEqual(EmbeddedSignature.objects.count(), 0) def test_new_signed_version(self): - with open(TEST_DOCUMENT_PATH, mode='rb') as file_object: - document = self.document_type.new_document( - file_object=file_object - ) + self.test_document_path = TEST_DOCUMENT_PATH + self.test_document = self.upload_document() with open(TEST_SIGNED_DOCUMENT_PATH, mode='rb') as file_object: - signed_version = document.new_version( + signed_version = self.test_document.new_version( file_object=file_object, comment='test comment 1' ) @@ -252,33 +210,20 @@ class DocumentSignaturesTestCase(BaseTestCase): self.assertEqual(signature.key_id, TEST_KEY_ID) -class EmbeddedSignaturesTestCase(BaseTestCase): - def setUp(self): - super(EmbeddedSignaturesTestCase, self).setUp() - - self.document_type = DocumentType.objects.create( - label=TEST_DOCUMENT_TYPE_LABEL - ) - - def tearDown(self): - self.document_type.delete() - super(EmbeddedSignaturesTestCase, self).tearDown() +class EmbeddedSignaturesTestCase(GenericDocumentTestCase): + auto_upload_document = False def test_unsigned_document_version_method(self): TEST_UNSIGNED_DOCUMENT_COUNT = 2 TEST_SIGNED_DOCUMENT_COUNT = 2 for count in range(TEST_UNSIGNED_DOCUMENT_COUNT): - with open(TEST_DOCUMENT_PATH, mode='rb') as file_object: - self.document_type.new_document( - file_object=file_object - ) + self.test_document_path = TEST_DOCUMENT_PATH + self.upload_document() for count in range(TEST_SIGNED_DOCUMENT_COUNT): - with open(TEST_SIGNED_DOCUMENT_PATH, mode='rb') as file_object: - self.document_type.new_document( - file_object=file_object - ) + self.test_document_path = TEST_SIGNED_DOCUMENT_PATH + self.upload_document() self.assertEqual( EmbeddedSignature.objects.unsigned_document_versions().count(), @@ -299,16 +244,12 @@ class EmbeddedSignaturesTestCase(BaseTestCase): TEST_SIGNED_DOCUMENT_COUNT = 2 for count in range(TEST_UNSIGNED_DOCUMENT_COUNT): - with open(TEST_DOCUMENT_PATH, mode='rb') as file_object: - self.document_type.new_document( - file_object=file_object - ) + self.test_document_path = TEST_DOCUMENT_PATH + self.upload_document() for count in range(TEST_SIGNED_DOCUMENT_COUNT): - with open(TEST_SIGNED_DOCUMENT_PATH, mode='rb') as file_object: - self.document_type.new_document( - file_object=file_object - ) + self.test_document_path = TEST_SIGNED_DOCUMENT_PATH + self.upload_document() self.assertEqual( EmbeddedSignature.objects.unsigned_document_versions().count(), @@ -327,19 +268,17 @@ class EmbeddedSignaturesTestCase(BaseTestCase): def test_signing(self): key = Key.objects.create(key_data=TEST_KEY_DATA) - with open(TEST_DOCUMENT_PATH, mode='rb') as file_object: - document = self.document_type.new_document( - file_object=file_object - ) + self.test_document_path = TEST_DOCUMENT_PATH + self.test_document = self.upload_document() - with document.latest_version.open() as file_object: + with self.test_document.latest_version.open() as file_object: file_object.seek(0, 2) original_size = file_object.tell() file_object.seek(0) original_hash = hashlib.sha256(file_object.read()).hexdigest() new_version = EmbeddedSignature.objects.sign_document_version( - document_version=document.latest_version, key=key, + document_version=self.test_document.latest_version, key=key, passphrase=TEST_KEY_PASSPHRASE ) diff --git a/mayan/apps/document_signatures/tests/test_views.py b/mayan/apps/document_signatures/tests/test_views.py index 630f12f906..b60d2aa06a 100644 --- a/mayan/apps/document_signatures/tests/test_views.py +++ b/mayan/apps/document_signatures/tests/test_views.py @@ -2,11 +2,8 @@ from __future__ import absolute_import, unicode_literals import logging -from django.core.files import File - from django_downloadview.test import assert_download_response -from mayan.apps.django_gpg.models import Key from mayan.apps.documents.models import DocumentVersion from mayan.apps.documents.tests import ( TEST_DOCUMENT_PATH, GenericDocumentViewTestCase @@ -21,290 +18,172 @@ from ..permissions import ( permission_document_version_signature_view ) -from .literals import ( - TEST_KEY_FILE, TEST_SIGNATURE_FILE_PATH, TEST_SIGNED_DOCUMENT_PATH -) +from .literals import TEST_SIGNATURE_FILE_PATH, TEST_SIGNED_DOCUMENT_PATH +from .mixins import SignaturesTestMixin TEST_UNSIGNED_DOCUMENT_COUNT = 4 TEST_SIGNED_DOCUMENT_COUNT = 2 -class SignaturesViewTestCase(GenericDocumentViewTestCase): - def _request_document_version_signature_list_view(self, document): - return self.get( - viewname='signatures:document_version_signature_list', - args=(document.latest_version.pk,) +class SignaturesViewTestCase(SignaturesTestMixin, GenericDocumentViewTestCase): + def _request_document_version_signature_delete_view(self): + return self.post( + viewname='signatures:document_version_signature_delete', + kwargs={'signature_id': self.test_signature.pk} ) - def test_signature_list_view_no_permission(self): - with open(TEST_KEY_FILE, mode='rb') as file_object: - Key.objects.create(key_data=file_object.read()) + def test_signature_delete_view_no_permission(self): + self._create_test_key() + self.test_document_path = TEST_DOCUMENT_PATH + self.test_document = self.upload_document() + self._upload_test_signature() - with open(TEST_DOCUMENT_PATH, mode='rb') as file_object: - document = self.document_type.new_document( - file_object=file_object - ) + response = self._request_document_version_signature_delete_view() + self.assertEqual(response.status_code, 404) + self.assertEqual(DetachedSignature.objects.count(), 1) - with open(TEST_SIGNATURE_FILE_PATH, mode='rb') as file_object: - DetachedSignature.objects.create( - document_version=document.latest_version, - signature_file=File(file_object) - ) - - self.login_user() - - response = self._request_document_version_signature_list_view( - document=document - ) - self.assertEqual(response.status_code, 403) - - def test_signature_list_view_with_access(self): - with open(TEST_KEY_FILE, mode='rb') as file_object: - Key.objects.create(key_data=file_object.read()) - - with open(TEST_DOCUMENT_PATH, mode='rb') as file_object: - document = self.document_type.new_document( - file_object=file_object - ) - - with open(TEST_SIGNATURE_FILE_PATH, mode='rb') as file_object: - DetachedSignature.objects.create( - document_version=document.latest_version, - signature_file=File(file_object) - ) - - self.login_user() + def test_signature_delete_view_with_access(self): + self._create_test_key() + self.test_document_path = TEST_DOCUMENT_PATH + self.test_document = self.upload_document() + self._upload_test_signature() self.grant_access( - obj=document, - permission=permission_document_version_signature_view + obj=self.test_document, + permission=permission_document_version_signature_delete ) + response = self._request_document_version_signature_delete_view() + self.assertEqual(response.status_code, 302) + self.assertEqual(DetachedSignature.objects.count(), 0) - response = self._request_document_version_signature_list_view( - document=document - ) - self.assertEqual(response.status_code, 200) - self.assertEqual(response.context['object_list'].count(), 1) - - def _request_document_version_signature_details_view(self, signature): + def _request_document_version_signature_details_view(self): return self.get( viewname='signatures:document_version_signature_details', - args=(signature.pk,) + kwargs={'signature_id': self.test_signature.pk} ) def test_signature_detail_view_no_permission(self): - with open(TEST_KEY_FILE, mode='rb') as file_object: - Key.objects.create(key_data=file_object.read()) + self._create_test_key() + self.test_document_path = TEST_DOCUMENT_PATH + self.test_document = self.upload_document() + self._upload_test_signature() - with open(TEST_DOCUMENT_PATH, mode='rb') as file_object: - document = self.document_type.new_document( - file_object=file_object - ) - - with open(TEST_SIGNATURE_FILE_PATH, mode='rb') as file_object: - signature = DetachedSignature.objects.create( - document_version=document.latest_version, - signature_file=File(file_object) - ) - - self.login_user() - - response = self._request_document_version_signature_details_view( - signature=signature - ) - self.assertEqual(response.status_code, 403) + response = self._request_document_version_signature_details_view() + self.assertEqual(response.status_code, 404) def test_signature_detail_view_with_access(self): - with open(TEST_KEY_FILE, mode='rb') as file_object: - Key.objects.create(key_data=file_object.read()) - - with open(TEST_DOCUMENT_PATH, mode='rb') as file_object: - document = self.document_type.new_document( - file_object=file_object - ) - - with open(TEST_SIGNATURE_FILE_PATH, mode='rb') as file_object: - signature = DetachedSignature.objects.create( - document_version=document.latest_version, - signature_file=File(file_object) - ) - - self.login_user() - + self._create_test_key() + self.test_document_path = TEST_DOCUMENT_PATH + self.test_document = self.upload_document() + self._upload_test_signature() self.grant_access( - obj=document, + obj=self.test_document, permission=permission_document_version_signature_view ) - response = self._request_document_version_signature_details_view( - signature=signature - ) + response = self._request_document_version_signature_details_view() self.assertContains( - response=response, text=signature.signature_id, status_code=200 + response=response, text=self.test_signature.signature_id, + status_code=200 ) - def _request_document_version_signature_upload_view(self, document_version): - with open(TEST_SIGNATURE_FILE_PATH, mode='rb') as file_object: - return self.post( - viewname='signatures:document_version_signature_upload', - args=(document_version.pk,), - data={'signature_file': file_object} - ) - - def test_signature_upload_view_no_permission(self): - with open(TEST_DOCUMENT_PATH, mode='rb') as file_object: - document = self.document_type.new_document( - file_object=file_object - ) - - self.login_user() - - response = self._request_document_version_signature_upload_view( - document_version=document.latest_version - ) - self.assertEqual(response.status_code, 403) - self.assertEqual(DetachedSignature.objects.count(), 0) - - def test_signature_upload_view_with_access(self): - with open(TEST_DOCUMENT_PATH, mode='rb') as file_object: - document = self.document_type.new_document( - file_object=file_object - ) - - self.login_user() - - self.grant_access( - obj=document, - permission=permission_document_version_signature_upload - ) - - response = self._request_document_version_signature_upload_view( - document_version=document.latest_version - ) - self.assertEqual(response.status_code, 302) - self.assertEqual(DetachedSignature.objects.count(), 1) - - def _request_document_version_signature_download_view(self, signature): + def _request_document_version_signature_download_view(self): return self.get( viewname='signatures:document_version_signature_download', - args=(signature.pk,), + kwargs={'signature_id': self.test_signature.pk} ) def test_signature_download_view_no_permission(self): - with open(TEST_DOCUMENT_PATH, mode='rb') as file_object: - document = self.document_type.new_document( - file_object=file_object - ) + self.test_document_path = TEST_DOCUMENT_PATH + self.test_document = self.upload_document() + self._upload_test_signature() - with open(TEST_SIGNATURE_FILE_PATH, mode='rb') as file_object: - signature = DetachedSignature.objects.create( - document_version=document.latest_version, - signature_file=File(file_object) - ) - - self.login_user() - - response = self._request_document_version_signature_download_view( - signature=signature - ) - self.assertEqual(response.status_code, 403) + response = self._request_document_version_signature_download_view() + self.assertEqual(response.status_code, 404) def test_signature_download_view_with_access(self): - with open(TEST_DOCUMENT_PATH, mode='rb') as file_object: - document = self.document_type.new_document( - file_object=file_object - ) + self.test_document_path = TEST_DOCUMENT_PATH + self.test_document = self.upload_document() - with open(TEST_SIGNATURE_FILE_PATH, mode='rb') as file_object: - signature = DetachedSignature.objects.create( - document_version=document.latest_version, - signature_file=File(file_object) - ) - - self.login_user() + self._upload_test_signature() self.grant_access( - obj=document, + obj=self.test_document, permission=permission_document_version_signature_download ) self.expected_content_type = 'application/octet-stream; charset=utf-8' - response = self._request_document_version_signature_download_view( - signature=signature - ) + response = self._request_document_version_signature_download_view() - with signature.signature_file as file_object: + with self.test_signature.signature_file as file_object: assert_download_response( self, response=response, content=file_object.read(), ) - def _request_document_version_signature_delete_view(self, signature): - return self.post( - viewname='signatures:document_version_signature_delete', - args=(signature.pk,) + def _request_document_version_signature_list_view(self): + return self.get( + viewname='signatures:document_version_signature_list', + kwargs={'document_version_id': self.test_document.latest_version.pk} ) - def test_signature_delete_view_no_permission(self): - with open(TEST_KEY_FILE, mode='rb') as file_object: - Key.objects.create(key_data=file_object.read()) + def test_signature_list_view_no_permission(self): + self._create_test_key() - with open(TEST_DOCUMENT_PATH, mode='rb') as file_object: - document = self.document_type.new_document( - file_object=file_object - ) + self.test_document_path = TEST_DOCUMENT_PATH + self.test_document = self.upload_document() - with open(TEST_SIGNATURE_FILE_PATH, mode='rb') as file_object: - signature = DetachedSignature.objects.create( - document_version=document.latest_version, - signature_file=File(file_object) - ) + self._upload_test_signature() - self.login_user() + response = self._request_document_version_signature_list_view() + self.assertEqual(response.status_code, 404) + + def test_signature_list_view_with_access(self): + self._create_test_key() + + self.test_document_path = TEST_DOCUMENT_PATH + self.test_document = self.upload_document() + + self._upload_test_signature() self.grant_access( - obj=document, + obj=self.test_document, permission=permission_document_version_signature_view ) - response = self._request_document_version_signature_delete_view( - signature=signature - ) - self.assertEqual(response.status_code, 403) - self.assertEqual(DetachedSignature.objects.count(), 1) - - def test_signature_delete_view_with_access(self): - with open(TEST_KEY_FILE, mode='rb') as file_object: - Key.objects.create(key_data=file_object.read()) - - with open(TEST_DOCUMENT_PATH, mode='rb') as file_object: - document = self.document_type.new_document( - file_object=file_object - ) + response = self._request_document_version_signature_list_view() + self.assertEqual(response.status_code, 200) + self.assertEqual(response.context['object_list'].count(), 1) + def _request_document_version_signature_upload_view(self): with open(TEST_SIGNATURE_FILE_PATH, mode='rb') as file_object: - signature = DetachedSignature.objects.create( - document_version=document.latest_version, - signature_file=File(file_object) + return self.post( + viewname='signatures:document_version_signature_upload', + kwargs={'document_version_id': self.test_document.latest_version.pk}, + data={'signature_file': file_object} ) - self.login_user() + def test_signature_upload_view_no_permission(self): + self.test_document_path = TEST_DOCUMENT_PATH + self.test_document = self.upload_document() - self.grant_access( - obj=document, - permission=permission_document_version_signature_delete - ) - self.grant_access( - obj=document, - permission=permission_document_version_signature_view - ) - - response = self._request_document_version_signature_delete_view( - signature=signature - ) - self.assertEqual(response.status_code, 302) + response = self._request_document_version_signature_upload_view() + self.assertEqual(response.status_code, 404) self.assertEqual(DetachedSignature.objects.count(), 0) + def test_signature_upload_view_with_access(self): + self.test_document_path = TEST_DOCUMENT_PATH + self.test_document = self.upload_document() + + self.grant_access( + obj=self.test_document, + permission=permission_document_version_signature_upload + ) + + response = self._request_document_version_signature_upload_view() + self.assertEqual(response.status_code, 302) + self.assertEqual(DetachedSignature.objects.count(), 1) + def _request_all_document_version_signature_verify_view(self): return self.post( viewname='signatures:all_document_version_signature_verify' @@ -322,16 +201,12 @@ class SignaturesViewTestCase(GenericDocumentViewTestCase): old_hooks = DocumentVersion._post_save_hooks DocumentVersion._post_save_hooks = {} for count in range(TEST_UNSIGNED_DOCUMENT_COUNT): - with open(TEST_DOCUMENT_PATH, mode='rb') as file_object: - self.document_type.new_document( - file_object=file_object - ) + self.test_document_path = TEST_DOCUMENT_PATH + self.upload_document() for count in range(TEST_SIGNED_DOCUMENT_COUNT): - with open(TEST_SIGNED_DOCUMENT_PATH, mode='rb') as file_object: - self.document_type.new_document( - file_object=file_object - ) + self.test_document_path = TEST_SIGNED_DOCUMENT_PATH + self.upload_document() self.assertEqual( EmbeddedSignature.objects.unsigned_document_versions().count(), @@ -340,8 +215,6 @@ class SignaturesViewTestCase(GenericDocumentViewTestCase): DocumentVersion._post_save_hooks = old_hooks - self.login_user() - response = self._request_all_document_version_signature_verify_view() self.assertEqual(response.status_code, 403) @@ -362,16 +235,12 @@ class SignaturesViewTestCase(GenericDocumentViewTestCase): old_hooks = DocumentVersion._post_save_hooks DocumentVersion._post_save_hooks = {} for count in range(TEST_UNSIGNED_DOCUMENT_COUNT): - with open(TEST_DOCUMENT_PATH, mode='rb') as file_object: - self.document_type.new_document( - file_object=file_object - ) + self.test_document_path = TEST_DOCUMENT_PATH + self.upload_document() for count in range(TEST_SIGNED_DOCUMENT_COUNT): - with open(TEST_SIGNED_DOCUMENT_PATH, mode='rb') as file_object: - self.document_type.new_document( - file_object=file_object - ) + self.test_document_path = TEST_SIGNED_DOCUMENT_PATH + self.upload_document() self.assertEqual( EmbeddedSignature.objects.unsigned_document_versions().count(), @@ -380,8 +249,6 @@ class SignaturesViewTestCase(GenericDocumentViewTestCase): DocumentVersion._post_save_hooks = old_hooks - self.login_user() - self.grant_permission( permission=permission_document_version_signature_verify ) diff --git a/mayan/apps/document_signatures/urls.py b/mayan/apps/document_signatures/urls.py index 0756750877..5f2383ca70 100644 --- a/mayan/apps/document_signatures/urls.py +++ b/mayan/apps/document_signatures/urls.py @@ -12,43 +12,43 @@ from .views import ( urlpatterns = [ url( - r'^(?P\d+)/details/$', - DocumentVersionSignatureDetailView.as_view(), - name='document_version_signature_details' + regex=r'^signatures/(?P\d+)/$', + name='document_version_signature_details', + view=DocumentVersionSignatureDetailView.as_view() ), url( - r'^signature/(?P\d+)/download/$', - DocumentVersionSignatureDownloadView.as_view(), - name='document_version_signature_download' + regex=r'^signatures/(?P\d+)/download/$', + name='document_version_signature_download', + view=DocumentVersionSignatureDownloadView.as_view() ), url( - r'^document/version/(?P\d+)/signatures/list/$', - DocumentVersionSignatureListView.as_view(), - name='document_version_signature_list' + regex=r'^signatures/(?P\d+)/delete/$', + name='document_version_signature_delete', + view=DocumentVersionSignatureDeleteView.as_view() ), url( - r'^documents/version/(?P\d+)/signature/detached/upload/$', - DocumentVersionSignatureUploadView.as_view(), - name='document_version_signature_upload' + regex=r'^documents/versions/(?P\d+)/signatures/$', + name='document_version_signature_list', + view=DocumentVersionSignatureListView.as_view() ), url( - r'^documents/version/(?P\d+)/signature/detached/create/$', - DocumentVersionDetachedSignatureCreateView.as_view(), - name='document_version_signature_detached_create' + regex=r'^documents/versions/(?P\d+)/signatures/detached/create/$', + name='document_version_signature_detached_create', + view=DocumentVersionDetachedSignatureCreateView.as_view() ), url( - r'^documents/version/(?P\d+)/signature/embedded/create/$', - DocumentVersionEmbeddedSignatureCreateView.as_view(), - name='document_version_signature_embedded_create' + regex=r'^documents/versions/(?P\d+)/signatures/detached/upload/$', + name='document_version_signature_upload', + view=DocumentVersionSignatureUploadView.as_view() ), url( - r'^signature/(?P\d+)/delete/$', - DocumentVersionSignatureDeleteView.as_view(), - name='document_version_signature_delete' + regex=r'^documents/versions/(?P\d+)/signatures/embedded/create/$', + name='document_version_signature_embedded_create', + view=DocumentVersionEmbeddedSignatureCreateView.as_view() ), url( - r'^tools/all/document/version/signature/verify/$', - AllDocumentSignatureVerifyView.as_view(), - name='all_document_version_signature_verify' - ), + regex=r'^tools/documents/versions/signatures/verify/$', + name='all_document_version_signature_verify', + view=AllDocumentSignatureVerifyView.as_view() + ) ] diff --git a/mayan/apps/document_signatures/views.py b/mayan/apps/document_signatures/views.py index 4be6427c45..e77f4aa261 100644 --- a/mayan/apps/document_signatures/views.py +++ b/mayan/apps/document_signatures/views.py @@ -5,20 +5,18 @@ import logging from django.contrib import messages from django.core.files import File from django.http import HttpResponseRedirect -from django.shortcuts import get_object_or_404 from django.template import RequestContext from django.urls import reverse from django.utils.encoding import force_text from django.utils.translation import ugettext_lazy as _ -from mayan.apps.acls.models import AccessControlList from mayan.apps.common.generics import ( ConfirmView, FormView, SingleObjectCreateView, SingleObjectDeleteView, SingleObjectDetailView, SingleObjectDownloadView, SingleObjectListView ) +from mayan.apps.common.mixins import ExternalObjectMixin from mayan.apps.common.utils import TemporaryFile from mayan.apps.django_gpg.exceptions import NeedPassphrase, PassphraseError -from mayan.apps.django_gpg.permissions import permission_key_sign from mayan.apps.documents.models import DocumentVersion from .forms import ( @@ -45,17 +43,16 @@ from .tasks import task_verify_missing_embedded_signature logger = logging.getLogger(__name__) -class DocumentVersionDetachedSignatureCreateView(FormView): +class DocumentVersionDetachedSignatureCreateView(ExternalObjectMixin, FormView): + external_object_class = DocumentVersion + external_object_permission = permission_document_version_sign_detached + external_object_pk_url_kwarg = 'document_version_id' form_class = DocumentVersionSignatureCreateForm def form_valid(self, form): key = form.cleaned_data['key'] passphrase = form.cleaned_data['passphrase'] or None - AccessControlList.objects.check_access( - permissions=permission_key_sign, user=self.request.user, obj=key - ) - try: with self.get_document_version().open() as file_object: detached_signature = key.sign_file( @@ -64,22 +61,23 @@ class DocumentVersionDetachedSignatureCreateView(FormView): ) except NeedPassphrase: messages.error( - self.request, _('Passphrase is needed to unlock this key.') + message=_('Passphrase is needed to unlock this key.'), + request=self.request ) return HttpResponseRedirect( - reverse( - 'signatures:document_version_signature_detached_create', - args=(self.get_document_version().pk,) + redirect_to=reverse( + viewname='signatures:document_version_signature_detached_create', + kwargs={'document_version_id': self.get_document_version().pk} ) ) except PassphraseError: messages.error( - self.request, _('Passphrase is incorrect.') + message=_('Passphrase is incorrect.'), request=self.request ) return HttpResponseRedirect( - reverse( - 'signatures:document_version_signature_detached_create', - args=(self.get_document_version().pk,) + redirect_to=reverse( + viewname='signatures:document_version_signature_detached_create', + kwargs={'document_version_id': self.get_document_version().pk} ) ) else: @@ -95,25 +93,16 @@ class DocumentVersionDetachedSignatureCreateView(FormView): temporary_file_object.close() messages.success( - self.request, _('Document version signed successfully.') + message=_('Document version signed successfully.'), + request=self.request ) return super( DocumentVersionDetachedSignatureCreateView, self ).form_valid(form) - def dispatch(self, request, *args, **kwargs): - AccessControlList.objects.check_access( - permissions=permission_document_version_sign_detached, - user=request.user, obj=self.get_document_version().document - ) - - return super( - DocumentVersionDetachedSignatureCreateView, self - ).dispatch(request, *args, **kwargs) - def get_document_version(self): - return get_object_or_404(klass=DocumentVersion, pk=self.kwargs['pk']) + return self.get_external_object() def get_extra_context(self): return { @@ -123,33 +112,26 @@ class DocumentVersionDetachedSignatureCreateView(FormView): ) % self.get_document_version(), } - def get_form_kwargs(self): - result = super( - DocumentVersionDetachedSignatureCreateView, self - ).get_form_kwargs() - - result.update({'user': self.request.user}) - - return result + def get_form_extra_kwargs(self): + return {'user': self.request.user} def get_post_action_redirect(self): return reverse( - 'signatures:document_version_signature_list', - args=(self.get_document_version().pk,) + viewname='signatures:document_version_signature_list', + kwargs={'document_version_id': self.get_document_version().pk} ) class DocumentVersionEmbeddedSignatureCreateView(FormView): + external_object_class = DocumentVersion + external_object_permission = permission_document_version_sign_embedded + external_object_pk_url_kwarg = 'document_version_id' form_class = DocumentVersionSignatureCreateForm def form_valid(self, form): key = form.cleaned_data['key'] passphrase = form.cleaned_data['passphrase'] or None - AccessControlList.objects.check_access( - permissions=permission_key_sign, user=self.request.user, obj=key - ) - try: with self.get_document_version().open() as file_object: signature_result = key.sign_file( @@ -157,22 +139,23 @@ class DocumentVersionEmbeddedSignatureCreateView(FormView): ) except NeedPassphrase: messages.error( - self.request, _('Passphrase is needed to unlock this key.') + message=_('Passphrase is needed to unlock this key.'), + request=self.request ) return HttpResponseRedirect( - reverse( - 'signatures:document_version_signature_embedded_create', - args=(self.get_document_version().pk,) + redirect_to=reverse( + viewname='signatures:document_version_signature_embedded_create', + kwargs={'document_version_id': self.get_document_version().pk} ) ) except PassphraseError: messages.error( - self.request, _('Passphrase is incorrect.') + message=_('Passphrase is incorrect.'), request=self.request ) return HttpResponseRedirect( - reverse( - 'signatures:document_version_signature_embedded_create', - args=(self.get_document_version().pk,) + redirect_to=reverse( + viewname='signatures:document_version_signature_embedded_create', + kwargs={'document_version_id': self.get_document_version().pk} ) ) else: @@ -187,13 +170,14 @@ class DocumentVersionEmbeddedSignatureCreateView(FormView): temporary_file_object.close() messages.success( - self.request, _('Document version signed successfully.') + message=_('Document version signed successfully.'), + request=self.request ) return HttpResponseRedirect( - reverse( - 'signatures:document_version_signature_list', - args=(new_version.pk,) + redirect_to=reverse( + viewname='signatures:document_version_signature_list', + kwargs={'document_version_id': new_version.pk} ) ) @@ -201,18 +185,8 @@ class DocumentVersionEmbeddedSignatureCreateView(FormView): DocumentVersionEmbeddedSignatureCreateView, self ).form_valid(form) - def dispatch(self, request, *args, **kwargs): - AccessControlList.objects.check_access( - permissions=permission_document_version_sign_embedded, - user=request.user, obj=self.get_document_version().document - ) - - return super( - DocumentVersionEmbeddedSignatureCreateView, self - ).dispatch(request, *args, **kwargs) - def get_document_version(self): - return get_object_or_404(klass=DocumentVersion, pk=self.kwargs['pk']) + return self.get_external_object() def get_extra_context(self): return { @@ -222,20 +196,13 @@ class DocumentVersionEmbeddedSignatureCreateView(FormView): ) % self.get_document_version(), } - def get_form_kwargs(self): - result = super( - DocumentVersionEmbeddedSignatureCreateView, self - ).get_form_kwargs() - - result.update({'user': self.request.user}) - - return result + def get_form_extra_kwargs(self): + return {'user': self.request.user} class DocumentVersionSignatureDeleteView(SingleObjectDeleteView): - model = DetachedSignature object_permission = permission_document_version_signature_delete - object_permission_related = 'document_version.document' + pk_url_kwarg = 'signature_id' def get_extra_context(self): return { @@ -246,15 +213,18 @@ class DocumentVersionSignatureDeleteView(SingleObjectDeleteView): def get_post_action_redirect(self): return reverse( - 'signatures:document_version_signature_list', - args=(self.get_object().document_version.pk,) + viewname='signatures:document_version_signature_list', + kwargs={'document_version_id': self.get_object().document_version.pk} ) + def get_object_list(self): + return SignatureBaseModel.objects.select_subclasses() + class DocumentVersionSignatureDetailView(SingleObjectDetailView): form_class = DocumentVersionSignatureDetailForm object_permission = permission_document_version_signature_view - object_permission_related = 'document_version.document' + pk_url_kwarg = 'signature_id' def get_extra_context(self): return { @@ -266,14 +236,13 @@ class DocumentVersionSignatureDetailView(SingleObjectDetailView): ) % self.get_object(), } - def get_queryset(self): + def get_object_list(self): return SignatureBaseModel.objects.select_subclasses() class DocumentVersionSignatureDownloadView(SingleObjectDownloadView): - model = DetachedSignature object_permission = permission_document_version_signature_download - object_permission_related = 'document_version.document' + pk_url_kwarg = 'signature_id' def get_file(self): signature = self.get_object() @@ -282,20 +251,17 @@ class DocumentVersionSignatureDownloadView(SingleObjectDownloadView): signature.signature_file, name=force_text(signature) ) + def get_object_list(self): + return SignatureBaseModel.objects.select_subclasses() -class DocumentVersionSignatureListView(SingleObjectListView): - def dispatch(self, request, *args, **kwargs): - AccessControlList.objects.check_access( - permissions=permission_document_version_signature_view, - user=request.user, obj=self.get_document_version() - ) - return super( - DocumentVersionSignatureListView, self - ).dispatch(request, *args, **kwargs) +class DocumentVersionSignatureListView(ExternalObjectMixin, SingleObjectListView): + external_object_class = DocumentVersion + external_object_permission = permission_document_version_signature_view + external_object_pk_url_kwarg = 'document_version_id' def get_document_version(self): - return get_object_or_404(klass=DocumentVersion, pk=self.kwargs['pk']) + return self.get_external_object() def get_extra_context(self): return { @@ -335,22 +301,15 @@ class DocumentVersionSignatureListView(SingleObjectListView): return self.get_document_version().signatures.all() -class DocumentVersionSignatureUploadView(SingleObjectCreateView): +class DocumentVersionSignatureUploadView(ExternalObjectMixin, SingleObjectCreateView): + external_object_class = DocumentVersion + external_object_permission = permission_document_version_signature_upload + external_object_pk_url_kwarg = 'document_version_id' fields = ('signature_file',) model = DetachedSignature - def dispatch(self, request, *args, **kwargs): - AccessControlList.objects.check_access( - permissions=permission_document_version_signature_upload, - user=request.user, obj=self.get_document_version() - ) - - return super( - DocumentVersionSignatureUploadView, self - ).dispatch(request, *args, **kwargs) - def get_document_version(self): - return get_object_or_404(klass=DocumentVersion, pk=self.kwargs['pk']) + return self.get_external_object() def get_extra_context(self): return { @@ -365,8 +324,8 @@ class DocumentVersionSignatureUploadView(SingleObjectCreateView): def get_post_action_redirect(self): return reverse( - 'signatures:document_version_signature_list', - args=(self.get_document_version().pk,) + viewname='signatures:document_version_signature_list', + kwargs={'document_version_id': self.get_document_version().pk} ) @@ -379,10 +338,11 @@ class AllDocumentSignatureVerifyView(ConfirmView): view_permission = permission_document_version_signature_verify def get_post_action_redirect(self): - return reverse('common:tools_list') + return reverse(viewname='common:tools_list') def view_action(self): task_verify_missing_embedded_signature.delay() messages.success( - self.request, _('Signature verification queued successfully.') + message=_('Signature verification queued successfully.'), + request=self.request ) From 33e0e694e34d0433ef86fa001c5cc659713a0985 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Fri, 25 Jan 2019 01:40:10 -0400 Subject: [PATCH 057/209] Smart settings: Remove the 'is_path' argument Signed-off-by: Roberto Rosario --- mayan/apps/common/settings.py | 6 ++---- mayan/apps/smart_settings/classes.py | 2 +- mayan/apps/sources/settings.py | 3 +-- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/mayan/apps/common/settings.py b/mayan/apps/common/settings.py index c97dbd07fa..c6079f42bf 100644 --- a/mayan/apps/common/settings.py +++ b/mayan/apps/common/settings.py @@ -53,8 +53,7 @@ setting_production_error_log_path = namespace.add_setting( global_name='COMMON_PRODUCTION_ERROR_LOG_PATH', default=os.path.join(settings.MEDIA_ROOT, 'error.log'), help_text=_( 'Path to the logfile that will track errors during production.' - ), - is_path=True + ) ) setting_project_title = namespace.add_setting( global_name='COMMON_PROJECT_TITLE', @@ -82,8 +81,7 @@ setting_temporary_directory = namespace.add_setting( help_text=_( 'Temporary directory used site wide to store thumbnails, previews ' 'and temporary files.' - ), - is_path=True + ) ) namespace = Namespace(label=_('Django'), name='django') diff --git a/mayan/apps/smart_settings/classes.py b/mayan/apps/smart_settings/classes.py index 4e1888f30a..fa5cfa767a 100644 --- a/mayan/apps/smart_settings/classes.py +++ b/mayan/apps/smart_settings/classes.py @@ -147,7 +147,7 @@ class Setting(object): path=settings.CONFIGURATION_LAST_GOOD_FILEPATH ) - def __init__(self, namespace, global_name, default, help_text=None, is_path=False, post_edit_function=None): + def __init__(self, namespace, global_name, default, help_text=None, post_edit_function=None): self.global_name = global_name self.default = default self.help_text = help_text diff --git a/mayan/apps/sources/settings.py b/mayan/apps/sources/settings.py index ae124f4959..0dacbb8a62 100644 --- a/mayan/apps/sources/settings.py +++ b/mayan/apps/sources/settings.py @@ -15,8 +15,7 @@ setting_scanimage_path = namespace.add_setting( global_name='SOURCES_SCANIMAGE_PATH', default=DEFAULT_SCANIMAGE_PATH, help_text=_( 'File path to the scanimage program used to control image scanners.' - ), - is_path=True + ) ) setting_staging_file_image_cache_storage = namespace.add_setting( global_name=DEFAULT_STAGING_FILE_CACHE_STORAGE_BACKEND, From 9261b6e68753172723796455820e1c86d0741f9a Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Mon, 28 Jan 2019 04:52:05 -0400 Subject: [PATCH 058/209] Remove deprecation comment With the removal of the support for a related field in .restrict_queryset() the deprecation comment can now be removed. Signed-off-by: Roberto Rosario --- mayan/apps/acls/managers.py | 5 ----- mayan/apps/documents/exceptions.py | 8 -------- ...d_document_views.py => test_trashed_document_views.py} | 0 3 files changed, 13 deletions(-) delete mode 100644 mayan/apps/documents/exceptions.py rename mayan/apps/documents/tests/{test_deleted_document_views.py => test_trashed_document_views.py} (100%) diff --git a/mayan/apps/acls/managers.py b/mayan/apps/acls/managers.py index b60e301d79..54e6b2ace9 100644 --- a/mayan/apps/acls/managers.py +++ b/mayan/apps/acls/managers.py @@ -138,7 +138,6 @@ class AccessControlListManager(models.Manager): 'produce a queryset from which to .get() the corresponding ' 'object in the local code.', InterfaceWarning ) - queryset = self.restrict_queryset( permission=permission, queryset=obj._meta.default_manager.all(), user=user @@ -212,10 +211,6 @@ class AccessControlListManager(models.Manager): ) def restrict_queryset(self, permission, queryset, user): - # `related_field_name` is left only for compatibility with check_access - # once check_access() is removed the `related_field_name` argument - # will be removed too. - # Check directly granted permission via a role try: Permission.check_user_permission(permission=permission, user=user) diff --git a/mayan/apps/documents/exceptions.py b/mayan/apps/documents/exceptions.py deleted file mode 100644 index 80f86ad910..0000000000 --- a/mayan/apps/documents/exceptions.py +++ /dev/null @@ -1,8 +0,0 @@ -from __future__ import unicode_literals - - -class DocumentException(Exception): - """ - Base documents warning - """ - pass diff --git a/mayan/apps/documents/tests/test_deleted_document_views.py b/mayan/apps/documents/tests/test_trashed_document_views.py similarity index 100% rename from mayan/apps/documents/tests/test_deleted_document_views.py rename to mayan/apps/documents/tests/test_trashed_document_views.py From 7532429b0bdf4521b9868018a6ee0d77c51c3906 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Mon, 28 Jan 2019 05:18:33 -0400 Subject: [PATCH 059/209] Refactor common generic views Add keyword arguments. Sort arguments. Unify the ObjectListPermissionFilterMixin and ObjectPermissionCheckMixin into the RestrictedQuerysetMixin. Add MultipleObjectDownloadView. Update SingleObjectDownloadView to do queryset filtering. The method that returns the base queryset for views is now named get_source_queryset(). The views now use .get_object_list as a multi object homologous of get_object. The queryset returned by .get_object_list is restricted by access. Make MultipleObjectMixin a subclass of Django's SingleObjectMixin to reduce repeated code. All generic views are now imported from common.generics and not from common.views. Signed-off-by: Roberto Rosario --- mayan/apps/common/generics.py | 218 ++++++++++++++++---------- mayan/apps/common/mixins.py | 184 ++++++++++++---------- mayan/apps/common/models.py | 2 +- mayan/apps/common/tests/test_views.py | 2 +- mayan/apps/common/views.py | 9 +- mayan/apps/common/widgets.py | 1 - 6 files changed, 237 insertions(+), 179 deletions(-) diff --git a/mayan/apps/common/generics.py b/mayan/apps/common/generics.py index f7afb7f1f8..bdd2f04e2a 100644 --- a/mayan/apps/common/generics.py +++ b/mayan/apps/common/generics.py @@ -27,29 +27,29 @@ from .icons import ( icon_sort_up ) from .literals import ( - TEXT_CHOICE_ITEMS, TEXT_CHOICE_LIST, TEXT_LIST_AS_ITEMS_VARIABLE_NAME, - TEXT_LIST_AS_ITEMS_PARAMETER, TEXT_SORT_FIELD_PARAMETER, - TEXT_SORT_FIELD_VARIABLE_NAME, TEXT_SORT_ORDER_CHOICE_ASCENDING, - TEXT_SORT_ORDER_PARAMETER, TEXT_SORT_ORDER_VARIABLE_NAME + TEXT_SORT_FIELD_PARAMETER, TEXT_SORT_FIELD_VARIABLE_NAME, + TEXT_SORT_ORDER_CHOICE_ASCENDING, TEXT_SORT_ORDER_PARAMETER, + TEXT_SORT_ORDER_VARIABLE_NAME ) from .mixins import ( DeleteExtraDataMixin, DynamicFormViewMixin, ExtraContextMixin, FormExtraKwargsMixin, ListModeMixin, MultipleObjectMixin, - ObjectActionMixin, ObjectListPermissionFilterMixin, ObjectNameMixin, - ObjectPermissionCheckMixin, RedirectionMixin, ViewPermissionCheckMixin + ObjectActionMixin, ObjectNameMixin, RedirectionMixin, + RestrictedQuerysetMixin, ViewPermissionCheckMixin ) from .settings import setting_paginate_by __all__ = ( - 'AssignRemoveView', 'ConfirmView', 'FormView', 'MultiFormView', - 'MultipleObjectConfirmActionView', 'MultipleObjectFormActionView', + 'AssignRemoveView', 'ConfirmView', 'FormView', + 'MultiFormView', 'MultipleObjectConfirmActionView', + 'MultipleObjectFormActionView', 'MultipleObjectDownloadView', 'SingleObjectCreateView', 'SingleObjectDeleteView', - 'SingleObjectDetailView', 'SingleObjectEditView', 'SingleObjectListView', - 'SimpleView' + 'SingleObjectDetailView', 'MultipleObjectDownloadView', + 'SingleObjectEditView', 'SingleObjectListView', 'SimpleView' ) -class AssignRemoveView(ExtraContextMixin, ViewPermissionCheckMixin, ObjectPermissionCheckMixin, TemplateView): +class AssignRemoveView(ExtraContextMixin, ViewPermissionCheckMixin, RestrictedQuerysetMixin, TemplateView): decode_content_type = False left_list_help_text = _( 'Select entries to be added. Hold Control to select multiple ' @@ -73,7 +73,7 @@ class AssignRemoveView(ExtraContextMixin, ViewPermissionCheckMixin, ObjectPermis def generate_choices(choices): results = [] for choice in choices: - ct = ContentType.objects.get_for_model(choice) + ct = ContentType.objects.get_for_model(model=choice) label = force_text(choice) results.append(('%s,%s' % (ct.model, choice.pk), '%s' % (label))) @@ -108,21 +108,21 @@ class AssignRemoveView(ExtraContextMixin, ViewPermissionCheckMixin, ObjectPermis def get(self, request, *args, **kwargs): self.unselected_list = ChoiceForm( - prefix=self.LEFT_LIST_NAME, choices=self.left_list(), - help_text=self.get_left_list_help_text() + choices=self.left_list(), help_text=self.get_left_list_help_text(), + prefix=self.LEFT_LIST_NAME ) self.selected_list = ChoiceForm( - prefix=self.RIGHT_LIST_NAME, choices=self.right_list(), + choices=self.right_list(), disabled_choices=self.get_disabled_choices(), - help_text=self.get_right_list_help_text() + help_text=self.get_right_list_help_text(), + prefix=self.RIGHT_LIST_NAME ) 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() + self.request.POST, choices=items_function(), prefix=prefix ) if form.is_valid(): @@ -156,12 +156,12 @@ class AssignRemoveView(ExtraContextMixin, ViewPermissionCheckMixin, ObjectPermis def post(self, request, *args, **kwargs): self.process_form( - prefix=self.LEFT_LIST_NAME, items_function=self.left_list, - action_function=self.add + action_function=self.add, items_function=self.left_list, + prefix=self.LEFT_LIST_NAME ) self.process_form( - prefix=self.RIGHT_LIST_NAME, items_function=self.right_list, - action_function=self.remove + action_function=self.remove, items_function=self.right_list, + prefix=self.RIGHT_LIST_NAME ) return self.get(request, *args, **kwargs) @@ -200,12 +200,12 @@ class AssignRemoveView(ExtraContextMixin, ViewPermissionCheckMixin, ObjectPermis return data -class ConfirmView(ObjectListPermissionFilterMixin, ObjectPermissionCheckMixin, ViewPermissionCheckMixin, ExtraContextMixin, RedirectionMixin, TemplateView): +class ConfirmView(RestrictedQuerysetMixin, ViewPermissionCheckMixin, ExtraContextMixin, RedirectionMixin, TemplateView): template_name = 'appearance/generic_confirm.html' def post(self, request, *args, **kwargs): self.view_action() - return HttpResponseRedirect(self.get_success_url()) + return HttpResponseRedirect(redirect_to=self.get_success_url()) class FormView(ViewPermissionCheckMixin, ExtraContextMixin, RedirectionMixin, FormExtraKwargsMixin, DjangoFormView): @@ -216,6 +216,62 @@ class DynamicFormView(DynamicFormViewMixin, FormView): pass +class DownloadViewBase(VirtualDownloadView): + TextIteratorIO = TextIteratorIO + VirtualFile = VirtualFile + + +class MultipleObjectDownloadView(RestrictedQuerysetMixin, MultipleObjectMixin, DownloadViewBase): + """ + View that support receiving multiple objects via a pk_list query. + """ + def __init__(self, *args, **kwargs): + result = super(MultipleObjectDownloadView, self).__init__(*args, **kwargs) + + if self.__class__.mro()[0].get_queryset != MultipleObjectDownloadView.get_queryset: + raise ImproperlyConfigured( + '%(cls)s is overloading the get_queryset method. Subclasses ' + 'should implement the get_source_queryset method instead. ' % { + 'cls': self.__class__.__name__ + } + ) + + return result + + def get_queryset(self): + try: + return super(MultipleObjectDownloadView, self).get_queryset() + except ImproperlyConfigured: + self.queryset = self.get_source_queryset() + return super(MultipleObjectDownloadView, self).get_queryset() + + +class SingleObjectDownloadView(RestrictedQuerysetMixin, SingleObjectMixin, DownloadViewBase): + """ + View that provides a .get_object() method to download content from a + single object. + """ + def __init__(self, *args, **kwargs): + result = super(SingleObjectDownloadView, self).__init__(*args, **kwargs) + + if self.__class__.mro()[0].get_queryset != SingleObjectDownloadView.get_queryset: + raise ImproperlyConfigured( + '%(cls)s is overloading the get_queryset method. Subclasses ' + 'should implement the get_source_queryset method instead. ' % { + 'cls': self.__class__.__name__ + } + ) + + return result + + def get_queryset(self): + try: + return super(SingleObjectDownloadView, self).get_queryset() + except ImproperlyConfigured: + self.queryset = self.get_source_queryset() + return super(SingleObjectDownloadView, self).get_queryset() + + class MultiFormView(DjangoFormView): prefix = None prefixes = {} @@ -238,7 +294,7 @@ class MultiFormView(DjangoFormView): self.all_forms_valid(forms) - return HttpResponseRedirect(self.get_success_url()) + return HttpResponseRedirect(redirect_to=self.get_success_url()) def forms_invalid(self, forms): return self.render_to_response(self.get_context_data(forms=forms)) @@ -298,12 +354,12 @@ class MultiFormView(DjangoFormView): forms = self.get_forms(form_classes) if all([form.is_valid() for form in forms.values()]): - return self.forms_valid(forms) + return self.forms_valid(forms=forms) else: - return self.forms_invalid(forms) + return self.forms_invalid(forms=forms) -class MultipleObjectFormActionView(ObjectActionMixin, MultipleObjectMixin, FormExtraKwargsMixin, ViewPermissionCheckMixin, ExtraContextMixin, RedirectionMixin, DjangoFormView): +class MultipleObjectFormActionView(ObjectActionMixin, ViewPermissionCheckMixin, RestrictedQuerysetMixin, MultipleObjectMixin, FormExtraKwargsMixin, ExtraContextMixin, RedirectionMixin, DjangoFormView): """ This view will present a form and upon receiving a POST request will perform an action on an object or queryset @@ -316,7 +372,7 @@ class MultipleObjectFormActionView(ObjectActionMixin, MultipleObjectMixin, FormE if self.__class__.mro()[0].get_queryset != MultipleObjectFormActionView.get_queryset: raise ImproperlyConfigured( '%(cls)s is overloading the get_queryset method. Subclasses ' - 'should implement the get_object_list method instead. ' % { + 'should implement the get_source_queryset method instead. ' % { 'cls': self.__class__.__name__ } ) @@ -331,16 +387,36 @@ class MultipleObjectFormActionView(ObjectActionMixin, MultipleObjectMixin, FormE try: return super(MultipleObjectFormActionView, self).get_queryset() except ImproperlyConfigured: - self.queryset = self.get_object_list() + self.queryset = self.get_source_queryset() return super(MultipleObjectFormActionView, self).get_queryset() -class MultipleObjectConfirmActionView(ObjectActionMixin, MultipleObjectMixin, ObjectListPermissionFilterMixin, ViewPermissionCheckMixin, ExtraContextMixin, RedirectionMixin, TemplateView): +class MultipleObjectConfirmActionView(ObjectActionMixin, ViewPermissionCheckMixin, RestrictedQuerysetMixin, MultipleObjectMixin, ExtraContextMixin, RedirectionMixin, TemplateView): template_name = 'appearance/generic_confirm.html' + def __init__(self, *args, **kwargs): + result = super(MultipleObjectConfirmActionView, self).__init__(*args, **kwargs) + + if self.__class__.mro()[0].get_queryset != MultipleObjectConfirmActionView.get_queryset: + raise ImproperlyConfigured( + '%(cls)s is overloading the get_queryset method. Subclasses ' + 'should implement the get_source_queryset method instead. ' % { + 'cls': self.__class__.__name__ + } + ) + + return result + + def get_queryset(self): + try: + return super(MultipleObjectConfirmActionView, self).get_queryset() + except ImproperlyConfigured: + self.queryset = self.get_source_queryset() + return super(MultipleObjectConfirmActionView, self).get_queryset() + def post(self, request, *args, **kwargs): self.view_action() - return HttpResponseRedirect(self.get_success_url()) + return HttpResponseRedirect(redirect_to=self.get_success_url()) class SimpleView(ViewPermissionCheckMixin, ExtraContextMixin, TemplateView): @@ -377,7 +453,7 @@ class SingleObjectCreateView(ObjectNameMixin, ViewPermissionCheckMixin, ExtraCon } messages.error( - request=self.request, message=error_message + message=error_message, request=self.request ) return super( SingleObjectCreateView, self @@ -402,13 +478,13 @@ class SingleObjectCreateView(ObjectNameMixin, ViewPermissionCheckMixin, ExtraCon context = self.get_context_data() messages.success( - self.request, - _( + message=_( '%(object)s created successfully.' - ) % {'object': self.get_object_name(context=context)} + ) % {'object': self.get_object_name(context=context)}, + request=self.request ) - return HttpResponseRedirect(self.get_success_url()) + return HttpResponseRedirect(redirect_to=self.get_success_url()) def get_error_message_duplicate(self): return self.error_message_duplicate @@ -418,7 +494,7 @@ class SingleObjectDynamicFormCreateView(DynamicFormViewMixin, SingleObjectCreate pass -class SingleObjectDeleteView(ObjectNameMixin, DeleteExtraDataMixin, ViewPermissionCheckMixin, ObjectPermissionCheckMixin, ExtraContextMixin, RedirectionMixin, DeleteView): +class SingleObjectDeleteView(ObjectNameMixin, DeleteExtraDataMixin, ViewPermissionCheckMixin, RestrictedQuerysetMixin, ExtraContextMixin, RedirectionMixin, DeleteView): template_name = 'appearance/generic_confirm.html' def __init__(self, *args, **kwargs): @@ -427,7 +503,7 @@ class SingleObjectDeleteView(ObjectNameMixin, DeleteExtraDataMixin, ViewPermissi if self.__class__.mro()[0].get_queryset != SingleObjectDeleteView.get_queryset: raise ImproperlyConfigured( '%(cls)s is overloading the get_queryset method. Subclasses ' - 'should implement the get_object_list method instead. ' % { + 'should implement the get_source_queryset method instead. ' % { 'cls': self.__class__.__name__ } ) @@ -443,20 +519,19 @@ class SingleObjectDeleteView(ObjectNameMixin, DeleteExtraDataMixin, ViewPermissi result = super(SingleObjectDeleteView, self).delete(request, *args, **kwargs) except Exception as exception: messages.error( - self.request, - _('%(object)s not deleted, error: %(error)s.') % { + message=_('%(object)s not deleted, error: %(error)s.') % { 'object': object_name, 'error': exception - } + }, request=self.request ) raise exception else: messages.success( - self.request, - _( + message=_( '%(object)s deleted successfully.' - ) % {'object': object_name} + ) % {'object': object_name}, + request=self.request ) return result @@ -470,11 +545,11 @@ class SingleObjectDeleteView(ObjectNameMixin, DeleteExtraDataMixin, ViewPermissi try: return super(SingleObjectDeleteView, self).get_queryset() except ImproperlyConfigured: - self.queryset = self.get_object_list() + self.queryset = self.get_source_queryset() return super(SingleObjectDeleteView, self).get_queryset() -class SingleObjectDetailView(ViewPermissionCheckMixin, ObjectPermissionCheckMixin, FormExtraKwargsMixin, ExtraContextMixin, ModelFormMixin, DetailView): +class SingleObjectDetailView(ViewPermissionCheckMixin, RestrictedQuerysetMixin, FormExtraKwargsMixin, ExtraContextMixin, ModelFormMixin, DetailView): template_name = 'appearance/generic_form.html' def __init__(self, *args, **kwargs): @@ -483,7 +558,7 @@ class SingleObjectDetailView(ViewPermissionCheckMixin, ObjectPermissionCheckMixi if self.__class__.mro()[0].get_queryset != SingleObjectDetailView.get_queryset: raise ImproperlyConfigured( '%(cls)s is overloading the get_queryset method. Subclasses ' - 'should implement the get_object_list method instead. ' % { + 'should implement the get_source_queryset method instead. ' % { 'cls': self.__class__.__name__ } ) @@ -499,36 +574,11 @@ class SingleObjectDetailView(ViewPermissionCheckMixin, ObjectPermissionCheckMixi try: return super(SingleObjectDetailView, self).get_queryset() except ImproperlyConfigured: - self.queryset = self.get_object_list() + self.queryset = self.get_source_queryset() return super(SingleObjectDetailView, self).get_queryset() -class SingleObjectDownloadView(ViewPermissionCheckMixin, ObjectPermissionCheckMixin, VirtualDownloadView, SingleObjectMixin): - TextIteratorIO = TextIteratorIO - VirtualFile = VirtualFile - - def __init__(self, *args, **kwargs): - result = super(SingleObjectDownloadView, self).__init__(*args, **kwargs) - - if self.__class__.mro()[0].get_queryset != SingleObjectDownloadView.get_queryset: - raise ImproperlyConfigured( - '%(cls)s is overloading the get_queryset method. Subclasses ' - 'should implement the get_object_list method instead. ' % { - 'cls': self.__class__.__name__ - } - ) - - return result - - def get_queryset(self): - try: - return super(SingleObjectDownloadView, self).get_queryset() - except ImproperlyConfigured: - self.queryset = self.get_object_list() - return super(SingleObjectDownloadView, self).get_queryset() - - -class SingleObjectEditView(ObjectNameMixin, ViewPermissionCheckMixin, ObjectPermissionCheckMixin, ExtraContextMixin, FormExtraKwargsMixin, RedirectionMixin, UpdateView): +class SingleObjectEditView(ObjectNameMixin, ViewPermissionCheckMixin, RestrictedQuerysetMixin, ExtraContextMixin, FormExtraKwargsMixin, RedirectionMixin, UpdateView): template_name = 'appearance/generic_form.html' def form_valid(self, form): @@ -552,24 +602,22 @@ class SingleObjectEditView(ObjectNameMixin, ViewPermissionCheckMixin, ObjectPerm self.object.save(**save_extra_data) except Exception as exception: messages.error( - self.request, - _('%(object)s not updated, error: %(error)s.') % { + message=_('%(object)s not updated, error: %(error)s.') % { 'object': object_name, 'error': exception - } + }, request=self.request ) return super( SingleObjectEditView, self ).form_invalid(form=form) else: messages.success( - self.request, - _( + message=_( '%(object)s updated successfully.' - ) % {'object': object_name} + ) % {'object': object_name}, request=self.request ) - return HttpResponseRedirect(self.get_success_url()) + return HttpResponseRedirect(redirect_to=self.get_success_url()) def get_object(self, queryset=None): obj = super(SingleObjectEditView, self).get_object(queryset=queryset) @@ -585,7 +633,7 @@ class SingleObjectDynamicFormEditView(DynamicFormViewMixin, SingleObjectEditView pass -class SingleObjectListView(ListModeMixin, PaginationMixin, ViewPermissionCheckMixin, ObjectListPermissionFilterMixin, ExtraContextMixin, RedirectionMixin, ListView): +class SingleObjectListView(ListModeMixin, PaginationMixin, ViewPermissionCheckMixin, RestrictedQuerysetMixin, ExtraContextMixin, RedirectionMixin, ListView): template_name = 'appearance/generic_list.html' def __init__(self, *args, **kwargs): @@ -594,7 +642,7 @@ class SingleObjectListView(ListModeMixin, PaginationMixin, ViewPermissionCheckMi if self.__class__.mro()[0].get_queryset != SingleObjectListView.get_queryset: raise ImproperlyConfigured( '%(cls)s is overloading the get_queryset method. Subclasses ' - 'should implement the get_object_list method instead. ' % { + 'should implement the get_source_queryset method instead. ' % { 'cls': self.__class__.__name__ } ) @@ -635,7 +683,7 @@ class SingleObjectListView(ListModeMixin, PaginationMixin, ViewPermissionCheckMi try: queryset = super(SingleObjectListView, self).get_queryset() except ImproperlyConfigured: - self.queryset = self.get_object_list() + self.queryset = self.get_source_queryset() queryset = super(SingleObjectListView, self).get_queryset() self.field_name = self.get_sort_field() diff --git a/mayan/apps/common/mixins.py b/mayan/apps/common/mixins.py index 14ef15a44e..9de85100e1 100644 --- a/mayan/apps/common/mixins.py +++ b/mayan/apps/common/mixins.py @@ -4,11 +4,11 @@ from django.conf import settings from django.contrib import messages from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ImproperlyConfigured, PermissionDenied -from django.db.models.query import QuerySet from django.http import Http404, HttpResponseRedirect from django.shortcuts import get_object_or_404, resolve_url from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ungettext +from django.views.generic.detail import SingleObjectMixin from mayan.apps.acls.models import AccessControlList from mayan.apps.permissions import Permission @@ -23,9 +23,8 @@ from .literals import ( __all__ = ( 'DeleteExtraDataMixin', 'DynamicFormViewMixin', 'ExtraContextMixin', 'FormExtraKwargsMixin', 'ListModeMixin', 'MultipleObjectMixin', - 'ObjectActionMixin', 'ObjectListPermissionFilterMixin', 'ObjectNameMixin', - 'ObjectPermissionCheckMixin', 'RedirectionMixin', - 'ViewPermissionCheckMixin' + 'ObjectActionMixin', 'ObjectNameMixin', 'RedirectionMixin', + 'RestrictedQuerysetMixin', 'ViewPermissionCheckMixin' ) @@ -204,39 +203,48 @@ class MultipleInstanceActionMixin(object): return HttpResponseRedirect(redirect_to=self.get_success_url()) -class MultipleObjectMixin(object): +class MultipleObjectMixin(SingleObjectMixin): """ Mixin that allows a view to work on a single or multiple objects. It can receive a pk, a slug or a list of IDs via an id_list query. The pk, slug, and ID list parameter name can be changed using the attributes: pk_url_kwargs, slug_url_kwarg, and pk_list_key. """ - model = None - object_permission = None pk_list_key = 'id_list' pk_list_separator = PK_LIST_SEPARATOR - pk_url_kwarg = 'pk' - queryset = None - slug_url_kwarg = 'slug' - def get_pk_list(self): - result = self.request.GET.get( - self.pk_list_key, self.request.POST.get(self.pk_list_key) - ) + def get(self, request, *args, **kwargs): + """ + Override BaseDetailView.get() + """ + return super(SingleObjectMixin, self).get(request, *args, **kwargs) - if result: - return result.split(self.pk_list_separator) - else: - return None + def get_context_data(self, **kwargs): + """ + Override SingleObjectMixin.get_context_data() + """ + return super(SingleObjectMixin, self).get_context_data(**kwargs) - def get_queryset(self): - if self.queryset is not None: - queryset = self.queryset - if isinstance(queryset, QuerySet): - queryset = queryset.all() - elif self.model is not None: - queryset = self.model._default_manager.all() + def get_object(self): + """ + Remove this method from the subclass + """ + raise AttributeError + def get_object_list(self, queryset=None): + """ + Returns the list of objects the view is displaying. + + By default this requires `self.queryset` and a `pk`, `slug` ro + `pk_list' argument in the URLconf, but subclasses can override this + to return any object. + """ + # Use a custom queryset if provided; this is required for subclasses + # like DateDetailView + if queryset is None: + queryset = self.get_queryset() + + # Next, try looking up by primary key. pk = self.kwargs.get(self.pk_url_kwarg) slug = self.kwargs.get(self.slug_url_kwarg) pk_list = self.get_pk_list() @@ -252,21 +260,37 @@ class MultipleObjectMixin(object): if pk_list is not None: queryset = queryset.filter(pk__in=self.get_pk_list()) + # If none of those are defined, it's an error. if pk is None and slug is None and pk_list is None: raise AttributeError( - 'Generic detail view %s must be called with ' - 'either an object pk, a slug or an id list.' + 'View %s must be called with ' + 'either an object pk, a slug or an pk list.' % self.__class__.__name__ ) - if self.object_permission: - return AccessControlList.objects.restrict_queryset( - permission=self.object_permission, queryset=queryset, - user=self.request.user + try: + # Get the single item from the filtered queryset + queryset.get() + except queryset.model.MultipleObjectsReturned: + # Queryset has more than one item, this is good. + return queryset + except queryset.model.DoesNotExist: + raise Http404( + _('No %(verbose_name)s found matching the query') % + {'verbose_name': queryset.model._meta.verbose_name} ) else: + # Queryset has one item, this is good. return queryset + def get_pk_list(self): + result = self.request.GET.get(self.pk_list_key) + + if result: + return result.split(self.pk_list_separator) + else: + return None + class ObjectActionMixin(object): """ @@ -311,36 +335,6 @@ class ObjectActionMixin(object): ) -class ObjectListPermissionFilterMixin(object): - """ - access_object_retrieve_method is used to have the entire view check - against an object permission and not the individual secondary items. - """ - access_object_retrieve_method = None - object_permission = None - - def dispatch(self, request, *args, **kwargs): - if self.access_object_retrieve_method and self.object_permission: - AccessControlList.objects.check_access( - obj=getattr(self, self.access_object_retrieve_method)(), - permissions=(self.object_permission,), user=request.user - ) - return super(ObjectListPermissionFilterMixin, self).dispatch( - request, *args, **kwargs - ) - - def get_queryset(self): - queryset = super(ObjectListPermissionFilterMixin, self).get_queryset() - - if not self.access_object_retrieve_method and self.object_permission: - return AccessControlList.objects.restrict_queryset( - permission=self.object_permission, queryset=queryset, - user=self.request.user - ) - else: - return queryset - - class ObjectNameMixin(object): def get_object_name(self, context=None): if not context: @@ -357,26 +351,6 @@ class ObjectNameMixin(object): return object_name -class ObjectPermissionCheckMixin(object): - """ - Filter the queryset of the view by the `object_permission` provided. - If no `object_permission` is provide the queryset will be returned - as is. - """ - object_permission = None - - def get_queryset(self): - queryset = super(ObjectPermissionCheckMixin, self).get_queryset() - - if self.object_permission: - return AccessControlList.objects.restrict_queryset( - permission=self.object_permission, queryset=queryset, - user=self.request.user - ) - - return queryset - - class RedirectionMixin(object): action_cancel_redirect = None post_action_redirect = None @@ -425,14 +399,56 @@ class RedirectionMixin(object): return self.next_url or self.previous_url +class RestrictedQuerysetMixin(object): + """ + Restrict the view's queryset against a permission via ACL checking. + Used to restrict the object list of a multiple object view or the source + queryset of the .get_object() method. + """ + model = None + object_permission = None + source_queryset = None + + def get_source_queryset(self): + if self.source_queryset is None: + if self.model: + return self.model._default_manager.all() + else: + raise ImproperlyConfigured( + "%(cls)s is missing a QuerySet. Define " + "%(cls)s.model, %(cls)s.source_queryset, or override " + "%(cls)s.get_source_queryset()." % { + 'cls': self.__class__.__name__ + } + ) + + return self.source_queryset.all() + + def get_queryset(self): + queryset = self.get_source_queryset() + + if self.object_permission: + return AccessControlList.objects.restrict_queryset( + permission=self.object_permission, queryset=queryset, + user=self.request.user + ) + else: + return queryset + + class ViewPermissionCheckMixin(object): + """ + Restrict access to the view based on the user's direct permissions from + roles. This mixing is used for views whose objects don't support ACLs or + for views that perform actions that are not related to a specify object or + object's permission like maintenance views. + """ view_permission = None def dispatch(self, request, *args, **kwargs): if self.view_permission: - Permission.check_permissions( - permissions=(self.view_permission,), - requester=self.request.user + Permission.check_user_permission( + permission=self.view_permission, user=self.request.user ) return super( diff --git a/mayan/apps/common/models.py b/mayan/apps/common/models.py index 148c3b1c98..de23671441 100644 --- a/mayan/apps/common/models.py +++ b/mayan/apps/common/models.py @@ -18,7 +18,7 @@ from .storages import storage_sharedupload logger = logging.getLogger(__name__) -#TODO: move outside of models.py +# TODO: move outside of models.py or as a static method of SharedUploadedFile def upload_to(instance, filename): return 'shared-file-{}'.format(uuid.uuid4().hex) diff --git a/mayan/apps/common/tests/test_views.py b/mayan/apps/common/tests/test_views.py index 2295e08fd4..865c7d782e 100644 --- a/mayan/apps/common/tests/test_views.py +++ b/mayan/apps/common/tests/test_views.py @@ -21,7 +21,7 @@ class CommonViewTestCase(GenericViewTestCase): def _create_error_log_entry(self): ModelPermission.register( - model=get_user_model(), permissions=(permission_error_log_view,) + model=get_user_model(), permission=permission_error_log_view ) ErrorLogEntry.objects.register(model=get_user_model()) diff --git a/mayan/apps/common/views.py b/mayan/apps/common/views.py index ca5e47fd2c..f7fc56e2ae 100644 --- a/mayan/apps/common/views.py +++ b/mayan/apps/common/views.py @@ -14,7 +14,6 @@ from django.utils.http import urlencode from django.utils.translation import ugettext_lazy as _ from django.views.generic import RedirectView, TemplateView -from mayan.apps.acls.models import AccessControlList from mayan.apps.common.mixins import ( ContentTypeViewMixin, ExternalObjectMixin ) @@ -24,12 +23,8 @@ from .forms import ( LicenseForm, LocaleProfileForm, LocaleProfileForm_view, PackagesLicensesForm ) -from .generics import ( # NOQA - AssignRemoveView, ConfirmView, FormView, MultiFormView, - MultipleObjectConfirmActionView, MultipleObjectFormActionView, SimpleView, - SingleObjectCreateView, SingleObjectDeleteView, SingleObjectDetailView, - SingleObjectDownloadView, SingleObjectDynamicFormCreateView, - SingleObjectDynamicFormEditView, SingleObjectEditView, SingleObjectListView +from .generics import ( + ConfirmView, SimpleView, SingleObjectEditView, SingleObjectListView ) from .icons import icon_object_error_list, icon_setup from .menus import menu_setup, menu_tools diff --git a/mayan/apps/common/widgets.py b/mayan/apps/common/widgets.py index 518b8ccda4..c6fe2d5851 100644 --- a/mayan/apps/common/widgets.py +++ b/mayan/apps/common/widgets.py @@ -3,7 +3,6 @@ from __future__ import unicode_literals from django import forms from django.template import Context, Template from django.utils.encoding import force_text -from django.utils.html import format_html from django.utils.safestring import mark_safe from .icons import icon_fail as default_icon_fail From 74dfa537879f5720d94eb7b59bb650930ff903f7 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Mon, 28 Jan 2019 05:25:48 -0400 Subject: [PATCH 060/209] Update documents app Rename the DeletedDocument proxy model to a TrashedDocument. Rename the deleted_document views to trashed_document. Rename the document and deleted_document URL parameters to trashed_document. Update URL parameters to the '_id' form. Add keyword arguments. Update use of .filter_by_access(). Enclose trashed document restore method in a transaction. Sort arguments. Update app for compliance with MERCs 5 and 6. Add document page view tests. Add favorite document view tests. Movernize tests. Replace use of urlencode with furl. Update views to use ExternalObjectMixin. Refactor the document and version download views. Rename the DocumentDocumentTypeEditView to DocumentChangeTypeView. Move the trashed document views to their own module. Signed-off-by: Roberto Rosario --- mayan/apps/documents/admin.py | 20 +- mayan/apps/documents/api_views.py | 236 +++++- mayan/apps/documents/apps.py | 82 +- mayan/apps/documents/dashboard_widgets.py | 58 +- mayan/apps/documents/events.py | 20 +- .../documents/forms/document_type_forms.py | 2 +- mayan/apps/documents/icons.py | 28 +- mayan/apps/documents/links.py | 213 +++--- mayan/apps/documents/managers.py | 18 +- .../apps/documents/models/document_models.py | 26 +- .../documents/models/document_page_models.py | 10 +- .../documents/models/document_type_models.py | 22 +- .../models/document_version_models.py | 23 +- mayan/apps/documents/permissions.py | 48 +- mayan/apps/documents/queues.py | 44 +- mayan/apps/documents/serializers.py | 264 +++++-- mayan/apps/documents/settings.py | 2 +- mayan/apps/documents/signals.py | 4 +- mayan/apps/documents/statistics.py | 59 +- mayan/apps/documents/tasks.py | 10 +- mayan/apps/documents/tests/mixins.py | 36 +- mayan/apps/documents/tests/test_api.py | 60 +- .../tests/test_document_page_views.py | 49 +- .../tests/test_document_type_views.py | 30 +- .../tests/test_document_version_views.py | 112 ++- .../documents/tests/test_document_views.py | 255 +++--- .../tests/test_duplicated_document_views.py | 8 +- mayan/apps/documents/tests/test_events.py | 69 +- mayan/apps/documents/tests/test_links.py | 71 +- mayan/apps/documents/tests/test_models.py | 20 +- mayan/apps/documents/tests/test_search.py | 4 +- .../tests/test_trashed_document_views.py | 135 ++-- mayan/apps/documents/urls.py | 257 ++++--- mayan/apps/documents/views/__init__.py | 1 + .../documents/views/document_page_views.py | 131 ++-- .../documents/views/document_type_views.py | 53 +- .../documents/views/document_version_views.py | 158 ++-- mayan/apps/documents/views/document_views.py | 723 ++++++------------ mayan/apps/documents/views/misc_views.py | 6 +- .../documents/views/trashed_document_views.py | 162 ++++ 40 files changed, 1914 insertions(+), 1615 deletions(-) create mode 100644 mayan/apps/documents/views/trashed_document_views.py diff --git a/mayan/apps/documents/admin.py b/mayan/apps/documents/admin.py index 62d30c0e1f..8e779bd58a 100644 --- a/mayan/apps/documents/admin.py +++ b/mayan/apps/documents/admin.py @@ -3,8 +3,8 @@ from __future__ import unicode_literals from django.contrib import admin from .models import ( - DeletedDocument, Document, DocumentPage, DocumentType, - DocumentTypeFilename, DocumentVersion, DuplicatedDocument, RecentDocument + Document, DocumentPage, DocumentType, DocumentTypeFilename, DocumentVersion, + DuplicatedDocument, RecentDocument, TrashedDocument ) @@ -29,14 +29,6 @@ class DocumentVersionInline(admin.StackedInline): allow_add = True -@admin.register(DeletedDocument) -class DeletedDocumentAdmin(admin.ModelAdmin): - date_hierarchy = 'deleted_date_time' - list_filter = ('document_type',) - list_display = ('uuid', 'label', 'document_type', 'deleted_date_time') - readonly_fields = ('uuid', 'document_type') - - @admin.register(Document) class DocumentAdmin(admin.ModelAdmin): date_hierarchy = 'date_added' @@ -69,3 +61,11 @@ class RecentDocumentAdmin(admin.ModelAdmin): list_display_links = ('document', 'datetime_accessed') list_filter = ('user',) readonly_fields = ('user', 'document', 'datetime_accessed') + + +@admin.register(TrashedDocument) +class TrashedDocumentAdmin(admin.ModelAdmin): + date_hierarchy = 'deleted_date_time' + list_filter = ('document_type',) + list_display = ('uuid', 'label', 'document_type', 'deleted_date_time') + readonly_fields = ('uuid', 'document_type') diff --git a/mayan/apps/documents/api_views.py b/mayan/apps/documents/api_views.py index 215d803056..23b506f58f 100644 --- a/mayan/apps/documents/api_views.py +++ b/mayan/apps/documents/api_views.py @@ -9,7 +9,7 @@ from django.shortcuts import get_object_or_404 from django.views.decorators.cache import cache_control, patch_cache_control from django_downloadview import DownloadMixin, VirtualFile -from rest_framework import generics, status +from rest_framework import generics, status, viewsets from rest_framework.response import Response from mayan.apps.acls.models import AccessControlList @@ -17,12 +17,12 @@ from mayan.apps.rest_api.filters import MayanObjectPermissionsFilter from mayan.apps.rest_api.permissions import MayanPermission from .literals import DOCUMENT_IMAGE_TASK_TIMEOUT -from .models import Document, DocumentType, RecentDocument +from .models import Document, DocumentVersion, DocumentType, RecentDocument from .permissions import ( - permission_document_create, permission_document_delete, + permission_document_create, permission_trashed_document_delete, permission_document_download, permission_document_edit, permission_document_new_version, permission_document_properties_edit, - permission_document_restore, permission_document_trash, + permission_trashed_document_restore, permission_document_trash, permission_document_type_create, permission_document_type_delete, permission_document_type_edit, permission_document_type_view, permission_document_version_revert, permission_document_version_view, @@ -30,10 +30,11 @@ from .permissions import ( ) from .serializers import ( DeletedDocumentSerializer, DocumentPageSerializer, DocumentSerializer, - DocumentTypeSerializer, DocumentVersionSerializer, NewDocumentSerializer, - NewDocumentVersionSerializer, RecentDocumentSerializer, - WritableDocumentSerializer, WritableDocumentTypeSerializer, - WritableDocumentVersionSerializer + DocumentTypeSerializer, DocumentVersionSerializer,# NewDocumentSerializer, + #NewDocumentVersionSerializer, RecentDocumentSerializer, + RecentDocumentSerializer, + #WritableDocumentSerializer, WritableDocumentTypeSerializer, + #WritableDocumentVersionSerializer ) from .settings import settings_document_page_image_cache_time from .tasks import task_generate_document_page_image @@ -41,12 +42,172 @@ from .tasks import task_generate_document_page_image logger = logging.getLogger(__name__) + +from rest_framework.decorators import action, detail_route +from rest_framework.response import Response + + +class DocumentViewSet(viewsets.ModelViewSet): + lookup_field = 'pk' + lookup_url_kwarg = 'document_id' + queryset = Document.objects.all() + serializer_class = DocumentSerializer + + """ + @action( + detail=True, lookup_field='pk', lookup_url_kwarg='document_id', + url_name='document-version-list', url_path='document_versions' + ) + def document_version_list(self, request, *args, **kwargs): + serializer = DocumentVersionSerializer( + instance=self.get_object().versions.all(), many=True, + context={'request': request} + ) + return Response(serializer.data) + """ + +class DocumentPageViewSet(viewsets.ModelViewSet): + lookup_field = 'pk' + lookup_url_kwarg = 'document_page_id' + serializer_class = DocumentPageSerializer + + def get_queryset(self): + return get_object_or_404( + klass=DocumentVersion, document_id=self.kwargs['document_id'], + pk=self.kwargs['document_version_id'] + ).pages.all() + + @action( + detail=True, lookup_field='pk', lookup_url_kwarg='document_page_id', + url_name='image', url_path='image' + ) + @cache_control(private=True) + def document_page_image(self, request, *args, **kwargs): + """ + asdasd + """ + transformation_dict = { + 'kwargs': {}, + 'name': {} + } + transformation_list = [] + + querystring = furl() + querystring.args.update(self.request.GET) + querystring.args.update(self.request.POST) + + for key, value in querystring.args.items(): + if key.startswith('transformation_'): + literal, index, element = key.split('_') + transformation_dict[element][index] = value + + for order, identifier in transformation_dict['name'].items(): + if order in transformation_dict['kwargs'].keys(): + kwargs = {} + for kwargs_entry in transformation_dict['kwargs'][order].split(','): + key, value = kwargs_entry.split(':') + kwargs[key] = float(value) + + transformation_list.append({ + 'name': identifier, + 'kwargs': kwargs + }) + + width = request.GET.get('width') + height = request.GET.get('height') + zoom = request.GET.get('zoom') + + if zoom: + zoom = int(zoom) + + rotation = request.GET.get('rotation') + + if rotation: + rotation = int(rotation) + + task = task_generate_document_page_image.apply_async( + kwargs=dict( + document_page_id=self.get_object().pk, width=width, + height=height, zoom=zoom, rotation=rotation, + transformation_list=transformation_list + ) + ) + + cache_filename = task.get(timeout=DOCUMENT_IMAGE_TASK_TIMEOUT) + with self.get_object().cache_partition.get_file(filename=cache_filename).open() as file_object: + response = HttpResponse(file_object.read(), content_type='image') + if '_hash' in request.GET: + patch_cache_control( + response, + max_age=settings_document_page_image_cache_time.value + ) + return response + + + """ + @action( + detail=True, lookup_field='pk', lookup_url_kwarg='document_page_id', + url_name='document-page-list', url_path='document_pages' + ) + def document_page_list(self, request, *args, **kwargs): + serializer = DocumentPageSerializer( + instance=self.get_object().versions.all(), many=True, + context={'request': request} + ) + return Response(serializer.data) + """ + +class DocumentTypeViewSet(viewsets.ModelViewSet): + lookup_field = 'pk' + lookup_url_kwarg = 'document_type_id' + queryset = DocumentType.objects.all() + serializer_class = DocumentTypeSerializer + + @action( + detail=True, lookup_field='pk', lookup_url_kwarg='document_type_id', + url_name='document-list', url_path='documents' + ) + def document_list(self, request, *args, **kwargs): + serializer = DocumentSerializer( + instance=self.get_object().documents.all(), many=True, + context={'request': request} + ) + return Response(serializer.data) + + +class DocumentVersionViewSet(viewsets.ModelViewSet): + lookup_field = 'pk' + lookup_url_kwarg = 'document_version_id' + serializer_class = DocumentVersionSerializer + + """ + @action( + detail=True, lookup_field='pk', lookup_url_kwarg='document_version_id', + url_name='document-pages-list', url_path='document_pages' + ) + def document_pages_list(self, request, *args, **kwargs): + serializer = DocumentPageSerializer( + instance=self.get_object().pages.all(), many=True, + context={'request': request} + ) + return Response(serializer.data) + """ + def get_queryset(self): + return get_object_or_404( + klass=Document, pk=self.kwargs['document_id'] + ).versions.all() + + +''' + + + class APIDeletedDocumentListView(generics.ListAPIView): """ Returns a list of all the trashed documents. """ filter_backends = (MayanObjectPermissionsFilter,) - lookup_url_kwarg = 'document_pk' + lookup_url_kwarg = 'document_id' mayan_object_permissions = {'GET': (permission_document_view,)} permission_classes = (MayanPermission,) queryset = Document.trash.all() @@ -59,9 +220,9 @@ class APIDeletedDocumentView(generics.RetrieveDestroyAPIView): delete: Delete the trashed document. get: Retreive the details of the trashed document. """ - lookup_url_kwarg = 'document_pk' + lookup_url_kwarg = 'document_id' mayan_object_permissions = { - 'DELETE': (permission_document_delete,), + 'DELETE': (permission_trashed_document_delete,), 'GET': (permission_document_view,) } permission_classes = (MayanPermission,) @@ -73,9 +234,9 @@ class APIDeletedDocumentRestoreView(generics.GenericAPIView): """ post: Restore a trashed document. """ - lookup_url_kwarg = 'document_pk' + lookup_url_kwarg = 'document_id' mayan_object_permissions = { - 'POST': (permission_document_restore,) + 'POST': (permission_trashed_document_restore,) } permission_classes = (MayanPermission,) queryset = Document.trash.all() @@ -95,7 +256,7 @@ class APIDocumentDownloadView(DownloadMixin, generics.RetrieveAPIView): """ get: Download the latest version of a document. """ - lookup_url_kwarg = 'document_pk' + lookup_url_kwarg = 'document_id' mayan_object_permissions = { 'GET': (permission_document_download,) } @@ -146,7 +307,7 @@ class APIDocumentListView(generics.ListCreateAPIView): def perform_create(self, serializer): AccessControlList.objects.check_access( - permissions=(permission_document_create,), user=self.request.user, + permission=permission_document_create,), user=self.request.user, obj=serializer.validated_data['document_type'] ) serializer.save(_user=self.request.user) @@ -156,7 +317,7 @@ class APIDocumentPageImageView(generics.RetrieveAPIView): """ get: Returns an image representation of the selected document. """ - lookup_url_kwarg = 'document_page_pk' + lookup_url_kwarg = 'document_page_id' def get_document(self): if self.request.method == 'GET': @@ -165,7 +326,7 @@ class APIDocumentPageImageView(generics.RetrieveAPIView): permission_required = permission_document_edit document = get_object_or_404( - klass=Document.passthrough, pk=self.kwargs['document_pk'] + klass=Document.passthrough, pk=self.kwargs['document_id'] ) AccessControlList.objects.check_access( @@ -175,7 +336,7 @@ class APIDocumentPageImageView(generics.RetrieveAPIView): def get_document_version(self): return get_object_or_404( - klass=self.get_document().versions.all(), pk=self.kwargs['document_version_pk'] + klass=self.get_document().versions.all(), pk=self.kwargs['document_version_id'] ) def get_queryset(self): @@ -253,7 +414,7 @@ class APIDocumentPageView(generics.RetrieveUpdateAPIView): patch: Edit the selected document page. put: Edit the selected document page. """ - lookup_url_kwarg = 'document_page_pk' + lookup_url_kwarg = 'document_page_id' serializer_class = DocumentPageSerializer def get_document(self): @@ -263,7 +424,7 @@ class APIDocumentPageView(generics.RetrieveUpdateAPIView): permission_required = permission_document_edit document = get_object_or_404( - klass=Document, pk=self.kwargs['document_pk'] + klass=Document, pk=self.kwargs['document_id'] ) AccessControlList.objects.check_access( @@ -274,7 +435,7 @@ class APIDocumentPageView(generics.RetrieveUpdateAPIView): def get_document_version(self): return get_object_or_404( klass=self.get_document().versions.all(), - pk=self.kwargs['document_version_pk'] + pk=self.kwargs['document_version_id'] ) def get_queryset(self): @@ -287,7 +448,7 @@ class APIDocumentTypeListView(generics.ListCreateAPIView): post: Create a new document type. """ filter_backends = (MayanObjectPermissionsFilter,) - lookup_url_kwarg = 'document_type_pk' + lookup_url_kwarg = 'document_type_id' mayan_object_permissions = {'GET': (permission_document_type_view,)} mayan_view_permissions = {'POST': (permission_document_type_create,)} permission_classes = (MayanPermission,) @@ -314,7 +475,7 @@ class APIDocumentTypeView(generics.RetrieveUpdateDestroyAPIView): patch: Edit the properties of the selected document type. put: Edit the properties of the selected document type. """ - lookup_url_kwarg = 'document_type_pk' + lookup_url_kwarg = 'document_type_id' mayan_object_permissions = { 'GET': (permission_document_type_view,), 'PUT': (permission_document_type_edit,), @@ -342,13 +503,13 @@ class APIDocumentTypeDocumentListView(generics.ListAPIView): Returns a list of all the documents of a particular document type. """ filter_backends = (MayanObjectPermissionsFilter,) - lookup_url_kwarg = 'document_type_pk' + lookup_url_kwarg = 'document_type_id' mayan_object_permissions = {'GET': (permission_document_view,)} serializer_class = DocumentSerializer def get_queryset(self): document_type = get_object_or_404( - klass=DocumentType, pk=self.kwargs['document_type_pk'] + klass=DocumentType, pk=self.kwargs['document_type_id'] ) AccessControlList.objects.check_access( permissions=permission_document_type_view, user=self.request.user, @@ -362,15 +523,15 @@ class APIDocumentVersionDownloadView(DownloadMixin, generics.RetrieveAPIView): """ get: Download a document version. """ - lookup_url_kwarg = 'document_version_pk' + lookup_url_kwarg = 'document_version_id' def get_document(self): document = get_object_or_404( - klass=Document, pk=self.kwargs['document_pk'] + klass=Document, pk=self.kwargs['document_id'] ) AccessControlList.objects.check_access( - permissions=(permission_document_download,), user=self.request.user, + permission=permission_document_download,), user=self.request.user, obj=document ) return document @@ -418,7 +579,7 @@ class APIDocumentView(generics.RetrieveUpdateDestroyAPIView): patch: Edit the properties of the selected document. put: Edit the properties of the selected document. """ - lookup_url_kwarg = 'document_pk' + lookup_url_kwarg = 'document_id' mayan_object_permissions = { 'GET': (permission_document_view,), 'PUT': (permission_document_properties_edit,), @@ -463,7 +624,7 @@ class APIDocumentVersionPageListView(generics.ListAPIView): def get_document(self): document = get_object_or_404( - klass=Document, pk=self.kwargs['document_pk'] + klass=Document, pk=self.kwargs['document_id'] ) AccessControlList.objects.check_access( @@ -474,7 +635,7 @@ class APIDocumentVersionPageListView(generics.ListAPIView): def get_document_version(self): return get_object_or_404( klass=self.get_document().versions.all(), - pk=self.kwargs['document_version_pk'] + pk=self.kwargs['document_version_id'] ) def get_queryset(self): @@ -490,7 +651,7 @@ class APIDocumentVersionsListView(generics.ListCreateAPIView): mayan_object_permissions = { 'GET': (permission_document_version_view,), } - mayan_permission_attribute_check = 'document' + #mayan_permission_attribute_check = 'document' permission_classes = (MayanPermission,) def create(self, request, *args, **kwargs): @@ -514,16 +675,16 @@ class APIDocumentVersionsListView(generics.ListCreateAPIView): def get_queryset(self): return get_object_or_404( - klass=Document, pk=self.kwargs['document_pk'] + klass=Document, pk=self.kwargs['document_id'] ).versions.all() def perform_create(self, serializer): document = get_object_or_404( - klass=Document, pk=self.kwargs['document_pk'] + klass=Document, pk=self.kwargs['document_id'] ) AccessControlList.objects.check_access( - permissions=(permission_document_new_version,), + permission=permission_document_new_version,), user=self.request.user, obj=document ) serializer.save(document=document, _user=self.request.user) @@ -536,7 +697,7 @@ class APIDocumentVersionView(generics.RetrieveUpdateDestroyAPIView): patch: Edit the selected document version. put: Edit the selected document version. """ - lookup_url_kwarg = 'document_version_pk' + lookup_url_kwarg = 'document_version_id' def get_document(self): if self.request.method == 'GET': @@ -547,7 +708,7 @@ class APIDocumentVersionView(generics.RetrieveUpdateDestroyAPIView): permission_required = permission_document_edit document = get_object_or_404( - klass=Document, pk=self.kwargs['document_pk'] + klass=Document, pk=self.kwargs['document_id'] ) AccessControlList.objects.check_access( @@ -569,3 +730,4 @@ class APIDocumentVersionView(generics.RetrieveUpdateDestroyAPIView): return DocumentVersionSerializer else: return WritableDocumentVersionSerializer +''' diff --git a/mayan/apps/documents/apps.py b/mayan/apps/documents/apps.py index 46afbccd13..2aad41516e 100644 --- a/mayan/apps/documents/apps.py +++ b/mayan/apps/documents/apps.py @@ -53,17 +53,17 @@ from .handlers import ( ) from .links import ( link_clear_image_cache, link_document_clear_transformations, - link_document_clone_transformations, link_document_delete, - link_document_document_type_edit, link_document_download, + link_document_clone_transformations, link_trashed_document_delete, + link_document_change_type, link_document_download, link_document_duplicates_list, link_document_edit, link_document_favorites_add, link_document_favorites_remove, - link_document_list, link_document_list_deleted, + link_document_list, link_trashed_document_list, link_document_list_favorites, link_document_list_recent_access, link_document_list_recent_added, link_document_multiple_clear_transformations, - link_document_multiple_delete, link_document_multiple_document_type_edit, + link_trashed_document_multiple_delete, link_document_multiple_change_type, link_document_multiple_download, link_document_multiple_favorites_add, - link_document_multiple_favorites_remove, link_document_multiple_restore, + link_document_multiple_favorites_remove, link_trashed_document_multiple_restore, link_document_multiple_trash, link_document_multiple_update_page_count, link_document_page_navigation_first, link_document_page_navigation_last, link_document_page_navigation_next, link_document_page_navigation_previous, @@ -72,7 +72,7 @@ from .links import ( link_document_page_view_reset, link_document_page_zoom_in, link_document_page_zoom_out, link_document_pages, link_document_preview, link_document_print, link_document_properties, - link_document_quick_download, link_document_restore, link_document_trash, + link_document_quick_download, link_trashed_document_restore, link_document_trash, link_document_type_create, link_document_type_delete, link_document_type_edit, link_document_type_filename_create, link_document_type_filename_delete, link_document_type_filename_edit, @@ -90,14 +90,14 @@ from .literals import ( ) from .menus import menu_documents from .permissions import ( - permission_document_create, permission_document_delete, + permission_document_create, permission_document_download, permission_document_edit, permission_document_new_version, permission_document_print, - permission_document_properties_edit, permission_document_restore, - permission_document_trash, permission_document_type_delete, - permission_document_type_edit, permission_document_type_view, - permission_document_version_revert, permission_document_version_view, - permission_document_view + permission_document_properties_edit, permission_document_trash, + permission_document_type_delete, permission_document_type_edit, + permission_document_type_view, permission_document_version_revert, + permission_document_version_view, permission_document_view, + permission_trashed_document_delete, permission_trashed_document_restore ) from .queues import * # NOQA # Just import to initialize the search models @@ -119,14 +119,18 @@ class DocumentsApp(MayanAppConfig): super(DocumentsApp, self).ready() from actstream import registry - DeletedDocument = self.get_model('DeletedDocument') - Document = self.get_model('Document') - DocumentPage = self.get_model('DocumentPage') - DocumentPageSearchResult = self.get_model('DocumentPageSearchResult') - DocumentType = self.get_model('DocumentType') - DocumentTypeFilename = self.get_model('DocumentTypeFilename') - DocumentVersion = self.get_model('DocumentVersion') - DuplicatedDocumentProxy = self.get_model('DuplicatedDocumentProxy') + Document = self.get_model(model_name='Document') + DocumentPage = self.get_model(model_name='DocumentPage') + DocumentPageSearchResult = self.get_model( + model_name='DocumentPageSearchResult' + ) + DocumentType = self.get_model(model_name='DocumentType') + DocumentTypeFilename = self.get_model(model_name='DocumentTypeFilename') + DocumentVersion = self.get_model(model_name='DocumentVersion') + DuplicatedDocumentProxy = self.get_model( + model_name='DuplicatedDocumentProxy' + ) + TrashedDocument = self.get_model(model_name='TrashedDocument') DynamicSerializerField.add_serializer( klass=Document, @@ -185,15 +189,16 @@ class DocumentsApp(MayanAppConfig): ModelPermission.register( model=Document, permissions=( permission_acl_edit, permission_acl_view, - permission_document_delete, permission_document_download, - permission_document_edit, permission_document_new_version, - permission_document_print, permission_document_properties_edit, - permission_document_restore, permission_document_trash, + permission_document_download, permission_document_edit, + permission_document_new_version, permission_document_print, + permission_document_properties_edit, permission_document_trash, permission_document_version_revert, permission_document_version_view, permission_document_view, permission_events_view, permission_transformation_create, permission_transformation_delete, permission_transformation_edit, permission_transformation_view, + permission_trashed_document_delete, + permission_trashed_document_restore ) ) @@ -297,22 +302,22 @@ class DocumentsApp(MayanAppConfig): widget=TwoStateWidget ) - # DeletedDocument + # TrashedDocument SourceColumn( attribute='label', is_identifier=True, is_sortable=True, - source=DeletedDocument + source=TrashedDocument ) SourceColumn( func=lambda context: document_page_thumbnail_widget.render( instance=context['object'] - ), label=_('Thumbnail'), source=DeletedDocument + ), label=_('Thumbnail'), source=TrashedDocument ) SourceColumn( - attribute='document_type', is_sortable=True, source=DeletedDocument + attribute='document_type', is_sortable=True, source=TrashedDocument ) SourceColumn( - attribute='get_rendered_deleted_date_time', source=DeletedDocument + attribute='get_rendered_deleted_date_time', source=TrashedDocument ) # DocumentVersion @@ -445,7 +450,7 @@ class DocumentsApp(MayanAppConfig): links=( link_document_list_recent_access, link_document_list_recent_added, link_document_list_favorites, - link_document_list, link_document_list_deleted, + link_document_list, link_trashed_document_list, link_duplicated_document_list, ) ) @@ -493,7 +498,7 @@ class DocumentsApp(MayanAppConfig): menu_sidebar.bind_links( links=(link_trash_can_empty,), sources=( - 'documents:document_list_deleted', 'documents:trash_can_empty' + 'documents:trashed_document_list', 'documents:trash_can_empty' ) ) @@ -501,7 +506,7 @@ class DocumentsApp(MayanAppConfig): menu_object.bind_links( links=( link_document_favorites_add, link_document_favorites_remove, - link_document_edit, link_document_document_type_edit, + link_document_edit, link_document_change_type, link_document_print, link_document_trash, link_document_quick_download, link_document_download, link_document_clear_transformations, @@ -510,8 +515,8 @@ class DocumentsApp(MayanAppConfig): ), sources=(Document,) ) menu_object.bind_links( - links=(link_document_restore, link_document_delete), - sources=(DeletedDocument,) + links=(link_trashed_document_restore, link_trashed_document_delete), + sources=(TrashedDocument,) ) # Document facet links @@ -548,13 +553,14 @@ class DocumentsApp(MayanAppConfig): link_document_multiple_clear_transformations, link_document_multiple_trash, link_document_multiple_download, link_document_multiple_update_page_count, - link_document_multiple_document_type_edit, + link_document_multiple_change_type, ), sources=(Document,) ) menu_multi_item.bind_links( links=( - link_document_multiple_restore, link_document_multiple_delete - ), sources=(DeletedDocument,) + link_trashed_document_multiple_restore, + link_trashed_document_multiple_delete + ), sources=(TrashedDocument,) ) # Document pages @@ -614,7 +620,7 @@ class DocumentsApp(MayanAppConfig): receiver=handler_scan_duplicates_for, ) - registry.register(DeletedDocument) + registry.register(TrashedDocument) registry.register(Document) registry.register(DocumentType) registry.register(DocumentVersion) diff --git a/mayan/apps/documents/dashboard_widgets.py b/mayan/apps/documents/dashboard_widgets.py index e862c98b1c..887ccd8f36 100644 --- a/mayan/apps/documents/dashboard_widgets.py +++ b/mayan/apps/documents/dashboard_widgets.py @@ -32,17 +32,19 @@ class DashboardWidgetDocumentPagesTotal(DashboardWidgetNumeric): DocumentPage = apps.get_model( app_label='documents', model_name='DocumentPage' ) - self.count = AccessControlList.objects.filter_by_access( - permission=permission_document_view, user=request.user, - queryset=DocumentPage.objects.all() + self.count = AccessControlList.objects.restrict_queryset( + permission=permission_document_view, + queryset=DocumentPage.objects.all(), user=request.user ).count() - return super(DashboardWidgetDocumentPagesTotal, self).render(request) + return super(DashboardWidgetDocumentPagesTotal, self).render( + request=request + ) class DashboardWidgetDocumentsTotal(DashboardWidgetNumeric): icon_class = icon_dashboard_total_document label = _('Total documents') - link = reverse_lazy('documents:document_list') + link = reverse_lazy(viewname='documents:document_list') def render(self, request): AccessControlList = apps.get_model( @@ -51,36 +53,40 @@ class DashboardWidgetDocumentsTotal(DashboardWidgetNumeric): Document = apps.get_model( app_label='documents', model_name='Document' ) - self.count = AccessControlList.objects.filter_by_access( - permission=permission_document_view, user=request.user, - queryset=Document.objects.all() + self.count = AccessControlList.objects.restrict_queryset( + permission=permission_document_view, + queryset=Document.objects.all(), user=request.user ).count() - return super(DashboardWidgetDocumentsTotal, self).render(request) + return super(DashboardWidgetDocumentsTotal, self).render( + request=request + ) class DashboardWidgetDocumentsInTrash(DashboardWidgetNumeric): icon_class = icon_dashboard_documents_in_trash label = _('Documents in trash') - link = reverse_lazy('documents:document_list_deleted') + link = reverse_lazy(viewname='documents:trashed_document_list') def render(self, request): AccessControlList = apps.get_model( app_label='acls', model_name='AccessControlList' ) - DeletedDocument = apps.get_model( - app_label='documents', model_name='DeletedDocument' + TrashedDocument = apps.get_model( + app_label='documents', model_name='TrashedDocument' ) - self.count = AccessControlList.objects.filter_by_access( - permission=permission_document_view, user=request.user, - queryset=DeletedDocument.objects.all() + self.count = AccessControlList.objects.restrict_queryset( + permission=permission_document_view, + queryset=TrashedDocument.objects.all(), user=request.user ).count() - return super(DashboardWidgetDocumentsInTrash, self).render(request) + return super(DashboardWidgetDocumentsInTrash, self).render( + request=request + ) class DashboardWidgetDocumentsTypesTotal(DashboardWidgetNumeric): icon_class = icon_dashboard_document_types label = _('Document types') - link = reverse_lazy('documents:document_type_list') + link = reverse_lazy(viewname='documents:document_type_list') def render(self, request): AccessControlList = apps.get_model( @@ -89,11 +95,13 @@ class DashboardWidgetDocumentsTypesTotal(DashboardWidgetNumeric): DocumentType = apps.get_model( app_label='documents', model_name='DocumentType' ) - self.count = AccessControlList.objects.filter_by_access( - permission=permission_document_type_view, user=request.user, - queryset=DocumentType.objects.all() + self.count = AccessControlList.objects.restrict_queryset( + permission=permission_document_type_view, + queryset=DocumentType.objects.all(), user=request.user ).count() - return super(DashboardWidgetDocumentsTypesTotal, self).render(request) + return super(DashboardWidgetDocumentsTypesTotal, self).render( + request=request + ) class DashboardWidgetDocumentsNewThisMonth(DashboardWidgetNumeric): @@ -106,7 +114,9 @@ class DashboardWidgetDocumentsNewThisMonth(DashboardWidgetNumeric): def render(self, request): self.count = new_documents_this_month(user=request.user) - return super(DashboardWidgetDocumentsNewThisMonth, self).render(request) + return super(DashboardWidgetDocumentsNewThisMonth, self).render( + request=request + ) class DashboardWidgetDocumentsPagesNewThisMonth(DashboardWidgetNumeric): @@ -119,4 +129,6 @@ class DashboardWidgetDocumentsPagesNewThisMonth(DashboardWidgetNumeric): def render(self, request): self.count = new_document_pages_this_month(user=request.user) - return super(DashboardWidgetDocumentsPagesNewThisMonth, self).render(request) + return super(DashboardWidgetDocumentsPagesNewThisMonth, self).render( + request=request + ) diff --git a/mayan/apps/documents/events.py b/mayan/apps/documents/events.py index 817970f27d..b841730d62 100644 --- a/mayan/apps/documents/events.py +++ b/mayan/apps/documents/events.py @@ -4,35 +4,35 @@ from django.utils.translation import ugettext_lazy as _ from mayan.apps.events import EventTypeNamespace -namespace = EventTypeNamespace(name='documents', label=_('Documents')) +namespace = EventTypeNamespace(label=_('Documents'), name='documents') event_document_create = namespace.add_event_type( - name='document_create', label=_('Document created') + label=_('Document created'), name='document_create' ) event_document_download = namespace.add_event_type( - name='document_download', label=_('Document downloaded') + label=_('Document downloaded'), name='document_download' ) event_document_new_version = namespace.add_event_type( - name='document_new_version', label=_('New version uploaded') + label=_('New version uploaded'), name='document_new_version' ) event_document_properties_edit = namespace.add_event_type( - name='document_edit', label=_('Document properties edited') + label=_('Document properties edited'), name='document_edit' ) # The type of an existing document is changed to another type event_document_type_change = namespace.add_event_type( - name='document_type_change', label=_('Document type changed') + label=_('Document type changed'), name='document_type_change' ) # A document type is created event_document_type_created = namespace.add_event_type( - name='document_type_created', label=_('Document type created') + label=_('Document type created'), name='document_type_created' ) # An existing document type is modified event_document_type_edited = namespace.add_event_type( - name='document_type_edit', label=_('Document type edited') + label=_('Document type edited'), name='document_type_edit' ) event_document_version_revert = namespace.add_event_type( - name='document_version_revert', label=_('Document version reverted') + label=_('Document version reverted'), name='document_version_revert' ) event_document_view = namespace.add_event_type( - name='document_view', label=_('Document viewed') + label=_('Document viewed'), name='document_view' ) diff --git a/mayan/apps/documents/forms/document_type_forms.py b/mayan/apps/documents/forms/document_type_forms.py index f3795803c3..d6da439be0 100644 --- a/mayan/apps/documents/forms/document_type_forms.py +++ b/mayan/apps/documents/forms/document_type_forms.py @@ -45,7 +45,7 @@ class DocumentTypeFilteredSelectForm(forms.Form): queryset = DocumentType.objects.all() if permission: - queryset = AccessControlList.objects.filter_by_access( + queryset = AccessControlList.objects.restrict_queryset( permission=permission, queryset=queryset, user=user ) diff --git a/mayan/apps/documents/icons.py b/mayan/apps/documents/icons.py index 5e3a005029..a0e68084cc 100644 --- a/mayan/apps/documents/icons.py +++ b/mayan/apps/documents/icons.py @@ -18,9 +18,6 @@ icon_dashboard_new_documents_this_month = Icon( icon_dashboard_total_document = Icon( driver_name='fontawesome', symbol='file' ) -icon_document_delete = Icon( - driver_name='fontawesome', symbol='times' -) icon_document_download = Icon( driver_name='fontawesome', symbol='download' ) @@ -42,9 +39,6 @@ icon_document_image_loading = Icon( driver_name='fontawesomecss', css_classes='far fa-clock fa-2x' ) icon_document_list = Icon(driver_name='fontawesome', symbol='book') -icon_document_list_deleted = Icon( - driver_name='fontawesome', symbol='trash-alt' -) icon_document_list_favorites = Icon(driver_name='fontawesome', symbol='star') icon_document_list_recent_access = Icon( driver_name='fontawesome', symbol='clock' @@ -52,12 +46,6 @@ icon_document_list_recent_access = Icon( icon_document_list_recent_added = Icon( driver_name='fontawesome', symbol='asterisk' ) -icon_document_multiple_delete = Icon( - driver_name='fontawesome', symbol='trash-alt' -) -icon_document_multiepl_restore = Icon( - driver_name='fontawesome', symbol='recycle' -) icon_document_page_navigation_first = Icon( driver_name='fontawesome', symbol='step-backward' ) @@ -95,7 +83,6 @@ icon_document_print = Icon( driver_name='fontawesome', symbol='print' ) icon_document_properties = Icon(driver_name='fontawesome', symbol='info') -icon_document_restore = Icon(driver_name='fontawesome', symbol='recycle') icon_document_trash = Icon( driver_name='fontawesome', symbol='trash-alt' ) @@ -141,3 +128,18 @@ icon_menu_documents = Icon(driver_name='fontawesome', symbol='book') icon_trash_can_empty = Icon( driver_name='fontawesome', symbol='trash-alt' ) +icon_trashed_document_delete = Icon( + driver_name='fontawesome', symbol='times' +) +icon_trashed_document_list = Icon( + driver_name='fontawesome', symbol='trash-alt' +) +icon_trashed_document_multiple_delete = Icon( + driver_name='fontawesome', symbol='trash-alt' +) +icon_trashed_document_multiple_restore = Icon( + driver_name='fontawesome', symbol='recycle' +) +icon_trashed_document_restore = Icon( + driver_name='fontawesome', symbol='recycle' +) diff --git a/mayan/apps/documents/links.py b/mayan/apps/documents/links.py index 8478b5b6c5..218c35180c 100644 --- a/mayan/apps/documents/links.py +++ b/mayan/apps/documents/links.py @@ -9,19 +9,18 @@ from mayan.apps.navigation import Link from .icons import ( icon_clear_image_cache, icon_document_duplicates_list, icon_document_list, - icon_document_list_deleted, icon_document_list_favorites, + icon_document_list_favorites, icon_document_list_recent_access, icon_document_list_recent_added, - icon_document_delete, icon_document_download, icon_document_edit, + icon_trashed_document_delete, icon_document_download, icon_document_edit, icon_document_favorites_add, icon_document_favorites_remove, - icon_document_multiple_delete, - icon_document_multiple_restore, icon_document_page_navigation_first, + icon_document_page_navigation_first, icon_document_page_navigation_last, icon_document_page_navigation_next, icon_document_page_navigation_previous, icon_document_page_return, icon_document_page_rotate_left, icon_document_page_rotate_right, icon_document_page_view, icon_document_page_view_reset, icon_document_page_zoom_in, icon_document_page_zoom_out, icon_document_pages, icon_document_preview, - icon_document_print, icon_document_properties, icon_document_restore, + icon_document_print, icon_document_properties, icon_trashed_document_restore, icon_document_trash, icon_document_type_create, icon_document_type_delete, icon_document_type_edit, icon_document_type_filename, icon_document_type_filename_create, @@ -30,17 +29,20 @@ from .icons import ( icon_document_version_list, icon_document_version_return_document, icon_document_version_return_list, icon_document_version_view, icon_duplicated_document_list, icon_duplicated_document_scan, - icon_trash_can_empty + icon_trash_can_empty, + icon_trashed_document_list, + icon_trashed_document_multiple_delete, + icon_trashed_document_multiple_restore, ) from .permissions import ( - permission_document_delete, permission_document_download, - permission_document_print, permission_document_properties_edit, - permission_document_restore, permission_document_tools, + permission_document_download, permission_document_print, + permission_document_properties_edit, permission_document_tools, permission_document_trash, permission_document_type_create, permission_document_type_delete, permission_document_type_edit, permission_document_type_view, permission_document_version_revert, permission_document_version_view, permission_document_view, - permission_empty_trash + permission_empty_trash, permission_trashed_document_delete, + permission_trashed_document_restore ) from .settings import setting_zoom_max_level, setting_zoom_min_level @@ -74,22 +76,26 @@ def is_min_zoom(context): # Facet link_document_preview = Link( - args='resolved_object.id', icon_class=icon_document_preview, + icon_class=icon_document_preview, + kwargs={'document_id': 'resolved_object.id'}, permission=permission_document_view, text=_('Preview'), view='documents:document_preview' ) link_document_properties = Link( - args='resolved_object.id', icon_class=icon_document_properties, + icon_class=icon_document_properties, + kwargs={'document_id': 'resolved_object.id'}, permission=permission_document_view, text=_('Properties'), view='documents:document_properties' ) link_document_version_list = Link( - args='resolved_object.pk', icon_class=icon_document_version_list, + icon_class=icon_document_version_list, + kwargs={'document_id': 'resolved_object.id'}, permission=permission_document_version_view, text=_('Versions'), view='documents:document_version_list' ) link_document_pages = Link( - args='resolved_object.pk', icon_class=icon_document_pages, + icon_class=icon_document_pages, + kwargs={'document_id': 'resolved_object.id'}, permission=permission_document_view, text=_('Pages'), view='documents:document_pages' ) @@ -97,68 +103,68 @@ link_document_pages = Link( # Actions link_document_clear_transformations = Link( args='resolved_object.id', permission=permission_transformation_delete, + kwargs={'document_id': 'resolved_object.id'}, text=_('Clear transformations'), view='documents:document_clear_transformations' ) link_document_clone_transformations = Link( - args='resolved_object.id', permission=permission_transformation_edit, + permission=permission_transformation_edit, + kwargs={'document_id': 'resolved_object.id'}, text=_('Clone transformations'), view='documents:document_clone_transformations' ) -link_document_delete = Link( - args='resolved_object.id', icon_class=icon_document_delete, - permission=permission_document_delete, tags='dangerous', - text=_('Delete'), view='documents:document_delete' -) link_document_favorites_add = Link( - args='resolved_object.id', icon_class=icon_document_favorites_add, + icon_class=icon_document_favorites_add, + kwargs={'document_id': 'resolved_object.id'}, permission=permission_document_view, text=_('Add to favorites'), view='documents:document_add_to_favorites' ) link_document_favorites_remove = Link( - args='resolved_object.id', icon_class=icon_document_favorites_remove, + icon_class=icon_document_favorites_remove, + kwargs={'document_id': 'resolved_object.id'}, permission=permission_document_view, text=_('Remove from favorites'), view='documents:document_remove_from_favorites' ) link_document_trash = Link( - args='resolved_object.id', icon_class=icon_document_trash, + icon_class=icon_document_trash, + kwargs={'document_id': 'resolved_object.id'}, permission=permission_document_trash, tags='dangerous', text=_('Move to trash'), view='documents:document_trash' ) link_document_edit = Link( - args='resolved_object.id', icon_class=icon_document_edit, + icon_class=icon_document_edit, + kwargs={'document_id': 'resolved_object.id'}, permission=permission_document_properties_edit, text=_('Edit properties'), view='documents:document_edit' ) -link_document_document_type_edit = Link( - args='resolved_object.id', +link_document_change_type = Link( + kwargs={'document_id': 'resolved_object.id'}, permission=permission_document_properties_edit, text=_('Change type'), - view='documents:document_document_type_edit' + view='documents:document_change_type' ) link_document_download = Link( - args='resolved_object.id', icon_class=icon_document_download, + icon_class=icon_document_download, + kwargs={'document_id': 'resolved_object.id'}, permission=permission_document_download, text=_('Advanced download'), view='documents:document_download_form' ) link_document_print = Link( - args='resolved_object.id', icon_class=icon_document_print, + icon_class=icon_document_print, + kwargs={'document_id': 'resolved_object.id'}, permission=permission_document_print, text=_('Print'), view='documents:document_print' ) link_document_quick_download = Link( - args='resolved_object.id', permission=permission_document_download, + permission=permission_document_download, + kwargs={'document_id': 'resolved_object.id'}, text=_('Quick download'), view='documents:document_download' ) link_document_update_page_count = Link( - args='resolved_object.pk', permission=permission_document_tools, + kwargs={'document_id': 'resolved_object.id'}, + permission=permission_document_tools, text=_('Recalculate page count'), view='documents:document_update_page_count' ) -link_document_restore = Link( - args='object.pk', icon_class=icon_document_restore, - permission=permission_document_restore, text=_('Restore'), - view='documents:document_restore' -) link_document_multiple_clear_transformations = Link( permission=permission_transformation_delete, text=_('Clear transformations'), @@ -168,10 +174,6 @@ link_document_multiple_trash = Link( tags='dangerous', text=_('Move to trash'), view='documents:document_multiple_trash' ) -link_document_multiple_delete = Link( - icon_class=icon_document_multiple_delete, tags='dangerous', - text=_('Delete'), view='documents:document_multiple_delete' -) link_document_multiple_favorites_add = Link( text=_('Add to favorites'), view='documents:document_multiple_add_to_favorites' @@ -180,9 +182,9 @@ link_document_multiple_favorites_remove = Link( text=_('Remove from favorites'), view='documents:document_multiple_remove_from_favorites' ) -link_document_multiple_document_type_edit = Link( +link_document_multiple_change_type = Link( text=_('Change type'), - view='documents:document_multiple_document_type_edit' + view='documents:document_multiple_change_type' ) link_document_multiple_download = Link( text=_('Advanced download'), @@ -192,31 +194,54 @@ link_document_multiple_update_page_count = Link( text=_('Recalculate page count'), view='documents:document_multiple_update_page_count' ) -link_document_multiple_restore = Link( - icon_class=icon_document_multiple_restore, text=_('Restore'), - view='documents:document_multiple_restore' + +link_trashed_document_delete = Link( + icon_class=icon_trashed_document_delete, + kwargs={'trashed_document_id': 'resolved_object.id'}, + permission=permission_trashed_document_delete, tags='dangerous', + text=_('Delete'), view='documents:trashed_document_delete' +) +link_trashed_document_list = Link( + icon_class=icon_trashed_document_list, text=_('Trash can'), + view='documents:trashed_document_list' +) +link_trashed_document_restore = Link( + icon_class=icon_trashed_document_restore, + kwargs={'trashed_document_id': 'resolved_object.id'}, + permission=permission_trashed_document_restore, text=_('Restore'), + view='documents:trashed_document_restore' +) +link_trashed_document_multiple_delete = Link( + icon_class=icon_trashed_document_multiple_delete, tags='dangerous', + text=_('Delete'), view='documents:trashed_document_multiple_delete' +) +link_trashed_document_multiple_restore = Link( + icon_class=icon_trashed_document_multiple_restore, text=_('Restore'), + view='documents:trashed_document_multiple_restore' ) # Versions link_document_version_download = Link( - args='resolved_object.pk', icon_class=icon_document_version_download, + icon_class=icon_document_version_download, + kwargs={'document_version_id': 'resolved_object.pk'}, permission=permission_document_download, text=_('Download version'), view='documents:document_version_download_form' ) link_document_version_return_document = Link( - args='resolved_object.document.pk', icon_class=icon_document_version_return_document, + kwargs={'document_id': 'resolved_object.document.pk'}, permission=permission_document_view, text=_('Document'), view='documents:document_preview' ) link_document_version_return_list = Link( - args='resolved_object.document.pk', icon_class=icon_document_version_return_list, + kwargs={'document_id': 'resolved_object.document.pk'}, permission=permission_document_version_view, text=_('Versions'), view='documents:document_version_list' ) link_document_version_view = Link( - args='resolved_object.pk', icon_class=icon_document_version_view, + icon_class=icon_document_version_view, + kwargs={'document_version_id': 'resolved_object.pk'}, permission=permission_document_version_view, text=_('Preview'), view='documents:document_version_view' ) @@ -238,10 +263,6 @@ link_document_list_recent_added = Link( icon_class=icon_document_list_recent_added, text=_('Recently added'), view='documents:document_list_recent_added' ) -link_document_list_deleted = Link( - icon_class=icon_document_list_deleted, text=_('Trash can'), - view='documents:document_list_deleted' -) # Tools link_clear_image_cache = Link( @@ -260,73 +281,80 @@ link_trash_can_empty = Link( # Document pages link_document_page_navigation_first = Link( - args='resolved_object.pk', conditional_disable=is_first_page, - icon_class=icon_document_page_navigation_first, - keep_query=True, permission=permission_document_view, - text=_('First page'), view='documents:document_page_navigation_first' + conditional_disable=is_first_page, + icon_class=icon_document_page_navigation_first, keep_query=True, + kwargs={'document_page_id': 'resolved_object.pk'}, + permission=permission_document_view, text=_('First page'), + view='documents:document_page_navigation_first' ) link_document_page_navigation_last = Link( - args='resolved_object.pk', conditional_disable=is_last_page, - icon_class=icon_document_page_navigation_last, - keep_query=True, text=_('Last page'), - permission=permission_document_view, + conditional_disable=is_last_page, + icon_class=icon_document_page_navigation_last, keep_query=True, + kwargs={'document_page_id': 'resolved_object.pk'}, + permission=permission_document_view, text=_('Last page'), view='documents:document_page_navigation_last' ) link_document_page_navigation_previous = Link( - args='resolved_object.pk', conditional_disable=is_first_page, - icon_class=icon_document_page_navigation_previous, - keep_query=True, permission=permission_document_view, - text=_('Previous page'), + conditional_disable=is_first_page, + icon_class=icon_document_page_navigation_previous, keep_query=True, + kwargs={'document_page_id': 'resolved_object.pk'}, + permission=permission_document_view, text=_('Previous page'), view='documents:document_page_navigation_previous' ) link_document_page_navigation_next = Link( - args='resolved_object.pk', conditional_disable=is_last_page, - icon_class=icon_document_page_navigation_next, - keep_query=True, text=_('Next page'), + conditional_disable=is_last_page, + icon_class=icon_document_page_navigation_next, keep_query=True, + kwargs={'document_page_id': 'resolved_object.pk'}, text=_('Next page'), permission=permission_document_view, view='documents:document_page_navigation_next' ) link_document_page_return = Link( - args='resolved_object.document.pk', icon_class=icon_document_page_return, + icon_class=icon_document_page_return, + kwargs={'document_id': 'resolved_object.document.pk'}, permission=permission_document_view, text=_('Document'), view='documents:document_preview' ) link_document_page_rotate_left = Link( - args='resolved_object.pk', icon_class=icon_document_page_rotate_left, - keep_query=True, permission=permission_document_view, - text=_('Rotate left'), view='documents:document_page_rotate_left' + icon_class=icon_document_page_rotate_left, keep_query=True, + kwargs={'document_page_id': 'resolved_object.pk'}, + permission=permission_document_view, text=_('Rotate left'), + view='documents:document_page_rotate_left' ) link_document_page_rotate_right = Link( - args='resolved_object.pk', icon_class=icon_document_page_rotate_right, - keep_query=True, permission=permission_document_view, - text=_('Rotate right'), view='documents:document_page_rotate_right' + icon_class=icon_document_page_rotate_right, keep_query=True, + kwargs={'document_page_id': 'resolved_object.pk'}, + permission=permission_document_view, text=_('Rotate right'), + view='documents:document_page_rotate_right' ) link_document_page_view = Link( icon_class=icon_document_page_view, + kwargs={'document_page_id': 'resolved_object.pk'}, permission=permission_document_view, text=_('Page image'), - view='documents:document_page_view', args='resolved_object.pk' + view='documents:document_page_view' ) link_document_page_view_reset = Link( icon_class=icon_document_page_view_reset, + kwargs={'document_page_id': 'resolved_object.pk'}, permission=permission_document_view, text=_('Reset view'), - view='documents:document_page_view_reset', args='resolved_object.pk' + view='documents:document_page_view_reset' ) link_document_page_zoom_in = Link( - args='resolved_object.pk', conditional_disable=is_max_zoom, - icon_class=icon_document_page_zoom_in, keep_query=True, + conditional_disable=is_max_zoom, icon_class=icon_document_page_zoom_in, + keep_query=True, kwargs={'document_page_id': 'resolved_object.pk'}, permission=permission_document_view, text=_('Zoom in'), view='documents:document_page_zoom_in' ) link_document_page_zoom_out = Link( - args='resolved_object.pk', conditional_disable=is_min_zoom, - icon_class=icon_document_page_zoom_out, keep_query=True, + conditional_disable=is_min_zoom, icon_class=icon_document_page_zoom_out, + keep_query=True, kwargs={'document_page_id': 'resolved_object.pk'}, permission=permission_document_view, text=_('Zoom out'), view='documents:document_page_zoom_out' ) # Document versions link_document_version_revert = Link( - args='object.pk', condition=is_not_current_version, + condition=is_not_current_version, + kwargs={'document_version_id': 'object.pk'}, permission=permission_document_version_revert, tags='dangerous', text=_('Revert'), view='documents:document_version_revert' ) @@ -338,32 +366,38 @@ link_document_type_create = Link( text=_('Create document type'), view='documents:document_type_create' ) link_document_type_delete = Link( - args='resolved_object.id', icon_class=icon_document_type_delete, + icon_class=icon_document_type_delete, + kwargs={'document_type_id': 'resolved_object.pk'}, permission=permission_document_type_delete, tags='dangerous', text=_('Delete'), view='documents:document_type_delete' ) link_document_type_edit = Link( - args='resolved_object.id', icon_class=icon_document_type_edit, + icon_class=icon_document_type_edit, + kwargs={'document_type_id': 'resolved_object.pk'}, permission=permission_document_type_edit, text=_('Edit'), view='documents:document_type_edit' ) link_document_type_filename_create = Link( - args='document_type.id', icon_class=icon_document_type_filename_create, + icon_class=icon_document_type_filename_create, + kwargs={'document_type_id': 'resolved_object.pk'}, permission=permission_document_type_edit, text=_('Add quick label to document type'), view='documents:document_type_filename_create' ) link_document_type_filename_delete = Link( - args='resolved_object.id', permission=permission_document_type_edit, + kwargs={'filename_id': 'resolved_object.pk'}, + permission=permission_document_type_edit, tags='dangerous', text=_('Delete'), view='documents:document_type_filename_delete' ) link_document_type_filename_edit = Link( - args='resolved_object.id', permission=permission_document_type_edit, + kwargs={'filename_id': 'resolved_object.pk'}, + permission=permission_document_type_edit, text=_('Edit'), view='documents:document_type_filename_edit' ) link_document_type_filename_list = Link( - args='resolved_object.id', icon_class=icon_document_type_filename, + icon_class=icon_document_type_filename, + kwargs={'document_type_id': 'resolved_object.pk'}, permission=permission_document_type_view, text=_('Quick labels'), view='documents:document_type_filename_list' ) @@ -382,7 +416,8 @@ link_duplicated_document_list = Link( view='documents:duplicated_document_list' ) link_document_duplicates_list = Link( - args='resolved_object.id', icon_class=icon_document_duplicates_list, + icon_class=icon_document_duplicates_list, + kwargs={'document_id': 'resolved_object.pk'}, permission=permission_document_view, text=_('Duplicates'), view='documents:document_duplicates_list' ) diff --git a/mayan/apps/documents/managers.py b/mayan/apps/documents/managers.py index bdc1ec343d..126a68f596 100644 --- a/mayan/apps/documents/managers.py +++ b/mayan/apps/documents/managers.py @@ -40,7 +40,9 @@ class DocumentPageManager(models.Manager): except DocumentVersion.DoesNotExist: raise self.model.DoesNotExist - return self.get(document_version__pk=document_version.pk, page_number=page_number) + return self.get( + document_version__id=document_version.pk, page_number=page_number + ) class DocumentTypeManager(models.Manager): @@ -61,13 +63,13 @@ class DocumentTypeManager(models.Manager): 'Document type: %s, has a deletion period delta of: %s', document_type, delta ) - for document in document_type.deleted_documents.filter(deleted_date_time__lt=now() - delta): + for trashed_document in document_type.trashed_documents.filter(deleted_date_time__lt=now() - delta): logger.info( 'Document "%s" with id: %d, trashed on: %s, exceded ' - 'delete period', document, document.pk, - document.deleted_date_time + 'delete period', trashed_document, trashed_document.pk, + trashed_document.deleted_date_time ) - document.delete() + trashed_document.delete() else: logger.info( 'Document type: %s, has a no retention delta', document_type @@ -120,7 +122,7 @@ class DocumentVersionManager(models.Manager): except Document.DoesNotExist: raise self.model.DoesNotExist - return self.get(document__pk=document.pk, checksum=checksum) + return self.get(document__id=document.pk, checksum=checksum) class DuplicatedDocumentManager(models.Manager): @@ -204,7 +206,7 @@ class FavoriteDocumentManager(models.Manager): except User.DoesNotExist: raise self.model.DoesNotExist - return self.get(document__pk=document.pk, user__pk=user.pk) + return self.get(document__id=document.pk, user__id=user.pk) def get_for_user(self, user): Document = apps.get_model( @@ -254,7 +256,7 @@ class RecentDocumentManager(models.Manager): raise self.model.DoesNotExist return self.get( - document__pk=document.pk, user__pk=user.pk, + document__id=document.pk, user__id=user.pk, datetime_accessed=datetime_accessed ) diff --git a/mayan/apps/documents/models/document_models.py b/mayan/apps/documents/models/document_models.py index 83d7e5167d..7d1c35ad1c 100644 --- a/mayan/apps/documents/models/document_models.py +++ b/mayan/apps/documents/models/document_models.py @@ -31,8 +31,8 @@ from ..signals import post_document_type_change from .document_type_models import DocumentType __all__ = ( - 'Document', 'DeletedDocument', 'DuplicatedDocument', 'FavoriteDocument', - 'RecentDocument' + 'Document', 'DuplicatedDocument', 'FavoriteDocument', 'RecentDocument', + 'TrashedDocument' ) logger = logging.getLogger(__name__) @@ -137,7 +137,7 @@ class Document(models.Model): def get_absolute_url(self): return reverse( viewname='documents:document_preview', - kwargs={'document_pk': self.pk} + kwargs={'document_id': self.pk} ) def get_api_image_url(self, *args, **kwargs): @@ -279,13 +279,6 @@ class Document(models.Model): return DocumentPage.objects.none() -class DeletedDocument(Document): - objects = TrashCanManager() - - class Meta: - proxy = True - - @python_2_unicode_compatible class DuplicatedDocument(models.Model): document = models.ForeignKey( @@ -316,9 +309,9 @@ class DuplicatedDocumentProxy(Document): verbose_name_plural = _('Duplicated documents') def get_duplicate_count(self, user): - queryset = AccessControlList.objects.filter_by_access( - permission=permission_document_view, user=user, - queryset=self.get_duplicates() + queryset = AccessControlList.objects.restrict_queryset( + permission=permission_document_view, queryset=self.get_duplicates(), + user=user ) return queryset.count() @@ -382,3 +375,10 @@ class RecentDocument(models.Model): def natural_key(self): return (self.datetime_accessed, self.document.natural_key(), self.user.natural_key()) natural_key.dependencies = ['documents.Document', settings.AUTH_USER_MODEL] + + +class TrashedDocument(Document): + objects = TrashCanManager() + + class Meta: + proxy = True diff --git a/mayan/apps/documents/models/document_page_models.py b/mayan/apps/documents/models/document_page_models.py index 6e59882360..7c3037e21a 100644 --- a/mayan/apps/documents/models/document_page_models.py +++ b/mayan/apps/documents/models/document_page_models.py @@ -103,7 +103,7 @@ class DocumentPage(models.Model): def get_absolute_url(self): return reverse( viewname='documents:document_page_view', - kwargs={'document_page_pk': self.pk} + kwargs={'document_page_id': self.pk} ) def get_api_image_url(self, *args, **kwargs): @@ -126,10 +126,10 @@ class DocumentPage(models.Model): final_url = furl() final_url.args = kwargs final_url.path = reverse( - viewname='rest_api:documentpage-image', kwargs={ - 'document_pk': self.document.pk, - 'document_version_pk': self.document_version.pk, - 'document_page_pk': self.pk + viewname='rest_api:document_page-image', kwargs={ + 'document_id': self.document.pk, + 'document_version_id': self.document_version.pk, + 'document_page_id': self.pk } ) final_url.args['_hash'] = transformations_hash diff --git a/mayan/apps/documents/models/document_type_models.py b/mayan/apps/documents/models/document_type_models.py index b57c123920..39a497d267 100644 --- a/mayan/apps/documents/models/document_type_models.py +++ b/mayan/apps/documents/models/document_type_models.py @@ -73,23 +73,16 @@ class DocumentType(models.Model): return super(DocumentType, self).delete(*args, **kwargs) - @property - def deleted_documents(self): - DeletedDocument = apps.get_model( - app_label='documents', model_name='DeletedDocument' - ) - return DeletedDocument.objects.filter(document_type=self) - def get_absolute_url(self): return reverse( viewname='documents:document_type_document_list', - kwargs={'document_type_pk': self.pk} + kwargs={'document_type_id': self.pk} ) def get_document_count(self, user): - queryset = AccessControlList.objects.filter_by_access( - permission=permission_document_view, user=user, - queryset=self.documents + queryset = AccessControlList.objects.restrict_queryset( + permission=permission_document_view, queryset=self.documents, + user=user ) return queryset.count() get_document_count.short_description = _('Documents') @@ -134,6 +127,13 @@ class DocumentType(models.Model): return result + @property + def trashed_documents(self): + TrashedDocument = apps.get_model( + app_label='documents', model_name='TrashedDocument' + ) + return TrashedDocument.objects.filter(document_type=self) + @python_2_unicode_compatible class DocumentTypeFilename(models.Model): diff --git a/mayan/apps/documents/models/document_version_models.py b/mayan/apps/documents/models/document_version_models.py index 102ec6d7f8..873ec1ad07 100644 --- a/mayan/apps/documents/models/document_version_models.py +++ b/mayan/apps/documents/models/document_version_models.py @@ -151,7 +151,7 @@ class DocumentVersion(models.Model): def get_absolute_url(self): return reverse( viewname='documents:document_version_view', - kwargs={'document_version_pk': self.pk} + kwargs={'document_version_id': self.pk} ) def get_api_image_url(self, *args, **kwargs): @@ -195,6 +195,7 @@ class DocumentVersion(models.Model): filename, self.get_rendered_timestamp(), extension ) else: + #TODO: use get_rendered_timestamp here return Template( '{{ instance.document }} - {{ instance.timestamp }}' ).render(context=Context({'instance': self})) @@ -205,6 +206,10 @@ class DocumentVersion(models.Model): ) get_rendered_timestamp.short_description = _('Date and time') + @property + def label(self): + return self.get_rendered_string() + def natural_key(self): return (self.checksum, self.document.natural_key()) natural_key.dependencies = ['documents.Document'] @@ -250,9 +255,19 @@ class DocumentVersion(models.Model): self.document, self ) - event_document_version_revert.commit(actor=_user, target=self.document) - for version in self.document.versions.filter(timestamp__gt=self.timestamp): - version.delete() + try: + with transaction.atomic(): + event_document_version_revert.commit( + actor=_user, target=self.document + ) + for version in self.document.versions.filter(timestamp__gt=self.timestamp): + version.delete() + except Exception as exception: + logger.error( + 'Error reverting document version for document "%s"; %s', + self.document, exception + ) + raise def save(self, *args, **kwargs): """ diff --git a/mayan/apps/documents/permissions.py b/mayan/apps/documents/permissions.py index 4cd78f4c89..ed88131c58 100644 --- a/mayan/apps/documents/permissions.py +++ b/mayan/apps/documents/permissions.py @@ -7,62 +7,62 @@ from mayan.apps.permissions import PermissionNamespace namespace = PermissionNamespace(label=_('Documents'), name='documents') permission_document_create = namespace.add_permission( - name='document_create', label=_('Create documents') -) -permission_document_delete = namespace.add_permission( - name='document_delete', label=_('Delete documents') + label=_('Create documents'), name='document_create' ) permission_document_trash = namespace.add_permission( - name='document_trash', label=_('Trash documents') + label=_('Trash documents'), name='document_trash' ) permission_document_download = namespace.add_permission( - name='document_download', label=_('Download documents') + label=_('Download documents'), name='document_download' ) permission_document_edit = namespace.add_permission( - name='document_edit', label=_('Edit documents') + label=_('Edit documents'), name='document_edit' ) permission_document_new_version = namespace.add_permission( - name='document_new_version', label=_('Create new document versions') + label=_('Create new document versions'), name='document_new_version' ) permission_document_properties_edit = namespace.add_permission( - name='document_properties_edit', label=_('Edit document properties') + label=_('Edit document properties'), name='document_properties_edit' ) permission_document_print = namespace.add_permission( - name='document_print', label=_('Print documents') -) -permission_document_restore = namespace.add_permission( - name='document_restore', label=_('Restore trashed document') + label=_('Print documents'), name='document_print' ) permission_document_tools = namespace.add_permission( - name='document_tools', label=_('Execute document modifying tools') + label=_('Execute document modifying tools'), name='document_tools' ) permission_document_version_revert = namespace.add_permission( - name='document_version_revert', - label=_('Revert documents to a previous version') + label=_('Revert documents to a previous version'), + name='document_version_revert' ) permission_document_version_view = namespace.add_permission( - name='document_version_view', - label=_('View documents\' versions list') + label=_('View documents\' versions list'), + name='document_version_view' ) permission_document_view = namespace.add_permission( - name='document_view', label=_('View documents') + label=_('View documents'), name='document_view' ) permission_empty_trash = namespace.add_permission( - name='document_empty_trash', label=_('Empty trash') + label=_('Empty trash'), name='document_empty_trash' +) +permission_trashed_document_delete = namespace.add_permission( + label=_('Delete trashed documents'), name='document_delete' +) +permission_trashed_document_restore = namespace.add_permission( + label=_('Restore trashed document'), name='document_restore' ) setup_namespace = PermissionNamespace( label=_('Document types'), name='documents_types' ) permission_document_type_create = setup_namespace.add_permission( - name='document_type_create', label=_('Create document types') + label=_('Create document types'), name='document_type_create' ) permission_document_type_delete = setup_namespace.add_permission( - name='document_type_delete', label=_('Delete document types') + label=_('Delete document types'), name='document_type_delete' ) permission_document_type_edit = setup_namespace.add_permission( - name='document_type_edit', label=_('Edit document types') + label=_('Edit document types'), name='document_type_edit' ) permission_document_type_view = setup_namespace.add_permission( - name='document_type_view', label=_('View document types') + label=_('View document types'), name='document_type_view' ) diff --git a/mayan/apps/documents/queues.py b/mayan/apps/documents/queues.py index c97f3dc61c..b7215372f3 100644 --- a/mayan/apps/documents/queues.py +++ b/mayan/apps/documents/queues.py @@ -6,55 +6,55 @@ from mayan.apps.common.queues import queue_tools from mayan.apps.task_manager.classes import CeleryQueue queue_converter = CeleryQueue( - name='converter', label=_('Converter'), transient=True + label=_('Converter'), name='converter', transient=True ) queue_documents_periodic = CeleryQueue( - name='documents_periodic', label=_('Documents periodic'), transient=True + label=_('Documents periodic'), name='documents_periodic', transient=True ) queue_uploads = CeleryQueue( - name='uploads', label=_('Uploads') + label=_('Uploads'), name='uploads' ) queue_documents = CeleryQueue( - name='documents', label=_('Documents') + label=_('Documents'), name='documents' ) queue_converter.add_task_type( - name='mayan.apps.documents.tasks.task_generate_document_page_image', - label=_('Generate document page image') + label=_('Generate document page image'), + name='mayan.apps.documents.tasks.task_generate_document_page_image' ) queue_documents.add_task_type( - name='mayan.apps.documents.tasks.task_delete_document', - label=_('Delete a document') + label=_('Delete a document'), + name='mayan.apps.documents.tasks.task_delete_document' ) queue_documents.add_task_type( - name='mayan.apps.documents.tasks.task_clean_empty_duplicate_lists', - label=_('Clean empty duplicate lists') + label=_('Clean empty duplicate lists'), + name='mayan.apps.documents.tasks.task_clean_empty_duplicate_lists' ) queue_documents_periodic.add_task_type( - name='mayan.apps.documents.tasks.task_check_delete_periods', - label=_('Check document type delete periods') + label=_('Check document type delete periods'), + name='mayan.apps.documents.tasks.task_check_delete_periods' ) queue_documents_periodic.add_task_type( - name='mayan.apps.documents.tasks.task_check_trash_periods', - label=_('Check document type trash periods') + label=_('Check document type trash periods'), + name='mayan.apps.documents.tasks.task_check_trash_periods' ) queue_documents_periodic.add_task_type( - name='mayan.apps.documents.tasks.task_delete_stubs', - label=_('Delete document stubs') + label=_('Delete document stubs'), + name='mayan.apps.documents.tasks.task_delete_stubs' ) queue_tools.add_task_type( - name='mayan.apps.documents.tasks.task_clear_image_cache', - label=_('Clear image cache') + label=_('Clear image cache'), + name='mayan.apps.documents.tasks.task_clear_image_cache' ) queue_uploads.add_task_type( - name='mayan.apps.documents.tasks.task_update_page_count', - label=_('Update document page count') + label=_('Update document page count'), + name='mayan.apps.documents.tasks.task_update_page_count' ) queue_uploads.add_task_type( - name='mayan.apps.documents.tasks.task_upload_new_version', - label=_('Upload new document version') + label=_('Upload new document version'), + name='mayan.apps.documents.tasks.task_upload_new_version' ) diff --git a/mayan/apps/documents/serializers.py b/mayan/apps/documents/serializers.py index 3811779877..594ae16f13 100644 --- a/mayan/apps/documents/serializers.py +++ b/mayan/apps/documents/serializers.py @@ -6,6 +6,7 @@ from rest_framework import serializers from rest_framework.reverse import reverse from mayan.apps.common.models import SharedUploadedFile +from mayan.apps.rest_api.relations import MultiKwargHyperlinkedIdentityField from .models import ( Document, DocumentPage, DocumentType, DocumentTypeFilename, @@ -16,40 +17,80 @@ from .tasks import task_upload_new_version class DocumentPageSerializer(serializers.HyperlinkedModelSerializer): - document_version_url = serializers.SerializerMethodField() - image_url = serializers.SerializerMethodField() - url = serializers.SerializerMethodField() + #document_versions_url = serializers.SerializerMethodField() + image_url = MultiKwargHyperlinkedIdentityField( + view_kwargs=( + { + 'lookup_field': 'pk', 'lookup_url_kwarg': 'document_page_id', + }, + { + 'lookup_field': 'document.pk', 'lookup_url_kwarg': 'document_id', + }, + { + 'lookup_field': 'document_version_id', 'lookup_url_kwarg': 'document_version_id', + }, + ), + view_name='rest_api:document_page-image' + ) + #url = serializers.SerializerMethodField() + document_version_url = MultiKwargHyperlinkedIdentityField( + view_kwargs=( + { + 'lookup_field': 'document_version_id', 'lookup_url_kwarg': 'document_version_id', + }, + { + 'lookup_field': 'document.pk', 'lookup_url_kwarg': 'document_id', + } + ), + view_name='rest_api:document_version-detail' + ) + url = MultiKwargHyperlinkedIdentityField( + view_kwargs=( + { + 'lookup_field': 'pk', 'lookup_url_kwarg': 'document_page_id', + }, + { + 'lookup_field': 'document.pk', 'lookup_url_kwarg': 'document_id', + }, + { + 'lookup_field': 'document_version_id', 'lookup_url_kwarg': 'document_version_id', + }, + ), + view_name='rest_api:document_page-detail' + ) class Meta: fields = ('document_version_url', 'image_url', 'page_number', 'url') + #fields = ('document_version_url', 'page_number', 'url') model = DocumentPage - def get_document_version_url(self, instance): + """ + def get_document_versions_url(self, instance): return reverse( viewname='rest_api:documentversion-detail', kwargs={ - 'document_pk': instance.document.pk, - 'document_version_pk': instance.document_version.pk + 'document_id': instance.document.pk, + 'document_version_id': instance.document_version.pk }, request=self.context['request'], format=self.context['format'] ) def get_image_url(self, instance): return reverse( viewname='rest_api:documentpage-image', kwargs={ - 'document_pk': instance.document.pk, - 'document_version_pk': instance.document_version.pk, - 'document_page_pk': instance.pk, + 'document_id': instance.document.pk, + 'document_version_id': instance.document_version.pk, + 'document_page_id': instance.pk, }, request=self.context['request'], format=self.context['format'] ) def get_url(self, instance): return reverse( viewname='rest_api:documentpage-detail', kwargs={ - 'document_pk': instance.document.pk, - 'document_version_pk': instance.document_version.pk, - 'document_page_pk': instance.pk, + 'document_id': instance.document.pk, + 'document_version_id': instance.document_version.pk, + 'document_page_id': instance.pk, }, request=self.context['request'], format=self.context['format'] ) - + """ class DocumentTypeFilenameSerializer(serializers.ModelSerializer): class Meta: @@ -59,33 +100,35 @@ class DocumentTypeFilenameSerializer(serializers.ModelSerializer): class DocumentTypeSerializer(serializers.HyperlinkedModelSerializer): documents_url = serializers.HyperlinkedIdentityField( - lookup_field='pk', lookup_url_kwarg='document_type_pk', - view_name='rest_api:documenttype-document-list' + lookup_url_kwarg='document_type_id', + view_name='rest_api:document_type-document-list' ) - documents_count = serializers.SerializerMethodField() - filenames = DocumentTypeFilenameSerializer(many=True, read_only=True) + #documents_count = serializers.SerializerMethodField() + #filenames = DocumentTypeFilenameSerializer(many=True, read_only=True) class Meta: extra_kwargs = { 'url': { - 'lookup_field': 'pk', 'lookup_url_kwarg': 'document_type_pk', - 'view_name': 'rest_api:documenttype-detail' + 'lookup_field': 'pk', 'lookup_url_kwarg': 'document_type_id', + 'view_name': 'rest_api:document_type-detail' } } fields = ( 'delete_time_period', 'delete_time_unit', 'documents_url', - 'documents_count', 'id', 'label', 'filenames', 'trash_time_period', + #'delete_time_period', 'delete_time_unit', + #'documents_count', 'id', 'label', 'filenames', 'trash_time_period', + 'id', 'label', 'trash_time_period', 'trash_time_unit', 'url' ) model = DocumentType - def get_documents_count(self, obj): - return obj.documents.count() - + #def get_documents_count(self, obj): + # return obj.documents.count() +""" class WritableDocumentTypeSerializer(serializers.ModelSerializer): documents_url = serializers.HyperlinkedIdentityField( - lookup_field='pk', lookup_url_kwarg='document_type_pk', + lookup_field='pk', lookup_url_kwarg='document_type_id', view_name='rest_api:documenttype-document-list' ) documents_count = serializers.SerializerMethodField() @@ -93,7 +136,7 @@ class WritableDocumentTypeSerializer(serializers.ModelSerializer): class Meta: extra_kwargs = { 'url': { - 'lookup_field': 'pk', 'lookup_url_kwarg': 'document_type_pk', + 'lookup_field': 'pk', 'lookup_url_kwarg': 'document_type_id', 'view_name': 'rest_api:documenttype-detail' } } @@ -106,26 +149,69 @@ class WritableDocumentTypeSerializer(serializers.ModelSerializer): def get_documents_count(self, obj): return obj.documents.count() - +""" class DocumentVersionSerializer(serializers.HyperlinkedModelSerializer): - document_url = serializers.SerializerMethodField() - download_url = serializers.SerializerMethodField() - pages_url = serializers.SerializerMethodField() + #document_url = serializers.SerializerMethodField() + #download_url = serializers.SerializerMethodField() + document_url = serializers.HyperlinkedIdentityField( + lookup_field='document_id', lookup_url_kwarg='document_id', + view_name='rest_api:document-detail' + ) + #pages_url = serializers.SerializerMethodField() + + pages_url = MultiKwargHyperlinkedIdentityField( + view_kwargs=( + { + 'lookup_field': 'document_id', 'lookup_url_kwarg': 'document_id', + }, + { + 'lookup_field': 'pk', 'lookup_url_kwarg': 'document_version_id', + } + ), + view_name='rest_api:document_page-list' + ) + size = serializers.SerializerMethodField() - url = serializers.SerializerMethodField() + #url = serializers.SerializerMethodField() + url = MultiKwargHyperlinkedIdentityField( + view_kwargs=( + { + 'lookup_field': 'pk', 'lookup_url_kwarg': 'document_version_id', + }, + { + 'lookup_field': 'document_id', 'lookup_url_kwarg': 'document_id', + }, + ), + view_name='rest_api:document_version-detail' + ) class Meta: extra_kwargs = { - 'document': { - 'lookup_field': 'pk', 'lookup_url_kwarg': 'document_pk', - 'view_name': 'rest_api:document-detail' - }, + #'document': { + # 'lookup_field': 'pk', 'lookup_url_kwarg': 'document_id', + # 'view_name': 'rest_api:document-detail' + #}, 'file': {'use_url': False}, + #'url': { + # 'view_kwargs': ( + # { + # 'lookup_field': 'pk', 'lookup_url_kwarg': 'document_version_id', + # }, + # { + # 'lookup_field': 'document__pk', 'lookup_url_kwarg': 'document_id', + # }, + # ), + # #'lookup_field': 'pk', 'lookup_url_kwarg': 'document_version_id', + # 'view_name': 'rest_api:document_version-detail' + #}, } fields = ( - 'checksum', 'comment', 'document_url', 'download_url', 'encoding', + #'checksum', 'comment', 'document_url', 'download_url', 'encoding', + 'checksum', 'comment', 'document_url', 'encoding', + #'checksum', 'comment', 'encoding', 'file', 'mimetype', 'pages_url', 'size', 'timestamp', 'url' + #'file', 'mimetype', 'size', 'timestamp', 'url' ) model = DocumentVersion read_only_fields = ('document', 'file', 'size') @@ -133,38 +219,62 @@ class DocumentVersionSerializer(serializers.HyperlinkedModelSerializer): def get_size(self, instance): return instance.size + def build_url_field(self, field_name, model_class): + """ + Create a field representing the object's own URL. + """ + field_class = self.serializer_url_field + field_kwargs = {'kwargs': 1} + + return field_class, field_kwargs + + """ def get_document_url(self, instance): return reverse( viewname='rest_api:document-detail', kwargs={ - 'document_pk': instance.document.pk - }, request=self.context['request'], format=self.context['format'] + 'document_id': instance.document.pk + }, request=self.context['request']#, format=self.context['format'] ) def get_download_url(self, instance): return reverse( - viewname='rest_api:documentversion-download', kwargs={ - 'document_pk': instance.document.pk, - 'document_version_pk': instance.pk - }, request=self.context['request'], format=self.context['format'] + viewname='rest_api:document_version-download', kwargs={ + 'document_id': instance.document.pk, + 'document_version_id': instance.pk + }, request=self.context['request']#, format=self.context['format'] ) def get_pages_url(self, instance): return reverse( - viewname='rest_api:documentversion-page-list', kwargs={ - 'document_pk': instance.document.pk, - 'document_version_pk': instance.pk - }, request=self.context['request'], format=self.context['format'] + viewname='rest_api:document_version-page-list', kwargs={ + 'document_id': instance.document.pk, + 'document_version_id': instance.pk + }, request=self.context['request']#, format=self.context['format'] ) - def get_url(self, instance): + + def get_url(self, obj, view_name, request, format): + print ' obj, view_name, request, format', obj, view_name, request, format + # Unsaved objects will not yet have a valid URL. + if hasattr(obj, 'pk') and obj.pk in (None, ''): + return None + + lookup_value = getattr(obj, self.lookup_field) + kwargs = {self.lookup_url_kwarg: lookup_value} + return self.reverse(view_name, kwargs=kwargs, request=request, format=format) + """ + """ + + def get_url(self, instance, *args, **kwargs): + print ', *args, **kwargs', args, kwargs return reverse( - viewname='rest_api:documentversion-detail', kwargs={ - 'document_pk': instance.document.pk, - 'document_version_pk': instance.pk - }, request=self.context['request'], format=self.context['format'] + viewname='rest_api:document_version-detail', kwargs={ + 'document_id': instance.document.pk, + 'document_version_id': instance.pk + }, request=self.context['request']#, format=self.context['format'] ) - - + """ +""" class WritableDocumentVersionSerializer(serializers.ModelSerializer): document_url = serializers.SerializerMethodField() download_url = serializers.SerializerMethodField() @@ -185,31 +295,31 @@ class WritableDocumentVersionSerializer(serializers.ModelSerializer): def get_document_url(self, instance): return reverse( viewname='rest_api:document-detail', kwargs={ - 'document_pk': instance.document.pk + 'document_id': instance.document.pk }, request=self.context['request'], format=self.context['format'] ) def get_download_url(self, instance): return reverse( viewname='rest_api:documentversion-download', kwargs={ - 'document_pk': instance.document.pk, - 'document_version_pk': instance.pk + 'document_id': instance.document.pk, + 'document_version_id': instance.pk }, request=self.context['request'], format=self.context['format'] ) def get_pages_url(self, instance): return reverse( viewname='rest_api:documentversion-page-list', kwargs={ - 'document_pk': instance.document.pk, - 'document_version_pk': instance.pk + 'document_id': instance.document.pk, + 'document_version_id': instance.pk }, request=self.context['request'], format=self.context['format'] ) def get_url(self, instance): return reverse( viewname='rest_api:documentversion-detail', kwargs={ - 'document_pk': instance.document.pk, - 'document_version_pk': instance.pk + 'document_id': instance.document.pk, + 'document_version_id': instance.pk }, request=self.context['request'], format=self.context['format'] ) @@ -228,23 +338,23 @@ class NewDocumentVersionSerializer(serializers.Serializer): document_id=document.pk, shared_uploaded_file_id=shared_uploaded_file.pk, user_id=_user.pk ) - +""" class DeletedDocumentSerializer(serializers.HyperlinkedModelSerializer): document_type_label = serializers.SerializerMethodField() restore = serializers.HyperlinkedIdentityField( - lookup_field='pk', lookup_url_kwarg='document_pk', + lookup_field='pk', lookup_url_kwarg='document_id', view_name='rest_api:trasheddocument-restore' ) class Meta: extra_kwargs = { 'document_type': { - 'lookup_field': 'pk', 'lookup_url_kwarg': 'document_type_pk', + 'lookup_field': 'pk', 'lookup_url_kwarg': 'document_type_id', 'view_name': 'rest_api:documenttype-detail' }, 'url': { - 'lookup_field': 'pk', 'lookup_url_kwarg': 'document_pk', + 'lookup_field': 'pk', 'lookup_url_kwarg': 'document_id', 'view_name': 'rest_api:trasheddocument-detail' } } @@ -264,41 +374,49 @@ class DeletedDocumentSerializer(serializers.HyperlinkedModelSerializer): class DocumentSerializer(serializers.HyperlinkedModelSerializer): - document_type = DocumentTypeSerializer() + document_type = DocumentTypeSerializer(read_only=True) + #document_type_url = serializers.HyperlinkedIdentityField( + # lookup_field='document_type_id', lookup_url_kwarg='document_type_id', + # view_name='rest_api:document_type-detail' + #) latest_version = DocumentVersionSerializer(many=False, read_only=True) versions_url = serializers.HyperlinkedIdentityField( - lookup_field='pk', lookup_url_kwarg='document_pk', - view_name='rest_api:document-version-list' + lookup_field='pk', lookup_url_kwarg='document_id', + view_name='rest_api:document_version-list' ) + #view_name='rest_api:document_type-document-list' class Meta: extra_kwargs = { 'document_type': { - 'lookup_field': 'pk', 'lookup_url_kwarg': 'document_type_pk', - 'view_name': 'rest_api:documenttype-detail' + 'lookup_field': 'pk', 'lookup_url_kwarg': 'document_type_id', + 'view_name': 'rest_api:document_type-detail' }, 'url': { - 'lookup_field': 'pk', 'lookup_url_kwarg': 'document_pk', + 'lookup_field': 'pk', 'lookup_url_kwarg': 'document_id', 'view_name': 'rest_api:document-detail' } } fields = ( 'date_added', 'description', 'document_type', 'id', 'label', + #'date_added', 'description', 'document_type_url', 'id', 'label', 'language', 'latest_version', 'url', 'uuid', 'versions_url', + #'language', 'url', 'uuid', 'versions_url' ) model = Document - read_only_fields = ('document_type',) + #read_only_fields = ('document_type', 'label') +""" class WritableDocumentSerializer(serializers.ModelSerializer): document_type = DocumentTypeSerializer(read_only=True) latest_version = DocumentVersionSerializer(many=False, read_only=True) versions = serializers.HyperlinkedIdentityField( - lookup_field='pk', lookup_url_kwarg='document_pk', + lookup_field='pk', lookup_url_kwarg='document_id', view_name='rest_api:document-version-list' ) url = serializers.HyperlinkedIdentityField( - lookup_field='pk', lookup_url_kwarg='document_pk', + lookup_field='pk', lookup_url_kwarg='document_id', view_name='rest_api:document-detail', ) @@ -344,7 +462,7 @@ class NewDocumentSerializer(serializers.ModelSerializer): 'description', 'document_type', 'id', 'file', 'label', 'language', ) model = Document - +""" class RecentDocumentSerializer(serializers.ModelSerializer): class Meta: diff --git a/mayan/apps/documents/settings.py b/mayan/apps/documents/settings.py index e120b2399d..356ee7a705 100644 --- a/mayan/apps/documents/settings.py +++ b/mayan/apps/documents/settings.py @@ -13,7 +13,7 @@ from .literals import ( ) from .utils import callback_update_cache_size -namespace = Namespace(name='documents', label=_('Documents')) +namespace = Namespace(label=_('Documents'), name='documents') setting_document_cache_maximum_size = namespace.add_setting( global_name='DOCUMENTS_CACHE_MAXIMUM_SIZE', diff --git a/mayan/apps/documents/signals.py b/mayan/apps/documents/signals.py index 0ec434bee1..425172366e 100644 --- a/mayan/apps/documents/signals.py +++ b/mayan/apps/documents/signals.py @@ -2,11 +2,11 @@ from __future__ import unicode_literals from django.dispatch import Signal -post_version_upload = Signal(providing_args=('instance',), use_caching=True) +post_document_created = Signal(providing_args=('instance',), use_caching=True) post_document_type_change = Signal( providing_args=('instance',), use_caching=True ) -post_document_created = Signal(providing_args=('instance',), use_caching=True) post_initial_document_type = Signal( providing_args=('instance',), use_caching=True ) +post_version_upload = Signal(providing_args=('instance',), use_caching=True) diff --git a/mayan/apps/documents/statistics.py b/mayan/apps/documents/statistics.py index d45a89b5a7..754466e27e 100644 --- a/mayan/apps/documents/statistics.py +++ b/mayan/apps/documents/statistics.py @@ -68,9 +68,8 @@ def new_documents_this_month(user=None): queryset = Document.objects.all() if user: - queryset = AccessControlList.objects.filter_by_access( - permission=permission_document_view, user=user, - queryset=queryset + queryset = AccessControlList.objects.restrict_queryset( + permission=permission_document_view, queryset=queryset, user=user ) qss = qsstats.QuerySetStats(queryset, 'date_added') @@ -110,9 +109,9 @@ def new_document_pages_this_month(user=None): queryset = DocumentPage.objects.all() if user: - queryset = AccessControlList.objects.filter_by_access( - permission=permission_document_view, user=user, - queryset=queryset + queryset = AccessControlList.objects.restrict_queryset( + permission=permission_document_view, queryset=queryset, + user=user ) qss = qsstats.QuerySetStats( @@ -230,44 +229,44 @@ def total_document_page_per_month(): namespace = StatisticNamespace(slug='documents', label=_('Documents')) namespace.add_statistic( - klass=StatisticLineChart, - slug='new-documents-per-month', - label=_('New documents per month'), func=new_documents_per_month, - minute='0' + klass=StatisticLineChart, + label=_('New documents per month'), + minute='0', + slug='new-documents-per-month' ) namespace.add_statistic( - klass=StatisticLineChart, - slug='new-document-versions-per-month', - label=_('New document versions per month'), func=new_document_versions_per_month, - minute='0' + klass=StatisticLineChart, + label=_('New document versions per month'), + minute='0', + slug='new-document-versions-per-month' ) namespace.add_statistic( - klass=StatisticLineChart, - slug='new-document-pages-per-month', - label=_('New document pages per month'), func=new_document_pages_per_month, - minute='0' + klass=StatisticLineChart, + label=_('New document pages per month'), + minute='0', + slug='new-document-pages-per-month' ) namespace.add_statistic( - klass=StatisticLineChart, - slug='total-documents-at-each-month', - label=_('Total documents at each month'), func=total_document_per_month, - minute='0' + klass=StatisticLineChart, + label=_('Total documents at each month'), + minute='0', + slug='total-documents-at-each-month' ) namespace.add_statistic( - klass=StatisticLineChart, - slug='total-document-versions-at-each-month', - label=_('Total document versions at each month'), func=total_document_version_per_month, - minute='0' + klass=StatisticLineChart, + label=_('Total document versions at each month'), + minute='0', + slug='total-document-versions-at-each-month' ) namespace.add_statistic( - klass=StatisticLineChart, - slug='total-document-pages-at-each-month', - label=_('Total document pages at each month'), func=total_document_page_per_month, - minute='0' + klass=StatisticLineChart, + label=_('Total document pages at each month'), + minute='0', + slug='total-document-pages-at-each-month' ) diff --git a/mayan/apps/documents/tasks.py b/mayan/apps/documents/tasks.py index 3ebebb63b6..ec5cc43120 100644 --- a/mayan/apps/documents/tasks.py +++ b/mayan/apps/documents/tasks.py @@ -58,14 +58,14 @@ def task_clear_image_cache(): @app.task(ignore_result=True) -def task_delete_document(deleted_document_id): - DeletedDocument = apps.get_model( - app_label='documents', model_name='DeletedDocument' +def task_delete_document(trashed_document_id): + TrashedDocument = apps.get_model( + app_label='documents', model_name='TrashedDocument' ) logger.debug('Executing') - deleted_document = DeletedDocument.objects.get(pk=deleted_document_id) - deleted_document.delete() + trashed_document = TrashedDocument.objects.get(pk=trashed_document_id) + trashed_document.delete() logger.debug('Finshed') diff --git a/mayan/apps/documents/tests/mixins.py b/mayan/apps/documents/tests/mixins.py index d43d1d4141..8a61720131 100644 --- a/mayan/apps/documents/tests/mixins.py +++ b/mayan/apps/documents/tests/mixins.py @@ -18,27 +18,25 @@ class DocumentTestMixin(object): auto_create_document_type = True auto_upload_document = True test_document_filename = TEST_SMALL_DOCUMENT_FILENAME + test_document_path = None def _create_document_type(self): self.document_type = DocumentType.objects.create( label=TEST_DOCUMENT_TYPE_LABEL ) - def upload_document(self, filename=None): - self._calculate_test_document_path() - - with open(self.test_document_path, mode='rb') as file_object: - document = self.document_type.new_document( - file_object=file_object, - label=filename or self.test_document_filename - ) - return document + def _create_document(self, *args, **kwargs): + """ + Alias for upload_document() + """ + self.test_document = self.upload_document(*args, **kwargs) def _calculate_test_document_path(self): - self.test_document_path = os.path.join( - settings.BASE_DIR, 'apps', 'documents', 'tests', 'contrib', - 'sample_documents', self.test_document_filename - ) + if not self.test_document_path: + self.test_document_path = os.path.join( + settings.BASE_DIR, 'apps', 'documents', 'tests', 'contrib', + 'sample_documents', self.test_document_filename + ) def setUp(self): super(DocumentTestMixin, self).setUp() @@ -54,6 +52,18 @@ class DocumentTestMixin(object): document_type.delete() super(DocumentTestMixin, self).tearDown() + def upload_document(self, document_type=None, filename=None): + self._calculate_test_document_path() + + document_type = document_type or self.document_type + + with open(self.test_document_path, mode='rb') as file_object: + document = document_type.new_document( + file_object=file_object, + label=filename or self.test_document_filename + ) + return document + class DocumentTypeQuickLabelTestMixin(object): def _create_quick_label(self): diff --git a/mayan/apps/documents/tests/test_api.py b/mayan/apps/documents/tests/test_api.py index 6fe4fa78e0..2299a4f03b 100644 --- a/mayan/apps/documents/tests/test_api.py +++ b/mayan/apps/documents/tests/test_api.py @@ -11,10 +11,10 @@ from mayan.apps.rest_api.tests import BaseAPITestCase from ..models import Document, DocumentType from ..permissions import ( - permission_document_create, permission_document_delete, - permission_document_download, permission_document_edit, - permission_document_new_version, permission_document_properties_edit, - permission_document_restore, permission_document_trash, + permission_document_create, permission_document_download, + permission_document_edit, permission_document_new_version, + permission_document_properties_edit, permission_document_trash, + permission_trashed_document_delete, permission_trashed_document_restore, permission_document_type_create, permission_document_type_delete, permission_document_type_edit, permission_document_version_revert, permission_document_version_view, permission_document_view @@ -61,7 +61,7 @@ class DocumentTypeAPITestCase(BaseAPITestCase): def _request_document_type_patch(self): return self.patch( viewname='rest_api:documenttype-detail', kwargs={ - 'document_type_pk': self.document_type.pk + 'document_type_id': self.document_type.pk }, data={'label': TEST_DOCUMENT_TYPE_LABEL_EDITED} ) @@ -90,7 +90,7 @@ class DocumentTypeAPITestCase(BaseAPITestCase): def _request_document_type_put(self): return self.put( viewname='rest_api:documenttype-detail', kwargs={ - 'document_type_pk': self.document_type.pk + 'document_type_id': self.document_type.pk }, data={'label': TEST_DOCUMENT_TYPE_LABEL_EDITED} ) @@ -119,7 +119,7 @@ class DocumentTypeAPITestCase(BaseAPITestCase): def _request_document_type_delete(self): return self.delete( viewname='rest_api:documenttype-detail', kwargs={ - 'document_type_pk': self.document_type.pk + 'document_type_id': self.document_type.pk } ) @@ -207,7 +207,7 @@ class DocumentAPITestCase(DocumentTestMixin, BaseAPITestCase): with open(TEST_DOCUMENT_PATH, mode='rb') as file_descriptor: return self.post( viewname='rest_api:document-version-list', kwargs={ - 'document_pk': self.document.pk + 'document_id': self.document.pk }, data={ 'comment': '', 'file': file_descriptor, } @@ -247,8 +247,8 @@ class DocumentAPITestCase(DocumentTestMixin, BaseAPITestCase): def _request_document_version_revert(self): return self.delete( viewname='rest_api:documentversion-detail', kwargs={ - 'document_pk': self.document.pk, - 'document_version_pk': self.document.latest_version.pk + 'document_id': self.document.pk, + 'document_version_id': self.document.latest_version.pk } ) @@ -275,7 +275,7 @@ class DocumentAPITestCase(DocumentTestMixin, BaseAPITestCase): def _request_document_version_list(self): return self.get( viewname='rest_api:document-version-list', - kwargs={'document_pk': self.document.pk} + kwargs={'document_id': self.document.pk} ) def test_document_version_list_no_permission(self): @@ -301,7 +301,7 @@ class DocumentAPITestCase(DocumentTestMixin, BaseAPITestCase): def _request_document_download(self): return self.get( viewname='rest_api:document-download', - kwargs={'document_pk': self.document.pk} + kwargs={'document_id': self.document.pk} ) def test_document_download_no_permission(self): @@ -329,8 +329,8 @@ class DocumentAPITestCase(DocumentTestMixin, BaseAPITestCase): def _request_document_version_download(self): return self.get( viewname='rest_api:documentversion-download', kwargs={ - 'document_pk': self.document.pk, - 'document_version_pk': self.document.latest_version.pk, + 'document_id': self.document.pk, + 'document_version_id': self.document.latest_version.pk, } ) @@ -362,8 +362,8 @@ class DocumentAPITestCase(DocumentTestMixin, BaseAPITestCase): response = self.get( viewname='rest_api:documentversion-download', kwargs={ - 'document_pk': self.document.pk, - 'document_version_pk': self.document.latest_version.pk, + 'document_id': self.document.pk, + 'document_version_id': self.document.latest_version.pk, }, data={'preserve_extension': True} ) @@ -380,8 +380,8 @@ class DocumentAPITestCase(DocumentTestMixin, BaseAPITestCase): def _request_document_version_edit_via_patch(self): return self.patch( viewname='rest_api:documentversion-detail', kwargs={ - 'document_pk': self.document.pk, - 'document_version_pk': self.document.latest_version.pk, + 'document_id': self.document.pk, + 'document_version_id': self.document.latest_version.pk, }, data={'comment': TEST_DOCUMENT_VERSION_COMMENT_EDITED} ) @@ -407,8 +407,8 @@ class DocumentAPITestCase(DocumentTestMixin, BaseAPITestCase): def _request_document_version_edit_via_put(self): return self.put( viewname='rest_api:documentversion-detail', kwargs={ - 'document_pk': self.document.pk, - 'document_version_pk': self.document.latest_version.pk, + 'document_id': self.document.pk, + 'document_version_id': self.document.latest_version.pk, }, data={'comment': TEST_DOCUMENT_VERSION_COMMENT_EDITED} ) @@ -434,7 +434,7 @@ class DocumentAPITestCase(DocumentTestMixin, BaseAPITestCase): def _request_document_description_edit_via_patch(self): return self.patch( viewname='rest_api:document-detail', - kwargs={'document_pk': self.document.pk}, + kwargs={'document_id': self.document.pk}, data={'description': TEST_DOCUMENT_DESCRIPTION_EDITED} ) @@ -459,7 +459,7 @@ class DocumentAPITestCase(DocumentTestMixin, BaseAPITestCase): def _request_document_description_edit_via_put(self): return self.put( viewname='rest_api:document-detail', - kwargs={'document_pk': self.document.pk}, + kwargs={'document_id': self.document.pk}, data={'description': TEST_DOCUMENT_DESCRIPTION_EDITED} ) @@ -491,8 +491,8 @@ class DocumentPageAPITestCase(DocumentTestMixin, BaseAPITestCase): page = self.document.pages.first() return self.get( viewname='rest_api:documentpage-image', kwargs={ - 'document_pk': page.document.pk, - 'document_version_pk': page.document_version.pk, 'document_page_pk': page.pk + 'document_id': page.document.pk, + 'document_version_id': page.document_version.pk, 'document_page_id': page.pk } ) @@ -518,7 +518,7 @@ class TrashedDocumentAPITestCase(DocumentTestMixin, BaseAPITestCase): def _request_document_move_to_trash(self): return self.delete( viewname='rest_api:document-detail', - kwargs={'document_pk': self.document.pk} + kwargs={'document_id': self.document.pk} ) def test_document_move_to_trash_no_permission(self): @@ -540,7 +540,7 @@ class TrashedDocumentAPITestCase(DocumentTestMixin, BaseAPITestCase): def _request_trashed_document_delete_view(self): return self.delete( viewname='rest_api:trasheddocument-detail', - kwargs={'document_pk': self.document.pk} + kwargs={'document_id': self.document.pk} ) def test_trashed_document_delete_from_trash_no_access(self): @@ -554,7 +554,7 @@ class TrashedDocumentAPITestCase(DocumentTestMixin, BaseAPITestCase): def test_trashed_document_delete_from_trash_with_access(self): self.document = self.upload_document() self.document.delete() - self.grant_access(permission=permission_document_delete, obj=self.document) + self.grant_access(permission=permission_trashed_document_delete, obj=self.document) response = self._request_trashed_document_delete_view() self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) self.assertEqual(Document.objects.count(), 0) @@ -563,7 +563,7 @@ class TrashedDocumentAPITestCase(DocumentTestMixin, BaseAPITestCase): def _request_trashed_document_detail_view(self): return self.get( viewname='rest_api:trasheddocument-detail', - kwargs={'document_pk': self.document.pk} + kwargs={'document_id': self.document.pk} ) def test_trashed_document_detail_view_no_access(self): @@ -608,7 +608,7 @@ class TrashedDocumentAPITestCase(DocumentTestMixin, BaseAPITestCase): def _request_trashed_document_restore_view(self): return self.post( viewname='rest_api:trasheddocument-restore', - kwargs={'document_pk': self.document.pk} + kwargs={'document_id': self.document.pk} ) def test_trashed_document_restore_no_access(self): @@ -622,7 +622,7 @@ class TrashedDocumentAPITestCase(DocumentTestMixin, BaseAPITestCase): def test_trashed_document_restore_with_access(self): self.document = self.upload_document() self.document.delete() - self.grant_access(permission=permission_document_restore, obj=self.document) + self.grant_access(permission=permission_trashed_document_restore, obj=self.document) response = self._request_trashed_document_restore_view() self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(Document.trash.count(), 0) diff --git a/mayan/apps/documents/tests/test_document_page_views.py b/mayan/apps/documents/tests/test_document_page_views.py index 7b85e54dec..d276c1e513 100644 --- a/mayan/apps/documents/tests/test_document_page_views.py +++ b/mayan/apps/documents/tests/test_document_page_views.py @@ -9,19 +9,15 @@ from .literals import TEST_MULTI_PAGE_TIFF class DocumentPageViewTestCase(GenericDocumentViewTestCase): - def setUp(self): - super(DocumentPageViewTestCase, self).setUp() - self.login_user() - def _document_page_list_view(self): return self.get( viewname='documents:document_pages', - kwargs={'document_pk': self.document.pk} + kwargs={'document_id': self.document.pk} ) def test_document_page_list_view_no_permission(self): response = self._document_page_list_view() - self.assertEqual(response.status_code, 403) + self.assertEqual(response.status_code, 404) def test_document_page_list_view_with_access(self): self.grant_access( @@ -32,18 +28,39 @@ class DocumentPageViewTestCase(GenericDocumentViewTestCase): response=response, text=self.document.label, status_code=200 ) + def _request_document_page_view(self, document_page): + return self.get( + viewname='documents:document_page_view', kwargs={ + 'document_page_id': document_page.pk + } + ) + + def test_document_page_view_no_permissions(self): + response = self._request_document_page_view( + document_page=self.document.pages.first() + ) + self.assertEqual(response.status_code, 404) + + def test_document_page_view_with_access(self): + self.grant_access( + obj=self.document, permission=permission_document_view + ) + response = self._request_document_page_view( + document_page=self.document.pages.first() + ) + self.assertContains( + response=response, text=force_text(self.document.pages.first()), + status_code=200 + ) + class DocumentPageNavigationViewTestCase(GenericDocumentViewTestCase): test_document_filename = TEST_MULTI_PAGE_TIFF - def setUp(self): - super(DocumentPageNavigationViewTestCase, self).setUp() - self.login_user() - def _request_document_page_navigation_next_view(self): return self.get( viewname='documents:document_page_navigation_next', - kwargs={'document_page_pk': self.document.pages.first().pk}, + kwargs={'document_page_id': self.document.pages.first().pk}, follow=True ) @@ -51,7 +68,6 @@ class DocumentPageNavigationViewTestCase(GenericDocumentViewTestCase): self.grant_access( obj=self.document, permission=permission_document_view ) - response = self._request_document_page_navigation_next_view() self.assertContains( @@ -62,7 +78,7 @@ class DocumentPageNavigationViewTestCase(GenericDocumentViewTestCase): def _request_document_page_navigation_last_view(self): return self.get( viewname='documents:document_page_navigation_last', - kwargs={'document_page_pk': self.document.pages.first().pk}, + kwargs={'document_page_id': self.document.pages.first().pk}, follow=True ) @@ -70,7 +86,6 @@ class DocumentPageNavigationViewTestCase(GenericDocumentViewTestCase): self.grant_access( obj=self.document, permission=permission_document_view ) - response = self._request_document_page_navigation_last_view() self.assertContains( @@ -81,7 +96,7 @@ class DocumentPageNavigationViewTestCase(GenericDocumentViewTestCase): def _request_document_page_navigation_previous_view(self): return self.get( viewname='documents:document_page_navigation_previous', - kwargs={'document_page_pk': self.document.pages.last().pk}, + kwargs={'document_page_id': self.document.pages.last().pk}, follow=True ) @@ -89,7 +104,6 @@ class DocumentPageNavigationViewTestCase(GenericDocumentViewTestCase): self.grant_access( obj=self.document, permission=permission_document_view ) - response = self._request_document_page_navigation_previous_view() self.assertContains( @@ -100,7 +114,7 @@ class DocumentPageNavigationViewTestCase(GenericDocumentViewTestCase): def _request_document_page_navigation_first_view(self): return self.get( viewname='documents:document_page_navigation_first', - kwargs={'document_page_pk': self.document.pages.last().pk}, + kwargs={'document_page_id': self.document.pages.last().pk}, follow=True ) @@ -108,7 +122,6 @@ class DocumentPageNavigationViewTestCase(GenericDocumentViewTestCase): self.grant_access( obj=self.document, permission=permission_document_view ) - response = self._request_document_page_navigation_first_view() self.assertContains( diff --git a/mayan/apps/documents/tests/test_document_type_views.py b/mayan/apps/documents/tests/test_document_type_views.py index c2b891975b..2bb6ab0f8c 100644 --- a/mayan/apps/documents/tests/test_document_type_views.py +++ b/mayan/apps/documents/tests/test_document_type_views.py @@ -16,10 +16,6 @@ from .mixins import DocumentTypeQuickLabelTestMixin class DocumentTypeViewsTestCase(GenericDocumentViewTestCase): - def setUp(self): - super(DocumentTypeViewsTestCase, self).setUp() - self.login_user() - def _request_document_type_create(self): return self.post( viewname='documents:document_type_create', @@ -50,12 +46,12 @@ class DocumentTypeViewsTestCase(GenericDocumentViewTestCase): def _request_document_type_delete(self): return self.post( viewname='documents:document_type_delete', - kwargs={'document_type_pk': self.document_type.pk} + kwargs={'document_type_id': self.document_type.pk} ) def test_document_type_delete_view_no_permission(self): response = self._request_document_type_delete() - self.assertEqual(response.status_code, 403) + self.assertEqual(response.status_code, 404) self.assertEqual(DocumentType.objects.count(), 1) def test_document_type_delete_view_with_access(self): @@ -69,7 +65,7 @@ class DocumentTypeViewsTestCase(GenericDocumentViewTestCase): def _request_document_type_edit(self): return self.post( viewname='documents:document_type_edit', - kwargs={'document_type_pk': self.document_type.pk}, + kwargs={'document_type_id': self.document_type.pk}, data={ 'label': TEST_DOCUMENT_TYPE_LABEL_EDITED, 'delete_time_period': DEFAULT_DELETE_PERIOD, @@ -79,7 +75,7 @@ class DocumentTypeViewsTestCase(GenericDocumentViewTestCase): def test_document_type_edit_view_no_permission(self): response = self._request_document_type_edit() - self.assertEqual(response.status_code, 403) + self.assertEqual(response.status_code, 404) self.document_type.refresh_from_db() self.assertEqual( self.document_type.label, TEST_DOCUMENT_TYPE_LABEL @@ -98,14 +94,10 @@ class DocumentTypeViewsTestCase(GenericDocumentViewTestCase): class DocumentTypeQuickLabelViewsTestCase(DocumentTypeQuickLabelTestMixin, GenericDocumentViewTestCase): - def setUp(self): - super(DocumentTypeQuickLabelViewsTestCase, self).setUp() - self.login_user() - def _request_quick_label_create(self): return self.post( viewname='documents:document_type_filename_create', - kwargs={'document_type_pk': self.document_type.pk}, + kwargs={'document_type_id': self.document_type.pk}, data={ 'filename': TEST_DOCUMENT_TYPE_QUICK_LABEL, } @@ -117,7 +109,7 @@ class DocumentTypeQuickLabelViewsTestCase(DocumentTypeQuickLabelTestMixin, Gener ) response = self._request_quick_label_create() - self.assertEqual(response.status_code, 403) + self.assertEqual(response.status_code, 404) self.assertEqual(self.document_type.filenames.count(), 0) def test_document_type_quick_label_create_with_access(self): @@ -132,7 +124,7 @@ class DocumentTypeQuickLabelViewsTestCase(DocumentTypeQuickLabelTestMixin, Gener def _request_quick_label_delete(self): return self.post( viewname='documents:document_type_filename_delete', - kwargs={'filename_pk': self.document_type_filename.pk}, + kwargs={'filename_id': self.document_type_filename.pk}, ) def test_document_type_quick_label_delete_no_access(self): @@ -158,7 +150,7 @@ class DocumentTypeQuickLabelViewsTestCase(DocumentTypeQuickLabelTestMixin, Gener def _request_quick_label_edit(self): return self.post( viewname='documents:document_type_filename_edit', - kwargs={'filename_pk': self.document_type_filename.pk}, + kwargs={'filename_id': self.document_type_filename.pk}, data={ 'filename': TEST_DOCUMENT_TYPE_QUICK_LABEL_EDITED, } @@ -168,7 +160,7 @@ class DocumentTypeQuickLabelViewsTestCase(DocumentTypeQuickLabelTestMixin, Gener self._create_quick_label() response = self._request_quick_label_edit() - self.assertEqual(response.status_code, 403) + self.assertEqual(response.status_code, 404) self.document_type_filename.refresh_from_db() self.assertEqual( self.document_type_filename.filename, @@ -193,13 +185,13 @@ class DocumentTypeQuickLabelViewsTestCase(DocumentTypeQuickLabelTestMixin, Gener def _request_quick_label_list_view(self): return self.get( viewname='documents:document_type_filename_list', - kwargs={'document_type_pk': self.document_type.pk}, + kwargs={'document_type_id': self.document_type.pk}, ) def test_document_type_quick_label_list_no_access(self): self._create_quick_label() response = self._request_quick_label_list_view() - self.assertEqual(response.status_code, 403) + self.assertEqual(response.status_code, 404) def test_document_type_quick_label_list_with_access(self): self._create_quick_label() diff --git a/mayan/apps/documents/tests/test_document_version_views.py b/mayan/apps/documents/tests/test_document_version_views.py index ae65ec32ec..d288d75ecd 100644 --- a/mayan/apps/documents/tests/test_document_version_views.py +++ b/mayan/apps/documents/tests/test_document_version_views.py @@ -1,7 +1,10 @@ from __future__ import unicode_literals +from django.utils.encoding import force_text + from ..permissions import ( - permission_document_version_revert, permission_document_version_view + permission_document_download, permission_document_version_revert, + permission_document_version_view ) from .base import GenericDocumentViewTestCase @@ -9,26 +12,101 @@ from .literals import TEST_SMALL_DOCUMENT_PATH, TEST_VERSION_COMMENT class DocumentVersionTestCase(GenericDocumentViewTestCase): - def setUp(self): - super(DocumentVersionTestCase, self).setUp() - self.login_user() - def _upload_new_version(self): with open(TEST_SMALL_DOCUMENT_PATH, mode='rb') as file_object: - self.document.new_version( + self.test_document_version = self.document.new_version( comment=TEST_VERSION_COMMENT, file_object=file_object ) + def _request_document_version_detail_view(self): + return self.get( + viewname='documents:document_version_view', + kwargs={'document_version_id': self.test_document_version.pk} + ) + + def test_document_version_detail_no_permission(self): + self._upload_new_version() + response = self._request_document_version_detail_view() + self.assertEqual(response.status_code, 404) + + def test_document_version_detail_with_access(self): + self._upload_new_version() + self.grant_access( + obj=self.document, permission=permission_document_version_view + ) + response = self._request_document_version_detail_view() + self.assertEqual(response.status_code, 200) + + def _request_document_version_download(self, data=None): + data = data or {} + return self.get( + viewname='documents:document_version_download', kwargs={ + 'document_version_id': self.document.latest_version.pk, + }, data=data + ) + + def test_document_version_download_view_no_permission(self): + response = self._request_document_version_download() + self.assertEqual(response.status_code, 404) + + def test_document_version_download_view_with_permission(self): + # Set the expected_content_type for + # common.tests.mixins.ContentTypeCheckMixin + self.expected_content_type = '{}; charset=utf-8'.format( + self.document.latest_version.mimetype + ) + + self.grant_access( + obj=self.document, permission=permission_document_download + ) + response = self._request_document_version_download() + self.assertEqual(response.status_code, 200) + + with self.document.open() as file_object: + self.assert_download_response( + response=response, content=file_object.read(), + basename=force_text(self.document.latest_version), + mime_type='{}; charset=utf-8'.format( + self.document.latest_version.mimetype + ) + ) + + def test_document_version_download_preserve_extension_view_with_permission(self): + # Set the expected_content_type for + # common.tests.mixins.ContentTypeCheckMixin + self.expected_content_type = '{}; charset=utf-8'.format( + self.document.latest_version.mimetype + ) + + self.grant_access( + obj=self.document, permission=permission_document_download + ) + response = self._request_document_version_download( + data={'preserve_extension': True} + ) + + self.assertEqual(response.status_code, 200) + + with self.document.open() as file_object: + self.assert_download_response( + response=response, content=file_object.read(), + basename=self.document.latest_version.get_rendered_string( + preserve_extension=True + ), mime_type='{}; charset=utf-8'.format( + self.document.latest_version.mimetype + ) + ) + def _request_document_version_list_view(self): return self.get( viewname='documents:document_version_list', - kwargs={'document_pk': self.document.pk} + kwargs={'document_id': self.document.pk} ) def test_document_version_list_no_permission(self): self._upload_new_version() response = self._request_document_version_list_view() - self.assertEqual(response.status_code, 403) + self.assertEqual(response.status_code, 404) def test_document_version_list_with_access(self): self._upload_new_version() @@ -40,32 +118,28 @@ class DocumentVersionTestCase(GenericDocumentViewTestCase): response=response, text=TEST_VERSION_COMMENT, status_code=200 ) - def _request_document_version_revert_view(self, document_version): + def _request_document_version_revert_view(self): return self.post( viewname='documents:document_version_revert', - kwargs={'document_version_pk': document_version.pk} + kwargs={'document_version_id': self.test_document_version_first.pk} ) def test_document_version_revert_no_permission(self): - first_version = self.document.latest_version + self.test_document_version_first = self.document.latest_version self._upload_new_version() - response = self._request_document_version_revert_view( - document_version=first_version - ) - self.assertEqual(response.status_code, 403) + response = self._request_document_version_revert_view() + self.assertEqual(response.status_code, 302) self.assertEqual(self.document.versions.count(), 2) def test_document_version_revert_with_access(self): - first_version = self.document.latest_version + self.test_document_version_first = self.document.latest_version self._upload_new_version() self.grant_access( obj=self.document, permission=permission_document_version_revert ) - response = self._request_document_version_revert_view( - document_version=first_version - ) + response = self._request_document_version_revert_view() self.assertEqual(response.status_code, 302) self.assertEqual(self.document.versions.count(), 1) diff --git a/mayan/apps/documents/tests/test_document_views.py b/mayan/apps/documents/tests/test_document_views.py index c5e80a178c..0d3420558e 100644 --- a/mayan/apps/documents/tests/test_document_views.py +++ b/mayan/apps/documents/tests/test_document_views.py @@ -3,18 +3,16 @@ from __future__ import unicode_literals import os from django.contrib.contenttypes.models import ContentType -from django.utils.encoding import force_text from mayan.apps.converter.models import Transformation from mayan.apps.converter.permissions import permission_transformation_delete from ..literals import PAGE_RANGE_ALL -from ..models import DeletedDocument, Document, DocumentType +from ..models import Document, DocumentType, FavoriteDocument from ..permissions import ( permission_document_create, permission_document_download, permission_document_print, permission_document_properties_edit, - permission_document_tools, permission_document_view, - permission_empty_trash + permission_document_tools, permission_document_view ) from .base import GenericDocumentViewTestCase @@ -26,19 +24,15 @@ from .mixins import DocumentTypeQuickLabelTestMixin class DocumentsViewsTestCase(GenericDocumentViewTestCase): - def setUp(self): - super(DocumentsViewsTestCase, self).setUp() - self.login_user() - def _request_document_properties_view(self): return self.get( viewname='documents:document_properties', - kwargs={'document_pk': self.document.pk} + kwargs={'document_id': self.document.pk} ) def test_document_view_no_permissions(self): response = self._request_document_properties_view() - self.assertEqual(response.status_code, 403) + self.assertEqual(response.status_code, 404) def test_document_view_with_permissions(self): self.grant_access( @@ -68,8 +62,8 @@ class DocumentsViewsTestCase(GenericDocumentViewTestCase): def _request_document_type_edit(self, document_type): return self.post( - viewname='documents:document_document_type_edit', - kwargs={'document_pk': self.document.pk}, + viewname='documents:document_change_type', + kwargs={'document_id': self.document.pk}, data={'document_type': document_type.pk} ) @@ -122,7 +116,7 @@ class DocumentsViewsTestCase(GenericDocumentViewTestCase): def _request_multiple_document_type_edit(self, document_type): return self.post( - viewname='documents:document_multiple_document_type_edit', + viewname='documents:document_multiple_change_type', data={ 'id_list': self.document.pk, 'document_type': document_type.pk @@ -174,40 +168,51 @@ class DocumentsViewsTestCase(GenericDocumentViewTestCase): Document.objects.first().document_type, document_type_2 ) - def _request_document_download_form_view(self): + def _request_document_download_form_get_view(self): return self.get( viewname='documents:document_download_form', - kwargs={'document_pk': self.document.pk} + kwargs={'document_id': self.document.pk} ) - def test_document_download_form_view_no_permission(self): - response = self._request_document_download_form_view() + def test_document_download_form_view_get_no_permission(self): + response = self._request_document_download_form_get_view() + self.assertEqual(response.status_code, 404) - self.assertNotContains( - response=response, text=self.document.label, status_code=200 - ) - - def test_document_download_form_view_with_access(self): + def test_document_download_form_get_view_with_access(self): self.grant_access( obj=self.document, permission=permission_document_download ) - response = self._request_document_download_form_view() + response = self._request_document_download_form_get_view() + self.assertEqual(response.status_code, 200) - self.assertContains( - response=response, text=self.document.label, status_code=200 + def _request_document_download_form_post_view(self): + return self.post( + viewname='documents:document_download_form', + kwargs={'document_id': self.document.pk} ) + def test_document_download_form_post_view_no_permission(self): + response = self._request_document_download_form_post_view() + self.assertEqual(response.status_code, 404) + + def test_document_download_form_post_view_with_access(self): + self.grant_access( + obj=self.document, permission=permission_document_download + ) + response = self._request_document_download_form_post_view() + self.assertEqual(response.status_code, 302) + def _request_document_download_view(self): return self.get( viewname='documents:document_download', - kwargs={'document_pk': self.document.pk} + kwargs={'document_id': self.document.pk} ) def test_document_download_view_no_permission(self): response = self._request_document_download_view() - self.assertEqual(response.status_code, 403) + self.assertEqual(response.status_code, 404) - def test_document_download_view_with_permission(self): + def test_document_download_view_with_access(self): # Set the expected_content_type for # common.tests.mixins.ContentTypeCheckMixin self.expected_content_type = '{}; charset=utf-8'.format( @@ -235,7 +240,7 @@ class DocumentsViewsTestCase(GenericDocumentViewTestCase): def test_document_multiple_download_view_no_permission(self): response = self._request_document_multiple_download_view() - self.assertEqual(response.status_code, 403) + self.assertEqual(response.status_code, 404) def test_document_multiple_download_view_with_permission(self): # Set the expected_content_type for @@ -257,70 +262,10 @@ class DocumentsViewsTestCase(GenericDocumentViewTestCase): mime_type=self.document.file_mimetype ) - def _request_document_version_download(self, data=None): - data = data or {} - return self.get( - viewname='documents:document_version_download', kwargs={ - 'document_version_pk': self.document.latest_version.pk, - }, data=data - ) - - def test_document_version_download_view_no_permission(self): - response = self._request_document_version_download() - self.assertEqual(response.status_code, 403) - - def test_document_version_download_view_with_permission(self): - # Set the expected_content_type for - # common.tests.mixins.ContentTypeCheckMixin - self.expected_content_type = '{}; charset=utf-8'.format( - self.document.latest_version.mimetype - ) - - self.grant_access( - obj=self.document, permission=permission_document_download - ) - response = self._request_document_version_download() - self.assertEqual(response.status_code, 200) - - with self.document.open() as file_object: - self.assert_download_response( - response=response, content=file_object.read(), - basename=force_text(self.document.latest_version), - mime_type='{}; charset=utf-8'.format( - self.document.latest_version.mimetype - ) - ) - - def test_document_version_download_preserve_extension_view_with_permission(self): - # Set the expected_content_type for - # common.tests.mixins.ContentTypeCheckMixin - self.expected_content_type = '{}; charset=utf-8'.format( - self.document.latest_version.mimetype - ) - - self.grant_access( - obj=self.document, permission=permission_document_download - ) - response = self._request_document_version_download( - data={'preserve_extension': True} - ) - - self.assertEqual(response.status_code, 200) - - with self.document.open() as file_object: - self.assert_download_response( - response=response, content=file_object.read(), - basename=self.document.latest_version.get_rendered_string( - preserve_extension=True - ), mime_type='{}; charset=utf-8'.format( - self.document.latest_version.mimetype - ) - ) - def _request_document_update_page_count_view(self): return self.post( viewname='documents:document_update_page_count', - kwargs={'document_pk': self.document.pk} + kwargs={'document_id': self.document.pk} ) def test_document_update_page_count_view_no_permission(self): @@ -372,7 +317,7 @@ class DocumentsViewsTestCase(GenericDocumentViewTestCase): def _request_document_clear_transformations_view(self): return self.post( viewname='documents:document_clear_transformations', - kwargs={'document_pk': self.document.pk} + kwargs={'document_id': self.document.pk} ) def test_document_clear_transformations_view_no_permission(self): @@ -482,57 +427,10 @@ class DocumentsViewsTestCase(GenericDocumentViewTestCase): Transformation.objects.get_for_model(document_page).count(), 0 ) - def _request_empty_trash_view(self): - return self.post(viewname='documents:trash_can_empty') - - def test_trash_can_empty_view_no_permission(self): - self.document.delete() - self.assertEqual(DeletedDocument.objects.count(), 1) - - response = self._request_empty_trash_view() - self.assertEqual(response.status_code, 403) - self.assertEqual(DeletedDocument.objects.count(), 1) - - def test_trash_can_empty_view_with_permission(self): - self.document.delete() - self.assertEqual(DeletedDocument.objects.count(), 1) - - self.grant_permission(permission=permission_empty_trash) - - response = self._request_empty_trash_view() - self.assertEqual(response.status_code, 302) - self.assertEqual(DeletedDocument.objects.count(), 0) - self.assertEqual(Document.objects.count(), 0) - - def _request_document_page_view(self, document_page): - return self.get( - viewname='documents:document_page_view', kwargs={ - 'document_page_pk': document_page.pk - } - ) - - def test_document_page_view_no_permissions(self): - response = self._request_document_page_view( - document_page=self.document.pages.first() - ) - self.assertEqual(response.status_code, 403) - - def test_document_page_view_with_access(self): - self.grant_access( - obj=self.document, permission=permission_document_view - ) - response = self._request_document_page_view( - document_page=self.document.pages.first() - ) - self.assertContains( - response=response, text=force_text(self.document.pages.first()), - status_code=200 - ) - def _request_document_print_view(self): return self.get( viewname='documents:document_print', kwargs={ - 'document_pk': self.document.pk + 'document_id': self.document.pk }, data={ 'page_group': PAGE_RANGE_ALL } @@ -540,7 +438,7 @@ class DocumentsViewsTestCase(GenericDocumentViewTestCase): def test_document_print_view_no_access(self): response = self._request_document_print_view() - self.assertEqual(response.status_code, 403) + self.assertEqual(response.status_code, 404) def test_document_print_view_with_access(self): self.grant_access( @@ -551,10 +449,6 @@ class DocumentsViewsTestCase(GenericDocumentViewTestCase): class DocumentsQuickLabelViewsTestCase(DocumentTypeQuickLabelTestMixin, GenericDocumentViewTestCase): - def setUp(self): - super(DocumentsQuickLabelViewsTestCase, self).setUp() - self.login_user() - def _request_document_quick_label_edit_view(self, extra_data=None): data = { 'document_type_available_filenames': self.document_type_filename.pk, @@ -566,14 +460,14 @@ class DocumentsQuickLabelViewsTestCase(DocumentTypeQuickLabelTestMixin, GenericD return self.post( viewname='documents:document_edit', - kwargs={'document_pk': self.document.pk}, + kwargs={'document_id': self.document.pk}, data=data ) def test_document_quick_label_no_permission(self): self._create_quick_label() response = self._request_document_quick_label_edit_view() - self.assertEqual(response.status_code, 403) + self.assertEqual(response.status_code, 404) self.document.refresh_from_db() def test_document_quick_label_with_access(self): @@ -621,3 +515,72 @@ class DocumentsQuickLabelViewsTestCase(DocumentTypeQuickLabelTestMixin, GenericD self.assertEqual( self.document.label, self.document_type_filename.filename ) + + +class FavoriteDocumentsTestCase(GenericDocumentViewTestCase): + def _request_document_add_to_favorites_view(self): + return self.post( + viewname='documents:document_add_to_favorites', + kwargs={'document_id': self.document.pk} + ) + + def test_document_add_to_favorites_view_no_permission(self): + response = self._request_document_add_to_favorites_view() + self.assertEqual(response.status_code, 302) + self.assertEqual(FavoriteDocument.objects.count(), 0) + + def test_document_add_to_favorites_view_with_access(self): + self.grant_access( + obj=self.document, permission=permission_document_view + ) + response = self._request_document_add_to_favorites_view() + self.assertEqual(response.status_code, 302) + self.assertEqual(FavoriteDocument.objects.count(), 1) + + def _document_add_to_favorites(self): + FavoriteDocument.objects.add_for_user( + document=self.document, user=self._test_case_user + ) + + def _request_document_list_favorites(self): + return self.get( + viewname='documents:document_list_favorites', + ) + + def test_document_list_favorites_view_no_permission(self): + self._document_add_to_favorites() + response = self._request_document_list_favorites() + self.assertNotContains( + response=response, text=self.document.label, status_code=200 + ) + + def test_document_list_favorites_view_with_access(self): + self._document_add_to_favorites() + self.grant_access( + obj=self.document, permission=permission_document_view + ) + response = self._request_document_list_favorites() + self.assertContains( + response=response, text=self.document.label, status_code=200 + ) + + def _request_document_remove_from_favorites(self): + return self.post( + viewname='documents:document_remove_from_favorites', + kwargs={'document_id': self.document.pk} + ) + + def test_document_remove_from_favorites_view_no_permission(self): + self._document_add_to_favorites() + response = self._request_document_remove_from_favorites() + self.assertEqual(response.status_code, 302) + self.assertEqual(FavoriteDocument.objects.count(), 1) + + def test_document_remove_from_favorites_view_with_access(self): + self._document_add_to_favorites() + self.grant_access( + obj=self.document, permission=permission_document_view + ) + response = self._request_document_remove_from_favorites() + self.assertEqual(response.status_code, 302) + self.assertEqual(FavoriteDocument.objects.count(), 0) diff --git a/mayan/apps/documents/tests/test_duplicated_document_views.py b/mayan/apps/documents/tests/test_duplicated_document_views.py index 7dea4e1123..780b04f4bb 100644 --- a/mayan/apps/documents/tests/test_duplicated_document_views.py +++ b/mayan/apps/documents/tests/test_duplicated_document_views.py @@ -7,10 +7,6 @@ from .literals import TEST_SMALL_DOCUMENT_FILENAME, TEST_SMALL_DOCUMENT_PATH class DuplicatedDocumentsViewsTestCase(GenericDocumentViewTestCase): - def setUp(self): - super(DuplicatedDocumentsViewsTestCase, self).setUp() - self.login_user() - def _upload_duplicate_document(self): with open(TEST_SMALL_DOCUMENT_PATH, mode='rb') as file_object: self.document_duplicate = self.document_type.new_document( @@ -23,7 +19,7 @@ class DuplicatedDocumentsViewsTestCase(GenericDocumentViewTestCase): def _request_document_duplicates_list_view(self): return self.get( viewname='documents:document_duplicates_list', - kwargs={'document_pk': self.document.pk} + kwargs={'document_id': self.document.pk} ) def test_duplicated_document_list_no_permissions(self): @@ -53,7 +49,7 @@ class DuplicatedDocumentsViewsTestCase(GenericDocumentViewTestCase): self._upload_duplicate_document() response = self._request_document_duplicates_list_view() - self.assertEqual(response.status_code, 403) + self.assertEqual(response.status_code, 404) def test_document_duplicates_list_with_access(self): self._upload_duplicate_document() diff --git a/mayan/apps/documents/tests/test_events.py b/mayan/apps/documents/tests/test_events.py index bd3b8859b4..c703a58bf8 100644 --- a/mayan/apps/documents/tests/test_events.py +++ b/mayan/apps/documents/tests/test_events.py @@ -3,10 +3,6 @@ from __future__ import unicode_literals from actstream.models import Action from django_downloadview import assert_download_response -from mayan.apps.user_management.tests.literals import ( - TEST_USER_PASSWORD, TEST_USER_USERNAME -) - from ..events import event_document_download, event_document_view from ..permissions import ( permission_document_download, permission_document_view @@ -21,38 +17,30 @@ TEST_TRANSFORMATION_ARGUMENT = 'degrees: 180' class DocumentEventsTestCase(GenericDocumentViewTestCase): - def test_document_download_event_no_permissions(self): - self.login( - username=TEST_USER_USERNAME, password=TEST_USER_PASSWORD + def _request_document_download_view(self): + return self.get( + viewname='documents:document_download', + kwargs={'document_id': self.document.pk} ) + def test_document_download_event_no_permissions(self): Action.objects.all().delete() - response = self.get( - viewname='documents:document_download', - kwargs={'document_pk': self.document.pk} - ) + response = self._request_document_download_view() - self.assertEqual(response.status_code, 403) + self.assertEqual(response.status_code, 404) self.assertEqual(list(Action.objects.any(obj=self.document)), []) def test_document_download_event_with_permissions(self): - self.login( - username=TEST_USER_USERNAME, password=TEST_USER_PASSWORD - ) - Action.objects.all().delete() - self.role.permissions.add( - permission_document_download.stored_permission + self.grant_access( + obj=self.document, permission=permission_document_download ) self.expected_content_type = 'image/png; charset=utf-8' - response = self.get( - viewname='documents:document_download', - kwargs={'document_pk': self.document.pk} - ) + response = self._request_document_download_view() # Download the file to close the file descriptor with self.document.open() as file_object: @@ -65,40 +53,31 @@ class DocumentEventsTestCase(GenericDocumentViewTestCase): self.assertEqual(event.verb, event_document_download.id) self.assertEqual(event.target, self.document) - self.assertEqual(event.actor, self.user) + self.assertEqual(event.actor, self._test_case_user) + + def _request_document_preview_view(self): + return self.get( + viewname='documents:document_preview', + kwargs={'document_id': self.document.pk} + ) def test_document_view_event_no_permissions(self): - self.login( - username=TEST_USER_USERNAME, password=TEST_USER_PASSWORD - ) - Action.objects.all().delete() - response = self.get( - viewname='documents:document_preview', - kwargs={'document_pk': self.document.pk} - ) + response = self._request_document_preview_view() - self.assertEqual(response.status_code, 403) + self.assertEqual(response.status_code, 404) self.assertEqual(list(Action.objects.any(obj=self.document)), []) - def test_document_view_event_with_permissions(self): - self.login( - username=TEST_USER_USERNAME, password=TEST_USER_PASSWORD - ) - + def test_document_view_event_with_access(self): Action.objects.all().delete() - self.role.permissions.add( - permission_document_view.stored_permission - ) - self.get( - viewname='documents:document_preview', - kwargs={'document_pk': self.document.pk} - ) + self.grant_access(obj=self.document, permission=permission_document_view) + response = self._request_document_preview_view() + self.assertEqual(response.status_code, 200) event = Action.objects.any(obj=self.document).first() self.assertEqual(event.verb, event_document_view.id) self.assertEqual(event.target, self.document) - self.assertEqual(event.actor, self.user) + self.assertEqual(event.actor, self._test_case_user) diff --git a/mayan/apps/documents/tests/test_links.py b/mayan/apps/documents/tests/test_links.py index b820099da3..c9d8080c8f 100644 --- a/mayan/apps/documents/tests/test_links.py +++ b/mayan/apps/documents/tests/test_links.py @@ -4,14 +4,13 @@ import time from django.urls import reverse -from mayan.apps.acls.models import AccessControlList - from ..links import ( - link_document_restore, link_document_version_download, + link_trashed_document_restore, link_document_version_download, link_document_version_revert ) +from ..models import TrashedDocument from ..permissions import ( - permission_document_download, permission_document_restore, + permission_document_download, permission_trashed_document_restore, permission_document_version_revert ) @@ -26,15 +25,13 @@ class DocumentsLinksTestCase(GenericDocumentViewTestCase): self.assertTrue(self.document.versions.count(), 2) - self.login_user() - self.add_test_view(test_object=self.document.versions.first()) context = self.get_test_view() resolved_link = link_document_version_revert.resolve(context=context) self.assertEqual(resolved_link, None) - def test_document_version_revert_link_with_permission(self): + def test_document_version_revert_link_with_access(self): # Needed by MySQL as milliseconds value is not store in timestamp # field time.sleep(1.01) @@ -44,13 +41,8 @@ class DocumentsLinksTestCase(GenericDocumentViewTestCase): self.assertTrue(self.document.versions.count(), 2) - self.login_user() - - acl = AccessControlList.objects.create( - content_object=self.document, role=self.role - ) - acl.permissions.add( - permission_document_version_revert.stored_permission + self.grant_access( + obj=self.document, permission=permission_document_version_revert ) self.add_test_view(test_object=self.document.versions.first()) @@ -61,27 +53,22 @@ class DocumentsLinksTestCase(GenericDocumentViewTestCase): self.assertEqual( resolved_link.url, reverse( - 'documents:document_version_revert', - args=(self.document.versions.first().pk,) + viewname='documents:document_version_revert', + kwargs={'document_version_id': self.document.versions.first().pk} ) ) def test_document_version_download_link_no_permission(self): - self.login_user() - self.add_test_view(test_object=self.document.latest_version) context = self.get_test_view() resolved_link = link_document_version_download.resolve(context=context) self.assertEqual(resolved_link, None) - def test_document_version_download_link_with_permission(self): - self.login_user() - - acl = AccessControlList.objects.create( - content_object=self.document, role=self.role + def test_document_version_download_link_with_access(self): + self.grant_access( + obj=self.document, permission=permission_document_download ) - acl.permissions.add(permission_document_download.stored_permission) self.add_test_view(test_object=self.document.latest_version) context = self.get_test_view() @@ -91,45 +78,41 @@ class DocumentsLinksTestCase(GenericDocumentViewTestCase): self.assertEqual( resolved_link.url, reverse( - 'documents:document_version_download_form', - args=(self.document.latest_version.pk,) + viewname='documents:document_version_download_form', + kwargs={'document_version_id': self.document.latest_version.pk} ) ) class DeletedDocumentsLinksTestCase(GenericDocumentViewTestCase): + def _request_trashed_document_restore_link(self): + self.add_test_view( + test_object=TrashedDocument.objects.get(pk=self.document.pk) + ) + context = self.get_test_view() + return link_trashed_document_restore.resolve(context=context) + def test_deleted_document_restore_link_no_permission(self): self.document.delete() - self.login_user() - - self.add_test_view(test_object=self.document) - context = self.get_test_view() - resolved_link = link_document_restore.resolve(context=context) + resolved_link = self._request_trashed_document_restore_link() self.assertEqual(resolved_link, None) - def test_deleted_document_restore_link_with_permission(self): + def test_deleted_document_restore_link_with_access(self): self.document.delete() - self.login_user() - - acl = AccessControlList.objects.create( - content_object=self.document, role=self.role - ) - acl.permissions.add( - permission_document_restore.stored_permission + self.grant_access( + obj=self.document, permission=permission_trashed_document_restore ) - self.add_test_view(test_object=self.document) - context = self.get_test_view() - resolved_link = link_document_restore.resolve(context=context) + resolved_link = self._request_trashed_document_restore_link() self.assertNotEqual(resolved_link, None) self.assertEqual( resolved_link.url, reverse( - 'documents:document_restore', - args=(self.document.pk,) + viewname='documents:trashed_document_restore', + kwargs={'trashed_document_id': self.document.pk} ) ) diff --git a/mayan/apps/documents/tests/test_models.py b/mayan/apps/documents/tests/test_models.py index e3f6173eda..4b1839a5c2 100644 --- a/mayan/apps/documents/tests/test_models.py +++ b/mayan/apps/documents/tests/test_models.py @@ -7,7 +7,7 @@ from mayan.apps.common.tests import BaseTestCase from ..literals import STUB_EXPIRATION_INTERVAL from ..models import ( - DeletedDocument, Document, DocumentType, DuplicatedDocument + Document, DocumentType, DuplicatedDocument, TrashedDocument ) from .base import GenericDocumentTestCase @@ -58,12 +58,12 @@ class DocumentTestCase(DocumentTestMixin, BaseTestCase): # Trash the document self.document.delete() - self.assertEqual(DeletedDocument.objects.count(), 1) + self.assertEqual(TrashedDocument.objects.count(), 1) self.assertEqual(Document.objects.count(), 0) # Restore the document self.document.restore() - self.assertEqual(DeletedDocument.objects.count(), 0) + self.assertEqual(TrashedDocument.objects.count(), 0) self.assertEqual(Document.objects.count(), 1) def test_trashing_documents(self): @@ -71,12 +71,12 @@ class DocumentTestCase(DocumentTestMixin, BaseTestCase): # Trash the document self.document.delete() - self.assertEqual(DeletedDocument.objects.count(), 1) + self.assertEqual(TrashedDocument.objects.count(), 1) self.assertEqual(Document.objects.count(), 0) # Delete the document self.document.delete() - self.assertEqual(DeletedDocument.objects.count(), 0) + self.assertEqual(TrashedDocument.objects.count(), 0) self.assertEqual(Document.objects.count(), 0) def test_auto_trashing(self): @@ -94,12 +94,12 @@ class DocumentTestCase(DocumentTestMixin, BaseTestCase): time.sleep(1.01) self.assertEqual(Document.objects.count(), 1) - self.assertEqual(DeletedDocument.objects.count(), 0) + self.assertEqual(TrashedDocument.objects.count(), 0) DocumentType.objects.check_trash_periods() self.assertEqual(Document.objects.count(), 0) - self.assertEqual(DeletedDocument.objects.count(), 1) + self.assertEqual(TrashedDocument.objects.count(), 1) def test_auto_delete(self): """ @@ -112,12 +112,12 @@ class DocumentTestCase(DocumentTestMixin, BaseTestCase): self.document_type.save() self.assertEqual(Document.objects.count(), 1) - self.assertEqual(DeletedDocument.objects.count(), 0) + self.assertEqual(TrashedDocument.objects.count(), 0) self.document.delete() self.assertEqual(Document.objects.count(), 0) - self.assertEqual(DeletedDocument.objects.count(), 1) + self.assertEqual(TrashedDocument.objects.count(), 1) # Needed by MySQL as milliseconds value is not stored in timestamp # field @@ -126,7 +126,7 @@ class DocumentTestCase(DocumentTestMixin, BaseTestCase): DocumentType.objects.check_delete_periods() self.assertEqual(Document.objects.count(), 0) - self.assertEqual(DeletedDocument.objects.count(), 0) + self.assertEqual(TrashedDocument.objects.count(), 0) class PDFCompatibilityTestCase(BaseTestCase): diff --git a/mayan/apps/documents/tests/test_search.py b/mayan/apps/documents/tests/test_search.py index b72b5eeed2..9f689e277c 100644 --- a/mayan/apps/documents/tests/test_search.py +++ b/mayan/apps/documents/tests/test_search.py @@ -9,12 +9,12 @@ from mayan.apps.documents.tests import DocumentTestMixin class DocumentSearchTestCase(DocumentTestMixin, BaseTestCase): def _perform_document_page_search(self): return document_page_search.search( - query_string={'q': self.document.label}, user=self.user + query_string={'q': self.document.label}, user=self._test_case_user ) def _perform_document_search(self): return document_search.search( - query_string={'q': self.document.label}, user=self.user + query_string={'q': self.document.label}, user=self._test_case_user ) def test_document_page_search_no_access(self): diff --git a/mayan/apps/documents/tests/test_trashed_document_views.py b/mayan/apps/documents/tests/test_trashed_document_views.py index f54ff227e2..543eb8c9df 100644 --- a/mayan/apps/documents/tests/test_trashed_document_views.py +++ b/mayan/apps/documents/tests/test_trashed_document_views.py @@ -1,56 +1,26 @@ from __future__ import unicode_literals -from ..models import DeletedDocument, Document +from ..models import Document, TrashedDocument from ..permissions import ( - permission_document_delete, permission_document_restore, - permission_document_trash, permission_document_view + permission_document_trash, permission_document_view, + permission_empty_trash, permission_trashed_document_delete, + permission_trashed_document_restore ) from .base import GenericDocumentViewTestCase -class DeletedDocumentTestCase(GenericDocumentViewTestCase): - def setUp(self): - super(DeletedDocumentTestCase, self).setUp() - self.login_user() - - def _request_document_restore_view(self): - return self.post( - viewname='documents:document_restore', - kwargs={'document_pk': self.document.pk} - ) - - def test_document_restore_view_no_permission(self): - self.document.delete() - self.assertEqual(Document.objects.count(), 0) - - response = self._request_document_restore_view() - self.assertEqual(response.status_code, 403) - self.assertEqual(DeletedDocument.objects.count(), 1) - self.assertEqual(Document.objects.count(), 0) - - def test_document_restore_view_with_access(self): - self.document.delete() - self.assertEqual(Document.objects.count(), 0) - - self.grant_access( - obj=self.document, permission=permission_document_restore - ) - response = self._request_document_restore_view() - self.assertEqual(response.status_code, 302) - self.assertEqual(DeletedDocument.objects.count(), 0) - self.assertEqual(Document.objects.count(), 1) - +class TrashedDocumentTestCase(GenericDocumentViewTestCase): def _request_document_trash_view(self): return self.post( viewname='documents:document_trash', - kwargs={'document_pk': self.document.pk} + kwargs={'document_id': self.document.pk} ) def test_document_trash_no_permissions(self): response = self._request_document_trash_view() - self.assertEqual(response.status_code, 403) - self.assertEqual(DeletedDocument.objects.count(), 0) + self.assertEqual(response.status_code, 302) + self.assertEqual(TrashedDocument.objects.count(), 0) self.assertEqual(Document.objects.count(), 1) def test_document_trash_with_access(self): @@ -60,58 +30,107 @@ class DeletedDocumentTestCase(GenericDocumentViewTestCase): response = self._request_document_trash_view() self.assertEqual(response.status_code, 302) - self.assertEqual(DeletedDocument.objects.count(), 1) + self.assertEqual(TrashedDocument.objects.count(), 1) self.assertEqual(Document.objects.count(), 0) - def _request_document_delete_view(self): + def _request_trashed_document_restore_view(self): return self.post( - viewname='documents:document_delete', - kwargs={'document_pk': self.document.pk} + viewname='documents:trashed_document_restore', + kwargs={'trashed_document_id': self.document.pk} ) - def test_document_delete_no_permissions(self): + def test_trashed_document_restore_view_no_permission(self): self.document.delete() self.assertEqual(Document.objects.count(), 0) - self.assertEqual(DeletedDocument.objects.count(), 1) - response = self._request_document_delete_view() - self.assertEqual(response.status_code, 403) + response = self._request_trashed_document_restore_view() + self.assertEqual(response.status_code, 302) + self.assertEqual(TrashedDocument.objects.count(), 1) self.assertEqual(Document.objects.count(), 0) - self.assertEqual(DeletedDocument.objects.count(), 1) - def test_document_delete_with_access(self): + def test_trashed_document_restore_view_with_access(self): self.document.delete() self.assertEqual(Document.objects.count(), 0) - self.assertEqual(DeletedDocument.objects.count(), 1) self.grant_access( - obj=self.document, permission=permission_document_delete + obj=self.document, permission=permission_trashed_document_restore + ) + response = self._request_trashed_document_restore_view() + self.assertEqual(response.status_code, 302) + self.assertEqual(TrashedDocument.objects.count(), 0) + self.assertEqual(Document.objects.count(), 1) + + def _request_trashed_document_delete_view(self): + return self.post( + viewname='documents:trashed_document_delete', + kwargs={'trashed_document_id': self.document.pk} ) - response = self._request_document_delete_view() + def test_trashed_document_delete_no_permissions(self): + self.document.delete() + self.assertEqual(Document.objects.count(), 0) + self.assertEqual(TrashedDocument.objects.count(), 1) + + response = self._request_trashed_document_delete_view() self.assertEqual(response.status_code, 302) - self.assertEqual(DeletedDocument.objects.count(), 0) + self.assertEqual(Document.objects.count(), 0) + self.assertEqual(TrashedDocument.objects.count(), 1) + + def test_trashed_document_delete_with_access(self): + self.document.delete() + self.assertEqual(Document.objects.count(), 0) + self.assertEqual(TrashedDocument.objects.count(), 1) + + self.grant_access( + obj=self.document, permission=permission_trashed_document_delete + ) + + response = self._request_trashed_document_delete_view() + self.assertEqual(response.status_code, 302) + self.assertEqual(TrashedDocument.objects.count(), 0) self.assertEqual(Document.objects.count(), 0) - def _request_document_list_deleted_view(self): - return self.get(viewname='documents:document_list_deleted') + def _request_trashed_document_list_view(self): + return self.get(viewname='documents:trashed_document_list') - def test_deleted_document_list_view_no_permissions(self): + def test_trashed_document_list_view_no_permissions(self): self.document.delete() - response = self._request_document_list_deleted_view() + response = self._request_trashed_document_list_view() self.assertNotContains( response=response, text=self.document.label, status_code=200 ) - def test_deleted_document_list_view_with_access(self): + def test_trashed_document_list_view_with_access(self): self.document.delete() self.grant_access( obj=self.document, permission=permission_document_view ) - response = self._request_document_list_deleted_view() + response = self._request_trashed_document_list_view() self.assertContains( response=response, text=self.document.label, status_code=200 ) + + def _request_empty_trash_view(self): + return self.post(viewname='documents:trash_can_empty') + + def test_trash_can_empty_view_no_permission(self): + self.document.delete() + self.assertEqual(TrashedDocument.objects.count(), 1) + + response = self._request_empty_trash_view() + self.assertEqual(response.status_code, 403) + self.assertEqual(TrashedDocument.objects.count(), 1) + + def test_trash_can_empty_view_with_permission(self): + self.document.delete() + self.assertEqual(TrashedDocument.objects.count(), 1) + + self.grant_permission(permission=permission_empty_trash) + + response = self._request_empty_trash_view() + self.assertEqual(response.status_code, 302) + self.assertEqual(TrashedDocument.objects.count(), 0) + self.assertEqual(Document.objects.count(), 0) diff --git a/mayan/apps/documents/urls.py b/mayan/apps/documents/urls.py index 92847752e4..bc1132fb23 100644 --- a/mayan/apps/documents/urls.py +++ b/mayan/apps/documents/urls.py @@ -2,29 +2,34 @@ from __future__ import unicode_literals from django.conf.urls import url +""" from .api_views import ( - APIDeletedDocumentListView, APIDeletedDocumentRestoreView, - APIDeletedDocumentView, APIDocumentDownloadView, APIDocumentListView, + APITrashedDocumentListView, APITrashedDocumentRestoreView, + APITrashedDocumentView, APIDocumentDownloadView, APIDocumentListView, APIDocumentPageImageView, APIDocumentPageView, APIDocumentTypeDocumentListView, APIDocumentTypeListView, APIDocumentTypeView, APIDocumentVersionDownloadView, APIDocumentVersionPageListView, APIDocumentVersionsListView, APIDocumentVersionView, APIDocumentView, APIRecentDocumentListView ) +""" +from .api_views import ( + DocumentPageViewSet, DocumentTypeViewSet, DocumentVersionViewSet, + DocumentViewSet +) + from .views import ( - ClearImageCacheView, DeletedDocumentDeleteManyView, - DeletedDocumentDeleteView, DeletedDocumentListView, - DocumentDocumentTypeEditView, DocumentDownloadFormView, + ClearImageCacheView, DocumentChangeTypeView, DocumentDownloadFormView, DocumentDownloadView, DocumentDuplicatesListView, DocumentEditView, DocumentListView, DocumentPageListView, DocumentPageNavigationFirst, DocumentPageNavigationLast, DocumentPageNavigationNext, DocumentPageNavigationPrevious, DocumentPageRotateLeftView, DocumentPageRotateRightView, DocumentPageView, DocumentPageViewResetView, DocumentPageZoomInView, DocumentPageZoomOutView, DocumentPreviewView, - DocumentPrint, DocumentRestoreManyView, DocumentRestoreView, - DocumentTransformationsClearView, DocumentTransformationsCloneView, - DocumentTrashManyView, DocumentTrashView, DocumentTypeCreateView, - DocumentTypeDeleteView, DocumentTypeDocumentListView, DocumentTypeEditView, + DocumentPrintView, DocumentTransformationsClearView, + DocumentTransformationsCloneView, DocumentTrashView, + DocumentTypeCreateView, DocumentTypeDeleteView, + DocumentTypeDocumentListView, DocumentTypeEditView, DocumentTypeFilenameCreateView, DocumentTypeFilenameDeleteView, DocumentTypeFilenameEditView, DocumentTypeFilenameListView, DocumentTypeListView, DocumentUpdatePageCountView, @@ -33,12 +38,51 @@ from .views import ( DocumentView, DuplicatedDocumentListView, EmptyTrashCanView, FavoriteAddView, FavoriteDocumentListView, FavoriteRemoveView, RecentAccessDocumentListView, RecentAddedDocumentListView, - ScanDuplicatedDocuments + ScanDuplicatedDocuments, TrashedDocumentDeleteView, TrashedDocumentListView, + TrashedDocumentRestoreView +) + +urlpatterns_trashed_documents = ( + url( + regex=r'^documents/(?P\d+)/trash/$', + name='document_trash', view=DocumentTrashView.as_view() + ), + url( + regex=r'^documents/multiple/trash/$', name='document_multiple_trash', + view=DocumentTrashView.as_view() + ), + url( + regex=r'^trashed_documents/$', name='trashed_document_list', + view=TrashedDocumentListView.as_view() + ), + url( + regex=r'^trashed_documents/(?P\d+)/restore/$', + name='trashed_document_restore', + view=TrashedDocumentRestoreView.as_view() + ), + url( + regex=r'^trashed_documents/multiple/restore/$', + name='trashed_document_multiple_restore', + view=TrashedDocumentRestoreView.as_view() + ), + url( + regex=r'^trashed_documents/(?P\d+)/delete/$', + name='trashed_document_delete', view=TrashedDocumentDeleteView.as_view() + ), + url( + regex=r'^trashed_documents/multiple/delete/$', + name='trashed_document_multiple_delete', + view=TrashedDocumentDeleteView.as_view() + ), + url( + regex=r'^trash/empty/$', name='trash_can_empty', + view=EmptyTrashCanView.as_view() + ) ) urlpatterns = [ url( - regex=r'^documents/all/$', name='document_list', + regex=r'^documents/$', name='document_list', view=DocumentListView.as_view() ), url( @@ -50,10 +94,8 @@ urlpatterns = [ regex=r'^documents/recent_added/$', name='document_list_recent_added', view=RecentAddedDocumentListView.as_view() ), - url( - regex=r'^documents/deleted/$', name='document_list_deleted', - view=DeletedDocumentListView.as_view() - ), + + url( regex=r'^documents/duplicated/$', name='duplicated_document_list', view=DuplicatedDocumentListView.as_view() @@ -63,20 +105,20 @@ urlpatterns = [ view=FavoriteDocumentListView.as_view() ), url( - regex=r'^documents/(?P\d+)/preview/$', + regex=r'^documents/(?P\d+)/preview/$', name='document_preview', view=DocumentPreviewView.as_view() ), url( - regex=r'^documents/(?P\d+)/properties/$', + regex=r'^documents/(?P\d+)/properties/$', name='document_properties', view=DocumentView.as_view() ), url( - regex=r'^documents/(?P\d+)/duplicates/$', + regex=r'^documents/(?P\d+)/duplicates/$', name='document_duplicates_list', view=DocumentDuplicatesListView.as_view() ), url( - regex=r'^documents/(?P\d+)/add_to_favorites/$', + regex=r'^documents/(?P\d+)/add_to_favorites/$', name='document_add_to_favorites', view=FavoriteAddView.as_view() ), url( @@ -85,7 +127,7 @@ urlpatterns = [ view=FavoriteAddView.as_view() ), url( - regex=r'^documents/(?P\d+)/remove_from_favorites/$', + regex=r'^documents/(?P\d+)/remove_from_favorites/$', name='document_remove_from_favorites', view=FavoriteRemoveView.as_view() ), @@ -95,50 +137,25 @@ urlpatterns = [ view=FavoriteRemoveView.as_view() ), url( - regex=r'^documents/(?P\d+)/restore/$', - name='document_restore', view=DocumentRestoreView.as_view() - ), - url( - regex=r'^documents/multiple/restore/$', - name='document_multiple_restore', - view=DocumentRestoreManyView.as_view() - ), - url( - regex=r'^documents/(?P\d+)/delete/$', - name='document_delete', view=DeletedDocumentDeleteView.as_view() - ), - url( - regex=r'^documents/multiple/delete/$', name='document_multiple_delete', - view=DeletedDocumentDeleteManyView.as_view() - ), - url( - regex=r'^documents/(?P\d+)/type/$', - name='document_document_type_edit', - view=DocumentDocumentTypeEditView.as_view() + regex=r'^documents/(?P\d+)/type/$', + name='document_change_type', + view=DocumentChangeTypeView.as_view() ), url( regex=r'^documents/multiple/type/$', - name='document_multiple_document_type_edit', - view=DocumentDocumentTypeEditView.as_view() + name='document_multiple_change_type', + view=DocumentChangeTypeView.as_view() ), url( - regex=r'^documents/(?P\d+)/trash/$', - name='document_trash', view=DocumentTrashView.as_view() - ), - url( - regex=r'^documents/multiple/trash/$', name='document_multiple_trash', - view=DocumentTrashManyView.as_view() - ), - url( - regex=r'^documents/(?P\d+)/edit/$', name='document_edit', + regex=r'^documents/(?P\d+)/edit/$', name='document_edit', view=DocumentEditView.as_view() ), url( - regex=r'^documents/(?P\d+)/print/$', - name='document_print', view=DocumentPrint.as_view() + regex=r'^documents/(?P\d+)/print/$', + name='document_print', view=DocumentPrintView.as_view() ), url( - regex=r'^documents/(?P\d+)/reset_page_count/$', + regex=r'^documents/(?P\d+)/reset_page_count/$', name='document_update_page_count', view=DocumentUpdatePageCountView.as_view() ), @@ -148,11 +165,11 @@ urlpatterns = [ view=DocumentUpdatePageCountView.as_view() ), url( - regex=r'^documents/(?P\d+)/download/form/$', + regex=r'^documents/(?P\d+)/download/form/$', name='document_download_form', view=DocumentDownloadFormView.as_view() ), url( - regex=r'^documents/(?P\d+)/download/$', + regex=r'^documents/(?P\d+)/download/$', name='document_download', view=DocumentDownloadView.as_view() ), url( @@ -165,46 +182,46 @@ urlpatterns = [ name='document_multiple_download', view=DocumentDownloadView.as_view() ), url( - regex=r'^documents/(?P\d+)/clear_transformations/$', + regex=r'^documents/(?P\d+)/transformations/clear/$', name='document_clear_transformations', view=DocumentTransformationsClearView.as_view() ), url( - regex=r'^documents/(?P\d+)/clone_transformations/$', + regex=r'^documents/(?P\d+)/transformations/clone/$', name='document_clone_transformations', view=DocumentTransformationsCloneView.as_view() ), url( - regex=r'^documents/(?P\d+)/version/all/$', + regex=r'^documents/(?P\d+)/versions/$', name='document_version_list', view=DocumentVersionListView.as_view() ), url( - regex=r'^documents/versions/(?P\d+)/download/form/$', + regex=r'^documents/versions/(?P\d+)/download/form/$', name='document_version_download_form', view=DocumentVersionDownloadFormView.as_view() ), url( - regex=r'^documents/versions/(?P\d+)/$', + regex=r'^documents/versions/(?P\d+)/$', name='document_version_view', view=DocumentVersionView.as_view() ), url( - regex=r'^documents/versions/(?P\d+)/download/$', + regex=r'^documents/versions/(?P\d+)/download/$', name='document_version_download', view=DocumentVersionDownloadView.as_view() ), url( - regex=r'^documents/versions/(?P\d+)/revert/$', + regex=r'^documents/versions/(?P\d+)/revert/$', name='document_version_revert', view=DocumentVersionRevertView.as_view() ), url( - regex=r'^documents/(?P\d+)/pages/$', + regex=r'^documents/(?P\d+)/pages/$', name='document_pages', view=DocumentPageListView.as_view() ), url( - regex=r'^documents/multiple/clear_transformations/$', + regex=r'^documents/multiple/transformations/clear/$', name='document_multiple_clear_transformations', view=DocumentTransformationsClearView.as_view() ), @@ -213,96 +230,92 @@ urlpatterns = [ view=ClearImageCacheView.as_view() ), url( - regex=r'^trash_can/empty/$', name='trash_can_empty', - view=EmptyTrashCanView.as_view() - ), - url( - regex=r'^pages/(?P\d+)/$', + regex=r'^documents/pages/(?P\d+)/$', name='document_page_view', view=DocumentPageView.as_view() ), url( - regex=r'^pages/(?P\d+)/navigation/next/$', + regex=r'^documents/pages/(?P\d+)/navigation/next/$', name='document_page_navigation_next', view=DocumentPageNavigationNext.as_view() ), url( - regex=r'^pages/(?P\d+)/navigation/previous/$', + regex=r'^documents/pages/(?P\d+)/navigation/previous/$', name='document_page_navigation_previous', view=DocumentPageNavigationPrevious.as_view() ), url( - regex=r'^pages/(?P\d+)/navigation/first/$', + regex=r'^documents/pages/(?P\d+)/navigation/first/$', name='document_page_navigation_first', view=DocumentPageNavigationFirst.as_view() ), url( - regex=r'^pages/(?P\d+)/navigation/last/$', + regex=r'^documents/pages/(?P\d+)/navigation/last/$', name='document_page_navigation_last', view=DocumentPageNavigationLast.as_view() ), url( - regex=r'^pages/(?P\d+)/zoom/in/$', + regex=r'^documents/pages/(?P\d+)/zoom/in/$', name='document_page_zoom_in', view=DocumentPageZoomInView.as_view() ), url( - regex=r'^pages/(?P\d+)/zoom/out/$', + regex=r'^documents/pages/(?P\d+)/zoom/out/$', name='document_page_zoom_out', view=DocumentPageZoomOutView.as_view() ), url( - regex=r'^pages/(?P\d+)/rotate/left/$', + regex=r'^documents/pages/(?P\d+)/rotate/left/$', name='document_page_rotate_left', view=DocumentPageRotateLeftView.as_view() ), url( - regex=r'^pages/(?P\d+)/rotate/right/$', + regex=r'^documents/pages/(?P\d+)/rotate/right/$', name='document_page_rotate_right', view=DocumentPageRotateRightView.as_view() ), url( - regex=r'^pages/(?P\d+)/reset/$', + regex=r'^documents/pages/(?P\d+)/reset/$', name='document_page_view_reset', view=DocumentPageViewResetView.as_view() ), # Admin views url( - regex=r'^types/$', name='document_type_list', + regex=r'^documents_types/$', name='document_type_list', view=DocumentTypeListView.as_view() ), url( - regex=r'^types/create/$', name='document_type_create', + regex=r'^documents_types/create/$', name='document_type_create', view=DocumentTypeCreateView.as_view() ), url( - regex=r'^types/(?P\d+)/edit/$', name='document_type_edit', - view=DocumentTypeEditView.as_view() + regex=r'^documents_types/(?P\d+)/edit/$', + name='document_type_edit', view=DocumentTypeEditView.as_view() ), url( - regex=r'^types/(?P\d+)/delete/$', name='document_type_delete', - view=DocumentTypeDeleteView.as_view() + regex=r'^documents_types/(?P\d+)/delete/$', + name='document_type_delete', view=DocumentTypeDeleteView.as_view() ), url( - regex=r'^types/(?P\d+)/documents/$', + regex=r'^documents_types/(?P\d+)/documents/$', name='document_type_document_list', view=DocumentTypeDocumentListView.as_view() ), url( - regex=r'^types/(?P\d+)/filenames/create/$', - name='document_type_filename_create', - view=DocumentTypeFilenameCreateView.as_view() - ), - url( - regex=r'^types/(?P\d+)/filenames/$', + regex=r'^documents_types/(?P\d+)/filenames/$', name='document_type_filename_list', view=DocumentTypeFilenameListView.as_view() ), url( - regex=r'^types/filenames/(?P\d+)/edit/$', + regex=r'^documents_types/(?P\d+)/filenames/create/$', + name='document_type_filename_create', + view=DocumentTypeFilenameCreateView.as_view() + ), + url( + regex=r'^documents_types/filenames/(?P\d+)/edit/$', name='document_type_filename_edit', view=DocumentTypeFilenameEditView.as_view() ), url( - regex=r'^types/filenames/(?P\d+)/delete/$', + regex=r'^documents_types/filenames/(?P\d+)/delete/$', name='document_type_filename_delete', view=DocumentTypeFilenameDeleteView.as_view() ), @@ -316,17 +329,20 @@ urlpatterns = [ ), ] -api_urls = [ +urlpatterns.extend(urlpatterns_trashed_documents) + +api_urls = [] +""" url( regex=r'^document_types/$', name='documenttype-list', view=APIDocumentTypeListView.as_view() ), url( - regex=r'^document_types/(?P\d+)/$', + regex=r'^document_types/(?P\d+)/$', name='documenttype-detail', view=APIDocumentTypeView.as_view() ), url( - regex=r'^document_types/(?P\d+)/documents/$', + regex=r'^document_types/(?P\d+)/documents/$', name='documenttype-document-list', view=APIDocumentTypeDocumentListView.as_view() ), @@ -335,30 +351,30 @@ api_urls = [ view=APIDocumentListView.as_view() ), url( - regex=r'^documents/(?P\d+)/$', name='document-detail', + regex=r'^documents/(?P\d+)/$', name='document-detail', view=APIDocumentView.as_view() ), url( - regex=r'^documents/(?P\d+)/download/$', + regex=r'^documents/(?P\d+)/download/$', name='document-download', view=APIDocumentDownloadView.as_view() ), url( - regex=r'^documents/(?P\d+)/versions/$', + regex=r'^documents/(?P\d+)/versions/$', name='document-version-list', view=APIDocumentVersionsListView.as_view() ), url( - regex=r'^documents/(?P\d+)/versions/(?P\d+)/$', + regex=r'^documents/(?P\d+)/versions/(?P\d+)/$', name='documentversion-detail', view=APIDocumentVersionView.as_view() ), url( - regex=r'^documents/(?P\d+)/versions/(?P\d+)/pages/$', + regex=r'^documents/(?P\d+)/versions/(?P\d+)/pages/$', name='documentversion-page-list', view=APIDocumentVersionPageListView.as_view() ), url( - regex=r'^documents/(?P\d+)/versions/(?P\d+)/download/$', + regex=r'^documents/(?P\d+)/versions/(?P\d+)/download/$', name='documentversion-download', view=APIDocumentVersionDownloadView.as_view() ), @@ -367,24 +383,45 @@ api_urls = [ view=APIRecentDocumentListView.as_view() ), url( - regex=r'^documents/(?P\d+)/versions/(?P\d+)/pages/(?P\d+)$', + regex=r'^documents/(?P\d+)/versions/(?P\d+)/pages/(?P\d+)$', name='documentpage-detail', view=APIDocumentPageView.as_view() ), url( - regex=r'^documents/(?P\d+)/versions/(?P\d+)/pages/(?P\d+)/image/$', + regex=r'^documents/(?P\d+)/versions/(?P\d+)/pages/(?P\d+)/image/$', name='documentpage-image', view=APIDocumentPageImageView.as_view() ), url( regex=r'^trashed_documents/$', name='trasheddocument-list', - view=APIDeletedDocumentListView.as_view() + view=APITrashedDocumentListView.as_view() ), url( - regex=r'^trashed_documents/(?P\d+)/$', - name='trasheddocument-detail', view=APIDeletedDocumentView.as_view() + regex=r'^trashed_documents/(?P\d+)/$', + name='trasheddocument-detail', view=APITrashedDocumentView.as_view() ), url( - regex=r'^trashed_documents/(?P\d+)/restore/$', + regex=r'^trashed_documents/(?P\d+)/restore/$', name='trasheddocument-restore', - view=APIDeletedDocumentRestoreView.as_view() + view=APITrashedDocumentRestoreView.as_view() ), ] +""" + + +api_router_entries = ( + { + 'prefix': r'documents', 'viewset': DocumentViewSet, + 'basename': 'document' + }, + { + 'prefix': r'document_types', 'viewset': DocumentTypeViewSet, + 'basename': 'document_type' + }, + { + 'prefix': r'documents/(?P\d+)/versions', + 'viewset': DocumentVersionViewSet, 'basename': 'document_version' + }, + { + 'prefix': r'documents/(?P\d+)/versions/(?P\d+)/pages', + 'viewset': DocumentPageViewSet, 'basename': 'document_page' + }, +) diff --git a/mayan/apps/documents/views/__init__.py b/mayan/apps/documents/views/__init__.py index 602a5c9dd7..5f07051fc7 100644 --- a/mayan/apps/documents/views/__init__.py +++ b/mayan/apps/documents/views/__init__.py @@ -3,3 +3,4 @@ from .document_type_views import * # NOQA from .document_version_views import * # NOQA from .document_views import * # NOQA from .misc_views import * # NOQA +from .trashed_document_views import * # NOQA diff --git a/mayan/apps/documents/views/document_page_views.py b/mayan/apps/documents/views/document_page_views.py index b4405c54ed..928bb852df 100644 --- a/mayan/apps/documents/views/document_page_views.py +++ b/mayan/apps/documents/views/document_page_views.py @@ -6,15 +6,14 @@ from furl import furl from django.conf import settings from django.contrib import messages -from django.shortcuts import get_object_or_404, resolve_url +from django.shortcuts import resolve_url from django.urls import reverse from django.utils.encoding import force_text -from django.utils.http import urlencode from django.utils.translation import ugettext_lazy as _ from django.views.generic import RedirectView -from mayan.apps.acls.models import AccessControlList from mayan.apps.common.generics import SimpleView, SingleObjectListView +from mayan.apps.common.mixins import ExternalObjectMixin from mayan.apps.common.utils import resolve from mayan.apps.converter.literals import DEFAULT_ROTATION, DEFAULT_ZOOM_LEVEL @@ -37,53 +36,40 @@ __all__ = ( logger = logging.getLogger(__name__) -class DocumentPageInteractiveTransformation(RedirectView): - def dispatch(self, request, *args, **kwargs): - obj = self.get_object() - - AccessControlList.objects.check_access( - obj=obj, permissions=permission_document_view, user=request.user, - ) - - return super(DocumentPageInteractiveTransformation, self).dispatch( - request, *args, **kwargs - ) +class DocumentPageInteractiveTransformation(ExternalObjectMixin, RedirectView): + external_object_class = DocumentPage + external_object_permission = permission_document_view + external_object_pk_url_kwarg = 'document_page_id' def get_object(self): - return get_object_or_404( - klass=DocumentPage, pk=self.kwargs['document_page_pk'] - ) + return self.get_external_object() def get_redirect_url(self, *args, **kwargs): - url = reverse( - viewname='documents:document_page_view', - kwargs={'document_page_pk': self.kwargs['document_page_pk']} - ) - query_dict = { - 'rotation': int( - self.request.GET.get('rotation', DEFAULT_ROTATION) - ), 'zoom': int(self.request.GET.get('zoom', DEFAULT_ZOOM_LEVEL)) + 'rotation': self.request.GET.get('rotation', DEFAULT_ROTATION), + 'zoom': self.request.GET.get('zoom', DEFAULT_ZOOM_LEVEL) } - self.transformation_function(query_dict) + url = furl( + args=query_dict, path=reverse( + viewname='documents:document_page_view', + kwargs={'document_page_id': self.kwargs['document_page_id']} + ) - return '{}?{}'.format(url, urlencode(query_dict)) - - -class DocumentPageListView(SingleObjectListView): - def dispatch(self, request, *args, **kwargs): - AccessControlList.objects.check_access( - obj=self.get_document(), permissions=permission_document_view, - user=self.request.user, ) - return super( - DocumentPageListView, self - ).dispatch(request, *args, **kwargs) + self.transformation_function(query_dict=query_dict) + + return url.tostr() + + +class DocumentPageListView(ExternalObjectMixin, SingleObjectListView): + external_object_class = Document + external_object_permission = permission_document_view + external_object_pk_url_kwarg = 'document_id' def get_document(self): - return get_object_or_404(klass=Document, pk=self.kwargs['document_pk']) + return self.get_external_object() def get_extra_context(self): return { @@ -95,27 +81,17 @@ class DocumentPageListView(SingleObjectListView): 'title': _('Pages for document: %s') % self.get_document(), } - def get_object_list(self): + def get_source_queryset(self): return self.get_document().pages.all() -class DocumentPageNavigationBase(RedirectView): - def dispatch(self, request, *args, **kwargs): - document_page = self.get_object() - - AccessControlList.objects.check_access( - permissions=permission_document_view, user=request.user, - obj=document_page.document - ) - - return super(DocumentPageNavigationBase, self).dispatch( - request, *args, **kwargs - ) +class DocumentPageNavigationBase(ExternalObjectMixin, RedirectView): + external_object_class = DocumentPage + external_object_permission = permission_document_view + external_object_pk_url_kwarg = 'document_page_id' def get_object(self): - return get_object_or_404( - klass=DocumentPage, pk=self.kwargs['document_page_pk'] - ) + return self.get_external_object() def get_redirect_url(self, *args, **kwargs): """ @@ -158,14 +134,14 @@ class DocumentPageNavigationFirst(DocumentPageNavigationBase): def get_new_kwargs(self): document_page = self.get_object() - return {'document_page_pk': document_page.siblings.first().pk} + return {'document_page_id': document_page.siblings.first().pk} class DocumentPageNavigationLast(DocumentPageNavigationBase): def get_new_kwargs(self): document_page = self.get_object() - return {'document_page_pk': document_page.siblings.last().pk} + return {'document_page_id': document_page.siblings.last().pk} class DocumentPageNavigationNext(DocumentPageNavigationBase): @@ -178,12 +154,12 @@ class DocumentPageNavigationNext(DocumentPageNavigationBase): ) except DocumentPage.DoesNotExist: messages.warning( - request=self.request, message=_( + message=_( 'There are no more pages in this document' - ) + ), request=self.request ) finally: - return {'document_page_pk': document_page.pk} + return {'document_page_id': document_page.pk} class DocumentPageNavigationPrevious(DocumentPageNavigationBase): @@ -196,47 +172,40 @@ class DocumentPageNavigationPrevious(DocumentPageNavigationBase): ) except DocumentPage.DoesNotExist: messages.warning( - request=self.request, message=_( + message=_( 'You are already at the first page of this document' - ) + ), request=self.request ) finally: - return {'document_page_pk': document_page.pk} + return {'document_page_id': document_page.pk} class DocumentPageRotateLeftView(DocumentPageInteractiveTransformation): def transformation_function(self, query_dict): query_dict['rotation'] = ( - query_dict['rotation'] - setting_rotation_step.value + int(query_dict['rotation']) - setting_rotation_step.value ) % 360 class DocumentPageRotateRightView(DocumentPageInteractiveTransformation): def transformation_function(self, query_dict): query_dict['rotation'] = ( - query_dict['rotation'] + setting_rotation_step.value + int(query_dict['rotation']) + setting_rotation_step.value ) % 360 -class DocumentPageView(SimpleView): +class DocumentPageView(ExternalObjectMixin, SimpleView): + external_object_class = DocumentPage + external_object_permission = permission_document_view + external_object_pk_url_kwarg = 'document_page_id' template_name = 'appearance/generic_form.html' - def dispatch(self, request, *args, **kwargs): - AccessControlList.objects.check_access( - permissions=permission_document_view, user=request.user, - obj=self.get_object().document - ) - - return super( - DocumentPageView, self - ).dispatch(request, *args, **kwargs) - def get_extra_context(self): zoom = int(self.request.GET.get('zoom', DEFAULT_ZOOM_LEVEL)) rotation = int(self.request.GET.get('rotation', DEFAULT_ROTATION)) document_page_form = DocumentPageForm( - instance=self.get_object(), zoom=zoom, rotation=rotation + instance=self.get_object(), rotation=rotation, zoom=zoom ) base_title = _('Image of: %s') % self.get_object() @@ -252,15 +221,13 @@ class DocumentPageView(SimpleView): 'navigation_object_list': ('page',), 'page': self.get_object(), 'rotation': rotation, - 'title': ' '.join((base_title, zoom_text,)), + 'title': ' '.join((base_title, zoom_text)), 'read_only': True, 'zoom': zoom, } def get_object(self): - return get_object_or_404( - klass=DocumentPage, pk=self.kwargs['document_page_pk'] - ) + return self.get_external_object() class DocumentPageViewResetView(RedirectView): @@ -269,7 +236,7 @@ class DocumentPageViewResetView(RedirectView): class DocumentPageZoomInView(DocumentPageInteractiveTransformation): def transformation_function(self, query_dict): - zoom = query_dict['zoom'] + setting_zoom_percent_step.value + zoom = int(query_dict['zoom']) + setting_zoom_percent_step.value if zoom > setting_zoom_max_level.value: zoom = setting_zoom_max_level.value @@ -279,7 +246,7 @@ class DocumentPageZoomInView(DocumentPageInteractiveTransformation): class DocumentPageZoomOutView(DocumentPageInteractiveTransformation): def transformation_function(self, query_dict): - zoom = query_dict['zoom'] - setting_zoom_percent_step.value + zoom = int(query_dict['zoom']) - setting_zoom_percent_step.value if zoom < setting_zoom_min_level.value: zoom = setting_zoom_min_level.value diff --git a/mayan/apps/documents/views/document_type_views.py b/mayan/apps/documents/views/document_type_views.py index 8835754ef5..becfbc4d3b 100644 --- a/mayan/apps/documents/views/document_type_views.py +++ b/mayan/apps/documents/views/document_type_views.py @@ -2,16 +2,15 @@ from __future__ import absolute_import, unicode_literals import logging -from django.shortcuts import get_object_or_404 from django.template import RequestContext from django.urls import reverse, reverse_lazy from django.utils.translation import ugettext_lazy as _ -from mayan.apps.acls.models import AccessControlList from mayan.apps.common.generics import ( SingleObjectCreateView, SingleObjectDeleteView, SingleObjectEditView, SingleObjectListView ) +from mayan.apps.common.mixins import ExternalObjectMixin from ..forms import DocumentTypeFilenameForm_create from ..icons import icon_document_type_filename, icon_document_type_setup @@ -60,7 +59,7 @@ class DocumentTypeDeleteView(SingleObjectDeleteView): model = DocumentType object_permission = permission_document_type_delete post_action_redirect = reverse_lazy(viewname='documents:document_type_list') - pk_url_kwarg = 'document_type_pk' + pk_url_kwarg = 'document_type_id' def get_extra_context(self): return { @@ -70,13 +69,17 @@ class DocumentTypeDeleteView(SingleObjectDeleteView): } -class DocumentTypeDocumentListView(DocumentListView): - def get_document_type(self): - return get_object_or_404(klass=DocumentType, pk=self.kwargs['document_type_pk']) +class DocumentTypeDocumentListView(ExternalObjectMixin, DocumentListView): + external_object_class = DocumentType + external_object_permission = permission_document_type_view + external_object_pk_url_kwarg = 'document_type_id' def get_document_queryset(self): return self.get_document_type().documents.all() + def get_document_type(self): + return self.get_external_object() + def get_extra_context(self): context = super(DocumentTypeDocumentListView, self).get_extra_context() context.update( @@ -95,7 +98,7 @@ class DocumentTypeEditView(SingleObjectEditView): ) model = DocumentType object_permission = permission_document_type_edit - pk_url_kwarg = 'document_type_pk' + pk_url_kwarg = 'document_type_id' post_action_redirect = reverse_lazy( viewname='documents:document_type_list' ) @@ -136,21 +139,14 @@ class DocumentTypeListView(SingleObjectListView): } -class DocumentTypeFilenameCreateView(SingleObjectCreateView): +class DocumentTypeFilenameCreateView(ExternalObjectMixin, SingleObjectCreateView): + external_object_class = DocumentType + external_object_permission = permission_document_type_edit + external_object_pk_url_kwarg = 'document_type_id' form_class = DocumentTypeFilenameForm_create - def dispatch(self, request, *args, **kwargs): - AccessControlList.objects.check_access( - permissions=permission_document_type_edit, user=request.user, - obj=self.get_document_type() - ) - - return super(DocumentTypeFilenameCreateView, self).dispatch( - request, *args, **kwargs - ) - def get_document_type(self): - return get_object_or_404(klass=DocumentType, pk=self.kwargs['document_type_pk']) + return self.get_external_object() def get_extra_context(self): return { @@ -168,7 +164,7 @@ class DocumentTypeFilenameCreateView(SingleObjectCreateView): class DocumentTypeFilenameDeleteView(SingleObjectDeleteView): model = DocumentTypeFilename object_permission = permission_document_type_edit - pk_url_kwarg = 'filename_pk' + pk_url_kwarg = 'filename_id' def get_extra_context(self): return { @@ -187,7 +183,7 @@ class DocumentTypeFilenameDeleteView(SingleObjectDeleteView): def get_post_action_redirect(self): return reverse( viewname='documents:document_type_filename_list', - kwargs={'document_type_pk': self.get_object().document_type.pk} + kwargs={'document_type_id': self.get_object().document_type.pk} ) @@ -195,7 +191,7 @@ class DocumentTypeFilenameEditView(SingleObjectEditView): fields = ('enabled', 'filename',) model = DocumentTypeFilename object_permission = permission_document_type_edit - pk_url_kwarg = 'filename_pk' + pk_url_kwarg = 'filename_id' def get_extra_context(self): document_type_filename = self.get_object() @@ -216,16 +212,17 @@ class DocumentTypeFilenameEditView(SingleObjectEditView): def get_post_action_redirect(self): return reverse( viewname='documents:document_type_filename_list', - kwargs={'document_type_pk': self.get_object().document_type.pk} + kwargs={'document_type_id': self.get_object().document_type.pk} ) -class DocumentTypeFilenameListView(SingleObjectListView): - access_object_retrieve_method = 'get_document_type' - object_permission = permission_document_type_view +class DocumentTypeFilenameListView(ExternalObjectMixin, SingleObjectListView): + external_object_class = DocumentType + external_object_permission = permission_document_type_view + external_object_pk_url_kwarg = 'document_type_id' def get_document_type(self): - return get_object_or_404(klass=DocumentType, pk=self.kwargs['document_type_pk']) + return self.get_external_object() def get_extra_context(self): return { @@ -255,5 +252,5 @@ class DocumentTypeFilenameListView(SingleObjectListView): ) % self.get_document_type(), } - def get_object_list(self): + def get_source_queryset(self): return self.get_document_type().filenames.all() diff --git a/mayan/apps/documents/views/document_version_views.py b/mayan/apps/documents/views/document_version_views.py index 2e999435bd..b5608346a3 100644 --- a/mayan/apps/documents/views/document_version_views.py +++ b/mayan/apps/documents/views/document_version_views.py @@ -3,15 +3,14 @@ from __future__ import absolute_import, unicode_literals import logging from django.contrib import messages -from django.shortcuts import get_object_or_404 from django.utils.translation import ugettext_lazy as _ -from mayan.apps.acls.models import AccessControlList from mayan.apps.common.generics import ( ConfirmView, SingleObjectDetailView, SingleObjectListView ) +from mayan.apps.common.mixins import ExternalObjectMixin -from ..events import event_document_view +from ..events import event_document_download, event_document_view from ..forms import DocumentVersionDownloadForm, DocumentVersionPreviewForm from ..models import Document, DocumentVersion from ..permissions import ( @@ -21,79 +20,22 @@ from ..permissions import ( from .document_views import DocumentDownloadFormView, DocumentDownloadView +__all__ = ( + 'DocumentVersionDownloadFormView', 'DocumentVersionDownloadView', + 'DocumentVersionListView', 'DocumentVersionRevertView', + 'DocumentVersionView' +) logger = logging.getLogger(__name__) -class DocumentVersionListView(SingleObjectListView): - def dispatch(self, request, *args, **kwargs): - AccessControlList.objects.check_access( - obj=self.get_document(), - permissions=permission_document_version_view, user=request.user - ) - - self.get_document().add_as_recent_document_for_user(user=request.user) - - return super( - DocumentVersionListView, self - ).dispatch(request, *args, **kwargs) - - def get_document(self): - return get_object_or_404(klass=Document, pk=self.kwargs['document_pk']) - - def get_extra_context(self): - return { - 'hide_object': True, - 'list_as_items': True, - 'object': self.get_document(), - 'table_cell_container_classes': 'td-container-thumbnail', - 'title': _('Versions of document: %s') % self.get_document(), - } - - def get_object_list(self): - return self.get_document().versions.order_by('-timestamp') - - -class DocumentVersionRevertView(ConfirmView): - object_permission = permission_document_version_revert - - def get_extra_context(self): - return { - 'message': _( - 'All later version after this one will be deleted too.' - ), - 'object': self.get_object().document, - 'title': _('Revert to this version?'), - } - - def get_object(self): - return get_object_or_404( - klass=DocumentVersion, pk=self.kwargs['document_version_pk'] - ) - - def view_action(self): - try: - self.get_object().revert(_user=self.request.user) - messages.success( - request=self.request, message=_( - 'Document version reverted successfully' - ) - ) - except Exception as exception: - messages.error( - request=self.request, - message=_('Error reverting document version; %s') % exception - ) - - class DocumentVersionDownloadFormView(DocumentDownloadFormView): form_class = DocumentVersionDownloadForm model = DocumentVersion - multiple_download_view = None - pk_url_kwarg = 'document_version_pk' + pk_url_kwarg = 'document_version_id' querystring_form_fields = ( 'compressed', 'zip_filename', 'preserve_extension' ) - single_download_view = 'documents:document_version_download' + viewname = 'documents:document_version_download' def get_extra_context(self): result = super( @@ -106,30 +48,26 @@ class DocumentVersionDownloadFormView(DocumentDownloadFormView): return result - def get_document_queryset(self): - id_list = self.request.GET.get( - 'id_list', self.request.POST.get('id_list', '') - ) - - if not id_list: - id_list = self.kwargs['document_version_pk'] - - return self.model.objects.filter( - pk__in=id_list.split(',') - ) - class DocumentVersionDownloadView(DocumentDownloadView): model = DocumentVersion object_permission = permission_document_download - pk_url_kwarg = 'document_version_pk' + pk_url_kwarg = 'document_version_id' + + @staticmethod + def commit_event(item, request): + # TODO: Improve by adding a document version download event + event_document_download.commit( + actor=request.user, + target=item.document + ) @staticmethod def get_item_file(item): return item.file def get_encoding(self): - return self.get_object().encoding + return self.get_object_list().first().encoding def get_item_label(self, item): preserve_extension = self.request.GET.get( @@ -143,13 +81,69 @@ class DocumentVersionDownloadView(DocumentDownloadView): return item.get_rendered_string(preserve_extension=preserve_extension) def get_mimetype(self): - return self.get_object().mimetype + return self.get_object_list().first().mimetype + + +class DocumentVersionListView(ExternalObjectMixin, SingleObjectListView): + external_object_class = Document + external_object_permission = permission_document_version_view + external_object_pk_url_kwarg = 'document_id' + + def get_document(self): + document = self.get_external_object() + document.add_as_recent_document_for_user(user=self.request.user) + return document + + def get_extra_context(self): + return { + 'hide_object': True, + 'list_as_items': True, + 'object': self.get_document(), + 'table_cell_container_classes': 'td-container-thumbnail', + 'title': _('Versions of document: %s') % self.get_document(), + } + + def get_source_queryset(self): + return self.get_document().versions.order_by('-timestamp') + + +class DocumentVersionRevertView(ExternalObjectMixin, ConfirmView): + external_object_class = DocumentVersion + external_object_permission = permission_document_version_revert + external_object_pk_url_kwarg = 'document_version_id' + + def get_extra_context(self): + return { + 'message': _( + 'All later version after this one will be deleted too.' + ), + 'object': self.get_object().document, + 'title': _('Revert to this version?'), + } + + def get_object(self): + return self.get_external_object() + + def view_action(self): + try: + self.get_object().revert(_user=self.request.user) + messages.success( + message=_( + 'Document version reverted successfully' + ), request=self.request + ) + except Exception as exception: + messages.error( + message=_('Error reverting document version; %s') % exception, + request=self.request + ) class DocumentVersionView(SingleObjectDetailView): form_class = DocumentVersionPreviewForm model = DocumentVersion object_permission = permission_document_version_view + pk_url_kwarg = 'document_version_id' def dispatch(self, request, *args, **kwargs): result = super( diff --git a/mayan/apps/documents/views/document_views.py b/mayan/apps/documents/views/document_views.py index 8f57427117..e94b10cae3 100644 --- a/mayan/apps/documents/views/document_views.py +++ b/mayan/apps/documents/views/document_views.py @@ -2,12 +2,13 @@ from __future__ import absolute_import, unicode_literals import logging +from furl import furl + from django.contrib import messages -from django.core.exceptions import PermissionDenied from django.http import HttpResponseRedirect from django.shortcuts import get_object_or_404 -from django.urls import reverse, reverse_lazy -from django.utils.http import urlencode +from django.urls import reverse +from django.utils.encoding import force_text from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ungettext @@ -15,11 +16,11 @@ from mayan.apps.acls.models import AccessControlList from mayan.apps.common.compressed_files import ZipArchive from mayan.apps.common.exceptions import ActionError from mayan.apps.common.generics import ( - ConfirmView, FormView, MultipleObjectConfirmActionView, - MultipleObjectFormActionView, SingleObjectDetailView, - SingleObjectDownloadView, SingleObjectEditView, SingleObjectListView + FormView, MultipleObjectConfirmActionView, MultipleObjectDownloadView, + MultipleObjectFormActionView, SingleObjectDetailView, SingleObjectEditView, + SingleObjectListView ) -from mayan.apps.common.mixins import MultipleInstanceActionMixin +from mayan.apps.common.mixins import ExternalObjectMixin from mayan.apps.converter.models import Transformation from mayan.apps.converter.permissions import ( permission_transformation_delete, permission_transformation_edit @@ -32,32 +33,198 @@ from ..forms import ( DocumentTypeFilteredSelectForm ) from ..icons import ( - icon_document_list, icon_document_list_deleted, - icon_document_list_favorites, icon_document_list_recent_access, - icon_document_list_recent_added, icon_duplicated_document_list + icon_document_list, icon_document_list_favorites, + icon_document_list_recent_access, icon_document_list_recent_added, + icon_duplicated_document_list ) from ..literals import DEFAULT_ZIP_FILENAME, PAGE_RANGE_RANGE from ..models import ( - DeletedDocument, Document, DuplicatedDocument, FavoriteDocument, - RecentDocument + Document, DuplicatedDocument, FavoriteDocument, RecentDocument ) from ..permissions import ( - permission_document_delete, permission_document_download, - permission_document_print, permission_document_properties_edit, - permission_document_restore, permission_document_tools, - permission_document_trash, permission_document_view, - permission_empty_trash + permission_document_download, permission_document_print, + permission_document_properties_edit, permission_document_tools, + permission_document_view ) from ..settings import ( setting_favorite_count, setting_print_height, setting_print_width, setting_recent_added_count ) -from ..tasks import task_delete_document, task_update_page_count +from ..tasks import task_update_page_count from ..utils import parse_range +__all__ = ( + 'DocumentChangeTypeView', 'DocumentDownloadFormView', + 'DocumentDownloadView', 'DocumentDuplicatesListView', 'DocumentEditView', + 'DocumentListView', 'DocumentPreviewView', 'DocumentPrintView', + 'DocumentTransformationsClearView', 'DocumentTransformationsCloneView', + 'DocumentUpdatePageCountView', 'DocumentView', 'DuplicatedDocumentListView', + 'FavoriteAddView', 'FavoriteDocumentListView', 'FavoriteRemoveView', + 'RecentAccessDocumentListView', 'RecentAddedDocumentListView' +) logger = logging.getLogger(__name__) +class DocumentChangeTypeView(MultipleObjectFormActionView): + form_class = DocumentTypeFilteredSelectForm + model = Document + object_permission = permission_document_properties_edit + pk_url_kwarg = 'document_id' + success_message = _( + 'Document type change request performed on %(count)d document' + ) + success_message_plural = _( + 'Document type change request performed on %(count)d documents' + ) + + def get_extra_context(self): + queryset = self.get_object_list() + + result = { + 'submit_label': _('Change'), + 'title': ungettext( + singular='Change the type of the selected document', + plural='Change the type of the selected documents', + number=queryset.count() + ) + } + + if queryset.count() == 1: + result.update( + { + 'object': queryset.first(), + 'title': _( + 'Change the type of the document: %s' + ) % queryset.first() + } + ) + + return result + + def get_form_extra_kwargs(self): + result = { + 'user': self.request.user + } + + return result + + def object_action(self, form, instance): + instance.set_document_type( + document_type=form.cleaned_data['document_type'], + _user=self.request.user + ) + + messages.success( + message=_( + 'Document type for "%s" changed successfully.' + ) % instance, request=self.request + ) + + +class DocumentDownloadFormView(MultipleObjectFormActionView): + form_class = DocumentDownloadForm + model = Document + object_permission = permission_document_download + pk_url_kwarg = 'document_id' + querystring_form_fields = ('compressed', 'zip_filename') + viewname = 'documents:document_multiple_download' + + def form_valid(self, form): + # Turn a queryset into a comma separated list of primary keys + id_list = ','.join( + [ + force_text(pk) for pk in self.get_object_list().values_list('pk', flat=True) + ] + ) + + # Construct URL with querystring to pass on to the next view + url = furl( + args={ + 'id_list': id_list + }, path=reverse(viewname=self.viewname) + ) + + # Pass the form field data as URL querystring to the next view + for field in self.querystring_form_fields: + data = form.cleaned_data[field] + if data: + url.args['field'] = data + + return HttpResponseRedirect(redirect_to=url.tostr()) + + def get_extra_context(self): + context = { + 'submit_label': _('Download'), + 'title': _('Download documents'), + } + + if self.queryset.count() == 1: + context['object'] = self.queryset.first() + + return context + + def get_form_kwargs(self): + kwargs = super(DocumentDownloadFormView, self).get_form_kwargs() + self.queryset = self.get_object_list() + kwargs.update({'queryset': self.queryset}) + return kwargs + + +class DocumentDownloadView(MultipleObjectDownloadView): + model = Document + object_permission = permission_document_download + pk_url_kwarg = 'document_id' + + @staticmethod + def commit_event(item, request): + event_document_download.commit( + actor=request.user, + target=item + ) + + @staticmethod + def get_item_file(item): + return item.open() + + def get_file(self): + queryset = self.get_object_list() + zip_filename = self.request.GET.get( + 'zip_filename', DEFAULT_ZIP_FILENAME + ) + + if self.request.GET.get('compressed') == 'True' or queryset.count() > 1: + compressed_file = ZipArchive() + compressed_file.create() + for item in queryset: + with DocumentDownloadView.get_item_file(item=item) as file_object: + compressed_file.add_file( + file_object=file_object, + filename=self.get_item_label(item=item) + ) + DocumentDownloadView.commit_event( + item=item, request=self.request + ) + + compressed_file.close() + + return DocumentDownloadView.VirtualFile( + compressed_file.as_file(zip_filename), name=zip_filename + ) + else: + item = queryset.first() + DocumentDownloadView.commit_event( + item=item, request=self.request + ) + + return DocumentDownloadView.VirtualFile( + DocumentDownloadView.get_item_file(item=item), + name=self.get_item_label(item=item) + ) + + def get_item_label(self, item): + return item.label + + class DocumentListView(SingleObjectListView): object_permission = permission_document_view @@ -66,11 +233,11 @@ class DocumentListView(SingleObjectListView): return super(DocumentListView, self).get_context_data(**kwargs) except Exception as exception: messages.error( - request=self.request, message=_( + message=_( 'Error retrieving document list: %(exception)s.' ) % { 'exception': exception - } + }, request=self.request ) self.object_list = Document.objects.none() return super(DocumentListView, self).get_context_data(**kwargs) @@ -97,150 +264,19 @@ class DocumentListView(SingleObjectListView): 'title': _('All documents'), } - def get_object_list(self): + def get_source_queryset(self): return self.get_document_queryset() -class DeletedDocumentDeleteView(ConfirmView): - extra_context = { - 'title': _('Delete the selected document?') - } - pk_url_kwarg = 'document_pk' - - def object_action(self, instance): - source_document = get_object_or_404( - klass=Document.passthrough, pk=instance.pk - ) - - AccessControlList.objects.check_access( - permissions=permission_document_delete, user=self.request.user, - obj=source_document - ) - - task_delete_document.apply_async( - kwargs={'deleted_document_id': instance.pk} - ) - - def view_action(self): - instance = get_object_or_404( - klass=DeletedDocument, pk=self.kwargs['document_pk'] - ) - self.object_action(instance=instance) - messages.success( - request=self.request, message=_( - 'Document: %(document)s deleted.' - ) % { - 'document': instance - } - ) - - -class DeletedDocumentDeleteManyView(MultipleInstanceActionMixin, DeletedDocumentDeleteView): - extra_context = { - 'title': _('Delete the selected documents?') - } - model = DeletedDocument - success_message = '%(count)d document deleted.' - success_message_plural = '%(count)d documents deleted.' - - -class DeletedDocumentListView(DocumentListView): - object_permission = None - - def get_document_queryset(self): - return AccessControlList.objects.filter_by_access( - permission=permission_document_view, - queryset=DeletedDocument.trash.all(), user=self.request.user - ) - - def get_extra_context(self): - context = super(DeletedDocumentListView, self).get_extra_context() - context.update( - { - 'hide_link': True, - 'no_results_icon': icon_document_list_deleted, - 'no_results_text': _( - 'To avoid loss of data, documents are not deleted ' - 'instantly. First, they are placed in the trash can. ' - 'From here they can be then finally deleted or restored.' - ), - 'no_results_title': _( - 'There are no documents in the trash can' - ), - 'title': _('Documents in trash'), - } - ) - return context - - -class DocumentDocumentTypeEditView(MultipleObjectFormActionView): - form_class = DocumentTypeFilteredSelectForm - model = Document - object_permission = permission_document_properties_edit - pk_url_kwarg = 'document_pk' - success_message = _( - 'Document type change request performed on %(count)d document' - ) - success_message_plural = _( - 'Document type change request performed on %(count)d documents' - ) - - def get_extra_context(self): - queryset = self.get_queryset() - - result = { - 'submit_label': _('Change'), - 'title': ungettext( - 'Change the type of the selected document', - 'Change the type of the selected documents', - queryset.count() - ) - } - - if queryset.count() == 1: - result.update( - { - 'object': queryset.first(), - 'title': _( - 'Change the type of the document: %s' - ) % queryset.first() - } - ) - - return result - - def get_form_extra_kwargs(self): - result = { - 'user': self.request.user - } - - return result - - def object_action(self, form, instance): - instance.set_document_type( - form.cleaned_data['document_type'], _user=self.request.user - ) - - messages.success( - request=self.request, message=_( - 'Document type for "%s" changed successfully.' - ) % instance - ) - - -class DocumentDuplicatesListView(DocumentListView): - def dispatch(self, request, *args, **kwargs): - AccessControlList.objects.check_access( - obj=self.get_document(), permissions=permission_document_view, - user=self.request.user - ) - - return super( - DocumentDuplicatesListView, self - ).dispatch(request, *args, **kwargs) +class DocumentDuplicatesListView(ExternalObjectMixin, DocumentListView): + external_object_class = Document + external_object_permission = permission_document_view + external_object_pk_url_kwarg = 'document_id' def get_document(self): - return get_object_or_404(klass=Document, pk=self.kwargs['document_pk']) + document = self.get_external_object() + document.add_as_recent_document_for_user(user=self.request.user) + return document def get_extra_context(self): context = super(DocumentDuplicatesListView, self).get_extra_context() @@ -260,7 +296,7 @@ class DocumentDuplicatesListView(DocumentListView): ) return context - def get_object_list(self): + def get_source_queryset(self): return self.get_document().get_duplicates() @@ -268,7 +304,7 @@ class DocumentEditView(SingleObjectEditView): form_class = DocumentForm model = Document object_permission = permission_document_properties_edit - pk_url_kwarg = 'document_pk' + pk_url_kwarg = 'document_id' def dispatch(self, request, *args, **kwargs): result = super( @@ -291,7 +327,7 @@ class DocumentEditView(SingleObjectEditView): def get_post_action_redirect(self): return reverse( viewname='documents:document_properties', - kwargs={'document_pk': self.get_object().pk} + kwargs={'document_id': self.get_object().pk} ) @@ -299,7 +335,7 @@ class DocumentPreviewView(SingleObjectDetailView): form_class = DocumentPreviewForm model = Document object_permission = permission_document_view - pk_url_kwarg = 'document_pk' + pk_url_kwarg = 'document_id' def dispatch(self, request, *args, **kwargs): result = super( @@ -320,99 +356,11 @@ class DocumentPreviewView(SingleObjectDetailView): } -class DocumentRestoreView(ConfirmView): - extra_context = { - 'title': _('Restore the selected document?') - } - - def object_action(self, instance): - source_document = get_object_or_404( - klass=Document.passthrough, pk=instance.pk - ) - - AccessControlList.objects.check_access( - obj=source_document, permissions=permission_document_restore, - user=self.request.user - ) - - instance.restore() - - def view_action(self): - instance = get_object_or_404( - klass=DeletedDocument, pk=self.kwargs['document_pk'] - ) - - self.object_action(instance=instance) - - messages.success( - request=self.request, message=_( - 'Document: %(document)s restored.' - ) % { - 'document': instance - } - ) - - -class DocumentRestoreManyView(MultipleInstanceActionMixin, DocumentRestoreView): - extra_context = { - 'title': _('Restore the selected documents?') - } - model = DeletedDocument - success_message = '%(count)d document restored.' - success_message_plural = '%(count)d documents restored.' - - -class DocumentTrashView(ConfirmView): - def get_extra_context(self): - return { - 'object': self.get_object(), - 'title': _('Move "%s" to the trash?') % self.get_object() - } - - def get_object(self): - return get_object_or_404(klass=Document, pk=self.kwargs['document_pk']) - - def get_post_action_redirect(self): - return reverse(viewname='documents:document_list_recent_access') - - def object_action(self, instance): - AccessControlList.objects.check_access( - obj=instance, permissions=permission_document_trash, - user=self.request.user - ) - - instance.delete() - - def view_action(self): - instance = self.get_object() - - self.object_action(instance=instance) - - messages.success( - request=self.request, message=_( - 'Document: %(document)s moved to trash successfully.' - ) % { - 'document': instance - } - ) - - -class DocumentTrashManyView(MultipleInstanceActionMixin, DocumentTrashView): - model = Document - success_message = '%(count)d document moved to the trash.' - success_message_plural = '%(count)d documents moved to the trash.' - - def get_extra_context(self): - return { - 'title': _('Move the selected documents to the trash?') - } - - class DocumentView(SingleObjectDetailView): form_class = DocumentPropertiesForm model = Document object_permission = permission_document_view - pk_url_kwarg = 'document_pk' + pk_url_kwarg = 'document_id' def dispatch(self, request, *args, **kwargs): result = super(DocumentView, self).dispatch(request, *args, **kwargs) @@ -427,195 +375,10 @@ class DocumentView(SingleObjectDetailView): } -class EmptyTrashCanView(ConfirmView): - extra_context = { - 'title': _('Empty trash?') - } - view_permission = permission_empty_trash - action_cancel_redirect = post_action_redirect = reverse_lazy( - viewname='documents:document_list_deleted' - ) - - def view_action(self): - for deleted_document in DeletedDocument.objects.all(): - task_delete_document.apply_async( - kwargs={'deleted_document_id': deleted_document.pk} - ) - - messages.success( - request=self.request, message=_('Trash emptied successfully') - ) - - -class DocumentDownloadFormView(FormView): - form_class = DocumentDownloadForm - model = Document - multiple_download_view = 'documents:document_multiple_download' - querystring_form_fields = ('compressed', 'zip_filename') - single_download_view = 'documents:document_download' - - def form_valid(self, form): - querystring_dictionary = {} - - for field in self.querystring_form_fields: - data = form.cleaned_data[field] - if data: - querystring_dictionary[field] = data - - querystring_dictionary.update( - { - 'id_list': ','.join( - map(str, self.queryset.values_list('pk', flat=True)) - ) - } - ) - - querystring = urlencode(querystring_dictionary, doseq=True) - - if self.queryset.count() > 1: - url = reverse(self.multiple_download_view) - else: - url = reverse( - viewname=self.single_download_view, - kwargs={'document_pk': self.queryset.first().pk} - ) - - return HttpResponseRedirect('{}?{}'.format(url, querystring)) - - def get_document_queryset(self): - id_list = self.request.GET.get( - 'id_list', self.request.POST.get('id_list', '') - ) - - if not id_list: - id_list = self.kwargs['document_pk'] - - return self.model.objects.filter( - pk__in=id_list.split(',') - ).filter(is_stub=False) - - def get_extra_context(self): - subtemplates_list = [ - { - 'name': 'appearance/generic_list_items_subtemplate.html', - 'context': { - 'hide_link': True, - 'hide_links': True, - 'hide_multi_item_actions': True, - 'object_list': self.queryset - } - } - ] - - context = { - 'submit_label': _('Download'), - 'subtemplates_list': subtemplates_list, - 'title': _('Download documents'), - } - - if self.queryset.count() == 1: - context['object'] = self.queryset.first() - - return context - - def get_form_kwargs(self): - kwargs = super(DocumentDownloadFormView, self).get_form_kwargs() - self.queryset = self.get_queryset() - kwargs.update({'queryset': self.queryset}) - return kwargs - - def get_queryset(self): - return AccessControlList.objects.filter_by_access( - permission=permission_document_download, - queryset=self.get_document_queryset(), user=self.request.user - ) - - -class DocumentDownloadView(SingleObjectDownloadView): - model = Document - # Set to None to disable the .get_object call - object_permission = None - pk_url_kwarg = 'document_pk' - - @staticmethod - def commit_event(item, request): - if isinstance(item, Document): - event_document_download.commit( - actor=request.user, - target=item - ) - else: - # TODO: Improve by adding a document version download event - event_document_download.commit( - actor=request.user, - target=item.document - ) - - @staticmethod - def get_item_file(item): - return item.open() - - def get_document_queryset(self): - id_list = self.request.GET.get( - 'id_list', self.request.POST.get('id_list', '') - ) - - if not id_list: - id_list = self.kwargs[self.pk_url_kwarg] - - queryset = self.model.objects.filter(pk__in=id_list.split(',')) - - return AccessControlList.objects.filter_by_access( - permission_document_download, self.request.user, queryset - ) - - def get_file(self): - queryset = self.get_document_queryset() - zip_filename = self.request.GET.get( - 'zip_filename', DEFAULT_ZIP_FILENAME - ) - - if self.request.GET.get('compressed') == 'True' or queryset.count() > 1: - compressed_file = ZipArchive() - compressed_file.create() - for item in queryset: - with DocumentDownloadView.get_item_file(item=item) as file_object: - compressed_file.add_file( - file_object=file_object, - filename=self.get_item_label(item=item) - ) - DocumentDownloadView.commit_event( - item=item, request=self.request - ) - - compressed_file.close() - - return DocumentDownloadView.VirtualFile( - compressed_file.as_file(zip_filename), name=zip_filename - ) - else: - item = queryset.first() - if item: - DocumentDownloadView.commit_event( - item=item, request=self.request - ) - else: - raise PermissionDenied - - return DocumentDownloadView.VirtualFile( - DocumentDownloadView.get_item_file(item=item), - name=self.get_item_label(item=item) - ) - - def get_item_label(self, item): - return item.label - - class DocumentUpdatePageCountView(MultipleObjectConfirmActionView): model = Document object_permission = permission_document_tools - pk_url_kwarg = 'document_pk' - + pk_url_kwarg = 'document_id' success_message = _( '%(count)d document queued for page count recalculation' ) @@ -654,20 +417,20 @@ class DocumentUpdatePageCountView(MultipleObjectConfirmActionView): ) else: messages.error( - request=self.request, message=_( + message=_( 'Document "%(document)s" is empty. Upload at least one ' 'document version before attempting to detect the ' 'page count.' ) % { 'document': instance, - } + }, request=self.request ) class DocumentTransformationsClearView(MultipleObjectConfirmActionView): model = Document object_permission = permission_transformation_delete - pk_url_kwarg = 'document_pk' + pk_url_kwarg = 'document_id' success_message = _( 'Transformation clear request processed for %(count)d document' ) @@ -705,16 +468,19 @@ class DocumentTransformationsClearView(MultipleObjectConfirmActionView): Transformation.objects.get_for_model(page).delete() except Exception as exception: messages.error( - request=self.request, message=_( + message=_( 'Error deleting the page transformations for ' 'document: %(document)s; %(error)s.' ) % { 'document': instance, 'error': exception - } + }, request=self.request ) -class DocumentTransformationsCloneView(FormView): +class DocumentTransformationsCloneView(ExternalObjectMixin, FormView): + external_object_class = Document + external_object_permission = permission_transformation_edit + external_object_pk_url_kwarg = 'document_id' form_class = DocumentPageNumberForm def form_valid(self, form): @@ -726,34 +492,29 @@ class DocumentTransformationsCloneView(FormView): ) for page in target_pages: - Transformation.objects.get_for_model(page).delete() + Transformation.objects.get_for_model(obj=page).delete() Transformation.objects.copy( source=form.cleaned_data['page'], targets=target_pages ) except Exception as exception: messages.error( - request=self.request, message=_( + message=_( 'Error deleting the page transformations for ' 'document: %(document)s; %(error)s.' ) % { 'document': instance, 'error': exception - } + }, request=self.request ) else: messages.success( - request=self.request, message=_( + message=_( 'Transformations cloned successfully.' - ) + ), request=self.request ) return super(DocumentTransformationsCloneView, self).form_valid(form=form) - def get_form_extra_kwargs(self): - return { - 'document': self.get_object() - } - def get_extra_context(self): instance = self.get_object() @@ -767,40 +528,28 @@ class DocumentTransformationsCloneView(FormView): return context + def get_form_extra_kwargs(self): + return { + 'document': self.get_object() + } + def get_object(self): - instance = get_object_or_404( - klass=Document, pk=self.kwargs['document_pk'] - ) - - AccessControlList.objects.check_access( - obj=instance, permissions=permission_transformation_edit, - user=self.request.user - ) - - instance.add_as_recent_document_for_user(user=self.request.user) - - return instance + document = self.get_external_object() + document.add_as_recent_document_for_user(user=self.request.user) + return document -class DocumentPrint(FormView): +class DocumentPrintView(FormView): form_class = DocumentPrintForm def dispatch(self, request, *args, **kwargs): - instance = self.get_object() - AccessControlList.objects.check_access( - obj=instance, permissions=permission_document_print, - user=self.request.user - ) - - instance.add_as_recent_document_for_user(user=self.request.user) - self.page_group = self.request.GET.get('page_group') self.page_range = self.request.GET.get('page_range') - return super(DocumentPrint, self).dispatch(request, *args, **kwargs) + return super(DocumentPrintView, self).dispatch(request, *args, **kwargs) def get(self, request, *args, **kwargs): if not self.page_group and not self.page_range: - return super(DocumentPrint, self).get(request, *args, **kwargs) + return super(DocumentPrintView, self).get(request, *args, **kwargs) else: instance = self.get_object() @@ -832,7 +581,7 @@ class DocumentPrint(FormView): context = { 'form_action': reverse( viewname='documents:document_print', - kwargs={'document_pk': instance.pk} + kwargs={'document_id': instance.pk} ), 'object': instance, 'submit_label': _('Submit'), @@ -844,7 +593,17 @@ class DocumentPrint(FormView): return context def get_object(self): - return get_object_or_404(klass=Document, pk=self.kwargs['document_pk']) + obj = get_object_or_404( + klass=self.get_object_list(), pk=self.kwargs['document_id'] + ) + obj.add_as_recent_document_for_user(user=self.request.user) + return obj + + def get_object_list(self): + return AccessControlList.objects.restrict_queryset( + permission=permission_document_print, queryset=Document.objects.all(), + user=self.request.user + ) def get_template_names(self): if self.page_group or self.page_range: @@ -923,7 +682,7 @@ class FavoriteAddView(MultipleObjectConfirmActionView): def object_action(self, form, instance): FavoriteDocument.objects.add_for_user( - user=self.request.user, document=instance + document=instance, user=self.request.user ) @@ -939,7 +698,7 @@ class FavoriteRemoveView(MultipleObjectConfirmActionView): ) def get_extra_context(self): - queryset = self.get_queryset() + queryset = self.get_object_list() return { 'submit_label': _('Remove'), @@ -954,7 +713,7 @@ class FavoriteRemoveView(MultipleObjectConfirmActionView): def object_action(self, form, instance): try: FavoriteDocument.objects.remove_for_user( - user=self.request.user, document=instance + document=instance, user=self.request.user ) except FavoriteDocument.DoesNotExist: raise ActionError diff --git a/mayan/apps/documents/views/misc_views.py b/mayan/apps/documents/views/misc_views.py index d5d1d22967..3f0791b76f 100644 --- a/mayan/apps/documents/views/misc_views.py +++ b/mayan/apps/documents/views/misc_views.py @@ -24,7 +24,8 @@ class ClearImageCacheView(ConfirmView): def view_action(self): task_clear_image_cache.apply_async() messages.success( - self.request, _('Document cache clearing queued successfully.') + message=_('Document cache clearing queued successfully.'), + request=self.request ) @@ -37,5 +38,6 @@ class ScanDuplicatedDocuments(ConfirmView): def view_action(self): task_scan_duplicates_all.apply_async() messages.success( - self.request, _('Duplicated document scan queued successfully.') + message=_('Duplicated document scan queued successfully.'), + request=self.request ) diff --git a/mayan/apps/documents/views/trashed_document_views.py b/mayan/apps/documents/views/trashed_document_views.py new file mode 100644 index 0000000000..c00414a73f --- /dev/null +++ b/mayan/apps/documents/views/trashed_document_views.py @@ -0,0 +1,162 @@ +from __future__ import absolute_import, unicode_literals + +import logging + +from django.contrib import messages +from django.urls import reverse_lazy +from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import ungettext + +from mayan.apps.acls.models import AccessControlList +from mayan.apps.common.generics import ( + ConfirmView, MultipleObjectConfirmActionView +) + +from ..icons import icon_trashed_document_list +from ..models import Document, TrashedDocument +from ..permissions import ( + permission_document_trash, permission_document_view, permission_empty_trash, + permission_trashed_document_delete, permission_trashed_document_restore +) +from ..tasks import task_delete_document + +from .document_views import DocumentListView + +__all__ = ( + 'DocumentTrashView', 'EmptyTrashCanView', 'TrashedDocumentDeleteView', + 'TrashedDocumentListView', 'TrashedDocumentRestoreView', +) +logger = logging.getLogger(__name__) + + +class DocumentTrashView(MultipleObjectConfirmActionView): + model = Document + object_permission = permission_document_trash + pk_url_kwarg = 'document_id' + success_message = _( + '%(count)d document moved to the trash.' + ) + success_message_plural = _( + '%(count)d documents moved to the trash.' + ) + + def get_extra_context(self): + queryset = self.get_object_list() + + result = { + 'title': ungettext( + single='Move the selected document to the trash?', + plural='Move the selected documents to the trash?', + number=queryset.count() + ) + } + + return result + + def object_action(self, form, instance): + instance.delete() + + +class EmptyTrashCanView(ConfirmView): + extra_context = { + 'title': _('Empty trash?') + } + view_permission = permission_empty_trash + action_cancel_redirect = post_action_redirect = reverse_lazy( + viewname='documents:trashed_document_list' + ) + + def view_action(self): + for trashed_document in TrashedDocument.objects.all(): + task_delete_document.apply_async( + kwargs={'trashed_document_id': trashed_document.pk} + ) + + messages.success( + request=self.request, message=_('Trash emptied successfully') + ) + + +class TrashedDocumentDeleteView(MultipleObjectConfirmActionView): + model = TrashedDocument + object_permission = permission_trashed_document_delete + pk_url_kwarg = 'trashed_document_id' + success_message = _( + '%(count)d trashed document deleted.' + ) + success_message_plural = _( + '%(count)d trashed documents deleted.' + ) + + def get_extra_context(self): + queryset = self.get_object_list() + + result = { + 'title': ungettext( + single='Delete the selected trashed document?', + plural='Delete the selected trashed documents?', + number=queryset.count() + ) + } + + return result + + def object_action(self, form, instance): + instance.delete() + + +class TrashedDocumentListView(DocumentListView): + object_permission = None + + def get_document_queryset(self): + return AccessControlList.objects.restrict_queryset( + permission=permission_document_view, + queryset=TrashedDocument.trash.all(), user=self.request.user + ) + + def get_extra_context(self): + context = super(TrashedDocumentListView, self).get_extra_context() + context.update( + { + 'hide_link': True, + 'no_results_icon': icon_trashed_document_list, + 'no_results_text': _( + 'To avoid loss of data, documents are not deleted ' + 'instantly. First, they are placed in the trash can. ' + 'From here they can be then finally deleted or restored.' + ), + 'no_results_title': _( + 'There are no documents in the trash can' + ), + 'title': _('Documents in trash'), + } + ) + return context + + +class TrashedDocumentRestoreView(MultipleObjectConfirmActionView): + model = TrashedDocument + object_permission = permission_trashed_document_restore + pk_url_kwarg = 'trashed_document_id' + success_message = _( + '%(count)d trashed document restored.' + ) + success_message_plural = _( + '%(count)d trashed documents restored.' + ) + + def get_extra_context(self): + queryset = self.get_object_list() + + result = { + 'title': ungettext( + single='Restore the selected trashed document?', + plural='Restore the selected trashed documents?', + number=queryset.count() + ) + } + + return result + + def object_action(self, form, instance): + instance.restore() From 27546dadd9c19b274f52024c412f278a3f543b76 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Mon, 28 Jan 2019 05:35:24 -0400 Subject: [PATCH 061/209] Navigation: Update ACL interface Update the check_permission interface usage. Use the model's default_manager instead of the explicit .objects manager. Signed-off-by: Roberto Rosario --- mayan/apps/navigation/utils.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/mayan/apps/navigation/utils.py b/mayan/apps/navigation/utils.py index 9750ab9138..0f76fdb5a3 100644 --- a/mayan/apps/navigation/utils.py +++ b/mayan/apps/navigation/utils.py @@ -27,18 +27,19 @@ def get_cascade_condition(app_label, model_name, object_permission, view_permiss if view_permission: try: - Permission.check_permissions( - requester=context.request.user, - permissions=(view_permission,) + Permission.check_user_permission( + permission=view_permission, + user=context.request.user ) except PermissionDenied: pass else: return True - queryset = AccessControlList.objects.filter_by_access( - permission=object_permission, user=context.request.user, - queryset=Model.objects.all() + queryset = AccessControlList.objects.restrict_queryset( + permission=object_permission, + queryset=Model._meta.default_manager.all(), + user=context.request.user ) return queryset.count() > 0 From eae5359cdfec6a94827301bc1c1bd2485f515e41 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Mon, 28 Jan 2019 05:36:37 -0400 Subject: [PATCH 062/209] Remove the old check_permissions implementation Signed-off-by: Roberto Rosario --- mayan/apps/permissions/classes.py | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/mayan/apps/permissions/classes.py b/mayan/apps/permissions/classes.py index 4481ed38b0..eb75ca8202 100644 --- a/mayan/apps/permissions/classes.py +++ b/mayan/apps/permissions/classes.py @@ -75,28 +75,6 @@ class Permission(object): cls._permissions.values(), key=lambda x: x.namespace.name ) - # Deprecated method - @classmethod - def check_permissions(cls, permissions, requester): - warnings.warn( - 'The method .check_permissions() is deprecated. Use ' - '.check_user_permission() instead.', InterfaceWarning - ) - - try: - for permission in permissions: - if permission.stored_permission.user_has_this(user=requester): - return True - except TypeError: - # Not a list of permissions, just one - if permissions.stored_permission.user_has_this(user=requester): - return True - - logger.debug( - 'User "%s" does not have permissions "%s"', requester, permissions - ) - raise PermissionDenied(_('Insufficient permissions.')) - @classmethod def check_user_permission(cls, permission, user): if permission.stored_permission.user_has_this(user=user): From c09b58894bf7931f0480d2ca6abcef1dfff12298 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Mon, 28 Jan 2019 05:39:07 -0400 Subject: [PATCH 063/209] Update views to import from common.generics Signed-off-by: Roberto Rosario --- mayan/apps/acls/views.py | 2 +- mayan/apps/cabinets/views.py | 2 +- mayan/apps/converter/views.py | 2 +- mayan/apps/document_indexing/views.py | 2 +- mayan/apps/document_states/views/workflow_instance_views.py | 2 +- mayan/apps/document_states/views/workflow_proxy_views.py | 2 +- mayan/apps/document_states/views/workflow_views.py | 2 +- mayan/apps/file_metadata/views.py | 2 +- mayan/apps/motd/views.py | 2 +- mayan/apps/permissions/views.py | 2 +- mayan/apps/smart_settings/views.py | 2 +- mayan/apps/sources/views.py | 6 +++--- 12 files changed, 14 insertions(+), 14 deletions(-) diff --git a/mayan/apps/acls/views.py b/mayan/apps/acls/views.py index 2028cc93a0..7e0b7b37b1 100644 --- a/mayan/apps/acls/views.py +++ b/mayan/apps/acls/views.py @@ -12,7 +12,7 @@ from django.utils.translation import ugettext_lazy as _ from mayan.apps.common.mixins import ( ContentTypeViewMixin, ExternalObjectMixin ) -from mayan.apps.common.views import ( +from mayan.apps.common.generics import ( AssignRemoveView, SingleObjectCreateView, SingleObjectDeleteView, SingleObjectListView ) diff --git a/mayan/apps/cabinets/views.py b/mayan/apps/cabinets/views.py index 1b4e1a559a..c244dd70ff 100644 --- a/mayan/apps/cabinets/views.py +++ b/mayan/apps/cabinets/views.py @@ -10,7 +10,7 @@ from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ungettext from mayan.apps.acls.models import AccessControlList -from mayan.apps.common.views import ( +from mayan.apps.common.generics import ( MultipleObjectFormActionView, SingleObjectCreateView, SingleObjectDeleteView, SingleObjectEditView, SingleObjectListView ) diff --git a/mayan/apps/converter/views.py b/mayan/apps/converter/views.py index b55789d0b1..8aa4b5089c 100644 --- a/mayan/apps/converter/views.py +++ b/mayan/apps/converter/views.py @@ -10,7 +10,7 @@ from django.urls import reverse from django.utils.translation import ugettext_lazy as _ from mayan.apps.acls.models import AccessControlList -from mayan.apps.common.views import ( +from mayan.apps.common.generics import ( SingleObjectCreateView, SingleObjectDeleteView, SingleObjectEditView, SingleObjectListView ) diff --git a/mayan/apps/document_indexing/views.py b/mayan/apps/document_indexing/views.py index 8b8ee0bb15..5b974d6cd1 100644 --- a/mayan/apps/document_indexing/views.py +++ b/mayan/apps/document_indexing/views.py @@ -9,7 +9,7 @@ from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ungettext from mayan.apps.acls.models import AccessControlList -from mayan.apps.common.views import ( +from mayan.apps.common.generics import ( AssignRemoveView, FormView, SingleObjectCreateView, SingleObjectDeleteView, SingleObjectEditView, SingleObjectListView ) diff --git a/mayan/apps/document_states/views/workflow_instance_views.py b/mayan/apps/document_states/views/workflow_instance_views.py index 8633531fcc..cc6a1be8cb 100644 --- a/mayan/apps/document_states/views/workflow_instance_views.py +++ b/mayan/apps/document_states/views/workflow_instance_views.py @@ -7,7 +7,7 @@ from django.utils.translation import ugettext_lazy as _ from mayan.apps.acls.models import AccessControlList from mayan.apps.common.mixins import ExternalObjectMixin -from mayan.apps.common.views import FormView, SingleObjectListView +from mayan.apps.common.generics import FormView, SingleObjectListView from mayan.apps.documents.models import Document from ..forms import WorkflowInstanceTransitionForm diff --git a/mayan/apps/document_states/views/workflow_proxy_views.py b/mayan/apps/document_states/views/workflow_proxy_views.py index aa9c90ede3..68cc93fcdc 100644 --- a/mayan/apps/document_states/views/workflow_proxy_views.py +++ b/mayan/apps/document_states/views/workflow_proxy_views.py @@ -5,7 +5,7 @@ from django.template import RequestContext from django.utils.translation import ugettext_lazy as _ from mayan.apps.common.mixins import ExternalObjectMixin -from mayan.apps.common.views import SingleObjectListView +from mayan.apps.common.generics import SingleObjectListView from mayan.apps.documents.models import Document from mayan.apps.documents.views import DocumentListView diff --git a/mayan/apps/document_states/views/workflow_views.py b/mayan/apps/document_states/views/workflow_views.py index d33af2624b..b124da1fce 100644 --- a/mayan/apps/document_states/views/workflow_views.py +++ b/mayan/apps/document_states/views/workflow_views.py @@ -9,7 +9,7 @@ from django.urls import reverse, reverse_lazy from django.utils.translation import ugettext_lazy as _ from mayan.apps.common.mixins import ExternalObjectMixin -from mayan.apps.common.views import ( +from mayan.apps.common.generics import ( AssignRemoveView, ConfirmView, FormView, SingleObjectCreateView, SingleObjectDeleteView, SingleObjectDetailView, SingleObjectDynamicFormCreateView, SingleObjectDynamicFormEditView, diff --git a/mayan/apps/file_metadata/views.py b/mayan/apps/file_metadata/views.py index 1efe7adaa4..7d87259270 100644 --- a/mayan/apps/file_metadata/views.py +++ b/mayan/apps/file_metadata/views.py @@ -8,7 +8,7 @@ from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ungettext from mayan.apps.acls.models import AccessControlList -from mayan.apps.common.views import ( +from mayan.apps.common.generics import ( FormView, MultipleObjectConfirmActionView, SingleObjectEditView, SingleObjectListView ) diff --git a/mayan/apps/motd/views.py b/mayan/apps/motd/views.py index 0abc8b2266..e7dd6202d7 100644 --- a/mayan/apps/motd/views.py +++ b/mayan/apps/motd/views.py @@ -6,7 +6,7 @@ from django.template import RequestContext from django.urls import reverse_lazy from django.utils.translation import ugettext_lazy as _ -from mayan.apps.common.views import ( +from mayan.apps.common.generics import ( SingleObjectCreateView, SingleObjectDeleteView, SingleObjectEditView, SingleObjectListView ) diff --git a/mayan/apps/permissions/views.py b/mayan/apps/permissions/views.py index db89b01d23..06c63c04dc 100644 --- a/mayan/apps/permissions/views.py +++ b/mayan/apps/permissions/views.py @@ -10,7 +10,7 @@ from django.utils.encoding import force_text from django.utils.translation import ugettext_lazy as _ from mayan.apps.acls.models import AccessControlList -from mayan.apps.common.views import ( +from mayan.apps.common.generics import ( AssignRemoveView, SingleObjectCreateView, SingleObjectDeleteView, SingleObjectEditView, SingleObjectListView ) diff --git a/mayan/apps/smart_settings/views.py b/mayan/apps/smart_settings/views.py index d723b9db92..92b881c594 100644 --- a/mayan/apps/smart_settings/views.py +++ b/mayan/apps/smart_settings/views.py @@ -5,7 +5,7 @@ from django.http import Http404 from django.urls import reverse from django.utils.translation import ugettext_lazy as _ -from mayan.apps.common.views import FormView, SingleObjectListView +from mayan.apps.common.generics import FormView, SingleObjectListView from .classes import Namespace, Setting from .forms import SettingForm diff --git a/mayan/apps/sources/views.py b/mayan/apps/sources/views.py index 96778a0dbe..14a02249fe 100644 --- a/mayan/apps/sources/views.py +++ b/mayan/apps/sources/views.py @@ -17,7 +17,7 @@ from mayan.apps.checkouts.models import NewVersionBlock from mayan.apps.common import menu_facet from mayan.apps.common.mixins import ExternalObjectMixin, ListModeMixin from mayan.apps.common.models import SharedUploadedFile -from mayan.apps.common.views import ( +from mayan.apps.common.generics import ( ConfirmView, MultiFormView, SingleObjectCreateView, SingleObjectDeleteView, SingleObjectEditView, SingleObjectListView ) @@ -361,7 +361,7 @@ class UploadInteractiveView(UploadBaseView): ) AccessControlList.objects.check_access( - permissions=permission_document_create, user=request.user, + permission=permission_document_create, user=request.user, obj=self.document_type ) @@ -541,7 +541,7 @@ class UploadInteractiveVersionView(UploadBaseView): ) AccessControlList.objects.check_access( - permissions=permission_document_new_version, + permission=permission_document_new_version, user=self.request.user, obj=self.document ) From fbb3a64bce1678174e2041be1c1e3fce82acbb9b Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Mon, 28 Jan 2019 05:40:22 -0400 Subject: [PATCH 064/209] Update check_access interface Signed-off-by: Roberto Rosario --- mayan/apps/events/classes.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mayan/apps/events/classes.py b/mayan/apps/events/classes.py index 065dc325ef..992c9ed9db 100644 --- a/mayan/apps/events/classes.py +++ b/mayan/apps/events/classes.py @@ -113,7 +113,7 @@ class EventType(object): if result.target: try: AccessControlList.objects.check_access( - permissions=permission_events_view, + permission=permission_events_view, user=user, obj=result.target ) except PermissionDenied: @@ -139,7 +139,7 @@ class EventType(object): if relationship.exists(): try: AccessControlList.objects.check_access( - permissions=permission_events_view, + permission=permission_events_view, user=user, obj=result.target ) except PermissionDenied: @@ -161,7 +161,7 @@ class EventType(object): if relationship.exists(): try: AccessControlList.objects.check_access( - permissions=permission_events_view, + permission=permission_events_view, user=user, obj=result.action_object ) except PermissionDenied: From ef5e0c2d864736ad58fb52553398c168c440b930 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Tue, 29 Jan 2019 04:12:44 -0400 Subject: [PATCH 065/209] Remove last usage of .filter_by_access() Signed-off-by: Roberto Rosario --- docs/mercs/merging-roles-and-groups.rst | 2 +- mayan/apps/acls/managers.py | 9 --------- mayan/apps/acls/tests/test_models.py | 8 ++++---- mayan/apps/cabinets/api_views.py | 2 +- mayan/apps/cabinets/models.py | 2 +- mayan/apps/cabinets/views.py | 2 +- mayan/apps/cabinets/widgets.py | 5 +++-- mayan/apps/checkouts/api_views.py | 8 ++++---- mayan/apps/checkouts/dashboard_widgets.py | 4 ++-- mayan/apps/checkouts/views.py | 2 +- mayan/apps/common/forms.py | 2 +- mayan/apps/document_indexing/forms.py | 2 +- mayan/apps/document_indexing/models.py | 4 ++-- mayan/apps/document_indexing/views.py | 2 +- mayan/apps/document_states/models.py | 2 +- mayan/apps/mailer/forms.py | 2 +- mayan/apps/mailer/views.py | 2 +- mayan/apps/metadata/views.py | 2 +- 18 files changed, 27 insertions(+), 35 deletions(-) diff --git a/docs/mercs/merging-roles-and-groups.rst b/docs/mercs/merging-roles-and-groups.rst index 34bb4aea72..edc5e2e9e5 100644 --- a/docs/mercs/merging-roles-and-groups.rst +++ b/docs/mercs/merging-roles-and-groups.rst @@ -63,5 +63,5 @@ Changes needed: the Role model's permissions many to many field. 4. Update the ``AccessControlList`` models roles field to point to the group models. -5. Update the role checks in the ``check_access`` and ``filter_by_access`` +5. Update the role checks in the ``check_access`` and ``restrict_queryset`` ``AccessControlList`` model manager methods. diff --git a/mayan/apps/acls/managers.py b/mayan/apps/acls/managers.py index 54e6b2ace9..9a3f378450 100644 --- a/mayan/apps/acls/managers.py +++ b/mayan/apps/acls/managers.py @@ -201,15 +201,6 @@ class AccessControlListManager(models.Manager): return acl - def filter_by_access(self, permission, queryset, user): - warnings.warn( - 'filter_by_access() is deprecated, use restrict_queryset().', - InterfaceWarning - ) - return self.restrict_queryset( - permission=permission, queryset=queryset, user=user - ) - def restrict_queryset(self, permission, queryset, user): # Check directly granted permission via a role try: diff --git a/mayan/apps/acls/tests/test_models.py b/mayan/apps/acls/tests/test_models.py index ec0c600925..e7657094a1 100644 --- a/mayan/apps/acls/tests/test_models.py +++ b/mayan/apps/acls/tests/test_models.py @@ -44,7 +44,7 @@ class PermissionTestCase(DocumentTestMixin, BaseTestCase): def test_filtering_without_permissions(self): self.assertEqual( - AccessControlList.objects.filter_by_access( + AccessControlList.objects.restrict_queryset( permission=permission_document_view, queryset=Document.objects.all(), user=self._test_case_user, ).count(), 0 @@ -71,7 +71,7 @@ class PermissionTestCase(DocumentTestMixin, BaseTestCase): acl.permissions.add(permission_document_view.stored_permission) self.assertQuerysetEqual( - AccessControlList.objects.filter_by_access( + AccessControlList.objects.restrict_queryset( permission=permission_document_view, queryset=Document.objects.all(), user=self._test_case_user ), (repr(self.test_document_1),) @@ -116,7 +116,7 @@ class PermissionTestCase(DocumentTestMixin, BaseTestCase): ) acl.permissions.add(permission_document_view.stored_permission) - result = AccessControlList.objects.filter_by_access( + result = AccessControlList.objects.restrict_queryset( permission=permission_document_view, queryset=Document.objects.all(), user=self._test_case_user ) @@ -143,7 +143,7 @@ class PermissionTestCase(DocumentTestMixin, BaseTestCase): ) acl.permissions.add(permission_document_view.stored_permission) - result = AccessControlList.objects.filter_by_access( + result = AccessControlList.objects.restrict_queryset( permission=permission_document_view, queryset=Document.objects.all(), user=self._test_case_user, ) diff --git a/mayan/apps/cabinets/api_views.py b/mayan/apps/cabinets/api_views.py index 8811607090..78fd0ff34b 100644 --- a/mayan/apps/cabinets/api_views.py +++ b/mayan/apps/cabinets/api_views.py @@ -140,7 +140,7 @@ class APICabinetDocumentListView(generics.ListCreateAPIView): def get_queryset(self): cabinet = self.get_cabinet() - return AccessControlList.objects.filter_by_access( + return AccessControlList.objects.restrict_queryset( permission_document_view, self.request.user, queryset=cabinet.documents.all() ) diff --git a/mayan/apps/cabinets/models.py b/mayan/apps/cabinets/models.py index 7129e63b8a..0ca6a02f93 100644 --- a/mayan/apps/cabinets/models.py +++ b/mayan/apps/cabinets/models.py @@ -73,7 +73,7 @@ class Cabinet(MPTTModel): Provide a queryset of the documents in a cabinet. The queryset is filtered by access. """ - return AccessControlList.objects.filter_by_access( + return AccessControlList.objects.restrict_queryset( permission=permission_document_view, queryset=self.documents, user=user ) diff --git a/mayan/apps/cabinets/views.py b/mayan/apps/cabinets/views.py index c244dd70ff..c61febaf0f 100644 --- a/mayan/apps/cabinets/views.py +++ b/mayan/apps/cabinets/views.py @@ -96,7 +96,7 @@ class CabinetDetailView(DocumentListView): template_name = 'cabinets/cabinet_details.html' def get_document_queryset(self): - queryset = AccessControlList.objects.filter_by_access( + queryset = AccessControlList.objects.restrict_queryset( permission=permission_document_view, queryset=self.get_object().documents.all(), user=self.request.user diff --git a/mayan/apps/cabinets/widgets.py b/mayan/apps/cabinets/widgets.py index 3e3f9f862a..aedf9b40df 100644 --- a/mayan/apps/cabinets/widgets.py +++ b/mayan/apps/cabinets/widgets.py @@ -42,8 +42,9 @@ def widget_document_cabinets(document, user): app_label='acls', model_name='AccessControlList' ) - cabinets = AccessControlList.objects.filter_by_access( - permission_cabinet_view, user, queryset=document.get_cabinets().all() + cabinets = AccessControlList.objects.restrict_queryset( + queryset=document.get_cabinets().all(), + permission=permission_cabinet_view, user=user ) return format_html_join( diff --git a/mayan/apps/checkouts/api_views.py b/mayan/apps/checkouts/api_views.py index d48dd2c669..ef8fc61d1b 100644 --- a/mayan/apps/checkouts/api_views.py +++ b/mayan/apps/checkouts/api_views.py @@ -33,11 +33,11 @@ class APICheckedoutDocumentListView(generics.ListCreateAPIView): return DocumentCheckoutSerializer def get_queryset(self): - filtered_documents = AccessControlList.objects.filter_by_access( + filtered_documents = AccessControlList.objects.restrict_queryset( permission=permission_document_view, user=self.request.user, queryset=DocumentCheckout.objects.checked_out_documents() ) - filtered_documents = AccessControlList.objects.filter_by_access( + filtered_documents = AccessControlList.objects.restrict_queryset( permission=permission_document_checkout_detail_view, user=self.request.user, queryset=filtered_documents ) @@ -56,11 +56,11 @@ class APICheckedoutDocumentView(generics.RetrieveDestroyAPIView): def get_queryset(self): if self.request.method == 'GET': - filtered_documents = AccessControlList.objects.filter_by_access( + filtered_documents = AccessControlList.objects.restrict_queryset( permission=permission_document_view, user=self.request.user, queryset=DocumentCheckout.objects.checked_out_documents() ) - filtered_documents = AccessControlList.objects.filter_by_access( + filtered_documents = AccessControlList.objects.restrict_queryset( permission=permission_document_checkout_detail_view, user=self.request.user, queryset=filtered_documents ) diff --git a/mayan/apps/checkouts/dashboard_widgets.py b/mayan/apps/checkouts/dashboard_widgets.py index 27a85d752d..334e133054 100644 --- a/mayan/apps/checkouts/dashboard_widgets.py +++ b/mayan/apps/checkouts/dashboard_widgets.py @@ -23,12 +23,12 @@ class DashboardWidgetTotalCheckouts(DashboardWidgetNumeric): DocumentCheckout = apps.get_model( app_label='checkouts', model_name='DocumentCheckout' ) - queryset = AccessControlList.objects.filter_by_access( + queryset = AccessControlList.objects.restrict_queryset( permission=permission_document_checkout_detail_view, queryset=DocumentCheckout.objects.checked_out_documents(), user=request.user, ) - queryset = AccessControlList.objects.filter_by_access( + queryset = AccessControlList.objects.restrict_queryset( permission=permission_document_view, queryset=queryset, user=request.user ) diff --git a/mayan/apps/checkouts/views.py b/mayan/apps/checkouts/views.py index a9e00d5e6d..a3f2ffc475 100644 --- a/mayan/apps/checkouts/views.py +++ b/mayan/apps/checkouts/views.py @@ -82,7 +82,7 @@ class CheckoutDocumentView(SingleObjectCreateView): class CheckoutListView(DocumentListView): def get_document_queryset(self): - return AccessControlList.objects.filter_by_access( + return AccessControlList.objects.restrict_queryset( permission=permission_document_checkout_detail_view, queryset=DocumentCheckout.objects.checked_out_documents(), user=self.request.user diff --git a/mayan/apps/common/forms.py b/mayan/apps/common/forms.py index 51a9ba685b..4d1c1828a6 100644 --- a/mayan/apps/common/forms.py +++ b/mayan/apps/common/forms.py @@ -248,7 +248,7 @@ class FilteredSelectionForm(forms.Form): widget_class = opts.widget_class if opts.permission: - queryset = AccessControlList.objects.filter_by_access( + queryset = AccessControlList.objects.restrict_queryset( permission=opts.permission, queryset=queryset, user=opts.user ) diff --git a/mayan/apps/document_indexing/forms.py b/mayan/apps/document_indexing/forms.py index acf6953c69..c9e2455d7f 100644 --- a/mayan/apps/document_indexing/forms.py +++ b/mayan/apps/document_indexing/forms.py @@ -22,7 +22,7 @@ class IndexListForm(forms.Form): def __init__(self, *args, **kwargs): user = kwargs.pop('user') super(IndexListForm, self).__init__(*args, **kwargs) - queryset = AccessControlList.objects.filter_by_access( + queryset = AccessControlList.objects.restrict_queryset( permission=permission_document_indexing_rebuild, queryset=Index.objects.filter(enabled=True), user=user diff --git a/mayan/apps/document_indexing/models.py b/mayan/apps/document_indexing/models.py index ba85ce8107..ee339d71a1 100644 --- a/mayan/apps/document_indexing/models.py +++ b/mayan/apps/document_indexing/models.py @@ -366,7 +366,7 @@ class IndexInstanceNode(MPTTModel): return self.get_descendants().count() def get_descendants_document_count(self, user): - return AccessControlList.objects.filter_by_access( + return AccessControlList.objects.restrict_queryset( permission=permission_document_view, queryset=Document.objects.filter( index_instance_nodes__in=self.get_descendants( @@ -387,7 +387,7 @@ class IndexInstanceNode(MPTTModel): def get_item_count(self, user): if self.index_template_node.link_documents: - queryset = AccessControlList.objects.filter_by_access( + queryset = AccessControlList.objects.restrict_queryset( permission=permission_document_view, queryset=self.documents, user=user ) diff --git a/mayan/apps/document_indexing/views.py b/mayan/apps/document_indexing/views.py index 5b974d6cd1..3729798e6d 100644 --- a/mayan/apps/document_indexing/views.py +++ b/mayan/apps/document_indexing/views.py @@ -102,7 +102,7 @@ class SetupIndexDocumentTypesView(AssignRemoveView): self.get_object().document_types.add(item) def get_document_queryset(self): - return AccessControlList.objects.filter_by_access( + return AccessControlList.objects.restrict_queryset( permission_document_view, queryset=DocumentType.objects.all(), user=self.request.user ) diff --git a/mayan/apps/document_states/models.py b/mayan/apps/document_states/models.py index c131c2a02b..039f2591bf 100644 --- a/mayan/apps/document_states/models.py +++ b/mayan/apps/document_states/models.py @@ -493,7 +493,7 @@ class WorkflowInstance(models.Model): If not ACL access to the workflow, filter transition options by each transition ACL access """ - queryset = AccessControlList.objects.filter_by_access( + queryset = AccessControlList.objects.restrict_queryset( permission=permission_workflow_transition, user=_user, queryset=queryset ) diff --git a/mayan/apps/mailer/forms.py b/mayan/apps/mailer/forms.py index 6c99ceeb5f..40d7a7de38 100644 --- a/mayan/apps/mailer/forms.py +++ b/mayan/apps/mailer/forms.py @@ -46,7 +46,7 @@ class DocumentMailForm(forms.Form): 'project_website': setting_project_url.value } - queryset = AccessControlList.objects.filter_by_access( + queryset = AccessControlList.objects.restrict_queryset( permission=permission_user_mailer_use, queryset=UserMailer.objects.filter(enabled=True), user=user ) diff --git a/mayan/apps/mailer/views.py b/mayan/apps/mailer/views.py index b57da3eef4..d4e12ef807 100644 --- a/mayan/apps/mailer/views.py +++ b/mayan/apps/mailer/views.py @@ -287,7 +287,7 @@ class UserMailerTestView(FormView): ) def get_queryset(self): - return AccessControlList.objects.filter_by_access( + return AccessControlList.objects.restrict_queryset( permission=permission_user_mailer_use, queryset=UserMailer.objects.all(), user=self.request.user ) diff --git a/mayan/apps/metadata/views.py b/mayan/apps/metadata/views.py index 8bf89b90a6..ca7846a875 100644 --- a/mayan/apps/metadata/views.py +++ b/mayan/apps/metadata/views.py @@ -733,7 +733,7 @@ class SetupDocumentTypeMetadataTypes(FormView): def get_queryset(self): queryset = self.submodel.objects.all() - return AccessControlList.objects.filter_by_access( + return AccessControlList.objects.restrict_queryset( permission=permission_document_type_edit, user=self.request.user, queryset=queryset ) From b5839c0662b84eff2483c9da9cd44dfc06b493de Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Tue, 29 Jan 2019 04:20:54 -0400 Subject: [PATCH 066/209] Refactor the tags app Remove the widget from the model. Add keyword arguments. Separate form widgets from html widgets. HTML widgets now go in the html_widgets module. Update the TagMultipleSelectionForm class to be a subclass of FilteredSelectionForm. Move Select2 specific JavaScript from the appearence app to the tags app. Update tag attachment and removal view names. Modernize tests. Add more tests. Consolidate repeated test code into test mixins. Update views to comply with MERCs 5 and 6. Use uniform nomeclature for URLs. Update URLs parameters to use the '_id' form. Signed-off-by: Roberto Rosario --- .../static/appearance/js/mayan_app.js | 24 +- mayan/apps/tags/admin.py | 2 +- mayan/apps/tags/api_views.py | 392 +++++++++++++----- mayan/apps/tags/apps.py | 82 ++-- mayan/apps/tags/events.py | 2 +- mayan/apps/tags/forms.py | 33 +- mayan/apps/tags/html_widgets.py | 26 ++ mayan/apps/tags/icons.py | 22 +- mayan/apps/tags/links.py | 42 +- mayan/apps/tags/methods.py | 12 +- mayan/apps/tags/models.py | 28 +- mayan/apps/tags/routers.py | 20 + mayan/apps/tags/search.py | 5 +- mayan/apps/tags/serializers.py | 336 +++++++++++++-- mayan/apps/tags/static/tags/js/tags_form.js | 26 ++ .../templates/tags/document_tags_widget.html | 5 + .../tags/forms/widgets/tag_select_option.html | 1 - mayan/apps/tags/tests/literals.py | 16 +- mayan/apps/tags/tests/mixins.py | 136 +++--- mayan/apps/tags/tests/test_api.py | 351 ++++++++-------- mayan/apps/tags/tests/test_events.py | 14 +- mayan/apps/tags/tests/test_indexing.py | 10 +- mayan/apps/tags/tests/test_links.py | 41 ++ mayan/apps/tags/tests/test_views.py | 361 ++++++++++------ mayan/apps/tags/tests/test_wizard_steps.py | 15 +- mayan/apps/tags/urls.py | 87 ++-- mayan/apps/tags/views.py | 159 +++---- mayan/apps/tags/widgets.py | 34 +- mayan/apps/tags/wizard_steps.py | 20 +- mayan/apps/tags/workflow_actions.py | 5 +- 30 files changed, 1489 insertions(+), 818 deletions(-) create mode 100644 mayan/apps/tags/html_widgets.py create mode 100644 mayan/apps/tags/routers.py create mode 100644 mayan/apps/tags/static/tags/js/tags_form.js create mode 100644 mayan/apps/tags/templates/tags/document_tags_widget.html create mode 100644 mayan/apps/tags/tests/test_links.py diff --git a/mayan/apps/appearance/static/appearance/js/mayan_app.js b/mayan/apps/appearance/static/appearance/js/mayan_app.js index b77e94859e..b3df0c8e14 100644 --- a/mayan/apps/appearance/static/appearance/js/mayan_app.js +++ b/mayan/apps/appearance/static/appearance/js/mayan_app.js @@ -8,10 +8,10 @@ class MayanApp { ajaxMenusOptions: [] } - this.ajaxSpinnerSeletor = '#ajax-spinner'; this.ajaxExecuting = false; this.ajaxMenusOptions = options.ajaxMenusOptions; this.ajaxMenuHashes = {}; + this.ajaxSpinnerSeletor = '#ajax-spinner'; this.window = $(window); } @@ -81,22 +81,6 @@ class MayanApp { }); } - static tagSelectionTemplate (tag, container) { - var $tag = $( - ' ' + tag.text + '' - ); - container[0].style.background = tag.element.dataset.color; - return $tag; - } - - static tagResultTemplate (tag) { - if (!tag.element) { return ''; } - var $tag = $( - ' ' + tag.text + '' - ); - return $tag; - } - static updateNavbarState () { var uri = new URI(window.location.hash); var uriFragment = uri.fragment(); @@ -445,12 +429,6 @@ class MayanApp { dropdownAutoWidth: true, width: '100%' }); - - $('.select2-tags').select2({ - templateSelection: MayanApp.tagSelectionTemplate, - templateResult: MayanApp.tagResultTemplate, - width: '100%' - }); } resizeFullHeight () { diff --git a/mayan/apps/tags/admin.py b/mayan/apps/tags/admin.py index 7d0f1a007a..a703ba2e03 100644 --- a/mayan/apps/tags/admin.py +++ b/mayan/apps/tags/admin.py @@ -8,4 +8,4 @@ from .models import Tag @admin.register(Tag) class TagAdmin(admin.ModelAdmin): filter_horizontal = ('documents',) - list_display = ('label', 'color', 'get_preview_widget') + list_display = ('label', 'color') diff --git a/mayan/apps/tags/api_views.py b/mayan/apps/tags/api_views.py index e4d109c08f..7018c54599 100644 --- a/mayan/apps/tags/api_views.py +++ b/mayan/apps/tags/api_views.py @@ -1,14 +1,20 @@ from __future__ import absolute_import, unicode_literals -from rest_framework import generics +from rest_framework import generics, status +from rest_framework.decorators import action from rest_framework.exceptions import ValidationError from rest_framework.response import Response -from mayan.apps.common.mixins import ExternalObjectViewMixin +from mayan.apps.common.mixins import ExternalObjectMixin +from mayan.apps.documents.api_views import DocumentViewSet from mayan.apps.documents.models import Document from mayan.apps.documents.permissions import permission_document_view from mayan.apps.documents.serializers import DocumentSerializer from mayan.apps.rest_api.filters import MayanObjectPermissionsFilter +from mayan.apps.rest_api.generics import ( + ListAPIView, ListCreateAPIView, RetrieveDestroyAPIView, + RetrieveUpdateDestroyAPIView +) from mayan.apps.rest_api.permissions import MayanPermission from .models import Tag @@ -17,116 +23,295 @@ from .permissions import ( permission_tag_edit, permission_tag_remove, permission_tag_view ) from .serializers import ( - DocumentTagSerializer, TagSerializer, WritableTagSerializer + DocumentTagAttachSerializer, DocumentTagSerializer, TagAttachSerializer, + TagRemoveSerializer, TagSerializer, ) -class APITagListView(generics.ListCreateAPIView): + +from django.conf.urls import url, include +from django.contrib.auth.models import User + +from rest_framework import routers, serializers, viewsets +from rest_framework.decorators import detail_route +from rest_framework.response import Response + +from drf_yasg.utils import swagger_auto_schema + + +class TagViewSet(viewsets.ModelViewSet): + filter_backends = (MayanObjectPermissionsFilter,) + lookup_field = 'pk' + lookup_url_kwarg='tag_id' + permission_classes = (MayanPermission,) + queryset = Tag.objects.all() + serializer_class = TagSerializer + + + #@swagger_auto_schema(operation_description='GET /articles/today/') + @swagger_auto_schema( + operation_description="partial_update description override", responses={200: TagAttachSerializer} + ) + @action( + detail=True, lookup_field='pk', lookup_url_kwarg='tag_id', + methods=('post',), serializer_class=TagAttachSerializer, + url_name='document-attach', url_path='attach' + ) + def attach(self, request, *args, **kwargs): + #print '!!! attach', args, kwargs#, self.context + #return Response({}) + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + #print '((((((((', serializer.validated_data + #self.perform_attach(serializer=serializer) + serializer.attach(instance=self.get_object()) + headers = self.get_success_headers(data=serializer.data) + return Response( + serializer.data, status=status.HTTP_200_OK, headers=headers + ) + + #def perform_attach(self, serializer): + # #print '!!!!', serializer + # serializer.attach(instance=self.get_object()) + + #def get_success_headers(self, data): + # try: + # return {'Location': str(data[api_settings.URL_FIELD_NAME])} + # except (TypeError, KeyError): + # return {} + + @action( + detail=True, lookup_field='pk', lookup_url_kwarg='tag_id', + url_name='document-list', url_path='documents' + ) + def document_list(self, request, *args, **kwargs): + queryset = self.get_object().documents.all() + + #TODO:Filter queryset + #queryset = self.filter_queryset(self.get_queryset()) + + page = self.paginate_queryset(queryset) + if page is not None: + #serializer = self.get_serializer(page, many=True) + serializer = DocumentSerializer(page, many=True, context={'request': request}) + return self.get_paginated_response(serializer.data) + + #serializer = self.get_serializer(queryset, many=True) + serializer = DocumentSerializer(queryset, many=True, context={'request': request}) + return Response(serializer.data) + + + #serializer = DocumentSerializer( + # instance=, many=True, + # context={'request': request} + #) + #return Response(serializer.data) + + + @action( + detail=True, lookup_field='pk', lookup_url_kwarg='tag_id', + methods=('post',), serializer_class=TagRemoveSerializer, + url_name='document-remove', url_path='remove' + ) + def remove(self, request, *args, **kwargs): + #print '!!! attach', args, kwargs#, self.context + #return Response({}) + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + #print '((((((((', serializer.validated_data + #self.perform_attach(serializer=serializer) + serializer.remove(instance=self.get_object()) + headers = self.get_success_headers(data=serializer.data) + return Response( + serializer.data, status=status.HTTP_200_OK, headers=headers + ) + + #def get_serializer_class(self, *args, **kwargs): + # #if self.action == 'attach': + # print '!!!!get_serializer_class', args, kwargs + # return TagAttachSerializer + + + +class DocumentTagViewSet(ExternalObjectMixin, viewsets.ReadOnlyModelViewSet): + external_object_class = Document + external_object_pk_url_kwarg = 'document_id' + external_object_permission = permission_tag_view + lookup_field = 'pk' + object_permission = { + 'list': permission_document_view, + 'retrieve': permission_document_view + } + serializer_class = DocumentTagSerializer + + @action( + detail=True, lookup_field='pk', lookup_url_kwarg='document_id', + methods=('post',), serializer_class=DocumentTagAttachSerializer, + url_name='tag-attach', url_path='attach' + ) + def attach(self, request, *args, **kwargs): + return Response({}) + + + ''' + serializer = DocumentSerializer( + instance=self.get_object().documents.all(), many=True, + context={'request': request} + ) + return Response(serializer.data) + ''' + + def get_document(self): + return self.get_external_object() + + def get_queryset(self): + #return self.get_document().get_tags(user=self.request.user).all() + return self.get_document().tags.all() + + #@detail_route(lookup_url_kwarg='tag_id') + #def document_list(self, request, *args, **kwargs): + # serializer = DocumentSerializer( + ## instance=self.get_object().documents.all(), many=True, + # context={'request': request} + # ) + # return Response(serializer.data) + +''' + +class APITagListView(ListCreateAPIView): """ get: Returns a list of all the tags. post: Create a new tag. """ - filter_backends = (MayanObjectPermissionsFilter,) - mayan_object_permissions = {'GET': (permission_tag_view,)} - mayan_view_permissions = {'POST': (permission_tag_create,)} - permission_classes = (MayanPermission,) + object_permission = {'GET': permission_tag_view} queryset = Tag.objects.all() - - def get_serializer(self, *args, **kwargs): - if not self.request: - return None - - return super(APITagListView, self).get_serializer(*args, **kwargs) - - def get_serializer_class(self): - if self.request.method == 'GET': - return TagSerializer - elif self.request.method == 'POST': - return WritableTagSerializer + serializer_class = TagSerializer + view_permission = {'POST': permission_tag_create} -class APITagView(generics.RetrieveUpdateDestroyAPIView): +class APITagView(RetrieveUpdateDestroyAPIView): """ delete: Delete the selected tag. get: Return the details of the selected tag. patch: Edit the selected tag. put: Edit the selected tag. """ - filter_backends = (MayanObjectPermissionsFilter,) lookup_url_kwarg = 'tag_pk' - mayan_object_permissions = { - 'DELETE': (permission_tag_delete,), - 'GET': (permission_tag_view,), - 'PATCH': (permission_tag_edit,), - 'PUT': (permission_tag_edit,) + object_permission = { + 'DELETE': permission_tag_delete, + 'GET': permission_tag_view, + 'PATCH': permission_tag_edit, + 'PUT': permission_tag_edit } queryset = Tag.objects.all() + serializer_class = TagSerializer - def get_serializer(self, *args, **kwargs): - if not self.request: - return None +## - return super(APITagView, self).get_serializer(*args, **kwargs) - - def get_serializer_class(self): - if self.request.method == 'GET': - return TagSerializer - else: - return WritableTagSerializer - - -class APITagDocumentListView(ExternalObjectViewMixin, generics.ListAPIView): +class APITagDocumentListView(ExternalObjectMixin, ListCreateAPIView): """ get: Returns a list of all the documents tagged by a particular tag. """ external_object_class = Tag external_object_pk_url_kwarg = 'tag_pk' external_object_permission = permission_tag_view - filter_backends = (MayanObjectPermissionsFilter,) - mayan_object_permissions = {'GET': (permission_document_view,)} - serializer_class = DocumentSerializer + object_permission = {'GET': permission_document_view} + serializer_class = TagDocumentSerializer def get_queryset(self): - return self.get_tag().documents.all() + return self.get_tag().get_documents(user=self.request.user).all() def get_tag(self): return self.get_external_object() +## +''' -class APIDocumentTagListView(ExternalObjectViewMixin, generics.ListCreateAPIView): + +''' +class APITagView(RetrieveDestroyAPIView): """ - get: Returns a list of all the tags attached to a document. - post: Attach a tag to a document. + delete: Delete the selected tag document. + get: Return the details of the selected tag document. """ + lookup_url_kwarg = 'tag_pk' + object_permission = { + 'DELETE': permission_tag_delete, + 'GET': permission_tag_view, + 'PATCH': permission_tag_edit, + 'PUT': permission_tag_edit + } + queryset = Tag.objects.all() + serializer_class = TagSerializer +''' +## +''' +class DocumentSourceMixin(ExternalObjectMixin): external_object_class = Document external_object_pk_url_kwarg = 'document_pk' - filter_backends = (MayanObjectPermissionsFilter,) - mayan_object_permissions = { - 'GET': (permission_tag_view,), - 'POST': (permission_tag_attach,) - } + serializer_class = DocumentTagSerializer def get_document(self): return self.get_external_object() def get_external_object_permission(self): - if self.request.method == 'POST': - return permission_tag_attach - else: - return permission_tag_view + permission_dictionary = { + 'DELETE': permission_tag_remove, + 'GET': permission_tag_view, + 'POST': permission_tag_attach + } + + return permission_dictionary.get(self.request.method) def get_queryset(self): - return self.get_document().get_tags().all() + return self.get_document().tags.all() - def get_serializer(self, *args, **kwargs): - if not self.request: - return None + def get_serializer_context(self): + context = super(DocumentSourceMixin, self).get_serializer_context() + if self.kwargs: + context.update( + { + 'document': self.get_document(), + } + ) - return super(APIDocumentTagListView, self).get_serializer(*args, **kwargs) + return context - def get_serializer_class(self): - return DocumentTagSerializer +class APIDocumentTagListView(DocumentSourceMixin, ListCreateAPIView): + """ + get: Returns a list of all the tags attached to a document. + post: Attach a tag to a document. + """ + #external_object_class = Document + #external_object_pk_url_kwarg = 'document_pk' + object_permission = { + 'GET': permission_tag_view, + #'POST': permission_tag_attach + } + #serializer_class = DocumentTagSerializer + + #def get_document(self): + # return self.get_external_object() + + #def get_external_object_permission(self): + # if self.request.method == 'POST': + # return permission_tag_attach + # else: + # return permission_tag_view + + #def get_queryset(self): + # #if self.request.method == 'POST': + # # permission = permission_tag_attach + # #else: + # # permission = permission_tag_view + # + # return self.get_document().tags().all() + # #return self.get_document().get_tags( + ## # permission=permission, user=self.request.user + # #).all() + + """ def get_serializer_context(self): """ Extra context provided to the serializer class. @@ -140,41 +325,44 @@ class APIDocumentTagListView(ExternalObjectViewMixin, generics.ListCreateAPIView ) return context + """ - -class APIDocumentTagView(ExternalObjectViewMixin, generics.RetrieveDestroyAPIView): +class APIDocumentTagView(DocumentSourceMixin, RetrieveDestroyAPIView): """ delete: Remove a tag from the selected document. get: Returns the details of the selected document tag. """ - external_object_class = Document - external_object_pk_url_kwarg = 'document_pk' - filter_backends = (MayanObjectPermissionsFilter,) + #external_object_class = Document + #external_object_pk_url_kwarg = 'document_pk' lookup_url_kwarg = 'tag_pk' - mayan_object_permissions = { - 'GET': (permission_tag_view,), - 'DELETE': (permission_tag_remove,) + mayan_object_permission = { + 'GET': permission_tag_view, + 'DELETE': permission_tag_remove } - serializer_class = DocumentTagSerializer + #serializer_class = DocumentTagSerializer - def get_document(self): - return self.get_external_object() + #def get_document(self): + # return self.get_external_object() - def get_external_object_permission(self): - if self.request.method == 'DELETE': - return permission_tag_remove - else: - return permission_tag_view + #def get_external_object_permission(self): + # if self.request.method == 'DELETE': + # return permission_tag_remove + # else: + # return permission_tag_view + """ def get_queryset(self): - return self.get_document().get_tags().all() + if self.request.method == 'DELETE': + permission = permission_tag_remove + else: + permission = permission_tag_view - def get_serializer(self, *args, **kwargs): - if not self.request: - return None - - return super(APIDocumentTagView, self).get_serializer(*args, **kwargs) + return self.get_document().get_tags( + permission=permission, user=self.request.user + ).all() + """ + """ def get_serializer_context(self): """ Extra context provided to the serializer class. @@ -188,15 +376,33 @@ class APIDocumentTagView(ExternalObjectViewMixin, generics.RetrieveDestroyAPIVie ) return context + """ def perform_destroy(self, instance): - try: - instance.documents.remove(self.get_document()) - except Exception as exception: - raise ValidationError(exception) + # try: + from mayan.apps.acls.models import AccessControlList + from rest_framework.generics import get_object_or_404 - def retrieve(self, request, *args, **kwargs): - instance = self.get_object() + queryset = AccessControlList.objects.restrict_queryset( + permission=permission_tag_remove, queryset=Tag.objects.all(), + user=self.request.user + ) + instance = get_object_or_404(queryset=queryset, pk=instance.pk) + #instance.attach_to( + # document=self.context['document'], + # user=self.context['request'].user + #) - serializer = self.get_serializer(instance) - return Response(serializer.data) + instance.remove_from( + document=self.get_document(), user=self.request.user + ) + #instance.documents.remove(self.get_document()) + # except Exception as exception: + # raise ValidationError(exception) + + #def retrieve(self, request, *args, **kwargs): + # instance = self.get_object() + + # serializer = self.get_serializer(instance) + # return Response(serializer.data) +''' diff --git a/mayan/apps/tags/apps.py b/mayan/apps/tags/apps.py index ee9c6617af..75209a5517 100644 --- a/mayan/apps/tags/apps.py +++ b/mayan/apps/tags/apps.py @@ -24,12 +24,13 @@ from .events import ( event_tag_attach, event_tag_created, event_tag_edited, event_tag_remove ) from .handlers import handler_index_document, handler_tag_pre_delete +from .html_widgets import DocumentTagsWidget, TagWidget from .links import ( - link_document_tag_list, link_multiple_documents_attach_tag, - link_multiple_documents_tag_remove, - link_single_document_multiple_tag_remove, link_tag_attach, - link_tag_create, link_tag_delete, link_tag_edit, link_tag_list, - link_tag_multiple_delete, link_tag_tagged_item_list + link_document_tag_list, link_document_multiple_tag_multiple_attach, + link_document_multiple_tag_multiple_remove, + link_document_tag_multiple_remove, link_document_tag_multiple_attach, + link_tag_create, link_tag_delete, link_tag_document_list, link_tag_edit, + link_tag_list, link_tag_multiple_delete ) from .menus import menu_tags from .methods import method_get_tags @@ -38,7 +39,6 @@ from .permissions import ( permission_tag_remove, permission_tag_view ) from .search import tag_search # NOQA -from .widgets import widget_document_tags class TagsApp(MayanAppConfig): @@ -63,13 +63,11 @@ class TagsApp(MayanAppConfig): app_label='documents', model_name='DocumentPageSearchResult' ) - DocumentTag = self.get_model('DocumentTag') - Tag = self.get_model('Tag') + DocumentTag = self.get_model(model_name='DocumentTag') + Tag = self.get_model(model_name='Tag') Document.add_to_class(name='get_tags', value=method_get_tags) - ModelAttribute(model=Document, name='get_tags') - ModelEventType.register( model=Tag, event_types=( event_tag_attach, event_tag_created, event_tag_edited, @@ -78,10 +76,10 @@ class TagsApp(MayanAppConfig): ) ModelField( - Document, name='tags__label' + model=Document, name='tags__label' ) ModelField( - Document, name='tags__color' + model=Document, name='tags__color' ) ModelPermission.register( @@ -96,29 +94,31 @@ class TagsApp(MayanAppConfig): permission_acl_edit, permission_acl_view, permission_events_view, permission_tag_attach, permission_tag_delete, permission_tag_edit, - permission_tag_remove, permission_tag_view, + permission_tag_remove, permission_tag_view ) ) SourceColumn( attribute='label', is_identifier=True, is_sortable=True, - source=DocumentTag, + source=DocumentTag ) SourceColumn( - attribute='get_preview_widget', source=DocumentTag + label=_('Preview'), source=DocumentTag, widget=TagWidget ) SourceColumn( - func=lambda context: widget_document_tags( - document=context['object'], user=context['request'].user - ), label=_('Tags'), source=Document - ) - - SourceColumn( - func=lambda context: widget_document_tags( - document=context['object'].document, + func=lambda context: context['object'].get_tags( + permission=permission_tag_view, user=context['request'].user - ), label=_('Tags'), source=DocumentPageSearchResult + ), label=_('Tags'), source=Document, widget=DocumentTagsWidget + ) + + SourceColumn( + func=lambda context: context['object'].document.get_tag( + permission=permission_tag_view, + user=context['request'].user + ), label=_('Tags'), source=DocumentPageSearchResult, + widget=DocumentTagsWidget ) SourceColumn( @@ -126,12 +126,12 @@ class TagsApp(MayanAppConfig): source=Tag ) SourceColumn( - attribute='get_preview_widget', source=Tag + label=_('Preview'), source=Tag, widget=TagWidget ) SourceColumn( func=lambda context: context['object'].get_document_count( user=context['request'].user - ), label=_('Documents'), source=Tag + ), include_label=True, label=_('Documents'), source=Tag ) document_page_search.add_model_field( @@ -147,19 +147,17 @@ class TagsApp(MayanAppConfig): links=( link_acl_list, link_events_for_object, link_object_event_types_user_subcriptions_list, - link_tag_tagged_item_list, - ), - sources=(Tag,) + link_tag_document_list, + ), sources=(Tag,) ) menu_main.bind_links(links=(menu_tags,), position=98) menu_multi_item.bind_links( links=( - link_multiple_documents_attach_tag, - link_multiple_documents_tag_remove - ), - sources=(Document,) + link_document_multiple_tag_multiple_attach, + link_document_multiple_tag_multiple_remove + ), sources=(Document,) ) menu_multi_item.bind_links( links=(link_tag_multiple_delete,), sources=(Tag,) @@ -167,14 +165,14 @@ class TagsApp(MayanAppConfig): menu_object.bind_links( links=( link_tag_edit, link_tag_delete - ), - sources=(Tag,) + ), sources=(Tag,) ) menu_sidebar.bind_links( - links=(link_tag_attach, link_single_document_multiple_tag_remove), - sources=( - 'tags:tag_attach', 'tags:document_tags', - 'tags:single_document_multiple_tag_remove' + links=( + link_document_tag_multiple_attach, link_document_tag_multiple_remove + ), sources=( + 'tags:document_tag_multiple_attach', 'tags:document_tag_list', + 'tags:document_tag_multiple_remove' ) ) menu_tags.bind_links( @@ -188,13 +186,11 @@ class TagsApp(MayanAppConfig): # Index update m2m_changed.connect( - handler_index_document, dispatch_uid='tags_handler_index_document', - sender=Tag.documents.through + receiver=handler_index_document, sender=Tag.documents.through ) pre_delete.connect( - handler_tag_pre_delete, dispatch_uid='tags_handler_tag_pre_delete', - sender=Tag + receiver=handler_tag_pre_delete, sender=Tag ) diff --git a/mayan/apps/tags/events.py b/mayan/apps/tags/events.py index 44f5793802..8f753bbbe9 100644 --- a/mayan/apps/tags/events.py +++ b/mayan/apps/tags/events.py @@ -4,7 +4,7 @@ from django.utils.translation import ugettext_lazy as _ from mayan.apps.events import EventTypeNamespace -namespace = EventTypeNamespace(name='tags', label=_('Tags')) +namespace = EventTypeNamespace(label=_('Tags'), name='tags') event_tag_attach = namespace.add_event_type( label=_('Tag attached to document'), name='attach' diff --git a/mayan/apps/tags/forms.py b/mayan/apps/tags/forms.py index 392de0d68f..30b60a9937 100644 --- a/mayan/apps/tags/forms.py +++ b/mayan/apps/tags/forms.py @@ -1,37 +1,22 @@ from __future__ import absolute_import, unicode_literals -import logging - from django import forms from django.utils.translation import ugettext_lazy as _ from mayan.apps.acls.models import AccessControlList +from mayan.apps.common.forms import FilteredSelectionForm from .models import Tag from .permissions import permission_tag_view from .widgets import TagFormWidget -logger = logging.getLogger(__name__) +class TagMultipleSelectionForm(FilteredSelectionForm): + class Media: + js = ('tags/js/tags_form.js',) -class TagMultipleSelectionForm(forms.Form): - def __init__(self, *args, **kwargs): - help_text = kwargs.pop('help_text', None) - permission = kwargs.pop('permission', permission_tag_view) - queryset = kwargs.pop('queryset', Tag.objects.all()) - user = kwargs.pop('user', None) - - logger.debug('user: %s', user) - super(TagMultipleSelectionForm, self).__init__(*args, **kwargs) - - queryset = AccessControlList.objects.filter_by_access( - permission=permission, queryset=queryset, user=user - ) - - self.fields['tags'] = forms.ModelMultipleChoiceField( - label=_('Tags'), help_text=help_text, - queryset=queryset, required=False, - widget=TagFormWidget( - attrs={'class': 'select2-tags'}, queryset=queryset - ) - ) + class Meta: + allow_multiple = True + field_name = 'tags' + label = _('Tags') + widget_attributes = {'class': 'select2-tags'} diff --git a/mayan/apps/tags/html_widgets.py b/mayan/apps/tags/html_widgets.py new file mode 100644 index 0000000000..94128dc441 --- /dev/null +++ b/mayan/apps/tags/html_widgets.py @@ -0,0 +1,26 @@ +from __future__ import absolute_import, unicode_literals + +from django.template.loader import render_to_string + + +class DocumentTagsWidget(object): + """ + A tag widget that displays the tags for the given document + """ + def render(self, name, value): + return render_to_string( + template_name='tags/document_tags_widget.html', + context={ + 'tags': value, + } + ) + + +class TagWidget(object): + def render(self, name, value): + return render_to_string( + template_name='tags/tag_widget.html', + context={ + 'tag': value, + } + ) diff --git a/mayan/apps/tags/icons.py b/mayan/apps/tags/icons.py index a5f55f6e96..7e84defc64 100644 --- a/mayan/apps/tags/icons.py +++ b/mayan/apps/tags/icons.py @@ -2,19 +2,22 @@ from __future__ import absolute_import, unicode_literals from mayan.apps.appearance.classes import Icon -icon_menu_tags = Icon(driver_name='fontawesome', symbol='tags') -icon_multiple_documents_tag_attach = Icon( - driver_name='fontawesome-dual', primary_symbol='tag', - secondary_symbol='arrow-right' -) -icon_multiple_documents_tag_remove = Icon( +icon_document_multiple_tag_multiple_remove = Icon( driver_name='fontawesome-dual', primary_symbol='tag', secondary_symbol='minus' ) -icon_tag_attach = Icon( +icon_document_tag_multiple_attach = Icon( driver_name='fontawesome-dual', primary_symbol='tag', secondary_symbol='arrow-right' ) +icon_document_tag_multiple_remove = Icon( + driver_name='fontawesome-dual', primary_symbol='tag', + secondary_symbol='minus' +) +icon_document_tag_multiple_remove_submit = Icon( + driver_name='fontawesome', symbol='minus' +) +icon_menu_tags = Icon(driver_name='fontawesome', symbol='tags') icon_tag_create = Icon( driver_name='fontawesome-dual', primary_symbol='tag', secondary_symbol='plus' @@ -25,8 +28,3 @@ icon_tag_delete_submit = Icon(driver_name='fontawesome', symbol='times') icon_tag_document_list = Icon(driver_name='fontawesome', symbol='tags') icon_tag_list = Icon(driver_name='fontawesome', symbol='tags') icon_tag_multiple_delete = Icon(driver_name='fontawesome', symbol='times') -icon_tag_remove = Icon( - driver_name='fontawesome-dual', primary_symbol='tag', - secondary_symbol='minus' -) -icon_tag_remove_submit = Icon(driver_name='fontawesome', symbol='minus') diff --git a/mayan/apps/tags/links.py b/mayan/apps/tags/links.py index 062a44f425..0a7a3ab980 100644 --- a/mayan/apps/tags/links.py +++ b/mayan/apps/tags/links.py @@ -6,10 +6,10 @@ from mayan.apps.documents.icons import icon_document_list from mayan.apps.navigation import Link, get_cascade_condition from .icons import ( - icon_document_multiple_tag_multiple_remove, icon_document_multiple_tag_multiple_remove, - icon_document_tag_multiple_attach, icon_tag_create, icon_tag_delete, icon_tag_edit, - icon_tag_document_list, icon_tag_list, icon_tag_multiple_delete, - icon_document_tag_multiple_remove + icon_document_multiple_tag_multiple_remove, + icon_document_tag_multiple_attach, icon_tag_create, icon_tag_delete, + icon_tag_edit, icon_tag_document_list, icon_tag_list, + icon_tag_multiple_delete, icon_document_tag_multiple_remove ) from .permissions import ( permission_tag_attach, permission_tag_create, permission_tag_delete, @@ -17,10 +17,6 @@ from .permissions import ( ) -link_document_tag_list = Link( - args='resolved_object.pk', icon_class=icon_tag_document_list, - permission=permission_tag_view, text=_('Tags'), view='tags:document_tags' -) link_document_multiple_tag_multiple_attach = Link( icon_class=icon_document_multiple_tag_multiple_remove, text=_('Attach tags'), view='tags:document_multiple_tag_multiple_attach' @@ -29,27 +25,37 @@ link_document_multiple_tag_multiple_remove = Link( icon_class=icon_document_multiple_tag_multiple_remove, text=_('Remove tag'), view='tags:document_multiple_tag_multiple_remove' ) +link_document_tag_list = Link( + icon_class=icon_tag_document_list, + kwargs={'document_id': 'resolved_object.pk'}, + permission=permission_tag_view, text=_('Tags'), + view='tags:document_tag_list' +) link_document_tag_multiple_attach = Link( - args='object.pk', icon_class=icon_document_tag_multiple_attach, - permission=permission_tag_attach, text=_('Attach tags'), - view='tags:document_tag_multiple_attach' + icon_class=icon_document_tag_multiple_attach, + kwargs={'document_id': 'object.pk'}, permission=permission_tag_attach, + text=_('Attach tags'), view='tags:document_tag_multiple_attach' ) link_document_tag_multiple_remove = Link( - args='object.id', icon_class=icon_document_tag_multiple_remove, - permission=permission_tag_remove, text=_('Remove tags'), - view='tags:document_tag_multiple_remove' + icon_class=icon_document_tag_multiple_remove, + kwargs={'document_id': 'object.pk'}, permission=permission_tag_remove, + text=_('Remove tags'), view='tags:document_tag_multiple_remove' ) link_tag_create = Link( icon_class=icon_tag_create, permission=permission_tag_create, text=_('Create new tag'), view='tags:tag_create' ) link_tag_delete = Link( - args='object.id', icon_class=icon_tag_delete, + icon_class=icon_tag_delete, kwargs={'tag_id': 'object.pk'}, permission=permission_tag_delete, tags='dangerous', text=_('Delete'), view='tags:tag_delete' ) +link_tag_document_list = Link( + icon_class=icon_document_list, kwargs={'tag_id': 'object.pk'}, + text=('Documents'), view='tags:tag_document_list' +) link_tag_edit = Link( - args='object.id', icon_class=icon_tag_edit, + icon_class=icon_tag_edit, kwargs={'tag_id': 'object.pk'}, permission=permission_tag_edit, text=_('Edit'), view='tags:tag_edit' ) link_tag_list = Link( @@ -62,7 +68,3 @@ link_tag_multiple_delete = Link( icon_class=icon_tag_multiple_delete, permission=permission_tag_delete, text=_('Delete'), view='tags:tag_multiple_delete' ) -link_tag_tagged_item_list = Link( - args='object.id', icon_class=icon_document_list, text=('Documents'), - view='tags:tag_tagged_item_list' -) diff --git a/mayan/apps/tags/methods.py b/mayan/apps/tags/methods.py index 83761fc92f..bfc0163ebc 100644 --- a/mayan/apps/tags/methods.py +++ b/mayan/apps/tags/methods.py @@ -3,10 +3,18 @@ from __future__ import unicode_literals from django.apps import apps from django.utils.translation import ugettext_lazy as _ +from .permissions import permission_tag_view -def method_get_tags(self): + +def method_get_tags(self, permission, user): + AccessControlList = apps.get_model( + app_label='acls', model_name='AccessControlList' + ) DocumentTag = apps.get_model(app_label='tags', model_name='DocumentTag') - return DocumentTag.objects.filter(documents=self) + return AccessControlList.objects.restrict_queryset( + permission=permission, + queryset=DocumentTag.objects.filter(documents=self), user=user + ) method_get_tags.help_text = _('Return a the tags attached to the document.') diff --git a/mayan/apps/tags/models.py b/mayan/apps/tags/models.py index 0e49ae2f27..5f2e6ec88b 100644 --- a/mayan/apps/tags/models.py +++ b/mayan/apps/tags/models.py @@ -15,7 +15,6 @@ from .events import ( event_tag_attach, event_tag_created, event_tag_edited, event_tag_remove ) from .managers import DocumentTagManager -from .widgets import widget_single_tag @python_2_unicode_compatible @@ -56,23 +55,22 @@ class Tag(models.Model): def get_absolute_url(self): return reverse( - viewname='tags:tag_tagged_item_list', kwargs={'tag_pk': str(self.pk)} + viewname='tags:tag_tagged_item_list', kwargs={ + 'tag_id': self.pk + } + ) + + def get_documents(self, user): + """ + Return a filtered queryset documents that have this tag attached. + """ + return AccessControlList.objects.restrict_queryset( + permission=permission_document_view, queryset=self.documents, + user=user ) def get_document_count(self, user): - """ - Return the numeric count of documents that have this tag attached. - The count if filtered by access. - """ - queryset = AccessControlList.objects.filter_by_access( - permission_document_view, user, queryset=self.documents - ) - - return queryset.count() - - def get_preview_widget(self): - return widget_single_tag(tag=self) - get_preview_widget.short_description = _('Preview') + return self.get_documents(user=user).count() def remove_from(self, document, user=None): """ diff --git a/mayan/apps/tags/routers.py b/mayan/apps/tags/routers.py new file mode 100644 index 0000000000..04f7371af6 --- /dev/null +++ b/mayan/apps/tags/routers.py @@ -0,0 +1,20 @@ +from __future__ import unicode_literals + +#from rest_framework import routers + +#router = routers.SimpleRouter() +#router.register(r'users', UserViewSet) +#router.register(r'accounts', AccountViewSet) +#urlpatterns = router.urls + +#router = routers.DefaultRouter() +#from mayan.apps.rest_api.api_views import router +#from mayan.apps.rest_api.urls import router + +from .api_views import TagViewSet + +router_entries = ( + {'prefix': r'tags', 'viewset': TagViewSet, 'base_name': 'tag'}, +) + +#router.register(prefix=r'tags', viewset=TagViewSet, basename='tag') diff --git a/mayan/apps/tags/search.py b/mayan/apps/tags/search.py index f16b8e75c1..d74bee54a8 100644 --- a/mayan/apps/tags/search.py +++ b/mayan/apps/tags/search.py @@ -7,9 +7,8 @@ from mayan.apps.dynamic_search.classes import SearchModel from .permissions import permission_tag_view tag_search = SearchModel( - app_label='tags', model_name='Tag', - permission=permission_tag_view, - serializer_string='mayan.apps.tags.serializers.TagSerializer' + app_label='tags', model_name='Tag', permission=permission_tag_view, + serializer_path='mayan.apps.tags.serializers.TagSerializer' ) tag_search.add_model_field( diff --git a/mayan/apps/tags/serializers.py b/mayan/apps/tags/serializers.py index 5048d10324..9f4d5c3012 100644 --- a/mayan/apps/tags/serializers.py +++ b/mayan/apps/tags/serializers.py @@ -8,34 +8,42 @@ from rest_framework.reverse import reverse from mayan.apps.acls.models import AccessControlList from mayan.apps.documents.models import Document +from mayan.apps.documents.serializers import DocumentSerializer +from mayan.apps.rest_api.relations import MultiKwargHyperlinkedIdentityField from .models import Tag from .permissions import permission_tag_attach class TagSerializer(serializers.HyperlinkedModelSerializer): - documents_url = serializers.HyperlinkedIdentityField( - lookup_field='pk', lookup_url_kwarg='tag_pk', - view_name='rest_api:tag-document-list' + attach_url = serializers.HyperlinkedIdentityField( + lookup_url_kwarg='tag_id', view_name='rest_api:tag-document-attach' + ) + + documents_url = serializers.HyperlinkedIdentityField( + lookup_url_kwarg='tag_id', view_name='rest_api:tag-document-list' + ) + + remove_url = serializers.HyperlinkedIdentityField( + lookup_url_kwarg='tag_id', view_name='rest_api:tag-document-remove' ) - documents_count = serializers.SerializerMethodField() class Meta: extra_kwargs = { 'url': { - 'lookup_field': 'pk', 'lookup_url_kwarg': 'tag_pk', + 'lookup_url_kwarg': 'tag_id', 'view_name': 'rest_api:tag-detail' - } + }, } fields = ( - 'color', 'documents_count', 'documents_url', 'id', 'label', 'url' + 'attach_url', 'color', 'documents_url', 'label', 'id', + 'remove_url', 'url' ) model = Tag - def get_documents_count(self, instance): - return instance.documents.count() +""" class WritableTagSerializer(serializers.ModelSerializer): documents_pk_list = serializers.CharField( help_text=_( @@ -81,37 +89,303 @@ class WritableTagSerializer(serializers.ModelSerializer): ) return instance - +""" class DocumentTagSerializer(TagSerializer): - document_tag_url = serializers.SerializerMethodField( - help_text=_( - 'API URL pointing to a tag in relation to the document ' - 'attached to it. This URL is different than the canonical ' - 'tag URL.' - ) - ) - tag_pk = serializers.IntegerField( - help_text=_('Primary key of the tag to be added.'), write_only=True - ) + #document_attach_url = serializers.HyperlinkedIdentityField( + # lookup_url_kwarg='document_id', view_name='rest_api:document-tag-attach' + #) class Meta(TagSerializer.Meta): - fields = TagSerializer.Meta.fields + ('document_tag_url', 'tag_pk') - read_only_fields = TagSerializer.Meta.fields + ('document_tag_url',) + #fields = TagSerializer.Meta.fields + ('document_attach_url',) + fields = TagSerializer.Meta.fields + #fields = TagSerializer.Meta.fields + ('document_tag_url', 'tag_pk') + #fields = TagSerializer.Meta.fields + ('tag_pk',) + #read_only_fields = TagSerializer.Meta.fields + ('document_attach_url',) + read_only_fields = TagSerializer.Meta.fields + #read_only_fields = TagSerializer.Meta.fields - def get_document_tag_url(self, instance): - return reverse( - viewname='rest_api:document-tag-detail', kwargs={ - 'document_pk': self.context['document'].pk, - 'tag_pk': instance.pk - }, request=self.context['request'], format=self.context['format'] + #related_models = ('tags',) + #related_models_kwargs = { + # 'documents': { + ## #'pk_list': 'tags_pk_list', 'model': Tag, + # #'model': Tag, + # 'object_permission': {'create': permission_tag_attach}, + # #'add_method': 'add', 'add_method_kwargs': 'document' + # } + #} + + ''' + def create(self, validated_data): + """ + queryset = Tag.objects.filter(pk__in=validated_data['tags_pk_list'].split(',')) + + #permission = self.object_permission.get('create') + + #if permission: + queryset = AccessControlList.objects.restrict_queryset( + permission=permission_tag_attach, queryset=queryset, + user=self.context['request'].user ) - def create(self, validated_data): - queryset = AccessControlList.objects.filter_by_access( + for tag in queryset.all(): + tag.attach_to( + document=self.context['document'], + user=self.context['request'].user + ) + """ + queryset = AccessControlList.objects.restrict_queryset( permission=permission_tag_attach, queryset=Tag.objects.all(), user=self.context['request'].user ) tag = get_object_or_404(queryset=queryset, pk=validated_data['tag_pk']) - tag.documents.add(self.context['document']) + tag.attach_to( + document=self.context['document'], + user=self.context['request'].user + ) return tag + #return None + ''' + + def get_document_tag_url(self, instance): + return reverse( + viewname='rest_api:document-tag-detail', kwargs={ + 'document_id': self.context['document'].pk, + 'tag_id': instance.pk + }, request=self.context['request'], format=self.context['format'] + ) + + +class DocumentTagAttachSerializer(serializers.Serializer): + tags_pk_list = serializers.CharField( + help_text=_( + 'Comma separated list of tag primary keys that will be attached ' + 'to this document.' + ), write_only=True + ) + + +class TagAttachSerializer(serializers.Serializer): +#class TagAttachSerializer(TagSerializer): + documents_pk_list = serializers.CharField( + help_text=_( + 'Comma separated list of document primary keys to which this ' + 'tag will be attached.' + ), write_only=True + ) + + #class Meta(TagSerializer.Meta): + # fields = TagSerializer.Meta.fields + ('documents_pk_list',) + # read_only_fields = TagSerializer.Meta.fields + + def attach(self, instance): + queryset = AccessControlList.objects.restrict_queryset( + permission=permission_tag_attach, queryset=Document.objects.all(), + user=self.context['request'].user + ) + + for document in queryset.filter(pk__in=self.validated_data['documents_pk_list'].split(',')): + instance.attach_to(document=document, user=self.context['request'].user) + + #print '@@@@@@@', self.validated_data['document_pk_list'] + #print '@@@@@@@', instance + #print '22222', validated_data + #print '!!!', self.data['document_pk_list'] + + +class TagRemoveSerializer(serializers.Serializer): + documents_pk_list = serializers.CharField( + help_text=_( + 'Comma separated list of document primary keys from which this ' + 'tag will be removed.' + ), write_only=True + ) + + def remove(self, instance): + queryset = AccessControlList.objects.restrict_queryset( + permission=permission_tag_attach, queryset=Document.objects.all(), + user=self.context['request'].user + ) + + for document in queryset.filter(pk__in=self.validated_data['documents_pk_list'].split(',')): + instance.remove_from(document=document, user=self.context['request'].user) + + + +class RelatedModel(object): + @classmethod + def generate(cls, serializer, validated_data): + result = [] + + kwargs = getattr(serializer.Meta, 'related_model_kwargs', {}) + kwargs.update({'serializer': serializer}) + + for field_name in getattr(serializer.Meta, 'related_models', []): + kwargs.update({'field_name': field_name}) + related_field = cls(**kwargs) + related_field.pop_pk_list(validated_data=validated_data) + result.append(related_field) + + return result + + def __init__(self, field_name, serializer, pk_list_field=None, model=None, object_permission=None): + self.field_name = field_name + self._pk_list_field = pk_list_field + self.model = model + self.object_permission = object_permission + self.serializer = serializer + + def create(self, instance): + field = self.get_field(instance=instance) + field.clear() + #model = self.get_model() + + queryset = self.get_model().objects.filter(pk__in=self.pk_list.split(',')) + + permission = self.object_permission.get('create') + + if permission: + queryset = AccessControlList.objects.restrict_queryset( + permission=permission, + queryset=queryset, + user=self.serializer.context['request'].user + ) + + self.related_add() + + field.add(*queryset) + #fieldqueryset=queryset) + + #def related_add(self, queryset): + # self.get_field().add(*queryset) + + + #def _get_m2m_field(self, instance): + # getattr(instance, m2m_field_name).all() + + """ + def _add_m2m(self, instance, m2m_pk_list, permission): + m2m_field = self._get_m2m_field() + m2m_field.clear() + + queryset = AccessControlList.objects.restrict_queryset( + permission=permission, + queryset=m2m_model.objects.filter(pk__in=m2m_pk_list.split(',')), + user=self.context['request'].user + ) + + #m2m_field.add(*queryset) + self._m2m_add(m2m_field=m2m_field, queryset=queryset) + """ + + def get_model(self): + return self.model or self.get_field.model + + def get_field(self, instance): + return getattr(instance, self.field_name) + + def get_pk_list_field_name(self): + return self._pk_list_field or '{}_pk_list'.format(self.field_name) + + def pop_pk_list(self, validated_data): + self.pk_list = validated_data.pop(self.get_pk_list_field_name(), '') + + +class RelatedModelSerializerMixin(object): + #m2m_pk_list_name = 'documents_pk_list' + #m2m_field_name = 'documents' + #m2m_model = Document + + """ + class Meta: + extra_kwargs = { + 'url': { + 'lookup_field': 'pk', 'lookup_url_kwarg': 'tag_pk', + 'view_name': 'rest_api:tag-detail' + } + } + fields = ( + 'color', 'documents_count', 'documents_pk_list', 'documents_url', + 'id', 'label', 'url' + ) + model = Tag + related_models = ('documents',) + related_models_kwargs = { + 'documents': { + 'pk_list_field': 'documents_pk_list', 'model': Document, + 'permissions': {'create': permission_tag_attach} + } + } + """ + + def _get_m2m_field(self, instance): + getattr(instance, m2m_field_name).all() + + def _add_m2m(self, instance, m2m_pk_list, permission): + m2m_field = self._get_m2m_field() + m2m_field.clear() + + queryset = AccessControlList.objects.restrict_queryset( + permission=permission, + queryset=m2m_model.objects.filter(pk__in=m2m_pk_list.split(',')), + user=self.context['request'].user + ) + + #m2m_field.add(*queryset) + self._m2m_add(m2m_field=m2m_field, queryset=queryset) + + #def _m2m_add(self, m2m_field, queryset): + # m2m_field.add(*queryset) + + def _m2m_add(self, m2m_field, queryset): + for document in queryset.all(): + m2m_field.add(document=document, user=self.context['request'].user) + + def create(self, validated_data): + related_objects = RelatedModel.generate( + serializer=self, validated_data=validated_data + ) + + instance = super(RelatedModelSerializerMixin, self).create( + validated_data=validated_data + ) + + #TODO: return a container class + #TODO:related_objects.create(instance=instance) + for related_object in related_objects: + related_object.create(instance=instance) + + #if m2m_pk_list: + ## self._add_m2m( + # instance=instance, m2m_pk_list=m2m_pk_list, + # permission=permission_tag_add + # ) + + return instance + + ''' + # Extract the related field data before calling the superclass + # .create() and avoid an error due to unknown field data. + + #related_models = self.Meta.related_models + + #self.Meta.related_models + related_models_dictionary = {} + for related_model in self.Meta.related_models: + + #if self.m2m_pk_list_name: + m2m_pk_list = validated_data.pop(self.get_related_model_pk_list(), '') + + instance = super(RelatedObjectSerializerMixin, self).create( + validated_data=validated_data + ) + + if m2m_pk_list: + self._add_m2m( + instance=instance, m2m_pk_list=m2m_pk_list, + permission=permission_tag_add + ) + + return instance + ''' + diff --git a/mayan/apps/tags/static/tags/js/tags_form.js b/mayan/apps/tags/static/tags/js/tags_form.js new file mode 100644 index 0000000000..290f1dd82f --- /dev/null +++ b/mayan/apps/tags/static/tags/js/tags_form.js @@ -0,0 +1,26 @@ +'use strict'; + +var tagSelectionTemplate = function (tag, container) { + var $tag = $( + ' ' + escape(tag.text) + '' + ); + container[0].style.background = tag.element.dataset.color; + return $tag; +} + +var tagResultTemplate = function (tag) { + if (!tag.element) { return ''; } + var $tag = $( + ' ' + escape(tag.text) + '' + ); + return $tag; +} + +jQuery(document).ready(function() { + $('.select2-tags').select2({ + templateSelection: tagSelectionTemplate, + templateResult: tagResultTemplate, + width: '100%' + }); +}); + diff --git a/mayan/apps/tags/templates/tags/document_tags_widget.html b/mayan/apps/tags/templates/tags/document_tags_widget.html new file mode 100644 index 0000000000..4d0e6fe825 --- /dev/null +++ b/mayan/apps/tags/templates/tags/document_tags_widget.html @@ -0,0 +1,5 @@ +
+ {% for tag in tags %} + {% include 'tags/tag_widget.html' with tag=tag %} + {% endfor %} +
diff --git a/mayan/apps/tags/templates/tags/forms/widgets/tag_select_option.html b/mayan/apps/tags/templates/tags/forms/widgets/tag_select_option.html index 8dabad9809..5a657c2915 100644 --- a/mayan/apps/tags/templates/tags/forms/widgets/tag_select_option.html +++ b/mayan/apps/tags/templates/tags/forms/widgets/tag_select_option.html @@ -1,2 +1 @@ {% include 'django/forms/widgets/select_option.html' %} - diff --git a/mayan/apps/tags/tests/literals.py b/mayan/apps/tags/tests/literals.py index 6949b2d9dd..1c08f4765b 100644 --- a/mayan/apps/tags/tests/literals.py +++ b/mayan/apps/tags/tests/literals.py @@ -9,15 +9,15 @@ TEST_TAG_INDEX_HAS_TAG = 'HAS_TAG' TEST_TAG_INDEX_NO_TAG = 'NO_TAG' TEST_TAG_INDEX_NODE_TEMPLATE = ''' {{% for tag in document.get_tags().all() %}} -{{% if tag.label == "{}" %}} -{} + {{% if tag.label == "{label}" %}} + {has_tag} + {{% else %}} + {not_tagged} + {{% endif %}} {{% else %}} -NO_TAG -{{% endif %}} -{{% else %}} -NO_TAG + {not_tagged} {{% endfor %}} '''.format( - TEST_TAG_LABEL, TEST_TAG_INDEX_HAS_TAG, TEST_TAG_INDEX_NO_TAG, - TEST_TAG_INDEX_NO_TAG + label=TEST_TAG_LABEL, has_tag=TEST_TAG_INDEX_HAS_TAG, + not_tagged=TEST_TAG_INDEX_NO_TAG ).replace('\n', '') diff --git a/mayan/apps/tags/tests/mixins.py b/mayan/apps/tags/tests/mixins.py index d93041edb5..2b4b1dd6f4 100644 --- a/mayan/apps/tags/tests/mixins.py +++ b/mayan/apps/tags/tests/mixins.py @@ -9,39 +9,100 @@ from .literals import ( class TagTestMixin(object): - def _create_tag(self): - self.tag = Tag.objects.create( + def _create_test_tag(self): + self.test_tag = Tag.objects.create( color=TEST_TAG_COLOR, label=TEST_TAG_LABEL ) + +class TagAPITestMixin(object): def _request_api_tag_create_view(self): return self.post( viewname='rest_api:tag-list', data={ - 'label': TEST_TAG_LABEL, 'color': TEST_TAG_COLOR + 'color': TEST_TAG_COLOR, 'label': TEST_TAG_LABEL + } + ) + + def _request_api_tag_create_and_attach_view(self): + return self.post( + viewname='rest_api:tag-list', data={ + 'color': TEST_TAG_COLOR, 'label': TEST_TAG_LABEL, + 'document_id_list': self.document.pk } ) def _request_api_tag_delete_view(self): return self.delete( - viewname='rest_api:tag-detail', kwargs={'tag_pk': self.tag.pk} + viewname='rest_api:tag-detail', kwargs={'tag_id': self.test_tag.pk} ) - def _request_api_tag_edit_via_patch_view(self): + def _request_api_tag_edit_patch_view(self): return self.patch( - viewname='rest_api:tag-detail', kwargs={'tag_pk': self.tag.pk}, data={ + viewname='rest_api:tag-detail', kwargs={ + 'tag_id': self.test_tag.pk + }, data={ 'label': TEST_TAG_LABEL_EDITED, 'color': TEST_TAG_COLOR_EDITED } ) - def _request_api_tag_edit_via_put_view(self): + def _request_api_tag_edit_put_view(self): return self.put( - viewname='rest_api:tag-detail', kwargs={'tag_pk': self.tag.pk}, data={ + viewname='rest_api:tag-detail', kwargs={ + 'tag_id': self.test_tag.pk + }, data={ 'label': TEST_TAG_LABEL_EDITED, 'color': TEST_TAG_COLOR_EDITED } ) + def _request_api_tag_list_view(self): + return self.get(viewname='rest_api:tag-list') + + +class TagViewTestMixin(object): + def _request_document_tag_multiple_attach_view(self): + return self.post( + viewname='tags:document_tag_multiple_attach', + kwargs={'document_id': self.document.pk}, data={ + 'tags': self.test_tag.pk, + } + ) + + def _request_document_multiple_tag_multiple_attach_view(self): + return self.post( + viewname='tags:document_multiple_tag_multiple_attach', data={ + 'id_list': self.document.pk, 'tags': self.test_tag.pk, + } + ) + + def _request_document_tag_multiple_remove_view(self): + return self.post( + viewname='tags:document_tag_multiple_remove', + kwargs={'document_id': self.document.pk}, data={ + 'tags': self.test_tag.pk, + } + ) + + def _request_document_multiple_tag_multiple_remove_view(self): + return self.post( + viewname='tags:document_multiple_tag_multiple_remove', + data={ + 'id_list': self.document.pk, + 'tags': self.test_tag.pk, + } + ) + + def _request_document_tag_list_view(self): + return self.get( + viewname='tags:document_tag_list', + kwargs={ + 'document_id': self.document.pk, + } + ) + + # Normal tag view + def _request_tag_create_view(self): return self.post( viewname='tags:tag_create', data={ @@ -52,68 +113,19 @@ class TagTestMixin(object): def _request_tag_delete_view(self): return self.post( - viewname='tags:tag_delete', kwargs={'tag_pk': self.tag.pk} + viewname='tags:tag_delete', kwargs={'tag_id': self.test_tag.pk}, ) def _request_tag_edit_view(self): return self.post( - viewname='tags:tag_edit', kwargs={'tag_pk': self.tag.pk}, data={ + viewname='tags:tag_edit', kwargs={'tag_id': self.test_tag.pk}, + data={ 'label': TEST_TAG_LABEL_EDITED, 'color': TEST_TAG_COLOR_EDITED } ) - def _request_multiple_delete_view(self): + def _request_tag_multiple_delete_view(self): return self.post( viewname='tags:tag_multiple_delete', - data={'id_list': self.tag.pk}, - ) - - def _request_edit_tag_view(self): - return self.post( - viewname='tags:tag_edit', kwargs={'tag_pk': self.tag.pk}, data={ - 'label': TEST_TAG_LABEL_EDITED, 'color': TEST_TAG_COLOR_EDITED - } - ) - - def _request_create_tag_view(self): - return self.post( - viewname='tags:tag_create', data={ - 'label': TEST_TAG_LABEL, - 'color': TEST_TAG_COLOR - } - ) - - def _request_attach_tag_view(self): - return self.post( - viewname='tags:tag_attach', - kwargs={'document_pk': self.document.pk}, data={ - 'tags': self.tag.pk, - 'user': self.user.pk - } - ) - - def _request_multiple_attach_tag_view(self): - return self.post( - viewname='tags:multiple_documents_tag_attach', data={ - 'id_list': self.document.pk, 'tags': self.tag.pk, - 'user': self.user.pk - } - ) - - def _request_single_document_multiple_tag_remove_view(self): - return self.post( - viewname='tags:single_document_multiple_tag_remove', - kwargs={'document_pk': self.document.pk}, data={ - 'id_list': self.document.pk, - 'tags': self.tag.pk, - } - ) - - def _request_multiple_documents_selection_tag_remove_view(self): - return self.post( - viewname='tags:multiple_documents_selection_tag_remove', - data={ - 'id_list': self.document.pk, - 'tags': self.tag.pk, - } + data={'id_list': self.test_tag.pk} ) diff --git a/mayan/apps/tags/tests/test_api.py b/mayan/apps/tags/tests/test_api.py index ec55335692..4c259f2de9 100644 --- a/mayan/apps/tags/tests/test_api.py +++ b/mayan/apps/tags/tests/test_api.py @@ -21,11 +21,7 @@ from .literals import ( from .mixins import TagTestMixin -class TagAPITestCase(TagTestMixin, DocumentTestMixin, BaseAPITestCase): - def setUp(self): - super(TagAPITestCase, self).setUp() - self.login_user() - +class TagAPITestCase(TagTestMixin, BaseAPITestCase): def test_tag_create_view_no_permission(self): response = self._request_api_tag_create_view() self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) @@ -46,284 +42,301 @@ class TagAPITestCase(TagTestMixin, DocumentTestMixin, BaseAPITestCase): self.assertEqual(tag.color, TEST_TAG_COLOR) def test_tag_delete_view_no_access(self): - self._create_tag() + self._create_test_tag() response = self._request_api_tag_delete_view() self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - self.assertTrue(self.tag in Tag.objects.all()) + self.assertTrue(self.test_tag in Tag.objects.all()) self.assertEqual(Tag.objects.all().count(), 1) def test_tag_delete_view_with_access(self): - self._create_tag() - self.grant_access(obj=self.tag, permission=permission_tag_delete) + self._create_test_tag() + self.grant_access(obj=self.test_tag, permission=permission_tag_delete) response = self._request_api_tag_delete_view() self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) self.assertEqual(Tag.objects.all().count(), 0) - def test_tag_edit_via_patch_no_access(self): - self._create_tag() - response = self._request_api_tag_edit_via_patch_view() + def test_tag_edit_patch_view_no_access(self): + self._create_test_tag() + response = self._request_api_tag_edit_patch_view() self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - self.tag.refresh_from_db() - self.assertEqual(self.tag.label, TEST_TAG_LABEL) - self.assertEqual(self.tag.color, TEST_TAG_COLOR) + self.test_tag.refresh_from_db() + self.assertEqual(self.test_tag.label, TEST_TAG_LABEL) + self.assertEqual(self.test_tag.color, TEST_TAG_COLOR) self.assertEqual(Tag.objects.all().count(), 1) - def test_tag_edit_via_patch_with_access(self): - self._create_tag() - self.grant_access(obj=self.tag, permission=permission_tag_edit) - response = self._request_api_tag_edit_via_patch_view() + def test_tag_edit_patch_view_with_access(self): + self._create_test_tag() + self.grant_access(obj=self.test_tag, permission=permission_tag_edit) + response = self._request_api_tag_edit_patch_view() self.assertEqual(response.status_code, status.HTTP_200_OK) - self.tag.refresh_from_db() - self.assertEqual(self.tag.label, TEST_TAG_LABEL_EDITED) - self.assertEqual(self.tag.color, TEST_TAG_COLOR_EDITED) + self.test_tag.refresh_from_db() + self.assertEqual(self.test_tag.label, TEST_TAG_LABEL_EDITED) + self.assertEqual(self.test_tag.color, TEST_TAG_COLOR_EDITED) self.assertEqual(Tag.objects.all().count(), 1) - def test_tag_edit_via_put_no_access(self): - self._create_tag() - response = self._request_api_tag_edit_via_put_view() + def test_tag_edit_put_view_no_access(self): + self._create_test_tag() + response = self._request_api_tag_edit_put_view() self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - self.tag.refresh_from_db() - self.assertEqual(self.tag.label, TEST_TAG_LABEL) - self.assertEqual(self.tag.color, TEST_TAG_COLOR) + self.test_tag.refresh_from_db() + self.assertEqual(self.test_tag.label, TEST_TAG_LABEL) + self.assertEqual(self.test_tag.color, TEST_TAG_COLOR) self.assertEqual(Tag.objects.all().count(), 1) - def test_tag_edit_via_put_with_access(self): - self._create_tag() - self.grant_access(obj=self.tag, permission=permission_tag_edit) - response = self._request_api_tag_edit_via_put_view() + def test_tag_edit_put_view_with_access(self): + self._create_test_tag() + self.grant_access(obj=self.test_tag, permission=permission_tag_edit) + response = self._request_api_tag_edit_put_view() self.assertEqual(response.status_code, status.HTTP_200_OK) - self.tag.refresh_from_db() - self.assertEqual(self.tag.label, TEST_TAG_LABEL_EDITED) - self.assertEqual(self.tag.color, TEST_TAG_COLOR_EDITED) + self.test_tag.refresh_from_db() + self.assertEqual(self.test_tag.label, TEST_TAG_LABEL_EDITED) + self.assertEqual(self.test_tag.color, TEST_TAG_COLOR_EDITED) self.assertEqual(Tag.objects.all().count(), 1) - -class DocumentAPITestCase(TagTestMixin, DocumentTestMixin, BaseAPITestCase): - auto_upload_document = False - - def setUp(self): - super(DocumentAPITestCase, self).setUp() - self.login_user() - - def _request_api_tag_document_list_view(self): - return self.get( - viewname='rest_api:tag-document-list', - kwargs={'tag_pk': self.tag.pk} - ) - - def test_tag_document_list_view_no_access(self): - self._create_tag() - self.document = self.upload_document() - self.tag.documents.add(self.document) - response = self._request_api_tag_document_list_view() - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - - def test_tag_document_list_view_with_tag_access(self): - self._create_tag() - self.document = self.upload_document() - self.tag.documents.add(self.document) - self.grant_access(obj=self.tag, permission=permission_tag_view) - response = self._request_api_tag_document_list_view() + def test_tag_list_view_no_access(self): + self._create_test_tag() + response = self._request_api_tag_list_view() self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data['count'], 0) - def test_tag_document_list_view_with_document_access(self): - self._create_tag() - self.document = self.upload_document() - self.tag.documents.add(self.document) - self.grant_access( - obj=self.document, permission=permission_document_view - ) - response = self._request_api_tag_document_list_view() - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - - def test_tag_document_list_view_with_access(self): - self._create_tag() - self.document = self.upload_document() - self.tag.documents.add(self.document) - self.grant_access(obj=self.tag, permission=permission_tag_view) - self.grant_access( - obj=self.document, permission=permission_document_view - ) - response = self._request_api_tag_document_list_view() + def test_tag_list_view_with_access(self): + self._create_test_tag() + self.grant_access(obj=self.test_tag, permission=permission_tag_view) + response = self._request_api_tag_list_view() self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual( - response.data['results'][0]['uuid'], - force_text(self.document.uuid) - ) + self.assertEqual(response.data['count'], 1) - def _request_api_document_attach_tag_view(self): + +class DocumentTagAPITestCase(TagTestMixin, DocumentTestMixin, BaseAPITestCase): + auto_upload_document = False + + def _request_api_document_tag_attach_view(self): return self.post( viewname='rest_api:document-tag-list', - kwargs={'document_pk': self.document.pk}, - data={'tag_pk': self.tag.pk} + kwargs={'document_id': self.test_document.pk}, + data={'tag_id': self.test_tag.pk} ) - def test_document_attach_tag_view_no_access(self): - self._create_tag() - self.document = self.upload_document() - response = self._request_api_document_attach_tag_view() + def test_document_tag_attach_view_no_access(self): + self._create_test_tag() + self.test_document = self.upload_document() + response = self._request_api_document_tag_attach_view() self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - self.assertTrue(self.tag not in self.document.tags.all()) + self.assertTrue(self.test_tag not in self.test_document.tags.all()) self.assertEqual(Tag.objects.all().count(), 1) - def test_document_attach_tag_view_with_document_access(self): - self._create_tag() - self.document = self.upload_document() - self.grant_access(obj=self.document, permission=permission_tag_attach) - response = self._request_api_document_attach_tag_view() + def test_document_tag_attach_view_with_document_access(self): + self._create_test_tag() + self.test_document = self.upload_document() + self.grant_access(obj=self.test_document, permission=permission_tag_attach) + response = self._request_api_document_tag_attach_view() self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - self.assertTrue(self.tag not in self.document.tags.all()) + self.assertTrue(self.test_tag not in self.test_document.tags.all()) self.assertEqual(Tag.objects.all().count(), 1) - def test_document_attach_tag_view_with_tag_access(self): - self._create_tag() - self.document = self.upload_document() - self.grant_access(obj=self.tag, permission=permission_tag_attach) - response = self._request_api_document_attach_tag_view() + def test_document_tag_attach_view_with_tag_access(self): + self._create_test_tag() + self.test_document = self.upload_document() + self.grant_access(obj=self.test_tag, permission=permission_tag_attach) + response = self._request_api_document_tag_attach_view() self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - self.assertTrue(self.tag not in self.document.tags.all()) + self.assertTrue(self.test_tag not in self.test_document.tags.all()) self.assertEqual(Tag.objects.all().count(), 1) - def test_document_attach_tag_view_with_full_access(self): - self._create_tag() - self.document = self.upload_document() - self.grant_access(obj=self.document, permission=permission_tag_attach) - self.grant_access(obj=self.tag, permission=permission_tag_attach) - response = self._request_api_document_attach_tag_view() + def test_document_tag_attach_view_with_full_access(self): + self._create_test_tag() + self.test_document = self.upload_document() + self.grant_access( + obj=self.test_document, permission=permission_tag_attach + ) + self.grant_access(obj=self.test_tag, permission=permission_tag_attach) + response = self._request_api_document_tag_attach_view() self.assertEqual(response.status_code, status.HTTP_201_CREATED) - self.assertTrue(self.tag in self.document.tags.all()) + self.assertTrue(self.test_tag in self.test_document.tags.all()) self.assertEqual(Tag.objects.all().count(), 1) def _request_api_document_tag_detail_view(self): return self.get( viewname='rest_api:document-tag-detail', kwargs={ - 'document_pk': self.document.pk, 'tag_pk': self.tag.pk + 'document_id': self.test_document.pk, 'tag_id': self.test_tag.pk } ) def test_document_tag_detail_view_no_permission(self): - self._create_tag() - self.document = self.upload_document() - self.tag.documents.add(self.document) + self._create_test_tag() + self.test_document = self.upload_document() + self.test_tag.documents.add(self.test_document) response = self._request_api_document_tag_detail_view() self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) def test_document_tag_detail_view_with_document_access(self): - self._create_tag() - self.document = self.upload_document() - self.tag.documents.add(self.document) + self._create_test_tag() + self.test_document = self.upload_document() + self.test_tag.documents.add(self.test_document) self.grant_access( - obj=self.document, permission=permission_document_view + obj=self.test_document, permission=permission_document_view ) response = self._request_api_document_tag_detail_view() self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) def test_document_tag_detail_view_with_tag_access(self): - self._create_tag() - self.document = self.upload_document() - self.tag.documents.add(self.document) - self.grant_access(obj=self.tag, permission=permission_tag_view) + self._create_test_tag() + self.test_document = self.upload_document() + self.test_tag.documents.add(self.test_document) + self.grant_access(obj=self.test_tag, permission=permission_tag_view) response = self._request_api_document_tag_detail_view() self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) def test_document_tag_detail_view_with_full_access(self): - self._create_tag() - self.document = self.upload_document() - self.tag.documents.add(self.document) - self.grant_access(obj=self.tag, permission=permission_tag_view) + self._create_test_tag() + self.test_document = self.upload_document() + self.test_tag.documents.add(self.test_document) + self.grant_access(obj=self.test_tag, permission=permission_tag_view) self.grant_access( - obj=self.document, permission=permission_tag_view + obj=self.test_document, permission=permission_tag_view ) response = self._request_api_document_tag_detail_view() self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data['label'], self.tag.label) + self.assertEqual(response.data['label'], self.test_tag.label) def _request_api_document_tag_list_view(self): return self.get( viewname='rest_api:document-tag-list', - kwargs={'document_pk': self.document.pk} + kwargs={'document_id': self.test_document.pk} ) def test_document_tag_list_view_no_access(self): - self._create_tag() - self.document = self.upload_document() - self.tag.documents.add(self.document) + self._create_test_tag() + self.test_document = self.upload_document() + self.test_tag.documents.add(self.test_document) response = self._request_api_document_tag_list_view() self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) def test_document_tag_list_view_with_document_access(self): - self._create_tag() - self.document = self.upload_document() - self.tag.documents.add(self.document) - self.grant_access(obj=self.document, permission=permission_tag_view) + self._create_test_tag() + self.test_document = self.upload_document() + self.test_tag.documents.add(self.test_document) + self.grant_access(obj=self.test_document, permission=permission_tag_view) response = self._request_api_document_tag_list_view() self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data['count'], 0) def test_document_tag_list_view_with_tag_access(self): - self._create_tag() - self.document = self.upload_document() - self.tag.documents.add(self.document) - self.grant_access(obj=self.tag, permission=permission_tag_view) + self._create_test_tag() + self.test_document = self.upload_document() + self.test_tag.documents.add(self.test_document) + self.grant_access(obj=self.test_tag, permission=permission_tag_view) response = self._request_api_document_tag_list_view() self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) def test_document_tag_list_view_with_full_access(self): - self._create_tag() - self.document = self.upload_document() - self.tag.documents.add(self.document) - self.grant_access(obj=self.document, permission=permission_tag_view) - self.grant_access(obj=self.tag, permission=permission_tag_view) + self._create_test_tag() + self.test_document = self.upload_document() + self.test_tag.documents.add(self.test_document) + self.grant_access(obj=self.test_document, permission=permission_tag_view) + self.grant_access(obj=self.test_tag, permission=permission_tag_view) response = self._request_api_document_tag_list_view() self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data['results'][0]['label'], self.tag.label) + self.assertEqual( + response.data['results'][0]['label'], self.test_tag.label + ) def _request_api_document_tag_remove_view(self): return self.delete( viewname='rest_api:document-tag-detail', kwargs={ - 'document_pk': self.document.pk, 'tag_pk': self.tag.pk + 'document_id': self.test_document.pk, 'tag_id': self.test_tag.pk } ) def test_document_tag_remove_view_no_access(self): - self._create_tag() - self.document = self.upload_document() - self.tag.documents.add(self.document) + self._create_test_tag() + self.test_document = self.upload_document() + self.test_tag.documents.add(self.test_document) response = self._request_api_document_tag_remove_view() self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - self.assertTrue(self.tag in self.document.tags.all()) + self.assertTrue(self.test_tag in self.test_document.tags.all()) self.assertEqual(Tag.objects.all().count(), 1) def test_document_tag_remove_view_with_document_access(self): - self._create_tag() - self.document = self.upload_document() - self.tag.documents.add(self.document) - self.grant_access(obj=self.document, permission=permission_tag_remove) + self._create_test_tag() + self.test_document = self.upload_document() + self.test_tag.documents.add(self.test_document) + self.grant_access(obj=self.test_document, permission=permission_tag_remove) response = self._request_api_document_tag_remove_view() self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - self.assertTrue(self.tag in self.document.tags.all()) + self.assertTrue(self.test_tag in self.test_document.tags.all()) self.assertEqual(Tag.objects.all().count(), 1) def test_document_tag_remove_view_with_tag_access(self): - self._create_tag() - self.document = self.upload_document() - self.tag.documents.add(self.document) - self.grant_access(obj=self.tag, permission=permission_tag_remove) + self._create_test_tag() + self.test_document = self.upload_document() + self.test_tag.documents.add(self.test_document) + self.grant_access(obj=self.test_tag, permission=permission_tag_remove) response = self._request_api_document_tag_remove_view() self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - self.assertTrue(self.tag in self.document.tags.all()) + self.assertTrue(self.test_tag in self.test_document.tags.all()) self.assertEqual(Tag.objects.all().count(), 1) def test_document_tag_remove_view_with_full_access(self): - self._create_tag() - self.document = self.upload_document() - self.tag.documents.add(self.document) + self._create_test_tag() + self.test_document = self.upload_document() + self.test_tag.documents.add(self.test_document) self.grant_access( - obj=self.document, permission=permission_tag_remove + obj=self.test_document, permission=permission_tag_remove ) - self.grant_access(obj=self.tag, permission=permission_tag_remove) + self.grant_access(obj=self.test_tag, permission=permission_tag_remove) response = self._request_api_document_tag_remove_view() self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) - self.assertFalse(self.tag in self.document.tags.all()) + self.assertFalse(self.test_tag in self.test_document.tags.all()) self.assertEqual(Tag.objects.all().count(), 1) + + +class TagDocumentAPITestCase(TagTestMixin, DocumentTestMixin, BaseAPITestCase): + auto_upload_document = False + + def _request_api_tag_document_list_view(self): + return self.get( + viewname='rest_api:tag-document-list', + kwargs={'tag_id': self.test_tag.pk} + ) + + def test_tag_document_list_view_no_access(self): + self._create_test_tag() + self.test_document = self.upload_document() + self.test_tag.documents.add(self.test_document) + response = self._request_api_tag_document_list_view() + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + def test_tag_document_list_view_with_tag_access(self): + self._create_test_tag() + self.test_document = self.upload_document() + self.test_tag.documents.add(self.test_document) + self.grant_access(obj=self.test_tag, permission=permission_tag_view) + response = self._request_api_tag_document_list_view() + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data['count'], 0) + + def test_tag_document_list_view_with_document_access(self): + self._create_test_tag() + self.test_document = self.upload_document() + self.test_tag.documents.add(self.test_document) + self.grant_access( + obj=self.test_document, permission=permission_document_view + ) + response = self._request_api_tag_document_list_view() + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + def test_tag_document_list_view_with_access(self): + self._create_test_tag() + self.test_document = self.upload_document() + self.test_tag.documents.add(self.test_document) + self.grant_access(obj=self.test_tag, permission=permission_tag_view) + self.grant_access( + obj=self.test_document, permission=permission_document_view + ) + response = self._request_api_tag_document_list_view() + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual( + response.data['results'][0]['uuid'], + force_text(self.test_document.uuid) + ) diff --git a/mayan/apps/tags/tests/test_events.py b/mayan/apps/tags/tests/test_events.py index 33fa2bf95d..46fcce25e8 100644 --- a/mayan/apps/tags/tests/test_events.py +++ b/mayan/apps/tags/tests/test_events.py @@ -10,7 +10,7 @@ from ..permissions import permission_tag_create, permission_tag_edit from .mixins import TagTestMixin - +#TODO: Add tests for event_tag_remove and event_tag_attach class TagEventsTestCase(TagTestMixin, GenericDocumentViewTestCase): def setUp(self): super(TagEventsTestCase, self).setUp() @@ -38,10 +38,10 @@ class TagEventsTestCase(TagTestMixin, GenericDocumentViewTestCase): self.assertEqual(event.verb, event_tag_created.id) self.assertEqual(event.target, tag) - self.assertEqual(event.actor, self.user) + self.assertEqual(event.actor, self._test_case_user) def test_tag_edit_event_no_permissions(self): - self._create_tag() + self._create_test_tag() Action.objects.all().delete() response = self._request_tag_edit_view() @@ -49,11 +49,11 @@ class TagEventsTestCase(TagTestMixin, GenericDocumentViewTestCase): self.assertEqual(Action.objects.count(), 0) def test_tag_edit_event_with_access(self): - self._create_tag() + self._create_test_tag() Action.objects.all().delete() self.grant_access( - permission=permission_tag_edit, obj=self.tag + permission=permission_tag_edit, obj=self.test_tag ) response = self._request_tag_edit_view() @@ -63,5 +63,5 @@ class TagEventsTestCase(TagTestMixin, GenericDocumentViewTestCase): event = Action.objects.first() self.assertEqual(event.verb, event_tag_edited.id) - self.assertEqual(event.target, self.tag) - self.assertEqual(event.actor, self.user) + self.assertEqual(event.target, self.test_tag) + self.assertEqual(event.actor, self._test_case_user) diff --git a/mayan/apps/tags/tests/test_indexing.py b/mayan/apps/tags/tests/test_indexing.py index 415ec251d0..ddd7925e16 100644 --- a/mayan/apps/tags/tests/test_indexing.py +++ b/mayan/apps/tags/tests/test_indexing.py @@ -11,9 +11,10 @@ from .literals import ( TEST_TAG_COLOR, TEST_TAG_LABEL, TEST_TAG_INDEX_HAS_TAG, TEST_TAG_INDEX_NO_TAG, TEST_TAG_INDEX_NODE_TEMPLATE ) +from .mixins import TagTestMixin -class TagSignalIndexingTestCase(DocumentTestMixin, BaseTestCase): +class TagSignalIndexingTestCase(TagTestMixin, DocumentTestMixin, BaseTestCase): auto_upload_document = False def test_tag_indexing(self): @@ -27,7 +28,8 @@ class TagSignalIndexingTestCase(DocumentTestMixin, BaseTestCase): link_documents=True ) - tag = Tag.objects.create(color=TEST_TAG_COLOR, label=TEST_TAG_LABEL) + self._create_test_tag() + self.document = self.upload_document() self.assertTrue( @@ -36,7 +38,7 @@ class TagSignalIndexingTestCase(DocumentTestMixin, BaseTestCase): ).documents.all() ) - tag.documents.add(self.document) + self.test_tag.documents.add(self.document) self.assertTrue( self.document in IndexInstanceNode.objects.get( @@ -44,7 +46,7 @@ class TagSignalIndexingTestCase(DocumentTestMixin, BaseTestCase): ).documents.all() ) - tag.delete() + self.test_tag.delete() self.assertTrue( self.document in IndexInstanceNode.objects.get( diff --git a/mayan/apps/tags/tests/test_links.py b/mayan/apps/tags/tests/test_links.py new file mode 100644 index 0000000000..8ef56f5dcb --- /dev/null +++ b/mayan/apps/tags/tests/test_links.py @@ -0,0 +1,41 @@ +from __future__ import unicode_literals + +from django.urls import reverse + +from mayan.apps.documents.tests import GenericDocumentViewTestCase + +from ..links import link_document_tag_list +from ..permissions import permission_tag_view + +from .mixins import TagTestMixin + + +class DocumentLinksTestCase(TagTestMixin, GenericDocumentViewTestCase): + def _request_document_tag_list_link(self): + self.add_test_view(test_object=self.document) + context = self.get_test_view() + return link_document_tag_list.resolve(context=context) + + def test_document_tag_list_no_permission(self): + self._create_test_tag() + resolved_link = self._request_document_tag_list_link() + self.assertEqual(resolved_link, None) + + def test_document_tag_list_with_full_access(self): + self._create_test_tag() + self.grant_access( + obj=self.document, permission=permission_tag_view + ) + self.grant_access( + obj=self.test_tag, permission=permission_tag_view + ) + resolved_link = self._request_document_tag_list_link() + + self.assertNotEqual(resolved_link, None) + self.assertEqual( + resolved_link.url, + reverse( + viewname='tags:document_tag_list', + kwargs={'document_id': self.document.pk} + ) + ) diff --git a/mayan/apps/tags/tests/test_views.py b/mayan/apps/tags/tests/test_views.py index 7f699357ca..bdfa59e73a 100644 --- a/mayan/apps/tags/tests/test_views.py +++ b/mayan/apps/tags/tests/test_views.py @@ -1,5 +1,7 @@ from __future__ import unicode_literals +from django.utils.encoding import force_text + from mayan.apps.common.tests import GenericViewTestCase from mayan.apps.documents.permissions import permission_document_view from mayan.apps.documents.tests import GenericDocumentViewTestCase @@ -14,21 +16,17 @@ from .literals import ( TEST_TAG_COLOR, TEST_TAG_COLOR_EDITED, TEST_TAG_LABEL, TEST_TAG_LABEL_EDITED ) -from .mixins import TagTestMixin +from .mixins import TagTestMixin, TagViewTestMixin -class TagViewTestCase(TagTestMixin, GenericViewTestCase): +class TagViewTestCase(TagViewTestMixin, TagTestMixin, GenericViewTestCase): def test_tag_create_view_no_permissions(self): - self.login_user() - response = self._request_tag_create_view() self.assertEqual(response.status_code, 403) self.assertEqual(Tag.objects.count(), 0) def test_tag_create_view_with_permissions(self): - self.login_user() - self.grant_permission(permission=permission_tag_create) response = self._request_tag_create_view() self.assertEqual(response.status_code, 302) @@ -39,18 +37,16 @@ class TagViewTestCase(TagTestMixin, GenericViewTestCase): self.assertEqual(tag.color, TEST_TAG_COLOR) def test_tag_delete_view_no_permissions(self): - self.login_user() - self._create_tag() + self._create_test_tag() response = self._request_tag_delete_view() - self.assertEqual(response.status_code, 302) + self.assertEqual(response.status_code, 404) self.assertEqual(Tag.objects.count(), 1) def test_tag_delete_view_with_access(self): - self.login_user() - self._create_tag() + self._create_test_tag() - self.grant_access(obj=self.tag, permission=permission_tag_delete) + self.grant_access(obj=self.test_tag, permission=permission_tag_delete) response = self._request_tag_delete_view() self.assertEqual(response.status_code, 302) @@ -58,190 +54,295 @@ class TagViewTestCase(TagTestMixin, GenericViewTestCase): self.assertEqual(Tag.objects.count(), 0) def test_tag_multiple_delete_view_no_permissions(self): - self.login_user() - self._create_tag() + self._create_test_tag() - response = self._request_multiple_delete_view() - self.assertEqual(response.status_code, 302) + response = self._request_tag_multiple_delete_view() + self.assertEqual(response.status_code, 404) self.assertEqual(Tag.objects.count(), 1) def test_tag_multiple_delete_view_with_access(self): - self.login_user() - self._create_tag() + self._create_test_tag() - self.grant_access(obj=self.tag, permission=permission_tag_delete) + self.grant_access(obj=self.test_tag, permission=permission_tag_delete) - response = self._request_multiple_delete_view() + response = self._request_tag_multiple_delete_view() self.assertEqual(response.status_code, 302) - self.assertEqual(Tag.objects.count(), 0) def test_tag_edit_view_no_permissions(self): - self.login_user() - self._create_tag() + self._create_test_tag() - response = self._request_edit_tag_view() + response = self._request_tag_edit_view() self.assertEqual(response.status_code, 404) - tag = Tag.objects.get(pk=self.tag.pk) + tag = Tag.objects.get(pk=self.test_tag.pk) self.assertEqual(tag.label, TEST_TAG_LABEL) self.assertEqual(tag.color, TEST_TAG_COLOR) def test_tag_edit_view_with_access(self): - self.login_user() - self._create_tag() + self._create_test_tag() - self.grant_access(obj=self.tag, permission=permission_tag_edit) + self.grant_access(obj=self.test_tag, permission=permission_tag_edit) - response = self._request_edit_tag_view() + response = self._request_tag_edit_view() self.assertEqual(response.status_code, 302) - tag = Tag.objects.get(pk=self.tag.pk) + tag = Tag.objects.get(pk=self.test_tag.pk) self.assertEqual(tag.label, TEST_TAG_LABEL_EDITED) self.assertEqual(tag.color, TEST_TAG_COLOR_EDITED) -class TagDocumentsViewTestCase(TagTestMixin, GenericDocumentViewTestCase): - def _request_document_list_view(self): - return self.get(viewname='documents:document_list') +class TagDocumentsViewTestCase(TagViewTestMixin, TagTestMixin, GenericDocumentViewTestCase): + def test_document_tag_attach_view_no_permission(self): + self._create_test_tag() - def test_document_tags_widget_no_permissions(self): - self.login_user() - self._create_tag() + response = self._request_document_tag_multiple_attach_view() + self.assertEqual(response.status_code, 404) + self.assertEqual(self.test_document.tags.count(), 0) - self.tag.documents.add(self.document) - response = self._request_document_list_view() - self.assertNotContains( - response=response, text=TEST_TAG_LABEL, status_code=200 - ) + def test_document_tag_attach_view_with_document_access(self): + self._create_test_tag() - def test_document_tags_widget_with_access(self): - self.login_user() - self._create_tag() - - self.tag.documents.add(self.document) - - self.grant_access(obj=self.tag, permission=permission_tag_view) self.grant_access( - obj=self.document, permission=permission_document_view + obj=self.test_document, permission=permission_tag_attach ) - - response = self._request_document_list_view() + response = self._request_document_tag_multiple_attach_view() self.assertContains( - response=response, text=TEST_TAG_LABEL, status_code=200 + response=response, text=force_text(self.test_document), + status_code=200 + ) + self.assertNotContains( + response=response, text=force_text(self.test_tag), status_code=200 ) - def test_document_attach_tag_view_no_permission(self): - self.login_user() - self._create_tag() + self.assertEqual(self.test_document.tags.count(), 0) - self.assertEqual(self.document.tags.count(), 0) + def test_document_tag_attach_view_with_tag_access(self): + self._create_test_tag() - self.grant_access(obj=self.tag, permission=permission_tag_attach) + self.grant_access(obj=self.test_tag, permission=permission_tag_attach) + response = self._request_document_tag_multiple_attach_view() + self.assertEqual(response.status_code, 404) - response = self._request_attach_tag_view() - # Redirect to previous URL and show warning message about having to - # select at least one object. - self.assertEqual(response.status_code, 302) - self.assertEqual(self.document.tags.count(), 0) + self.assertEqual(self.test_document.tags.count(), 0) - def test_document_attach_tag_view_with_access(self): - self.login_user() - self._create_tag() + def test_document_tag_attach_view_with_full_access(self): + self._create_test_tag() - self.assertEqual(self.document.tags.count(), 0) - - self.grant_access(obj=self.document, permission=permission_tag_attach) - self.grant_access(obj=self.tag, permission=permission_tag_attach) - # permission_tag_view is needed because the form filters the - # choices - self.grant_access(obj=self.tag, permission=permission_tag_view) - - response = self._request_attach_tag_view() + self.grant_access( + obj=self.test_document, permission=permission_tag_attach + ) + self.grant_access(obj=self.test_tag, permission=permission_tag_attach) + response = self._request_document_tag_multiple_attach_view() self.assertEqual(response.status_code, 302) self.assertQuerysetEqual( - self.document.tags.all(), (repr(self.tag),) + self.test_document.tags.all(), (repr(self.test_tag),) ) - def test_document_multiple_attach_tag_view_no_permission(self): - self.login_user() - self._create_tag() - self.grant_permission(permission=permission_tag_view) + def test_document_single_tag_attach_view_with_full_access(self): + """ + Test to make sure only the tag is attached to the selected document + """ + self._create_test_tag() + self.test_document_2 = self.upload_document() - response = self._request_multiple_attach_tag_view() - self.assertEqual(response.status_code, 200) - self.assertEqual(self.document.tags.count(), 0) - - def test_document_multiple_attach_tag_view_with_access(self): - self.login_user() - self._create_tag() - - self.grant_access(obj=self.document, permission=permission_tag_attach) - self.grant_access(obj=self.tag, permission=permission_tag_attach) - - # permission_tag_view is needed because the form filters the - # choices - self.grant_access(obj=self.tag, permission=permission_tag_view) - - response = self._request_multiple_attach_tag_view() + self.grant_access( + obj=self.test_document, permission=permission_tag_attach + ) + self.grant_access( + obj=self.test_document_2, permission=permission_tag_attach + ) + self.grant_access(obj=self.test_tag, permission=permission_tag_attach) + response = self._request_document_tag_multiple_attach_view() self.assertEqual(response.status_code, 302) self.assertQuerysetEqual( - self.document.tags.all(), (repr(self.tag),) + self.test_document.tags.all(), (repr(self.test_tag),) ) - def test_single_document_multiple_tag_remove_view_no_permissions(self): - self.login_user() - self._create_tag() + self.assertEqual(self.test_document_2.tags.count(), 0) - self.document.tags.add(self.tag) + def test_document_multiple_tag_attach_view_no_permission(self): + self._create_test_tag() - self.grant_access(obj=self.tag, permission=permission_tag_view) + response = self._request_document_multiple_tag_multiple_attach_view() + self.assertEqual(response.status_code, 404) + self.assertEqual(self.test_document.tags.count(), 0) - response = self._request_single_document_multiple_tag_remove_view() - self.assertEqual(response.status_code, 200) + def test_document_multiple_tag_attach_view_with_document_access(self): + self._create_test_tag() - self.assertQuerysetEqual(self.document.tags.all(), (repr(self.tag),)) + self.grant_access( + obj=self.test_document, permission=permission_tag_attach + ) - def test_single_document_multiple_tag_remove_view_with_access(self): - self.login_user() - self._create_tag() + response = self._request_document_multiple_tag_multiple_attach_view() - self.document.tags.add(self.tag) + self.assertContains( + response=response, text=force_text(self.test_document), + status_code=200 + ) + self.assertNotContains( + response=response, text=force_text(self.test_tag), status_code=200 + ) - self.grant_access(obj=self.document, permission=permission_tag_remove) - self.grant_access(obj=self.tag, permission=permission_tag_remove) - self.grant_access(obj=self.tag, permission=permission_tag_view) + self.assertEqual(self.test_document.tags.count(), 0) - response = self._request_single_document_multiple_tag_remove_view() + def test_document_multiple_tag_attach_view_with_tag_access(self): + self._create_test_tag() + + self.grant_access(obj=self.test_tag, permission=permission_tag_attach) + + response = self._request_document_multiple_tag_multiple_attach_view() + self.assertEqual(response.status_code, 404) + self.assertEqual(self.test_document.tags.count(), 0) + + def test_document_multiple_tag_attach_view_with_full_access(self): + self._create_test_tag() + + self.grant_access( + obj=self.test_document, permission=permission_tag_attach + ) + self.grant_access(obj=self.test_tag, permission=permission_tag_attach) + + response = self._request_document_multiple_tag_multiple_attach_view() self.assertEqual(response.status_code, 302) - self.assertEqual(self.document.tags.count(), 0) + self.assertQuerysetEqual( + self.test_document.tags.all(), (repr(self.test_tag),) + ) - def test_multiple_documents_selection_tag_remove_view_no_permissions(self): - self.login_user() - self._create_tag() + def test_document_tag_multiple_remove_view_no_permissions(self): + self._create_test_tag() - self.document.tags.add(self.tag) + self.test_document.tags.add(self.test_tag) - self.grant_access(obj=self.tag, permission=permission_tag_view) + response = self._request_document_tag_multiple_remove_view() + self.assertEqual(response.status_code, 404) - response = self._request_multiple_documents_selection_tag_remove_view() - self.assertEqual(response.status_code, 200) + self.assertQuerysetEqual( + self.test_document.tags.all(), (repr(self.test_tag),) + ) - self.assertQuerysetEqual(self.document.tags.all(), (repr(self.tag),)) + def test_document_tag_multiple_remove_view_with_document_access(self): + self._create_test_tag() - def test_multiple_documents_selection_tag_remove_view_with_access(self): - self.login_user() - self._create_tag() + self.test_document.tags.add(self.test_tag) - self.document.tags.add(self.tag) + self.grant_access( + obj=self.test_document, permission=permission_tag_remove + ) + response = self._request_document_tag_multiple_remove_view() + self.assertNotContains( + response=response, text=self.test_tag, status_code=200 + ) + self.assertContains( + response=response, text=self.test_document, status_code=200 + ) - self.grant_access(obj=self.document, permission=permission_tag_remove) - self.grant_access(obj=self.tag, permission=permission_tag_remove) - self.grant_access(obj=self.tag, permission=permission_tag_view) + self.assertEqual(self.test_document.tags.count(), 1) - response = self._request_multiple_documents_selection_tag_remove_view() + def test_document_tag_multiple_remove_view_with_tag_access(self): + self._create_test_tag() + + self.test_document.tags.add(self.test_tag) + + self.grant_access(obj=self.test_tag, permission=permission_tag_remove) + + response = self._request_document_tag_multiple_remove_view() + self.assertEqual(response.status_code, 404) + + self.assertEqual(self.test_document.tags.count(), 1) + + def test_document_tag_multiple_remove_view_with_full_access(self): + self._create_test_tag() + + self.test_document.tags.add(self.test_tag) + + self.grant_access( + obj=self.test_document, permission=permission_tag_remove + ) + self.grant_access(obj=self.test_tag, permission=permission_tag_remove) + + response = self._request_document_tag_multiple_remove_view() self.assertEqual(response.status_code, 302) - self.assertEqual(self.document.tags.count(), 0) + self.assertEqual(self.test_document.tags.count(), 0) + + def test_document_tags_list_no_permissions(self): + self._create_test_tag() + + self.test_tag.documents.add(self.test_document) + + response = self._request_document_tag_list_view() + self.assertNotContains( + response=response, text=force_text(self.test_tag), status_code=404 + ) + + def test_document_tags_list_with_document_access(self): + self._create_test_tag() + + self.test_tag.documents.add(self.test_document) + + self.grant_access( + obj=self.test_document, permission=permission_tag_view + ) + + response = self._request_document_tag_list_view() + self.assertNotContains( + response=response, text=force_text(self.test_tag), status_code=200 + ) + + def test_document_tags_list_with_tag_access(self): + self._create_test_tag() + + self.test_tag.documents.add(self.test_document) + + self.grant_access(obj=self.test_tag, permission=permission_tag_view) + + response = self._request_document_tag_list_view() + self.assertNotContains( + response=response, text=force_text(self.test_tag), status_code=404 + ) + + def test_document_tags_list_with_full_access(self): + self._create_test_tag() + + self.test_tag.documents.add(self.test_document) + + self.grant_access(obj=self.test_tag, permission=permission_tag_view) + self.grant_access( + obj=self.test_document, permission=permission_tag_view + ) + + response = self._request_document_tag_list_view() + self.assertContains( + response=response, text=force_text(self.test_tag), status_code=200 + ) + + def test_document_multiple_tag_remove_view_no_permissions(self): + self._create_test_tag() + + self.test_document.tags.add(self.test_tag) + + response = self._request_document_multiple_tag_multiple_remove_view() + self.assertEqual(response.status_code, 404) + + self.assertQuerysetEqual( + self.test_document.tags.all(), (repr(self.test_tag),) + ) + + def test_document_multiple_tag_remove_view_with_full_access(self): + self._create_test_tag() + + self.test_document.tags.add(self.test_tag) + + self.grant_access( + obj=self.test_document, permission=permission_tag_remove + ) + self.grant_access(obj=self.test_tag, permission=permission_tag_remove) + + response = self._request_document_multiple_tag_multiple_remove_view() + self.assertEqual(response.status_code, 302) + + self.assertEqual(self.test_document.tags.count(), 0) diff --git a/mayan/apps/tags/tests/test_wizard_steps.py b/mayan/apps/tags/tests/test_wizard_steps.py index edc1732ce5..9acfd0971b 100644 --- a/mayan/apps/tags/tests/test_wizard_steps.py +++ b/mayan/apps/tags/tests/test_wizard_steps.py @@ -13,12 +13,12 @@ from mayan.apps.sources.tests.literals import ( from ..models import Tag from .literals import TEST_TAG_COLOR, TEST_TAG_LABEL +from .mixins import TagTestMixin -class TaggedDocumentUploadTestCase(GenericDocumentViewTestCase): +class TaggedDocumentUploadTestCase(TagTestMixin, GenericDocumentViewTestCase): def setUp(self): super(TaggedDocumentUploadTestCase, self).setUp() - self.login_user() self.source = WebFormSource.objects.create( enabled=True, label=TEST_SOURCE_LABEL, uncompress=TEST_SOURCE_UNCOMPRESS_N @@ -34,20 +34,15 @@ class TaggedDocumentUploadTestCase(GenericDocumentViewTestCase): data={ 'document_type_id': self.document_type.pk, 'source-file': file_object, - 'tags': self.tag.pk + 'tags': self.test_tag.pk } ) - def _create_tag(self): - self.tag = Tag.objects.create( - color=TEST_TAG_COLOR, label=TEST_TAG_LABEL - ) - def test_upload_interactive_view_with_access(self): - self._create_tag() + self._create_test_tag() self.grant_access( permission=permission_document_create, obj=self.document_type ) response = self._request_upload_interactive_document_create_view() self.assertEqual(response.status_code, 302) - self.assertTrue(self.tag in Document.objects.first().tags.all()) + self.assertTrue(self.test_tag in Document.objects.first().tags.all()) diff --git a/mayan/apps/tags/urls.py b/mayan/apps/tags/urls.py index 67857d8a46..66ac0b2659 100644 --- a/mayan/apps/tags/urls.py +++ b/mayan/apps/tags/urls.py @@ -2,66 +2,75 @@ from __future__ import unicode_literals from django.conf.urls import url -from .api_views import ( - APIDocumentTagView, APIDocumentTagListView, APITagDocumentListView, - APITagListView, APITagView -) +#from .api_views import ( +# APIDocumentTagView, APIDocumentTagListView, APITagDocumentListView, +# APITagListView, APITagView +#) +from .api_views import DocumentTagViewSet, TagViewSet + from .views import ( DocumentTagListView, TagAttachActionView, TagCreateView, - TagDeleteActionView, TagEditView, TagListView, TagRemoveActionView, - TagTaggedItemListView + TagDeleteActionView, TagDocumentListView, TagEditView, TagListView, + TagRemoveActionView ) urlpatterns = [ - url(regex=r'^tags/list/$', name='tag_list', view=TagListView.as_view()), + url( + regex=r'^documents/(?P\d+)/tags/$', + name='document_tag_list', view=DocumentTagListView.as_view() + ), + url( + regex=r'^documents/(?P\d+)/tags/multiple/attach/$', + name='document_tag_multiple_attach', view=TagAttachActionView.as_view() + ), + url( + regex=r'^documents/(?P\d+)/tags/multiple/remove/$', + name='document_tag_multiple_remove', + view=TagRemoveActionView.as_view() + ), + url( + regex=r'^documents/multiple/attach/$', + name='document_multiple_tag_multiple_attach', + view=TagAttachActionView.as_view() + ), + url( + regex=r'^documents/multiple/tags/remove/$', + name='document_multiple_tag_multiple_remove', + view=TagRemoveActionView.as_view() + ), + url(regex=r'^tags/$', name='tag_list', view=TagListView.as_view()), url( regex=r'^tags/create/$', name='tag_create', view=TagCreateView.as_view() ), url( - regex=r'^tags/(?P\d+)/delete/$', name='tag_delete', + regex=r'^tags/(?P\d+)/delete/$', name='tag_delete', view=TagDeleteActionView.as_view() ), url( - regex=r'^tags/(?P\d+)/edit/$', name='tag_edit', + regex=r'^tags/(?P\d+)/edit/$', name='tag_edit', view=TagEditView.as_view() ), url( - regex=r'^tags/(?P\d+)/documents/$', - name='tag_tagged_item_list', view=TagTaggedItemListView.as_view() + regex=r'^tags/(?P\d+)/documents/$', + name='tag_document_list', view=TagDocumentListView.as_view() ), url( regex=r'^tags/multiple/delete/$', name='tag_multiple_delete', view=TagDeleteActionView.as_view() - ), - url( - regex=r'^tags/multiple/remove/document/(?P\d+)/$', - name='single_document_multiple_tag_remove', - view=TagRemoveActionView.as_view() - ), - url( - regex=r'^tags/multiple/remove/document/multiple/$', - name='multiple_documents_selection_tag_remove', - view=TagRemoveActionView.as_view() - ), - - url( - regex=r'^documents/(?P\d+)/attach/$', - name='tag_attach', view=TagAttachActionView.as_view() - ), - url( - regex=r'^documents/multiple/attach//$', - name='multiple_documents_tag_attach', - view=TagAttachActionView.as_view() - ), - - url( - regex=r'^documents/(?P\d+)/tags/$', name='document_tags', - view=DocumentTagListView.as_view(), - ), + ) ] -api_urls = [ + +api_router_entries = ( + {'prefix': r'tags', 'viewset': TagViewSet, 'basename': 'tag'}, + { + 'prefix': r'documents/(?P\d+)/tags', + 'viewset': DocumentTagViewSet, 'basename': 'document_tag' + }, +) + +""" url( regex=r'^tags/(?P\d+)/documents/$', name='tag-document-list', view=APITagDocumentListView.as_view(), @@ -79,4 +88,4 @@ api_urls = [ regex=r'^documents/(?P\d+)/tags/(?P[0-9]+)/$', name='document-tag-detail', view=APIDocumentTagView.as_view() ), -] +""" diff --git a/mayan/apps/tags/views.py b/mayan/apps/tags/views.py index 1c0e8050f4..d36149333c 100644 --- a/mayan/apps/tags/views.py +++ b/mayan/apps/tags/views.py @@ -9,19 +9,20 @@ from django.urls import reverse_lazy from django.utils.translation import ugettext_lazy as _, ungettext from mayan.apps.acls.models import AccessControlList -from mayan.apps.common.views import ( +from mayan.apps.common.generics import ( MultipleObjectFormActionView, MultipleObjectConfirmActionView, SingleObjectCreateView, SingleObjectEditView, SingleObjectListView ) +from mayan.apps.common.mixins import ExternalObjectMixin from mayan.apps.documents.models import Document from mayan.apps.documents.views import DocumentListView from mayan.apps.documents.permissions import permission_document_view from .forms import TagMultipleSelectionForm from .icons import ( - icon_menu_tags, icon_tag_delete_submit, icon_tag_remove_submit + icon_menu_tags, icon_tag_delete_submit, icon_document_tag_multiple_remove_submit ) -from .links import link_tag_attach, link_tag_create +from .links import link_document_tag_multiple_attach, link_tag_create from .models import Tag from .permissions import ( permission_tag_attach, permission_tag_create, permission_tag_delete, @@ -34,7 +35,7 @@ logger = logging.getLogger(__name__) class TagAttachActionView(MultipleObjectFormActionView): form_class = TagMultipleSelectionForm model = Document - pk_url_kwarg = 'document_pk' + pk_url_kwarg = 'document_id' object_permission = permission_tag_attach success_message = _('Tag attach request performed on %(count)d document') success_message_plural = _( @@ -42,7 +43,7 @@ class TagAttachActionView(MultipleObjectFormActionView): ) def get_extra_context(self): - queryset = self.get_queryset() + queryset = self.get_object_list() result = { 'submit_label': _('Attach'), @@ -66,7 +67,7 @@ class TagAttachActionView(MultipleObjectFormActionView): return result def get_form_extra_kwargs(self): - queryset = self.get_queryset() + queryset = self.get_object_list() result = { 'help_text': _('Tags to be attached.'), 'permission': permission_tag_attach, @@ -85,44 +86,45 @@ class TagAttachActionView(MultipleObjectFormActionView): return result def get_post_action_redirect(self): - queryset = self.get_queryset() + queryset = self.get_object_list() if queryset.count() == 1: return reverse( - viewname='tags:document_tags', kwargs={ - 'document_pk': queryset.first().pk + viewname='tags:document_tag_list', kwargs={ + 'document_id': queryset.first().pk } ) else: return super(TagAttachActionView, self).get_post_action_redirect() def object_action(self, form, instance): - attached_tags = instance.get_tags() + attached_tags = instance.get_tags( + permission=permission_tag_attach, user=self.request.user + ) - for tag in form.cleaned_data['tags']: - AccessControlList.objects.check_access( - obj=tag, permissions=permission_tag_attach, - user=self.request.user - ) + tag_list = AccessControlList.objects.restrict_queryset( + permission=permission_tag_attach, queryset=form.cleaned_data['tags'], + user=self.request.user + ) + for tag in tag_list: if tag in attached_tags: messages.warning( - self.request, _( + message=_( 'Document "%(document)s" is already tagged as ' '"%(tag)s"' ) % { 'document': instance, 'tag': tag - } + }, request=self.request ) else: tag.attach_to(document=instance, user=self.request.user) messages.success( - self.request, - _( + message=_( 'Tag "%(tag)s" attached successfully to document ' '"%(document)s".' ) % { 'document': instance, 'tag': tag - } + }, request=self.request ) @@ -130,7 +132,7 @@ class TagCreateView(SingleObjectCreateView): extra_context = {'title': _('Create tag')} fields = ('label', 'color') model = Tag - post_action_redirect = reverse_lazy('tags:tag_list') + post_action_redirect = reverse_lazy(viewname='tags:tag_list') view_permission = permission_tag_create def get_save_extra_data(self): @@ -139,8 +141,8 @@ class TagCreateView(SingleObjectCreateView): class TagDeleteActionView(MultipleObjectConfirmActionView): model = Tag - pk_url_kwarg = 'tag_pk' - post_action_redirect = reverse_lazy('tags:tag_list') + pk_url_kwarg = 'tag_id' + post_action_redirect = reverse_lazy(viewname='tags:tag_list') object_permission = permission_tag_delete success_message = _('Tag delete request performed on %(count)d tag') success_message_plural = _( @@ -155,9 +157,9 @@ class TagDeleteActionView(MultipleObjectConfirmActionView): 'submit_icon_class': icon_tag_delete_submit, 'submit_label': _('Delete'), 'title': ungettext( - 'Delete the selected tag?', - 'Delete the selected tags?', - queryset.count() + singular='Delete the selected tag?', + plural='Delete the selected tags?', + number=queryset.count() ) } @@ -175,13 +177,14 @@ class TagDeleteActionView(MultipleObjectConfirmActionView): try: instance.delete() messages.success( - self.request, _('Tag "%s" deleted successfully.') % instance + message=_('Tag "%s" deleted successfully.') % instance, + request=self.request ) except Exception as exception: messages.error( - self.request, _('Error deleting tag "%(tag)s": %(error)s') % { + message=_('Error deleting tag "%(tag)s": %(error)s') % { 'tag': instance, 'error': exception - } + }, request=self.request ) @@ -189,9 +192,8 @@ class TagEditView(SingleObjectEditView): fields = ('label', 'color') model = Tag object_permission = permission_tag_edit - object_permission_raise_404 = True - pk_url_kwarg = 'tag_pk' - post_action_redirect = reverse_lazy('tags:tag_list') + pk_url_kwarg = 'tag_id' + post_action_redirect = reverse_lazy(viewname='tags:tag_list') def get_extra_context(self): return { @@ -222,22 +224,24 @@ class TagListView(SingleObjectListView): 'title': _('Tags'), } - def get_object_list(self): - return self.get_tag_queryset() - - def get_tag_queryset(self): + def get_source_queryset(self): + #return self.get_tag_queryset() return Tag.objects.all() + #def get_tag_queryset(self): + # return Tag.objects.all() -class TagTaggedItemListView(DocumentListView): - def get_tag(self): - return get_object_or_404(klass=Tag, pk=self.kwargs['tag_pk']) + +class TagDocumentListView(ExternalObjectMixin, DocumentListView): + external_object_class = Tag + external_object_permission = permission_tag_view + external_object_pk_url_kwarg = 'tag_id' def get_document_queryset(self): - return self.get_tag().documents.all() + return self.get_tag().get_documents(user=self.request.user).all() def get_extra_context(self): - context = super(TagTaggedItemListView, self).get_extra_context() + context = super(TagDocumentListView, self).get_extra_context() context.update( { 'column_class': 'col-xs-12 col-sm-6 col-md-4 col-lg-3', @@ -247,57 +251,56 @@ class TagTaggedItemListView(DocumentListView): ) return context + def get_tag(self): + return self.get_external_object() -class DocumentTagListView(TagListView): - def dispatch(self, request, *args, **kwargs): - self.document = get_object_or_404( - klass=Document, pk=self.kwargs['document_pk'] - ) - AccessControlList.objects.check_access( - permissions=permission_document_view, user=request.user, - obj=self.document - ) - - return super(DocumentTagListView, self).dispatch( - request, *args, **kwargs - ) +class DocumentTagListView(ExternalObjectMixin, TagListView): + external_object_class = Document + external_object_permission = permission_tag_view + external_object_pk_url_kwarg = 'document_id' def get_extra_context(self): context = super(DocumentTagListView, self).get_extra_context() context.update( { 'hide_link': True, - 'no_results_main_link': link_tag_attach.resolve( + 'no_results_main_link': link_document_tag_multiple_attach.resolve( context=RequestContext( - self.request, {'object': self.document} + self.request, {'object': self.get_external_object()} ) ), 'no_results_title': _('Document has no tags attached'), - 'object': self.document, - 'title': _('Tags for document: %s') % self.document, + 'object': self.get_external_object(), + 'title': _( + 'Tags for document: %s' + ) % self.get_external_object(), } ) return context - def get_tag_queryset(self): - return self.document.get_tags().all() + #def get_tag_queryset(self): + def get_source_queryset(self): + return self.get_external_object().get_tags( + permission=permission_tag_view, user=self.request.user + ).all() class TagRemoveActionView(MultipleObjectFormActionView): form_class = TagMultipleSelectionForm model = Document object_permission = permission_tag_remove + pk_url_kwarg = 'document_id' success_message = _('Tag remove request performed on %(count)d document') success_message_plural = _( 'Tag remove request performed on %(count)d documents' ) def get_extra_context(self): - queryset = self.get_queryset() + queryset = self.get_object_list() result = { - 'submit_icon_class': icon_tag_remove_submit, + 'submit_icon_class': icon_document_tag_multiple_remove_submit, 'submit_label': _('Remove'), 'title': ungettext( singular='Remove tags to %(count)d document', @@ -321,7 +324,7 @@ class TagRemoveActionView(MultipleObjectFormActionView): return result def get_form_extra_kwargs(self): - queryset = self.get_queryset() + queryset = self.get_object_list() result = { 'help_text': _('Tags to be removed.'), 'permission': permission_tag_remove, @@ -338,41 +341,43 @@ class TagRemoveActionView(MultipleObjectFormActionView): return result def get_post_action_redirect(self): - queryset = self.get_queryset() + queryset = self.get_object_list() if queryset.count() == 1: return reverse( - viewname='tags:document_tags', kwargs={ - 'document_pk': queryset.first().pk + viewname='tags:document_tag_list', kwargs={ + 'document_id': queryset.first().pk } ) else: return super(TagRemoveActionView, self).get_post_action_redirect() def object_action(self, form, instance): - attached_tags = instance.get_tags() + attached_tags = instance.get_tags( + permission=permission_tag_remove, user=self.request.user + ) - for tag in form.cleaned_data['tags']: - AccessControlList.objects.check_access( - obj=tag, permissions=permission_tag_remove, - user=self.request.user - ) + tag_list = AccessControlList.objects.restrict_queryset( + permission=permission_tag_remove, + queryset=form.cleaned_data['tags'], + user=self.request.user + ) + for tag in tag_list: if tag not in attached_tags: messages.warning( - self.request, _( + message=_( 'Document "%(document)s" wasn\'t tagged as "%(tag)s' ) % { 'document': instance, 'tag': tag - } + }, request=self.request ) else: tag.remove_from(document=instance, user=self.request.user) messages.success( - self.request, - _( + message=_( 'Tag "%(tag)s" removed successfully from document ' '"%(document)s".' ) % { 'document': instance, 'tag': tag - } + }, request=self.request ) diff --git a/mayan/apps/tags/widgets.py b/mayan/apps/tags/widgets.py index 4dc95af64e..2157429dd1 100644 --- a/mayan/apps/tags/widgets.py +++ b/mayan/apps/tags/widgets.py @@ -18,39 +18,11 @@ class TagFormWidget(forms.SelectMultiple): def create_option(self, name, value, label, selected, index, subindex=None, attrs=None): result = super(TagFormWidget, self).create_option( - name=name, value=value, label='{}'.format(conditional_escape(label)), - selected=selected, index=index, subindex=subindex, attrs=attrs + attrs=attrs, index=index, + label='{}'.format(conditional_escape(label)), name=name, + selected=selected, subindex=subindex, value=value ) result['attrs']['data-color'] = self.queryset.get(pk=value).color return result - - -def widget_document_tags(document, user): - """ - A tag widget that displays the tags for the given document - """ - AccessControlList = apps.get_model( - app_label='acls', model_name='AccessControlList' - ) - - result = ['
'] - - tags = AccessControlList.objects.filter_by_access( - permission_tag_view, user, queryset=document.get_tags().all() - ) - - for tag in tags: - result.append(widget_single_tag(tag)) - - result.append('
') - - if tags: - return mark_safe(''.join(result)) - else: - return '' - - -def widget_single_tag(tag): - return render_to_string('tags/tag_widget.html', {'tag': tag}) diff --git a/mayan/apps/tags/wizard_steps.py b/mayan/apps/tags/wizard_steps.py index 84dd5c6055..5abac4bb90 100644 --- a/mayan/apps/tags/wizard_steps.py +++ b/mayan/apps/tags/wizard_steps.py @@ -22,17 +22,10 @@ class WizardStepTags(WizardStep): Tag = apps.get_model(app_label='tags', model_name='Tag') return Tag.objects.exists() - @classmethod - def get_form_kwargs(self, wizard): - return { - 'help_text': _('Tags to be attached.'), - 'user': wizard.request.user - } - @classmethod def done(cls, wizard): result = {} - cleaned_data = wizard.get_cleaned_data_for_step(cls.name) + cleaned_data = wizard.get_cleaned_data_for_step(step=cls.name) if cleaned_data: result['tags'] = [ force_text(tag.pk) for tag in cleaned_data['tags'] @@ -40,13 +33,20 @@ class WizardStepTags(WizardStep): return result + @classmethod + def get_form_kwargs(self, wizard): + return { + 'help_text': _('Tags to be attached.'), + 'user': wizard.request.user + } + @classmethod def step_post_upload_process(cls, document, querystring=None): - furl_instance = furl(querystring) + furl_instance = furl(args=querystring) Tag = apps.get_model(app_label='tags', model_name='Tag') for tag in Tag.objects.filter(pk__in=furl_instance.args.getlist('tags')): tag.documents.add(document) -WizardStep.register(WizardStepTags) +WizardStep.register(step=WizardStepTags) diff --git a/mayan/apps/tags/workflow_actions.py b/mayan/apps/tags/workflow_actions.py index b04aeef0c5..12018f98cd 100644 --- a/mayan/apps/tags/workflow_actions.py +++ b/mayan/apps/tags/workflow_actions.py @@ -38,8 +38,9 @@ class AttachTagAction(WorkflowAction): user = request.user logger.debug('user: %s', user) - queryset = AccessControlList.objects.filter_by_access( - self.permission, user, queryset=Tag.objects.all() + queryset = AccessControlList.objects.restrict_queryset( + permission=self.permission, queryset=Tag.objects.all(), + user=user ) self.fields['tags']['kwargs']['queryset'] = queryset From da638dc7f9dd8ccad51d6aaf3c8b03556f1773d4 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Tue, 29 Jan 2019 04:27:25 -0400 Subject: [PATCH 067/209] Sort class property Signed-off-by: Roberto Rosario --- mayan/apps/sources/wizards.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mayan/apps/sources/wizards.py b/mayan/apps/sources/wizards.py index b279b16029..b4724acacb 100644 --- a/mayan/apps/sources/wizards.py +++ b/mayan/apps/sources/wizards.py @@ -18,8 +18,8 @@ from .icons import icon_wizard_submit class WizardStep(object): - _registry = {} _deregistry = {} + _registry = {} @classmethod def deregister(cls, step): From a64bc61810ed3b67ed36073e69b894cf679e3f01 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Tue, 29 Jan 2019 04:27:50 -0400 Subject: [PATCH 068/209] Allow defining SourceColumns without attributes SourceColums that don't specify an attibute or function will receive the instance itself instead. Signed-off-by: Roberto Rosario --- mayan/apps/navigation/classes.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/mayan/apps/navigation/classes.py b/mayan/apps/navigation/classes.py index b6ec8f9e50..a009827667 100644 --- a/mayan/apps/navigation/classes.py +++ b/mayan/apps/navigation/classes.py @@ -578,10 +578,6 @@ class SourceColumn(object): self.label = None self.views = views or [] self.widget = widget - if not attribute and not func: - raise NavigationError( - 'Must provide either an attribute or a function' - ) self._calculate_label() @@ -636,11 +632,13 @@ class SourceColumn(object): if self.attribute: result = resolve_attribute( - obj=context['object'], attribute=self.attribute, - kwargs=self.kwargs + attribute=self.attribute, kwargs=self.kwargs, + obj=context['object'] ) elif self.func: result = self.func(context=context, **self.kwargs) + else: + result = context['object'] if self.widget: widget_instance = self.widget() From fcfe7686fa38c8eef18cba25e2b27271849568da Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Tue, 29 Jan 2019 04:29:27 -0400 Subject: [PATCH 069/209] Update document transformation links and views Update the URL nomeclature for uniformity. Add document transformation link tests and improve the transformation view tests. Signed-off-by: Roberto Rosario --- mayan/apps/documents/apps.py | 19 ++-- mayan/apps/documents/links.py | 75 ++++++++------- mayan/apps/documents/search.py | 4 +- mayan/apps/documents/tests/mixins.py | 25 ++++- .../documents/tests/test_document_views.py | 24 ++--- mayan/apps/documents/tests/test_links.py | 94 +++++++++++-------- mayan/apps/documents/urls.py | 6 +- 7 files changed, 141 insertions(+), 106 deletions(-) diff --git a/mayan/apps/documents/apps.py b/mayan/apps/documents/apps.py index 2aad41516e..8ce6417d97 100644 --- a/mayan/apps/documents/apps.py +++ b/mayan/apps/documents/apps.py @@ -52,19 +52,19 @@ from .handlers import ( handler_remove_empty_duplicates_lists, handler_scan_duplicates_for ) from .links import ( - link_clear_image_cache, link_document_clear_transformations, - link_document_clone_transformations, link_trashed_document_delete, + link_clear_image_cache, link_trashed_document_delete, link_document_change_type, link_document_download, link_document_duplicates_list, link_document_edit, link_document_favorites_add, link_document_favorites_remove, link_document_list, link_trashed_document_list, link_document_list_favorites, link_document_list_recent_access, link_document_list_recent_added, - link_document_multiple_clear_transformations, - link_trashed_document_multiple_delete, link_document_multiple_change_type, + link_document_multiple_change_type, link_document_multiple_download, link_document_multiple_favorites_add, link_document_multiple_favorites_remove, link_trashed_document_multiple_restore, - link_document_multiple_trash, link_document_multiple_update_page_count, + link_document_multiple_trash, + link_document_multiple_transformations_clear, + link_document_multiple_update_page_count, link_document_page_navigation_first, link_document_page_navigation_last, link_document_page_navigation_next, link_document_page_navigation_previous, link_document_page_return, link_document_page_rotate_left, @@ -82,7 +82,8 @@ from .links import ( link_document_version_return_document, link_document_version_return_list, link_document_version_revert, link_document_version_view, link_duplicated_document_list, link_duplicated_document_scan, - link_trash_can_empty + link_document_transformations_clear, link_document_transformations_clone, + link_trash_can_empty, link_trashed_document_multiple_delete ) from .literals import ( CHECK_DELETE_PERIOD_INTERVAL, CHECK_TRASH_PERIOD_INTERVAL, @@ -509,8 +510,8 @@ class DocumentsApp(MayanAppConfig): link_document_edit, link_document_change_type, link_document_print, link_document_trash, link_document_quick_download, link_document_download, - link_document_clear_transformations, - link_document_clone_transformations, + link_document_transformations_clear, + link_document_transformations_clone, link_document_update_page_count, ), sources=(Document,) ) @@ -550,7 +551,7 @@ class DocumentsApp(MayanAppConfig): links=( link_document_multiple_favorites_add, link_document_multiple_favorites_remove, - link_document_multiple_clear_transformations, + link_document_multiple_transformations_clear, link_document_multiple_trash, link_document_multiple_download, link_document_multiple_update_page_count, link_document_multiple_change_type, diff --git a/mayan/apps/documents/links.py b/mayan/apps/documents/links.py index 218c35180c..af73b91656 100644 --- a/mayan/apps/documents/links.py +++ b/mayan/apps/documents/links.py @@ -101,18 +101,6 @@ link_document_pages = Link( ) # Actions -link_document_clear_transformations = Link( - args='resolved_object.id', permission=permission_transformation_delete, - kwargs={'document_id': 'resolved_object.id'}, - text=_('Clear transformations'), - view='documents:document_clear_transformations' -) -link_document_clone_transformations = Link( - permission=permission_transformation_edit, - kwargs={'document_id': 'resolved_object.id'}, - text=_('Clone transformations'), - view='documents:document_clone_transformations' -) link_document_favorites_add = Link( icon_class=icon_document_favorites_add, kwargs={'document_id': 'resolved_object.id'}, @@ -125,12 +113,6 @@ link_document_favorites_remove = Link( permission=permission_document_view, text=_('Remove from favorites'), view='documents:document_remove_from_favorites' ) -link_document_trash = Link( - icon_class=icon_document_trash, - kwargs={'document_id': 'resolved_object.id'}, - permission=permission_document_trash, tags='dangerous', - text=_('Move to trash'), view='documents:document_trash' -) link_document_edit = Link( icon_class=icon_document_edit, kwargs={'document_id': 'resolved_object.id'}, @@ -148,27 +130,10 @@ link_document_download = Link( permission=permission_document_download, text=_('Advanced download'), view='documents:document_download_form' ) -link_document_print = Link( - icon_class=icon_document_print, - kwargs={'document_id': 'resolved_object.id'}, - permission=permission_document_print, text=_('Print'), - view='documents:document_print' -) -link_document_quick_download = Link( - permission=permission_document_download, - kwargs={'document_id': 'resolved_object.id'}, - text=_('Quick download'), view='documents:document_download' -) -link_document_update_page_count = Link( - kwargs={'document_id': 'resolved_object.id'}, - permission=permission_document_tools, - text=_('Recalculate page count'), - view='documents:document_update_page_count' -) -link_document_multiple_clear_transformations = Link( +link_document_multiple_transformations_clear = Link( permission=permission_transformation_delete, text=_('Clear transformations'), - view='documents:document_multiple_clear_transformations' + view='documents:document_multiple_transformations_clear' ) link_document_multiple_trash = Link( tags='dangerous', text=_('Move to trash'), @@ -194,7 +159,41 @@ link_document_multiple_update_page_count = Link( text=_('Recalculate page count'), view='documents:document_multiple_update_page_count' ) - +link_document_print = Link( + icon_class=icon_document_print, + kwargs={'document_id': 'resolved_object.id'}, + permission=permission_document_print, text=_('Print'), + view='documents:document_print' +) +link_document_quick_download = Link( + permission=permission_document_download, + kwargs={'document_id': 'resolved_object.id'}, + text=_('Quick download'), view='documents:document_download' +) +link_document_transformations_clear = Link( + permission=permission_transformation_delete, + kwargs={'document_id': 'resolved_object.id'}, + text=_('Clear transformations'), + view='documents:document_transformations_clear' +) +link_document_transformations_clone = Link( + permission=permission_transformation_edit, + kwargs={'document_id': 'resolved_object.id'}, + text=_('Clone transformations'), + view='documents:document_transformations_clone' +) +link_document_trash = Link( + icon_class=icon_document_trash, + kwargs={'document_id': 'resolved_object.id'}, + permission=permission_document_trash, tags='dangerous', + text=_('Move to trash'), view='documents:document_trash' +) +link_document_update_page_count = Link( + kwargs={'document_id': 'resolved_object.id'}, + permission=permission_document_tools, + text=_('Recalculate page count'), + view='documents:document_update_page_count' +) link_trashed_document_delete = Link( icon_class=icon_trashed_document_delete, kwargs={'trashed_document_id': 'resolved_object.id'}, diff --git a/mayan/apps/documents/search.py b/mayan/apps/documents/search.py index d8dfe7d53a..9d2a333e00 100644 --- a/mayan/apps/documents/search.py +++ b/mayan/apps/documents/search.py @@ -9,7 +9,7 @@ from .permissions import permission_document_view document_search = SearchModel( app_label='documents', model_name='Document', permission=permission_document_view, - serializer_string='mayan.apps.documents.serializers.DocumentSerializer' + serializer_path='mayan.apps.documents.serializers.DocumentSerializer' ) document_search.add_model_field( @@ -27,7 +27,7 @@ document_search.add_model_field( document_page_search = SearchModel( app_label='documents', model_name='DocumentPageSearchResult', permission=permission_document_view, - serializer_string='mayan.apps.documents.serializers.DocumentPageSerializer' + serializer_path='mayan.apps.documents.serializers.DocumentPageSerializer' ) document_page_search.add_model_field( diff --git a/mayan/apps/documents/tests/mixins.py b/mayan/apps/documents/tests/mixins.py index 8a61720131..52d423578d 100644 --- a/mayan/apps/documents/tests/mixins.py +++ b/mayan/apps/documents/tests/mixins.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals import os +import time from django.conf import settings @@ -19,6 +20,7 @@ class DocumentTestMixin(object): auto_upload_document = True test_document_filename = TEST_SMALL_DOCUMENT_FILENAME test_document_path = None + use_document_stub = False def _create_document_type(self): self.document_type = DocumentType.objects.create( @@ -31,6 +33,16 @@ class DocumentTestMixin(object): """ self.test_document = self.upload_document(*args, **kwargs) + def _create_document_version(self): + # Needed by MySQL as timestamp value doesn't include milliseconds + # resolution + time.sleep(1.01) + + self._calculate_test_document_path() + + with open(self.test_document_path, mode='rb') as file_object: + self.test_document.new_version(file_object=file_object) + def _calculate_test_document_path(self): if not self.test_document_path: self.test_document_path = os.path.join( @@ -46,6 +58,7 @@ class DocumentTestMixin(object): if self.auto_upload_document: self.document = self.upload_document() + self.test_document = self.document def tearDown(self): for document_type in DocumentType.objects.all(): @@ -57,11 +70,17 @@ class DocumentTestMixin(object): document_type = document_type or self.document_type - with open(self.test_document_path, mode='rb') as file_object: - document = document_type.new_document( - file_object=file_object, + if self.use_document_stub: + document = document_type.documents.create( label=filename or self.test_document_filename ) + else: + with open(self.test_document_path, mode='rb') as file_object: + document = document_type.new_document( + file_object=file_object, + label=filename or self.test_document_filename + ) + return document diff --git a/mayan/apps/documents/tests/test_document_views.py b/mayan/apps/documents/tests/test_document_views.py index 0d3420558e..189900c18b 100644 --- a/mayan/apps/documents/tests/test_document_views.py +++ b/mayan/apps/documents/tests/test_document_views.py @@ -314,13 +314,13 @@ class DocumentsViewsTestCase(GenericDocumentViewTestCase): self.assertEqual(response.status_code, 302) self.assertEqual(self.document.pages.count(), page_count) - def _request_document_clear_transformations_view(self): + def _request_document_transformations_clear_view(self): return self.post( - viewname='documents:document_clear_transformations', + viewname='documents:document_transformations_clear', kwargs={'document_id': self.document.pk} ) - def test_document_clear_transformations_view_no_permission(self): + def test_document_transformations_clear_view_no_permission(self): document_page = self.document.pages.first() content_type = ContentType.objects.get_for_model(document_page) transformation = Transformation.objects.create( @@ -338,14 +338,14 @@ class DocumentsViewsTestCase(GenericDocumentViewTestCase): obj=self.document, permission=permission_document_view ) - response = self._request_document_clear_transformations_view() + response = self._request_document_transformations_clear_view() self.assertEqual(response.status_code, 302) self.assertQuerysetEqual( Transformation.objects.get_for_model(document_page), (repr(transformation),) ) - def test_document_clear_transformations_view_with_access(self): + def test_document_transformations_clear_view_with_access(self): document_page = self.document.pages.first() content_type = ContentType.objects.get_for_model(document_page) transformation = Transformation.objects.create( @@ -365,19 +365,19 @@ class DocumentsViewsTestCase(GenericDocumentViewTestCase): obj=self.document, permission=permission_document_view ) - response = self._request_document_clear_transformations_view() + response = self._request_document_transformations_clear_view() self.assertEqual(response.status_code, 302) self.assertEqual( Transformation.objects.get_for_model(document_page).count(), 0 ) - def _request_document_multiple_clear_transformations(self): + def _request_document_multiple_transformations_clear(self): return self.post( - viewname='documents:document_multiple_clear_transformations', + viewname='documents:document_multiple_transformations_clear', data={'id_list': self.document.pk} ) - def test_document_multiple_clear_transformations_view_no_permission(self): + def test_document_multiple_transformations_clear_view_no_permission(self): document_page = self.document.pages.first() content_type = ContentType.objects.get_for_model(document_page) transformation = Transformation.objects.create( @@ -393,14 +393,14 @@ class DocumentsViewsTestCase(GenericDocumentViewTestCase): self.grant_permission(permission=permission_document_view) - response = self._request_document_multiple_clear_transformations() + response = self._request_document_multiple_transformations_clear() self.assertEqual(response.status_code, 302) self.assertQuerysetEqual( Transformation.objects.get_for_model(document_page), (repr(transformation),) ) - def test_document_multiple_clear_transformations_view_with_access(self): + def test_document_multiple_transformations_clear_view_with_access(self): document_page = self.document.pages.first() content_type = ContentType.objects.get_for_model(document_page) transformation = Transformation.objects.create( @@ -421,7 +421,7 @@ class DocumentsViewsTestCase(GenericDocumentViewTestCase): obj=self.document, permission=permission_transformation_delete ) - response = self._request_document_multiple_clear_transformations() + response = self._request_document_multiple_transformations_clear() self.assertEqual(response.status_code, 302) self.assertEqual( Transformation.objects.get_for_model(document_page).count(), 0 diff --git a/mayan/apps/documents/tests/test_links.py b/mayan/apps/documents/tests/test_links.py index c9d8080c8f..ad36dc24dd 100644 --- a/mayan/apps/documents/tests/test_links.py +++ b/mayan/apps/documents/tests/test_links.py @@ -1,54 +1,45 @@ from __future__ import unicode_literals -import time - from django.urls import reverse +from mayan.apps.converter.permissions import ( + permission_transformation_delete, permission_transformation_edit +) + from ..links import ( - link_trashed_document_restore, link_document_version_download, - link_document_version_revert + link_document_transformations_clear, link_document_transformations_clone, + link_document_version_revert, link_trashed_document_restore ) from ..models import TrashedDocument from ..permissions import ( - permission_document_download, permission_trashed_document_restore, - permission_document_version_revert + permission_trashed_document_restore, permission_document_version_revert ) from .base import GenericDocumentViewTestCase -from .literals import TEST_SMALL_DOCUMENT_PATH class DocumentsLinksTestCase(GenericDocumentViewTestCase): - def test_document_version_revert_link_no_permission(self): - with open(TEST_SMALL_DOCUMENT_PATH, mode='rb') as file_object: - self.document.new_version(file_object=file_object) - - self.assertTrue(self.document.versions.count(), 2) + use_document_stub = False + def _resolve_document_version_revert_link(self): self.add_test_view(test_object=self.document.versions.first()) context = self.get_test_view() - resolved_link = link_document_version_revert.resolve(context=context) + return link_document_version_revert.resolve(context=context) + def test_document_version_revert_link_no_permission(self): + self._create_document_version() + + resolved_link = self._resolve_document_version_revert_link() self.assertEqual(resolved_link, None) def test_document_version_revert_link_with_access(self): - # Needed by MySQL as milliseconds value is not store in timestamp - # field - time.sleep(1.01) - - with open(TEST_SMALL_DOCUMENT_PATH, mode='rb') as file_object: - self.document.new_version(file_object=file_object) - - self.assertTrue(self.document.versions.count(), 2) + self._create_document_version() self.grant_access( obj=self.document, permission=permission_document_version_revert ) - self.add_test_view(test_object=self.document.versions.first()) - context = self.get_test_view() - resolved_link = link_document_version_revert.resolve(context=context) - + resolved_link = self._resolve_document_version_revert_link() self.assertNotEqual(resolved_link, None) self.assertEqual( resolved_link.url, @@ -58,34 +49,59 @@ class DocumentsLinksTestCase(GenericDocumentViewTestCase): ) ) - def test_document_version_download_link_no_permission(self): - self.add_test_view(test_object=self.document.latest_version) + def _resolve_document_transformations_clear_link(self): + self.add_test_view(test_object=self.document) context = self.get_test_view() - resolved_link = link_document_version_download.resolve(context=context) + return link_document_transformations_clear.resolve(context=context) + def test_document_transformations_clone_link_no_permission(self): + resolved_link = self._resolve_document_transformations_clear_link() self.assertEqual(resolved_link, None) - def test_document_version_download_link_with_access(self): + def test_document_transformations_clone_link_with_access(self): self.grant_access( - obj=self.document, permission=permission_document_download + obj=self.document, permission=permission_transformation_delete ) - self.add_test_view(test_object=self.document.latest_version) - context = self.get_test_view() - resolved_link = link_document_version_download.resolve(context=context) - + resolved_link = self._resolve_document_transformations_clear_link() self.assertNotEqual(resolved_link, None) self.assertEqual( resolved_link.url, reverse( - viewname='documents:document_version_download_form', - kwargs={'document_version_id': self.document.latest_version.pk} + viewname='documents:document_transformations_clear', + kwargs={'document_id': self.document.pk} + ) + ) + + def _resolve_document_transformations_clone_link(self): + self.add_test_view(test_object=self.document) + context = self.get_test_view() + return link_document_transformations_clone.resolve(context=context) + + def test_document_transformations_clone_link_no_permission(self): + resolved_link = self._resolve_document_transformations_clone_link() + self.assertEqual(resolved_link, None) + + def test_document_transformations_clone_link_with_access(self): + self.grant_access( + obj=self.document, permission=permission_transformation_edit + ) + + resolved_link = self._resolve_document_transformations_clone_link() + self.assertNotEqual(resolved_link, None) + self.assertEqual( + resolved_link.url, + reverse( + viewname='documents:document_transformations_clone', + kwargs={'document_id': self.document.pk} ) ) class DeletedDocumentsLinksTestCase(GenericDocumentViewTestCase): - def _request_trashed_document_restore_link(self): + use_document_stub = True + + def _resolve_trashed_document_restore_link(self): self.add_test_view( test_object=TrashedDocument.objects.get(pk=self.document.pk) ) @@ -95,7 +111,7 @@ class DeletedDocumentsLinksTestCase(GenericDocumentViewTestCase): def test_deleted_document_restore_link_no_permission(self): self.document.delete() - resolved_link = self._request_trashed_document_restore_link() + resolved_link = self._resolve_trashed_document_restore_link() self.assertEqual(resolved_link, None) @@ -106,7 +122,7 @@ class DeletedDocumentsLinksTestCase(GenericDocumentViewTestCase): obj=self.document, permission=permission_trashed_document_restore ) - resolved_link = self._request_trashed_document_restore_link() + resolved_link = self._resolve_trashed_document_restore_link() self.assertNotEqual(resolved_link, None) self.assertEqual( diff --git a/mayan/apps/documents/urls.py b/mayan/apps/documents/urls.py index bc1132fb23..0f593bc352 100644 --- a/mayan/apps/documents/urls.py +++ b/mayan/apps/documents/urls.py @@ -183,12 +183,12 @@ urlpatterns = [ ), url( regex=r'^documents/(?P\d+)/transformations/clear/$', - name='document_clear_transformations', + name='document_transformations_clear', view=DocumentTransformationsClearView.as_view() ), url( regex=r'^documents/(?P\d+)/transformations/clone/$', - name='document_clone_transformations', + name='document_transformations_clone', view=DocumentTransformationsCloneView.as_view() ), url( @@ -222,7 +222,7 @@ urlpatterns = [ ), url( regex=r'^documents/multiple/transformations/clear/$', - name='document_multiple_clear_transformations', + name='document_multiple_transformations_clear', view=DocumentTransformationsClearView.as_view() ), url( From b4188de72749e7e76749b00d694ac0dbed26a56f Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Tue, 29 Jan 2019 04:30:32 -0400 Subject: [PATCH 070/209] Allow passing id_lists from POST requests Normally the MultipleObjectMixin class view only allows id_list from the GET request. This is updated to allow that query from POST requests like those produced by the view tests. Signed-off-by: Roberto Rosario --- mayan/apps/common/mixins.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/mayan/apps/common/mixins.py b/mayan/apps/common/mixins.py index 9de85100e1..9513ddba79 100644 --- a/mayan/apps/common/mixins.py +++ b/mayan/apps/common/mixins.py @@ -284,7 +284,12 @@ class MultipleObjectMixin(SingleObjectMixin): return queryset def get_pk_list(self): - result = self.request.GET.get(self.pk_list_key) + # Accept pk_list even on POST request to allowing direct requests + # to the view bypassing the initial GET request to submit the form. + # Example: when the view is called from a test or a custom UI + result = self.request.GET.get( + self.pk_list_key, self.request.POST.get(self.pk_list_key) + ) if result: return result.split(self.pk_list_separator) @@ -316,7 +321,7 @@ class ObjectActionMixin(object): def view_action(self, form=None): self.action_count = 0 - for instance in self.get_queryset(): + for instance in self.get_object_list(): try: self.object_action(form=form, instance=instance) except PermissionDenied: From 3bd33db0234c7dc2c609510110cb682892a78c2c Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Tue, 29 Jan 2019 04:32:57 -0400 Subject: [PATCH 071/209] Update serializer_string to serializer_path Signed-off-by: Roberto Rosario --- mayan/apps/cabinets/search.py | 2 +- mayan/apps/dynamic_search/classes.py | 6 +++--- mayan/apps/metadata/search.py | 2 +- mayan/apps/permissions/search.py | 2 +- mayan/apps/user_management/search.py | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/mayan/apps/cabinets/search.py b/mayan/apps/cabinets/search.py index 24107d6f69..ed1194341c 100644 --- a/mayan/apps/cabinets/search.py +++ b/mayan/apps/cabinets/search.py @@ -9,7 +9,7 @@ from .permissions import permission_cabinet_view cabinet_search = SearchModel( app_label='cabinets', model_name='Cabinet', permission=permission_cabinet_view, - serializer_string='mayan.apps.cabinets.serializers.CabinetSerializer' + serializer_path='mayan.apps.cabinets.serializers.CabinetSerializer' ) cabinet_search.add_model_field( diff --git a/mayan/apps/dynamic_search/classes.py b/mayan/apps/dynamic_search/classes.py index a37d1a3f21..d49f5af4dc 100644 --- a/mayan/apps/dynamic_search/classes.py +++ b/mayan/apps/dynamic_search/classes.py @@ -90,17 +90,17 @@ class SearchModel(object): except KeyError: raise KeyError(_('No search model matching the query')) if not hasattr(result, 'serializer'): - result.serializer = import_string(result.serializer_string) + result.serializer = import_string(result.serializer_path) return result - def __init__(self, app_label, model_name, serializer_string, label=None, permission=None): + def __init__(self, app_label, model_name, serializer_path, label=None, permission=None): self.app_label = app_label self.model_name = model_name self.search_fields = [] self._model = None # Lazy self._label = label - self.serializer_string = serializer_string + self.serializer_path = serializer_path self.permission = permission self.__class__._registry[self.get_full_name()] = self diff --git a/mayan/apps/metadata/search.py b/mayan/apps/metadata/search.py index ba11ff1ca2..37023357ea 100644 --- a/mayan/apps/metadata/search.py +++ b/mayan/apps/metadata/search.py @@ -9,7 +9,7 @@ from .permissions import permission_metadata_type_view metadata_type_search = SearchModel( app_label='metadata', model_name='MetadataType', permission=permission_metadata_type_view, - serializer_string='mayan.apps.metadata.serializers.MetadataTypeSerializer' + serializer_path='mayan.apps.metadata.serializers.MetadataTypeSerializer' ) metadata_type_search.add_model_field( diff --git a/mayan/apps/permissions/search.py b/mayan/apps/permissions/search.py index 21ac3f9c52..a5aca3473f 100644 --- a/mayan/apps/permissions/search.py +++ b/mayan/apps/permissions/search.py @@ -9,7 +9,7 @@ from .permissions import permission_role_view role_search = SearchModel( app_label='permissions', model_name='Role', permission=permission_role_view, - serializer_string='mayan.apps.permissions.serializers.RoleSerializer' + serializer_path='mayan.apps.permissions.serializers.RoleSerializer' ) role_search.add_model_field( diff --git a/mayan/apps/user_management/search.py b/mayan/apps/user_management/search.py index 54e7a26dc2..6ebf50546b 100644 --- a/mayan/apps/user_management/search.py +++ b/mayan/apps/user_management/search.py @@ -12,7 +12,7 @@ user_app, user_model = settings.AUTH_USER_MODEL.split('.') user_search = SearchModel( app_label=user_app, model_name=user_model, permission=permission_user_view, - serializer_string='mayan.apps.user_management.serializers.UserSerializer' + serializer_path='mayan.apps.user_management.serializers.UserSerializer' ) user_search.add_model_field( @@ -34,7 +34,7 @@ user_search.add_model_field( group_search = SearchModel( app_label='auth', model_name='Group', permission=permission_group_view, - serializer_string='user_management.serializers.GroupSerializer' + serializer_path='user_management.serializers.GroupSerializer' ) group_search.add_model_field( From f65f36336179aa08aa90a75fe7fb778868c8cd67 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Tue, 29 Jan 2019 13:35:10 -0400 Subject: [PATCH 072/209] Refactor user management app Add keyword arguments. Update view resolutions and URL parameters to the '_id' form. Remove code from create and edit subclasses and user the super class error checking. Cache the view object instead of using .get_object() every time. Movernize tests. Update views to comply with MERCs 5 and 6. Split UserTestMixin into mixins for Groups and Users tests. Add super delete and detail tests. Remove redundant superuser filtering from views. Add transactions to views that also commit events. Signed-off-by: Roberto Rosario --- mayan/apps/user_management/api_views.py | 53 ++- mayan/apps/user_management/apps.py | 21 +- mayan/apps/user_management/events.py | 2 +- mayan/apps/user_management/icons.py | 2 +- mayan/apps/user_management/links.py | 37 ++- mayan/apps/user_management/methods.py | 4 +- mayan/apps/user_management/models.py | 5 +- mayan/apps/user_management/permissions.py | 20 +- mayan/apps/user_management/search.py | 3 +- mayan/apps/user_management/serializers.py | 73 ++--- mayan/apps/user_management/tests/__init__.py | 1 + mayan/apps/user_management/tests/literals.py | 20 +- mayan/apps/user_management/tests/mixins.py | 144 ++++++--- mayan/apps/user_management/tests/test_api.py | 297 ++++++++++------- .../apps/user_management/tests/test_events.py | 33 +- .../apps/user_management/tests/test_models.py | 2 +- .../apps/user_management/tests/test_views.py | 301 ++++++++++++++---- mayan/apps/user_management/urls.py | 111 ++++--- mayan/apps/user_management/utils.py | 18 +- mayan/apps/user_management/views.py | 290 +++++++++-------- 20 files changed, 886 insertions(+), 551 deletions(-) diff --git a/mayan/apps/user_management/api_views.py b/mayan/apps/user_management/api_views.py index 2d914424db..47b467fe72 100644 --- a/mayan/apps/user_management/api_views.py +++ b/mayan/apps/user_management/api_views.py @@ -7,6 +7,7 @@ from django.shortcuts import get_object_or_404 from rest_framework import generics from mayan.apps.acls.models import AccessControlList +from mayan.apps.common.mixins import ExternalObjectMixin from mayan.apps.rest_api.filters import MayanObjectPermissionsFilter from mayan.apps.rest_api.permissions import MayanPermission @@ -16,7 +17,7 @@ from .permissions import ( permission_user_edit, permission_user_view ) from .serializers import ( - GroupSerializer, UserSerializer, UserGroupListSerializer + GroupSerializer, UserSerializer#, UserGroupListSerializer ) @@ -53,6 +54,7 @@ class APIGroupView(generics.RetrieveUpdateDestroyAPIView): patch: Partially edit the selected group. put: Edit the selected group. """ + lookup_url_kwarg = 'group_pk' mayan_object_permissions = { 'GET': (permission_group_view,), 'PUT': (permission_group_edit,), @@ -84,6 +86,7 @@ class APIUserView(generics.RetrieveUpdateDestroyAPIView): patch: Partially edit the selected user. put: Edit the selected user. """ + lookup_url_kwarg = 'user_pk' mayan_object_permissions = { 'GET': (permission_user_view,), 'PUT': (permission_user_edit,), @@ -95,16 +98,28 @@ class APIUserView(generics.RetrieveUpdateDestroyAPIView): serializer_class = UserSerializer -class APIUserGroupList(generics.ListCreateAPIView): +class APIUserGroupList(ExternalObjectMixin, generics.ListCreateAPIView): """ get: Returns a list of all the groups to which an user belongs. post: Add a user to a list of groups. """ + external_object_pk_url_kwarg = 'user_pk' + filter_backends = (MayanObjectPermissionsFilter,) mayan_object_permissions = { - 'GET': (permission_user_view,), - 'POST': (permission_user_edit,) + 'GET': (permission_group_view,), + 'POST': (permission_group_edit,) } - permission_classes = (MayanPermission,) + + def get_external_object_permission(self): + if self.request.method == 'POST': + return permission_user_edit + else: + return permission_user_view + + def get_external_object_queryset(self): + return get_user_model().objects.exclude(is_staff=True).exclude( + is_superuser=True + ) def get_serializer(self, *args, **kwargs): if not self.request: @@ -113,10 +128,10 @@ class APIUserGroupList(generics.ListCreateAPIView): return super(APIUserGroupList, self).get_serializer(*args, **kwargs) def get_serializer_class(self): - if self.request.method == 'GET': + if self.request.method == 'POST': + return UserSerializer + else: return GroupSerializer - elif self.request.method == 'POST': - return UserGroupListSerializer def get_serializer_context(self): """ @@ -133,26 +148,10 @@ class APIUserGroupList(generics.ListCreateAPIView): return context def get_queryset(self): - user = self.get_user() - - return AccessControlList.objects.filter_by_access( - permission_group_view, self.request.user, - queryset=user.groups.order_by('id') - ) + return self.get_user().groups.order_by('id') def get_user(self): - if self.request.method == 'GET': - permission = permission_user_view - else: - permission = permission_user_edit - - user = get_object_or_404(klass=get_user_model(), pk=self.kwargs['pk']) - - AccessControlList.objects.check_access( - permissions=(permission,), user=self.request.user, - obj=user - ) - return user + return self.get_external_object() def perform_create(self, serializer): - serializer.save(user=self.get_user(), _user=self.request.user) + return serializer.save(user=self.get_object(), _user=self.request.user) diff --git a/mayan/apps/user_management/apps.py b/mayan/apps/user_management/apps.py index 875fc8066c..2ce6953c21 100644 --- a/mayan/apps/user_management/apps.py +++ b/mayan/apps/user_management/apps.py @@ -40,7 +40,7 @@ from .permissions import ( permission_user_view ) from .search import * # NOQA -from .utils import get_groups, get_users +from .utils import lookup_get_groups, lookup_get_users class UserManagementApp(MayanAppConfig): @@ -65,15 +65,15 @@ class UserManagementApp(MayanAppConfig): MetadataLookup( description=_('All the groups.'), name='groups', - value=get_groups + value=lookup_get_groups ) MetadataLookup( description=_('All the users.'), name='users', - value=get_users + value=lookup_get_users ) ModelEventType.register( - model=User, event_types=(event_user_edited,) + event_types=(event_user_edited,), model=User ) ModelPermission.register( @@ -129,7 +129,9 @@ class UserManagementApp(MayanAppConfig): menu_list_facet.bind_links( links=( - link_acl_list, link_group_members, + link_acl_list, link_events_for_object, + link_object_event_types_user_subcriptions_list, + link_group_members, ), sources=(Group,) ) @@ -138,8 +140,7 @@ class UserManagementApp(MayanAppConfig): link_acl_list, link_events_for_object, link_object_event_types_user_subcriptions_list, link_user_groups, - ), - sources=(User,) + ), sources=(User,) ) menu_multi_item.bind_links( @@ -147,12 +148,10 @@ class UserManagementApp(MayanAppConfig): sources=('user_management:user_list',) ) menu_object.bind_links( - links=(link_group_edit,), - sources=(Group,) + links=(link_group_edit,), sources=(Group,) ) menu_object.bind_links( - links=(link_group_delete,), position=99, - sources=(Group,) + links=(link_group_delete,), position=99, sources=(Group,) ) menu_object.bind_links( links=( diff --git a/mayan/apps/user_management/events.py b/mayan/apps/user_management/events.py index 571e1427aa..2d18763e36 100644 --- a/mayan/apps/user_management/events.py +++ b/mayan/apps/user_management/events.py @@ -5,7 +5,7 @@ from django.utils.translation import ugettext_lazy as _ from mayan.apps.events import EventTypeNamespace namespace = EventTypeNamespace( - name='user_management', label=_('User management') + label=_('User management'), name='user_management' ) event_group_created = namespace.add_event_type( diff --git a/mayan/apps/user_management/icons.py b/mayan/apps/user_management/icons.py index cf942721d5..934894b748 100644 --- a/mayan/apps/user_management/icons.py +++ b/mayan/apps/user_management/icons.py @@ -16,7 +16,7 @@ icon_user_delete = Icon(driver_name='fontawesome', symbol='times') icon_user_edit = Icon(driver_name='fontawesome', symbol='pencil-alt') icon_user_list = Icon(driver_name='fontawesome', symbol='user') icon_user_multiple_delete = icon_user_delete +icon_user_multiple_set_password = Icon(driver_name='fontawesome', symbol='key') icon_user_set_options = Icon(driver_name='fontawesome', symbol='cog') icon_user_set_password = Icon(driver_name='fontawesome', symbol='key') icon_user_setup = Icon(driver_name='fontawesome', symbol='user') -icon_user_multiple_set_password = icon_user_set_password diff --git a/mayan/apps/user_management/links.py b/mayan/apps/user_management/links.py index ed75f17ab8..bab25debcc 100644 --- a/mayan/apps/user_management/links.py +++ b/mayan/apps/user_management/links.py @@ -38,24 +38,23 @@ link_group_create = Link( text=_('Create new group'), view='user_management:group_create' ) link_group_delete = Link( - args='object.id', icon_class=icon_group_delete, + icon_class=icon_group_delete, kwargs={'group_id': 'object.pk'}, permission=permission_group_delete, tags='dangerous', - text=_('Delete'), view='user_management:group_delete', + text=_('Delete'), view='user_management:group_delete' ) link_group_edit = Link( - args='object.id', icon_class=icon_group_edit, + icon_class=icon_group_edit, kwargs={'group_id': 'object.pk'}, permission=permission_group_edit, text=_('Edit'), - view='user_management:group_edit', + view='user_management:group_edit' ) link_group_list = Link( icon_class=icon_group_list, permission=permission_group_view, - text=_('Groups'), - view='user_management:group_list' + text=_('Groups'), view='user_management:group_list' ) link_group_members = Link( - args='object.id', icon_class=icon_group_members, + icon_class=icon_group_members, kwargs={'group_id': 'object.pk'}, permission=permission_group_edit, text=_('Users'), - view='user_management:group_members', + view='user_management:group_members' ) link_group_setup = Link( icon_class=icon_group_setup, permission=permission_group_view, @@ -66,19 +65,19 @@ link_user_create = Link( text=_('Create new user'), view='user_management:user_create' ) link_user_delete = Link( - args='object.id', icon_class=icon_user_delete, + icon_class=icon_user_delete, kwargs={'user_id': 'object.pk'}, permission=permission_user_delete, tags='dangerous', text=_('Delete'), - view='user_management:user_delete', + view='user_management:user_delete' ) link_user_edit = Link( - args='object.id', icon_class=icon_user_edit, + icon_class=icon_user_edit, kwargs={'user_id': 'object.pk'}, permission=permission_user_edit, text=_('Edit'), - view='user_management:user_edit', + view='user_management:user_edit' ) link_user_groups = Link( - args='object.id', condition=condition_is_not_superuser, - icon_class=icon_group, permission=permission_user_edit, - text=_('Groups'), view='user_management:user_groups', + condition=condition_is_not_superuser, icon_class=icon_group, + kwargs={'user_id': 'object.pk'}, permission=permission_user_edit, + text=_('Groups'), view='user_management:user_groups' ) link_user_list = Link( icon_class=icon_user_list, permission=permission_user_view, @@ -95,14 +94,14 @@ link_user_multiple_set_password = Link( view='user_management:user_multiple_set_password' ) link_user_set_options = Link( - args='object.id', icon_class=icon_user_set_options, + icon_class=icon_user_set_options, kwargs={'user_id': 'object.pk'}, permission=permission_user_edit, text=_('User options'), - view='user_management:user_options', + view='user_management:user_options' ) link_user_set_password = Link( - args='object.id', icon_class=icon_user_set_password, + icon_class=icon_user_set_password, kwargs={'user_id': 'object.pk'}, permission=permission_user_edit, text=_('Set password'), - view='user_management:user_set_password', + view='user_management:user_set_password' ) link_user_setup = Link( icon_class=icon_user_setup, permission=permission_user_view, diff --git a/mayan/apps/user_management/methods.py b/mayan/apps/user_management/methods.py index 06baed3e71..23bd71b0a8 100644 --- a/mayan/apps/user_management/methods.py +++ b/mayan/apps/user_management/methods.py @@ -4,4 +4,6 @@ from django.shortcuts import reverse def method_get_absolute_url(self): - return reverse(viewname='user_management:user_details', args=(self.pk,)) + return reverse( + viewname='user_management:user_details', kwargs={'user_id': self.pk} + ) diff --git a/mayan/apps/user_management/models.py b/mayan/apps/user_management/models.py index 878a8c1bc6..a0a98a5713 100644 --- a/mayan/apps/user_management/models.py +++ b/mayan/apps/user_management/models.py @@ -18,8 +18,9 @@ class UserOptions(models.Model): to=settings.AUTH_USER_MODEL, unique=True, verbose_name=_('User') ) block_password_change = models.BooleanField( - default=False, - verbose_name=_('Forbid this user from changing their password.') + default=False, verbose_name=_( + 'Forbid this user from changing their password.' + ) ) objects = UserOptionsManager() diff --git a/mayan/apps/user_management/permissions.py b/mayan/apps/user_management/permissions.py index 944bbc3ef3..08328a0d24 100644 --- a/mayan/apps/user_management/permissions.py +++ b/mayan/apps/user_management/permissions.py @@ -4,29 +4,31 @@ from django.utils.translation import ugettext_lazy as _ from mayan.apps.permissions import PermissionNamespace -namespace = PermissionNamespace(label=_('User management'), name='user_management') +namespace = PermissionNamespace( + label=_('User management'), name='user_management' +) permission_group_create = namespace.add_permission( - name='group_create', label=_('Create new groups') + label=_('Create new groups'), name='group_create' ) permission_group_delete = namespace.add_permission( - name='group_delete', label=_('Delete existing groups') + label=_('Delete existing groups'), name='group_delete' ) permission_group_edit = namespace.add_permission( - name='group_edit', label=_('Edit existing groups') + label=_('Edit existing groups'), name='group_edit' ) permission_group_view = namespace.add_permission( - name='group_view', label=_('View existing groups') + label=_('View existing groups'), name='group_view' ) permission_user_create = namespace.add_permission( - name='user_create', label=_('Create new users') + label=_('Create new users'), name='user_create' ) permission_user_delete = namespace.add_permission( - name='user_delete', label=_('Delete existing users') + label=_('Delete existing users'), name='user_delete' ) permission_user_edit = namespace.add_permission( - name='user_edit', label=_('Edit existing users') + label=_('Edit existing users'), name='user_edit' ) permission_user_view = namespace.add_permission( - name='user_view', label=_('View existing users') + label=_('View existing users'), name='user_view' ) diff --git a/mayan/apps/user_management/search.py b/mayan/apps/user_management/search.py index 6ebf50546b..370b733fb7 100644 --- a/mayan/apps/user_management/search.py +++ b/mayan/apps/user_management/search.py @@ -32,8 +32,7 @@ user_search.add_model_field( ) group_search = SearchModel( - app_label='auth', model_name='Group', - permission=permission_group_view, + app_label='auth', model_name='Group', permission=permission_group_view, serializer_path='user_management.serializers.GroupSerializer' ) diff --git a/mayan/apps/user_management/serializers.py b/mayan/apps/user_management/serializers.py index 559b2c6969..6cd8205d46 100644 --- a/mayan/apps/user_management/serializers.py +++ b/mayan/apps/user_management/serializers.py @@ -11,7 +11,7 @@ from rest_framework.exceptions import ValidationError from mayan.apps.acls.models import AccessControlList -from .permissions import permission_group_view +from .permissions import permission_group_edit, permission_group_view class GroupSerializer(serializers.HyperlinkedModelSerializer): @@ -19,7 +19,10 @@ class GroupSerializer(serializers.HyperlinkedModelSerializer): class Meta: extra_kwargs = { - 'url': {'view_name': 'rest_api:group-detail'} + 'url': { + 'lookup_field': 'pk', 'lookup_url_kwarg': 'group_pk', + 'view_name': 'rest_api:group-detail' + } } fields = ('id', 'name', 'url', 'users_count') model = Group @@ -28,41 +31,13 @@ class GroupSerializer(serializers.HyperlinkedModelSerializer): return instance.user_set.count() -class UserGroupListSerializer(serializers.Serializer): - group_pk_list = serializers.CharField( +class UserSerializer(serializers.HyperlinkedModelSerializer): + groups = GroupSerializer(many=True, read_only=True, required=False) + groups_pk_list = serializers.CharField( help_text=_( 'Comma separated list of group primary keys to assign this ' 'user to.' - ) - ) - - def create(self, validated_data): - validated_data['user'].groups.clear() - try: - pk_list = validated_data['group_pk_list'].split(',') - - for group in Group.objects.filter(pk__in=pk_list): - try: - AccessControlList.objects.check_access( - permissions=(permission_group_view,), - user=self.context['request'].user, obj=group - ) - except PermissionDenied: - pass - else: - validated_data['user'].groups.add(group) - except Exception as exception: - raise ValidationError(exception) - - return {'group_pk_list': validated_data['group_pk_list']} - - -class UserSerializer(serializers.HyperlinkedModelSerializer): - groups = GroupSerializer(many=True, read_only=True) - groups_pk_list = serializers.CharField( - help_text=_( - 'List of group primary keys to which to add the user.' - ), required=False + ), required=False, write_only=True ) password = serializers.CharField( required=False, style={'input_type': 'password'}, write_only=True @@ -70,7 +45,10 @@ class UserSerializer(serializers.HyperlinkedModelSerializer): class Meta: extra_kwargs = { - 'url': {'view_name': 'rest_api:user-detail'} + 'url': { + 'lookup_field': 'pk', 'lookup_url_kwarg': 'user_pk', + 'view_name': 'rest_api:user-detail' + } } fields = ( 'first_name', 'date_joined', 'email', 'groups', 'groups_pk_list', @@ -81,13 +59,19 @@ class UserSerializer(serializers.HyperlinkedModelSerializer): read_only_fields = ('groups', 'is_active', 'last_login', 'date_joined') write_only_fields = ('password', 'group_pk_list') - def _add_groups(self, instance): - instance.groups.add( - *Group.objects.filter(pk__in=self.groups_pk_list.split(',')) + def _add_groups(self, instance, groups_pk_list): + instance.groups.clear() + + queryset = AccessControlList.objects.restrict_queryset( + permission=permission_group_edit, + queryset=Group.objects.filter(pk__in=groups_pk_list.split(',')), + user=self.context['request'].user ) + instance.groups.add(*queryset) + def create(self, validated_data): - self.groups_pk_list = validated_data.pop('groups_pk_list', '') + groups_pk_list = validated_data.pop('groups_pk_list', '') password = validated_data.pop('password', None) instance = super(UserSerializer, self).create(validated_data) @@ -95,13 +79,13 @@ class UserSerializer(serializers.HyperlinkedModelSerializer): instance.set_password(password) instance.save() - if self.groups_pk_list: - self._add_groups(instance=instance) + if groups_pk_list: + self._add_groups(instance=instance, groups_pk_list=groups_pk_list) return instance def update(self, instance, validated_data): - self.groups_pk_list = validated_data.pop('groups_pk_list', '') + groups_pk_list = validated_data.pop('groups_pk_list', '') if 'password' in validated_data: instance.set_password(validated_data['password']) @@ -109,9 +93,8 @@ class UserSerializer(serializers.HyperlinkedModelSerializer): instance = super(UserSerializer, self).update(instance, validated_data) - if self.groups_pk_list: - instance.groups.clear() - self._add_groups(instance=instance) + if groups_pk_list: + self._add_groups(instance=instance, groups_pk_list=groups_pk_list) return instance diff --git a/mayan/apps/user_management/tests/__init__.py b/mayan/apps/user_management/tests/__init__.py index 124ec460a6..33ff9e78ba 100644 --- a/mayan/apps/user_management/tests/__init__.py +++ b/mayan/apps/user_management/tests/__init__.py @@ -1 +1,2 @@ from .literals import * # NOQA +from .mixins import * # NOQA diff --git a/mayan/apps/user_management/tests/literals.py b/mayan/apps/user_management/tests/literals.py index 0cc6ddadc8..9b9b7db24a 100644 --- a/mayan/apps/user_management/tests/literals.py +++ b/mayan/apps/user_management/tests/literals.py @@ -5,26 +5,28 @@ __all__ = ( 'TEST_USER_PASSWORD', 'TEST_USER_PASSWORD_EDITED', 'TEST_USER_USERNAME' ) -TEST_CASE_ADMIN_EMAIL = 'admin@example.com' -TEST_CASE_ADMIN_PASSWORD = 'test admin password' -TEST_CASE_ADMIN_USERNAME = 'test_admin' +TEST_CASE_ADMIN_EMAIL = 'case_admin@example.com' +TEST_CASE_ADMIN_PASSWORD = 'test case admin password' +TEST_CASE_ADMIN_USERNAME = 'test_case_admin' -TEST_CASE_GROUP_NAME = 'test group' -TEST_CASE_USER_EMAIL = 'user@example.com' -TEST_CASE_USER_PASSWORD = 'test user password' -TEST_CASE_USER_USERNAME = 'test_user' +TEST_CASE_GROUP_NAME = 'test case group' + +TEST_CASE_USER_EMAIL = 'test_case_user@example.com' +TEST_CASE_USER_PASSWORD = 'test case user password' +TEST_CASE_USER_USERNAME = 'test_case_user' TEST_GROUP_NAME = 'test group' TEST_GROUP_NAME_EDITED = 'test group edited' TEST_GROUP_2_NAME = 'test group 2' TEST_GROUP_2_NAME_EDITED = 'test group 2 edited' -TEST_USER_EMAIL = 'user@example.com' + +TEST_USER_EMAIL = 'testuser@example.com' TEST_USER_PASSWORD = 'test user password' TEST_USER_PASSWORD_EDITED = 'test user password edited' TEST_USER_USERNAME = 'test_user' TEST_USER_USERNAME_EDITED = 'test_user_edited' -TEST_USER_2_EMAIL = 'user2@example.com' +TEST_USER_2_EMAIL = 'testuser2@example.com' TEST_USER_2_PASSWORD = 'test user 2 password' TEST_USER_2_PASSWORD_EDITED = 'test user 2 password edited' TEST_USER_2_USERNAME = 'test_user_2' diff --git a/mayan/apps/user_management/tests/mixins.py b/mayan/apps/user_management/tests/mixins.py index 81643866ef..cd2c26ec43 100644 --- a/mayan/apps/user_management/tests/mixins.py +++ b/mayan/apps/user_management/tests/mixins.py @@ -5,97 +5,117 @@ from django.contrib.auth.models import Group from .literals import ( TEST_CASE_ADMIN_EMAIL, TEST_CASE_ADMIN_PASSWORD, TEST_CASE_ADMIN_USERNAME, - TEST_CASE_GROUP_NAME, TEST_CASE_USER_EMAIL, TEST_CASE_USER_PASSWORD, - TEST_CASE_USER_USERNAME, TEST_GROUP_NAME, TEST_GROUP_2_NAME, - TEST_GROUP_2_NAME_EDITED, TEST_USER_2_EMAIL, TEST_USER_2_PASSWORD, - TEST_USER_EMAIL, TEST_USER_USERNAME, TEST_USER_PASSWORD, - TEST_USER_2_USERNAME, TEST_USER_2_USERNAME_EDITED + TEST_CASE_GROUP_NAME, TEST_GROUP_NAME_EDITED, TEST_CASE_USER_EMAIL, + TEST_CASE_USER_PASSWORD, TEST_CASE_USER_USERNAME, TEST_GROUP_NAME, + TEST_USER_EMAIL, TEST_USER_USERNAME, TEST_USER_USERNAME_EDITED, + TEST_USER_PASSWORD ) +__all__ = ('GroupTestMixin', 'UserTestCaseMixin', 'UserTestMixin') + class UserTestCaseMixin(object): - auto_login_admin = False + """ + This TestCaseMixin is used to create an user and group to execute the + test case, these are used to just create an identity which is required by + most of the code in the project, these are not meant to be acted upon + (edited, deleted, etc). To create a test users or groups to modify, use + the UserTestMixin instead and the respective test_user and test_group. + The user and group created by this mixin will be prepended with + _test_case_{...}. The _test_case_user and _test_case_group are meant + to be used by other test case mixins like the ACLs test case mixin which + adds shorthand methods to create ACL entries to test access control. + """ + auto_login_superuser = False auto_login_user = True + create_test_case_superuser = False + create_test_case_user = True def setUp(self): super(UserTestCaseMixin, self).setUp() - if self.auto_login_user: - self._test_case_user = get_user_model().objects.create_user( - username=TEST_CASE_USER_USERNAME, email=TEST_CASE_USER_EMAIL, - password=TEST_CASE_USER_PASSWORD - ) - self.login_user() - self._test_case_group = Group.objects.create(name=TEST_GROUP_NAME) + if self.create_test_case_user: + self._create_test_case_user() + self._create_test_case_group() self._test_case_group.user_set.add(self._test_case_user) - elif self.auto_login_admin: - self._test_case_admin_user = get_user_model().objects.create_superuser( - username=TEST_CASE_ADMIN_USERNAME, email=TEST_CASE_ADMIN_EMAIL, - password=TEST_CASE_ADMIN_PASSWORD - ) - self.login_admin_user() + + if self.auto_login_user: + self.login_user() + + elif self.create_test_case_superuser: + self._create_test_case_superuser() + + if self.auto_login_superuser: + self.login_superuser() def tearDown(self): self.client.logout() super(UserTestCaseMixin, self).tearDown() + def _create_test_case_group(self): + self._test_case_group = Group.objects.create(name=TEST_CASE_GROUP_NAME) + + def _create_test_case_superuser(self): + self._test_case_superuser = get_user_model().objects.create_superuser( + username=TEST_CASE_ADMIN_USERNAME, email=TEST_CASE_ADMIN_EMAIL, + password=TEST_CASE_ADMIN_PASSWORD + ) + + def _create_test_case_user(self): + self._test_case_user = get_user_model().objects.create_user( + username=TEST_CASE_USER_USERNAME, email=TEST_CASE_USER_EMAIL, + password=TEST_CASE_USER_PASSWORD + ) + def login(self, *args, **kwargs): logged_in = self.client.login(*args, **kwargs) return logged_in - def login_user(self): - self.login( - username=TEST_CASE_USER_USERNAME, password=TEST_CASE_USER_PASSWORD - ) - - def login_admin_user(self): + def login_superuser(self): self.login( username=TEST_CASE_ADMIN_USERNAME, password=TEST_CASE_ADMIN_PASSWORD ) + def login_user(self): + self.login( + username=TEST_CASE_USER_USERNAME, password=TEST_CASE_USER_PASSWORD + ) + def logout(self): self.client.logout() -class UserTestMixin(object): +class GroupTestMixin(object): def _create_test_group(self): - self.test_group = Group.objects.create(name=TEST_GROUP_2_NAME) + self.test_group = Group.objects.create(name=TEST_GROUP_NAME) def _edit_test_group(self): - self.test_group.name = TEST_GROUP_2_NAME_EDITED + self.test_group.name = TEST_GROUP_NAME_EDITED self.test_group.save() - def _create_test_user(self): - self.test_user = get_user_model().objects.create( - username=TEST_USER_2_USERNAME, email=TEST_USER_2_EMAIL, - password=TEST_USER_2_PASSWORD - ) - - # Group views - def _request_test_group_create_view(self): reponse = self.post( viewname='user_management:group_create', data={ - 'name': TEST_GROUP_2_NAME + 'name': TEST_GROUP_NAME } ) - self.test_group = Group.objects.filter(name=TEST_GROUP_2_NAME).first() + self.test_group = Group.objects.filter(name=TEST_GROUP_NAME).first() return reponse def _request_test_group_delete_view(self): return self.post( viewname='user_management:group_delete', kwargs={ - 'group_pk': self.test_group.pk + 'group_id': self.test_group.pk } ) def _request_test_group_edit_view(self): return self.post( viewname='user_management:group_edit', kwargs={ - 'group_pk': self.test_group.pk + 'group_id': self.test_group.pk }, data={ - 'name': TEST_GROUP_2_NAME_EDITED + 'name': TEST_GROUP_NAME_EDITED } ) @@ -105,41 +125,65 @@ class UserTestMixin(object): def _request_test_group_members_view(self): return self.get( viewname='user_management:group_members', - kwargs={'group_pk': self.test_group.pk} + kwargs={'group_id': self.test_group.pk} ) - # User views + +class UserTestMixin(object): + def _create_test_superuser(self): + self.test_superuser = get_user_model().objects.create_superuser( + username=TEST_CASE_ADMIN_USERNAME, email=TEST_CASE_ADMIN_EMAIL, + password=TEST_CASE_ADMIN_PASSWORD + ) + + def _create_test_user(self): + self.test_user = get_user_model().objects.create( + username=TEST_USER_USERNAME, email=TEST_USER_EMAIL, + password=TEST_USER_PASSWORD + ) + + def _request_test_superuser_delete_view(self): + return self.post( + viewname='user_management:user_delete', + kwargs={'user_id': self.test_superuser.pk} + ) + + def _request_test_superuser_detail_view(self): + return self.get( + viewname='user_management:user_details', + kwargs={'user_id': self.test_superuser.pk} + ) def _request_test_user_create_view(self): reponse = self.post( viewname='user_management:user_create', data={ - 'username': TEST_USER_2_USERNAME, - 'password': TEST_USER_2_PASSWORD + 'username': TEST_USER_USERNAME, + 'password': TEST_USER_PASSWORD } ) self.test_user = get_user_model().objects.filter( - username=TEST_USER_2_USERNAME + username=TEST_USER_USERNAME ).first() return reponse def _request_test_user_delete_view(self): return self.post( viewname='user_management:user_delete', - kwargs={'user_pk': self.test_user.pk} + kwargs={'user_id': self.test_user.pk} ) def _request_test_user_edit_view(self): return self.post( viewname='user_management:user_edit', kwargs={ - 'user_pk': self.test_user.pk + 'user_id': self.test_user.pk }, data={ - 'username': TEST_USER_2_USERNAME_EDITED + 'username': TEST_USER_USERNAME_EDITED } ) def _request_test_user_groups_view(self): return self.get( viewname='user_management:user_groups', - kwargs={'user_pk': self.test_user.pk} + kwargs={'user_id': self.test_user.pk} ) diff --git a/mayan/apps/user_management/tests/test_api.py b/mayan/apps/user_management/tests/test_api.py index e1cb7cc181..d30e474828 100644 --- a/mayan/apps/user_management/tests/test_api.py +++ b/mayan/apps/user_management/tests/test_api.py @@ -22,9 +22,9 @@ from .literals import ( from .mixins import UserTestMixin -class UserManagementUserAPITestCase(UserTestMixin, BaseAPITestCase): +class UserAPITestCase(UserTestMixin, BaseAPITestCase): def setUp(self): - super(UserManagementUserAPITestCase, self).setUp() + super(UserAPITestCase, self).setUp() self.login_user() def _request_api_test_user_create(self): @@ -54,22 +54,25 @@ class UserManagementUserAPITestCase(UserTestMixin, BaseAPITestCase): viewname='rest_api:user-list', data={ 'email': TEST_USER_2_EMAIL, 'password': TEST_USER_2_PASSWORD, 'username': TEST_USER_2_USERNAME, - 'groups_pk_list': self.test_groups_pk_list + 'groups_id_list': self.test_groups_id_list } ) + """ def test_user_create_with_group_no_permission(self): self._create_test_group() - self.test_groups_pk_list = '{}'.format(self.test_group.pk) + self.test_groups_id_list = '{}'.format(self.test_group.pk) response = self._request_api_create_test_user_with_extra_data() self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - def test_user_create_with_group_with_permission(self): + def test_user_create_with_group_with_user_access(self): self._create_test_group() - self.test_groups_pk_list = '{}'.format(self.test_group.pk) + self.test_groups_id_list = '{}'.format(self.test_group.pk) - self.grant_permission(permission=permission_user_create) + self.grant_access( + obj=self.test_user, permission=permission_user_create + ) response = self._request_api_create_test_user_with_extra_data() self.assertEqual(response.status_code, status.HTTP_201_CREATED) @@ -78,22 +81,57 @@ class UserManagementUserAPITestCase(UserTestMixin, BaseAPITestCase): self.assertEqual(user.username, TEST_USER_2_USERNAME) self.assertQuerysetEqual(user.groups.all(), (repr(self.test_group),)) + + def test_user_create_with_group_with_user_access(self): + self._create_test_group() + self.test_groups_id_list = '{}'.format(self.test_group.pk) + + self.grant_access( + obj=self.test_user, permission=permission_user_create + ) + response = self._request_api_create_test_user_with_extra_data() + + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + user = get_user_model().objects.get(pk=response.data['id']) + self.assertEqual(user.username, TEST_USER_2_USERNAME) + self.assertQuerysetEqual(user.groups.all(), (repr(self.test_group),)) + """ + def test_user_create_with_groups_no_permission(self): group_1 = Group.objects.create(name='test group 1') group_2 = Group.objects.create(name='test group 2') - self.test_groups_pk_list = '{},{}'.format(group_1.pk, group_2.pk) + self.test_groups_id_list = '{},{}'.format(group_1.pk, group_2.pk) response = self._request_api_create_test_user_with_extra_data() self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - def test_user_create_with_groups_with_permission(self): + def test_user_create_with_groups_with_user_permission(self): group_1 = Group.objects.create(name='test group 1') group_2 = Group.objects.create(name='test group 2') - self.test_groups_pk_list = '{},{}'.format(group_1.pk, group_2.pk) + self.test_groups_id_list = '{},{}'.format(group_1.pk, group_2.pk) self.grant_permission(permission=permission_user_create) response = self._request_api_create_test_user_with_extra_data() self.assertEqual(response.status_code, status.HTTP_201_CREATED) + user = get_user_model().objects.get(pk=response.data['id']) + self.assertEqual(user.username, TEST_USER_2_USERNAME) + #self.assertQuerysetEqual( + # user.groups.all().order_by('name'), (repr(group_1), repr(group_2)) + #) + self.assertEqual(user.groups.count(), 0) + + def test_user_create_with_groups_with_full_access(self): + group_1 = Group.objects.create(name='test group 1') + group_2 = Group.objects.create(name='test group 2') + self.test_groups_id_list = '{},{}'.format(group_1.pk, group_2.pk) + self.grant_permission(permission=permission_user_create) + self.grant_access(obj=group_1, permission=permission_group_edit) + self.grant_access(obj=group_2, permission=permission_group_edit) + response = self._request_api_create_test_user_with_extra_data() + + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + user = get_user_model().objects.get(pk=response.data['id']) self.assertEqual(user.username, TEST_USER_2_USERNAME) self.assertQuerysetEqual( @@ -105,10 +143,6 @@ class UserManagementUserAPITestCase(UserTestMixin, BaseAPITestCase): def test_user_create_login(self): self._create_test_user() - self.login( - username=TEST_USER_2_USERNAME, password=TEST_USER_2_PASSWORD - ) - self.assertTrue( self.login( username=TEST_USER_2_USERNAME, password=TEST_USER_2_PASSWORD @@ -117,15 +151,18 @@ class UserManagementUserAPITestCase(UserTestMixin, BaseAPITestCase): # User password change - def test_user_create_login_password_change_no_access(self): - self._create_test_user() - - self.patch( - viewname='rest_api:user-detail', args=(self.test_user.pk,), data={ + def _request_api_user_password_change(self): + return self.patch( + viewname='rest_api:user-detail', + kwargs={'user_id': self.test_user.pk}, data={ 'password': TEST_USER_2_PASSWORD_EDITED, } ) + def test_user_create_login_password_change_no_access(self): + self._create_test_user() + self._request_api_user_password_change() + self.assertFalse( self.client.login( username=TEST_USER_2_USERNAME, @@ -136,12 +173,8 @@ class UserManagementUserAPITestCase(UserTestMixin, BaseAPITestCase): def test_user_create_login_password_change_with_access(self): self._create_test_user() - self.grant_access(permission=permission_user_edit, obj=self.test_user) - self.patch( - viewname='rest_api:user-detail', args=(self.test_user.pk,), data={ - 'password': TEST_USER_2_PASSWORD_EDITED, - } - ) + self.grant_access(obj=self.test_user, permission=permission_user_edit) + self._request_api_user_password_change() self.assertTrue( self.client.login( @@ -154,7 +187,8 @@ class UserManagementUserAPITestCase(UserTestMixin, BaseAPITestCase): def _request_api_test_user_edit_via_put(self): return self.put( - viewname='rest_api:user-detail', args=(self.test_user.pk,), + viewname='rest_api:user-detail', + kwargs={'user_id': self.test_user.pk}, data={'username': TEST_USER_2_USERNAME_EDITED} ) @@ -162,14 +196,14 @@ class UserManagementUserAPITestCase(UserTestMixin, BaseAPITestCase): self._create_test_user() response = self._request_api_test_user_edit_via_put() - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) self.test_user.refresh_from_db() self.assertEqual(self.test_user.username, TEST_USER_2_USERNAME) def test_user_edit_via_put_with_access(self): self._create_test_user() - self.grant_access(permission=permission_user_edit, obj=self.test_user) + self.grant_access(obj=self.test_user, permission=permission_user_edit) response = self._request_api_test_user_edit_via_put() self.assertEqual(response.status_code, status.HTTP_200_OK) @@ -179,7 +213,8 @@ class UserManagementUserAPITestCase(UserTestMixin, BaseAPITestCase): def _request_api_test_user_edit_via_patch(self): return self.patch( - viewname='rest_api:user-detail', args=(self.test_user.pk,), + viewname='rest_api:user-detail', + kwargs={'user_id': self.test_user.pk}, data={'username': TEST_USER_2_USERNAME_EDITED} ) @@ -187,14 +222,14 @@ class UserManagementUserAPITestCase(UserTestMixin, BaseAPITestCase): self._create_test_user() response = self._request_api_test_user_edit_via_patch() - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) self.test_user.refresh_from_db() self.assertEqual(self.test_user.username, TEST_USER_2_USERNAME) def test_user_edit_via_patch_with_access(self): self._create_test_user() - self.grant_access(permission=permission_user_edit, obj=self.test_user) + self.grant_access(obj=self.test_user, permission=permission_user_edit) response = self._request_api_test_user_edit_via_patch() self.assertEqual(response.status_code, status.HTTP_200_OK) @@ -204,8 +239,9 @@ class UserManagementUserAPITestCase(UserTestMixin, BaseAPITestCase): def _request_api_test_user_edit_via_patch_with_extra_data(self): return self.patch( - viewname='rest_api:user-detail', args=(self.test_user.pk,), - data={'groups_pk_list': '{}'.format(self.test_group.pk)} + viewname='rest_api:user-detail', + kwargs={'user_id': self.test_user.pk}, + data={'groups_id_list': '{}'.format(self.test_group.pk)} ) def test_user_edit_add_groups_via_patch_no_access(self): @@ -214,7 +250,7 @@ class UserManagementUserAPITestCase(UserTestMixin, BaseAPITestCase): response = self._request_api_test_user_edit_via_patch_with_extra_data() - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) self.test_user.refresh_from_db() self.assertEqual(self.test_user.username, TEST_USER_2_USERNAME) @@ -226,7 +262,8 @@ class UserManagementUserAPITestCase(UserTestMixin, BaseAPITestCase): def test_user_edit_add_groups_via_patch_with_access(self): self._create_test_group() self._create_test_user() - self.grant_access(permission=permission_user_edit, obj=self.test_user) + self.grant_access(obj=self.test_user, permission=permission_user_edit) + self.grant_access(obj=self.test_group, permission=permission_group_edit) response = self._request_api_test_user_edit_via_patch_with_extra_data() self.assertEqual(response.status_code, status.HTTP_200_OK) @@ -242,13 +279,14 @@ class UserManagementUserAPITestCase(UserTestMixin, BaseAPITestCase): def _request_api_test_user_delete(self): return self.delete( - viewname='rest_api:user-detail', args=(self.test_user.pk,) + viewname='rest_api:user-detail', + kwargs={'user_id': self.test_user.pk} ) def test_user_delete_no_access(self): self._create_test_user() response = self._request_api_test_user_delete() - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) self.assertTrue( get_user_model().objects.filter(pk=self.test_user.pk).exists() @@ -257,7 +295,7 @@ class UserManagementUserAPITestCase(UserTestMixin, BaseAPITestCase): def test_user_delete_with_access(self): self._create_test_user() self.grant_access( - permission=permission_user_delete, obj=self.test_user + obj=self.test_user, permission=permission_user_delete ) response = self._request_api_test_user_delete() self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) @@ -266,11 +304,12 @@ class UserManagementUserAPITestCase(UserTestMixin, BaseAPITestCase): get_user_model().objects.filter(pk=self.test_user.pk).exists() ) - # User view + # User group listview def _request_api_test_user_group_view(self): return self.get( - viewname='rest_api:users-group-list', args=(self.test_user.pk,) + viewname='rest_api:users-group-list', + kwargs={'user_id': self.test_user.pk} ) def test_user_group_list_no_access(self): @@ -278,13 +317,13 @@ class UserManagementUserAPITestCase(UserTestMixin, BaseAPITestCase): self._create_test_user() self.test_user.groups.add(group) response = self._request_api_test_user_group_view() - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) def test_user_group_list_with_user_access(self): group = Group.objects.create(name=TEST_GROUP_2_NAME) self._create_test_user() self.test_user.groups.add(group) - self.grant_access(permission=permission_user_view, obj=self.test_user) + self.grant_access(obj=self.test_user, permission=permission_user_view) response = self._request_api_test_user_group_view() self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data['count'], 0) @@ -294,46 +333,44 @@ class UserManagementUserAPITestCase(UserTestMixin, BaseAPITestCase): self._create_test_user() self.test_user.groups.add(self.test_group) self.grant_access( - permission=permission_group_view, obj=self.test_group + obj=self.test_group, permission=permission_group_view ) response = self._request_api_test_user_group_view() - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) def test_user_group_list_with_access(self): self._create_test_group() self._create_test_user() self.test_user.groups.add(self.test_group) - self.grant_access(permission=permission_user_view, obj=self.test_user) + self.grant_access(obj=self.test_user, permission=permission_user_view) self.grant_access( - permission=permission_group_view, obj=self.test_group + obj=self.test_group, permission=permission_group_view ) response = self._request_api_test_user_group_view() self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data['count'], 1) def _request_api_test_user_group_add(self): - return self.post( - viewname='rest_api:users-group-list', args=(self.test_user.pk,), - data={'group_pk_list': '{}'.format(self.test_group.pk)} + return self.patch( + viewname='rest_api:user-detail', + kwargs={'user_id': self.test_user.pk}, + data={'group_id_list': '{}'.format(self.test_group.pk)} ) def test_user_group_add_no_access(self): self._create_test_group() self._create_test_user() response = self._request_api_test_user_group_add() - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) self.test_user.refresh_from_db() self.assertEqual(self.test_group.user_set.first(), None) def test_user_group_add_with_user_access(self): self._create_test_group() self._create_test_user() - self.grant_access(permission=permission_user_edit, obj=self.test_user) + self.grant_access(obj=self.test_user, permission=permission_user_edit) response = self._request_api_test_user_group_add() - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - # FIXME: Should this endpoint return a 201 or a 200 since - # the user is being edited and there is not resource creation - # happening. + self.assertEqual(response.status_code, status.HTTP_200_OK) self.test_user.refresh_from_db() self.assertEqual(self.test_group.user_set.first(), None) @@ -341,38 +378,32 @@ class UserManagementUserAPITestCase(UserTestMixin, BaseAPITestCase): self._create_test_group() self._create_test_user() self.grant_access( - permission=permission_group_view, obj=self.test_group + obj=self.test_group, permission=permission_group_edit ) response = self._request_api_test_user_group_add() - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - # FIXME: Should this endpoint return a 201 or a 200 since - # the user is being edited and there is not resource creation - # happening. + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) self.test_user.refresh_from_db() self.assertEqual(self.test_group.user_set.first(), None) - def test_user_group_add_with_access(self): + def test_user_group_add_with_full_access(self): self._create_test_group() self._create_test_user() - self.grant_access(permission=permission_user_edit, obj=self.test_user) + self.grant_access(obj=self.test_user, permission=permission_user_edit) self.grant_access( - permission=permission_group_view, obj=self.test_group + obj=self.test_group, permission=permission_group_edit ) response = self._request_api_test_user_group_add() - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - # FIXME: Should this endpoint return a 201 or a 200 since - # the user is being edited and there is not resource creation - # happening. + self.assertEqual(response.status_code, status.HTTP_200_OK) self.test_user.refresh_from_db() self.assertEqual(self.test_group.user_set.first(), self.test_user) -class UserManagementGroupAPITestCase(BaseAPITestCase): +class GroupAPITestCase(UserTestMixin, BaseAPITestCase): def setUp(self): - super(UserManagementGroupAPITestCase, self).setUp() + super(GroupAPITestCase, self).setUp() self.login_user() - def _request_api_test_group_create(self): + def _request_api_test_group_create_view(self): return self.post( viewname='rest_api:group-list', data={ 'name': TEST_GROUP_2_NAME @@ -380,7 +411,7 @@ class UserManagementGroupAPITestCase(BaseAPITestCase): ) def test_group_create_no_permission(self): - response = self._request_api_test_group_create() + response = self._request_api_test_group_create_view() self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) self.assertFalse( TEST_GROUP_2_NAME in list( @@ -390,7 +421,7 @@ class UserManagementGroupAPITestCase(BaseAPITestCase): def test_group_create_with_permission(self): self.grant_permission(permission=permission_group_create) - response = self._request_api_test_group_create() + response = self._request_api_test_group_create_view() self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertTrue( TEST_GROUP_2_NAME in list( @@ -398,43 +429,17 @@ class UserManagementGroupAPITestCase(BaseAPITestCase): ) ) - def _request_api_test_group_edit_via_patch(self): - return self.patch( - viewname='rest_api:group-detail', args=(self.test_group.pk,), - data={ - 'name': TEST_GROUP_2_NAME_EDITED - } - ) - - def test_group_edit_via_patch_no_access(self): - self.test_group = Group.objects.create(name=TEST_GROUP_2_NAME) - response = self._request_api_test_group_edit_via_patch() - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - - self.test_group.refresh_from_db() - self.assertEqual(self.test_group.name, TEST_GROUP_2_NAME) - - def test_group_edit_via_patch_with_access(self): - self.test_group = Group.objects.create(name=TEST_GROUP_2_NAME) - self.grant_access( - permission=permission_group_edit, obj=self.test_group - ) - response = self._request_api_test_group_edit_via_patch() - self.assertEqual(response.status_code, status.HTTP_200_OK) - - self.test_group.refresh_from_db() - self.assertEqual(self.test_group.name, TEST_GROUP_2_NAME_EDITED) - - def _request_api_test_group_delete(self): + def _request_api_test_group_delete_view(self): return self.delete( - viewname='rest_api:group-detail', args=(self.test_group.pk,) + viewname='rest_api:group-detail', + kwargs={'group_id': self.test_group.pk} ) def test_group_delete_no_access(self): - self.test_group = Group.objects.create(name=TEST_GROUP_2_NAME) - response = self._request_api_test_group_delete() + self._create_test_group() + response = self._request_api_test_group_delete_view() - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) self.assertTrue( TEST_GROUP_2_NAME in list( Group.objects.values_list('name', flat=True) @@ -442,9 +447,11 @@ class UserManagementGroupAPITestCase(BaseAPITestCase): ) def test_group_delete_with_access(self): - self.test_group = Group.objects.create(name=TEST_GROUP_2_NAME) - self.grant_access(permission=permission_group_delete, obj=self.test_group) - response = self._request_api_test_group_delete() + self._create_test_group() + self.grant_access( + obj=self.test_group, permission=permission_group_delete + ) + response = self._request_api_test_group_delete_view() self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) self.assertFalse( @@ -452,3 +459,79 @@ class UserManagementGroupAPITestCase(BaseAPITestCase): Group.objects.values_list('name', flat=True) ) ) + + def _request_api_test_group_detail_view(self): + return self.get( + viewname='rest_api:group-detail', + kwargs={'group_id': self.test_group.pk} + ) + + def test_group_detail_no_access(self): + self._create_test_group() + response = self._request_api_test_group_detail_view() + + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + self.assertNotEqual( + self.test_group.name, response.data.get('name', None) + ) + + def test_group_detail_with_access(self): + self._create_test_group() + self.grant_access( + obj=self.test_group, permission=permission_group_view + ) + response = self._request_api_test_group_detail_view() + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(self.test_group.name, response.data.get('name', None)) + + + def _request_api_test_group_edit_via_patch_view(self): + return self.patch( + viewname='rest_api:group-detail', + kwargs={'group_id': self.test_group.pk}, + data={ + 'name': TEST_GROUP_2_NAME_EDITED + } + ) + + def test_group_edit_via_patch_no_access(self): + self._create_test_group() + response = self._request_api_test_group_edit_via_patch_view() + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + self.test_group.refresh_from_db() + self.assertEqual(self.test_group.name, TEST_GROUP_2_NAME) + + def test_group_edit_via_patch_with_access(self): + self._create_test_group() + self.grant_access( + obj=self.test_group, permission=permission_group_edit + ) + response = self._request_api_test_group_edit_via_patch_view() + self.assertEqual(response.status_code, status.HTTP_200_OK) + + self.test_group.refresh_from_db() + self.assertEqual(self.test_group.name, TEST_GROUP_2_NAME_EDITED) + + def _request_api_test_group_list_view(self): + return self.get(viewname='rest_api:group-list') + + def test_group_list_no_access(self): + self._create_test_group() + response = self._request_api_test_group_list_view() + self.assertNotContains( + response=response, text=self.test_group.name, + status_code=status.HTTP_200_OK + ) + + def test_group_list_with_access(self): + self._create_test_group() + self.grant_access( + obj=self.test_group, permission=permission_group_view + ) + response = self._request_api_test_group_list_view() + self.assertContains( + response=response, text=self.test_group.name, + status_code=status.HTTP_200_OK + ) diff --git a/mayan/apps/user_management/tests/test_events.py b/mayan/apps/user_management/tests/test_events.py index 9e4e2c4c73..e6d630c89f 100644 --- a/mayan/apps/user_management/tests/test_events.py +++ b/mayan/apps/user_management/tests/test_events.py @@ -4,47 +4,60 @@ from actstream.models import Action from mayan.apps.common.tests import GenericViewTestCase +from ..permissions import ( + permission_group_create, permission_group_edit, permission_user_create, + permission_user_edit +) + from ..events import ( event_group_created, event_group_edited, event_user_created, event_user_edited ) -from .mixins import UserTestMixin +from .mixins import GroupTestMixin, UserTestMixin -class GroupEventsTestCase(UserTestMixin, GenericViewTestCase): - auto_create_group = False - +class GroupEventsTestCase(GroupTestMixin, UserTestMixin, GenericViewTestCase): def test_group_create_event(self): - self.login_admin_user() Action.objects.all().delete() + + self.grant_permission( + permission=permission_group_create + ) self._request_test_group_create_view() self.assertEqual(Action.objects.last().target, self.test_group) self.assertEqual(Action.objects.last().verb, event_group_created.id) def test_group_edit_event(self): - self.login_admin_user() self._create_test_group() Action.objects.all().delete() + + self.grant_access( + obj=self.test_group, permission=permission_group_edit + ) self._request_test_group_edit_view() self.assertEqual(Action.objects.last().target, self.test_group) self.assertEqual(Action.objects.last().verb, event_group_edited.id) class UserEventsTestCase(UserTestMixin, GenericViewTestCase): - auto_create_group = False - def test_user_create_event(self): - self.login_admin_user() Action.objects.all().delete() + + self.grant_permission( + permission=permission_user_create + ) self._request_test_user_create_view() self.assertEqual(Action.objects.last().target, self.test_user) self.assertEqual(Action.objects.last().verb, event_user_created.id) def test_user_edit_event(self): - self.login_admin_user() self._create_test_user() Action.objects.all().delete() + + self.grant_access( + obj=self.test_user, permission=permission_user_edit + ) self._request_test_user_edit_view() self.assertEqual(Action.objects.last().target, self.test_user) self.assertEqual(Action.objects.last().verb, event_user_edited.id) diff --git a/mayan/apps/user_management/tests/test_models.py b/mayan/apps/user_management/tests/test_models.py index 7e96927453..e913a1262c 100644 --- a/mayan/apps/user_management/tests/test_models.py +++ b/mayan/apps/user_management/tests/test_models.py @@ -7,5 +7,5 @@ from .mixins import UserTestMixin class UserTestCase(UserTestMixin, BaseTestCase): def test_natural_keys(self): - self._create_user() + self._create_test_user() self._test_database_conversion('auth', 'user_management') diff --git a/mayan/apps/user_management/tests/test_views.py b/mayan/apps/user_management/tests/test_views.py index 12a8659fe0..a3bb41ddaa 100644 --- a/mayan/apps/user_management/tests/test_views.py +++ b/mayan/apps/user_management/tests/test_views.py @@ -6,70 +6,253 @@ from django.contrib.auth.models import Group from mayan.apps.common.tests import GenericViewTestCase from mayan.apps.documents.tests import GenericDocumentViewTestCase from mayan.apps.metadata.models import MetadataType -from mayan.apps.metadata.permissions import permission_metadata_document_edit +from mayan.apps.metadata.permissions import permission_document_metadata_edit from mayan.apps.metadata.tests.literals import ( TEST_METADATA_TYPE_LABEL, TEST_METADATA_TYPE_NAME, ) from ..permissions import ( - permission_user_create, permission_user_delete, permission_user_edit + permission_group_create, permission_group_delete, permission_group_edit, + permission_group_view, permission_user_create, permission_user_delete, + permission_user_edit, permission_user_view ) from .literals import ( - TEST_USER_PASSWORD_EDITED, TEST_USER_USERNAME, TEST_USER_2_USERNAME + TEST_GROUP_NAME, TEST_GROUP_NAME_EDITED, TEST_USER_PASSWORD_EDITED, + TEST_USER_USERNAME ) -from .mixins import UserTestMixin +from .mixins import GroupTestMixin, UserTestMixin TEST_USER_TO_DELETE_USERNAME = 'user_to_delete' -class UserManagementViewTestCase(UserTestMixin, GenericViewTestCase): - def setUp(self): - super(UserManagementViewTestCase, self).setUp() - self.login_user() +class GroupViewsTestCase(GroupTestMixin, UserTestMixin, GenericViewTestCase): + def test_group_create_view_no_permission(self): + response = self._request_test_group_create_view() + self.assertEqual(response.status_code, 403) + self.assertEqual(Group.objects.count(), 1) - #def _request_test_user_create_view(self): - # return self.post( - # viewname='user_management:user_create', data={ - # 'username': TEST_USER_2_USERNAME - # } - # ) + def test_group_create_view_with_permission(self): + self.grant_permission(permission=permission_group_create) + response = self._request_test_group_create_view() + self.assertEqual(response.status_code, 302) + self.assertEqual(Group.objects.count(), 2) + def test_group_delete_view_no_permission(self): + self._create_test_group() + response = self._request_test_group_delete_view() + self.assertEqual(response.status_code, 404) + self.assertEqual(Group.objects.count(), 2) + + def test_group_delete_view_with_access(self): + self._create_test_group() + self.grant_access( + obj=self.test_group, permission=permission_group_delete + ) + response = self._request_test_group_delete_view() + self.assertEqual(response.status_code, 302) + self.assertEqual(Group.objects.count(), 1) + + def test_group_edit_view_no_permission(self): + self._create_test_group() + response = self._request_test_group_edit_view() + self.assertEqual(response.status_code, 404) + self.test_group.refresh_from_db() + self.assertEqual(self.test_group.name, TEST_GROUP_NAME) + + def test_group_edit_view_with_access(self): + self._create_test_group() + self.grant_access( + obj=self.test_group, permission=permission_group_edit + ) + response = self._request_test_group_edit_view() + self.assertEqual(response.status_code, 302) + self.test_group.refresh_from_db() + self.assertEqual(self.test_group.name, TEST_GROUP_NAME_EDITED) + + def test_group_list_view_no_permission(self): + self._create_test_group() + response = self._request_test_group_list_view() + self.assertNotContains( + response=response, text=self.test_group.name, status_code=200 + ) + + def test_group_list_view_with_permission(self): + self._create_test_group() + self.grant_access( + obj=self.test_group, permission=permission_group_view + ) + response = self._request_test_group_list_view() + self.assertContains( + response=response, text=self.test_group.name, status_code=200 + ) + + def test_group_members_view_no_permission(self): + self._create_test_user() + self._create_test_group() + self.test_user.groups.add(self.test_group) + response = self._request_test_group_members_view() + self.assertEqual(response.status_code, 404) + + def test_group_members_view_with_group_access(self): + self._create_test_user() + self._create_test_group() + self.test_user.groups.add(self.test_group) + self.grant_access( + obj=self.test_group, permission=permission_group_edit + ) + response = self._request_test_group_members_view() + self.assertContains( + response=response, text=self.test_group.name, status_code=200 + ) + self.assertNotContains( + response=response, text=self.test_user.username, status_code=200 + ) + + def test_group_members_view_with_user_access(self): + self._create_test_user() + self._create_test_group() + self.test_user.groups.add(self.test_group) + self.grant_access(obj=self.test_user, permission=permission_user_edit) + response = self._request_test_group_members_view() + self.assertNotContains( + response=response, text=self.test_group.name, status_code=404 + ) + + def test_group_members_view_with_full_access(self): + self._create_test_user() + self._create_test_group() + self.test_user.groups.add(self.test_group) + self.grant_access( + obj=self.test_group, permission=permission_group_edit + ) + self.grant_access(obj=self.test_user, permission=permission_user_edit) + response = self._request_test_group_members_view() + self.assertContains( + response=response, text=self.test_user.username, status_code=200 + ) + self.assertContains( + response=response, text=self.test_group.name, status_code=200 + ) + + +class UserViewsTestCase(GroupTestMixin, UserTestMixin, GenericViewTestCase): def test_user_create_view_no_permission(self): + user_count = get_user_model().objects.count() + response = self._request_test_user_create_view() self.assertEqual(response.status_code, 403) - self.assertEqual(get_user_model().objects.count(), 2) - self.assertFalse(TEST_USER_2_USERNAME in get_user_model().objects.values_list('username', flat=True)) + + self.assertEqual(get_user_model().objects.count(), user_count) + self.assertFalse(TEST_USER_USERNAME in get_user_model().objects.values_list('username', flat=True)) def test_user_create_view_with_permission(self): + user_count = get_user_model().objects.count() + self.grant_permission(permission=permission_user_create) response = self._request_test_user_create_view() self.assertEqual(response.status_code, 302) - self.assertEqual(get_user_model().objects.count(), 3) - self.assertTrue(TEST_USER_2_USERNAME in get_user_model().objects.values_list('username', flat=True)) - def _request_user_groups_view(self): - return self.post( - viewname='user_management:user_groups', args=(self.test_user.pk,) + self.assertEqual(get_user_model().objects.count(), user_count + 1) + self.assertTrue(TEST_USER_USERNAME in get_user_model().objects.values_list('username', flat=True)) + + def test_user_delete_view_no_access(self): + self._create_test_user() + user_count = get_user_model().objects.count() + + response = self._request_test_user_delete_view() + self.assertEqual(response.status_code, 404) + + self.assertEqual(get_user_model().objects.count(), user_count) + + def test_user_delete_view_with_access(self): + self._create_test_user() + user_count = get_user_model().objects.count() + + self.grant_access( + obj=self.test_user, permission=permission_user_delete ) + response = self._request_test_user_delete_view() + self.assertEqual(response.status_code, 302) + + self.assertEqual(get_user_model().objects.count(), user_count - 1) + + def test_superuser_delete_view_with_access(self): + self._create_test_superuser() + + superuser_count = get_user_model().objects.filter(is_superuser=True).count() + self.grant_access( + obj=self.test_superuser, permission=permission_user_delete + ) + response = self._request_test_superuser_delete_view() + self.assertEqual(response.status_code, 404) + self.assertEqual( + get_user_model().objects.filter(is_superuser=True).count(), + superuser_count + ) + + def test_superuser_detail_view_with_access(self): + self._create_test_superuser() + + self.grant_access( + obj=self.test_superuser, permission=permission_user_view + ) + response = self._request_test_superuser_detail_view() + self.assertEqual(response.status_code, 404) def test_user_groups_view_no_permission(self): self._create_test_user() - response = self._request_user_groups_view() - self.assertEqual(response.status_code, 403) + self._create_test_group() + self.test_user.groups.add(self.test_group) + response = self._request_test_user_groups_view() + self.assertEqual(response.status_code, 404) - def test_user_groups_view_with_access(self): + def test_user_groups_view_with_group_access(self): self._create_test_user() - self.grant_access(permission=permission_user_edit, obj=self.test_user) + self._create_test_group() + self.test_user.groups.add(self.test_group) + self.grant_access( + obj=self.test_group, permission=permission_group_edit + ) + response = self._request_test_user_groups_view() + self.assertNotContains( + response=response, text=self.test_user.username, status_code=404 + ) - response = self._request_user_groups_view() + def test_user_groups_view_with_user_access(self): + self._create_test_user() + self._create_test_group() + self.test_user.groups.add(self.test_group) + self.grant_access(obj=self.test_user, permission=permission_user_edit) + response = self._request_test_user_groups_view() self.assertContains( response=response, text=self.test_user.username, status_code=200 ) + self.assertNotContains( + response=response, text=self.test_group.name, status_code=200 + ) + + def test_user_groups_view_with_full_access(self): + self._create_test_user() + self._create_test_group() + self.test_user.groups.add(self.test_group) + self.grant_access( + obj=self.test_group, permission=permission_group_edit + ) + self.grant_access(obj=self.test_user, permission=permission_user_edit) + response = self._request_test_user_groups_view() + + self.assertContains( + response=response, text=self.test_user.username, status_code=200 + ) + self.assertContains( + response=response, text=self.test_group.name, status_code=200 + ) def _request_set_password_view(self, password): return self.post( - viewname='user_management:user_set_password', args=(self.test_user.pk,), + viewname='user_management:user_set_password', + kwargs={'user_id': self.test_user.pk}, data={ 'new_password1': password, 'new_password2': password } @@ -81,13 +264,14 @@ class UserManagementViewTestCase(UserTestMixin, GenericViewTestCase): password=TEST_USER_PASSWORD_EDITED ) - self.assertEqual(response.status_code, 403) + self.assertEqual(response.status_code, 404) self.logout() with self.assertRaises(AssertionError): self.login( - username=TEST_USER_2_USERNAME, password=TEST_USER_PASSWORD_EDITED + username=self.test_user.username, + password=TEST_USER_PASSWORD_EDITED ) response = self.get(viewname='user_management:current_user_details') @@ -96,7 +280,7 @@ class UserManagementViewTestCase(UserTestMixin, GenericViewTestCase): def test_user_set_password_view_with_access(self): self._create_test_user() - self.grant_access(permission=permission_user_edit, obj=self.test_user) + self.grant_access(obj=self.test_user, permission=permission_user_edit) response = self._request_set_password_view( password=TEST_USER_PASSWORD_EDITED @@ -106,7 +290,7 @@ class UserManagementViewTestCase(UserTestMixin, GenericViewTestCase): self.logout() self.login( - username=TEST_USER_2_USERNAME, password=TEST_USER_PASSWORD_EDITED + username=TEST_USER_USERNAME, password=TEST_USER_PASSWORD_EDITED ) response = self.get(viewname='user_management:current_user_details') @@ -128,13 +312,14 @@ class UserManagementViewTestCase(UserTestMixin, GenericViewTestCase): password=TEST_USER_PASSWORD_EDITED ) - self.assertEqual(response.status_code, 403) + self.assertEqual(response.status_code, 404) self.logout() with self.assertRaises(AssertionError): self.login( - username=TEST_USER_2_USERNAME, password=TEST_USER_PASSWORD_EDITED + username=self.test_user.username, + password=TEST_USER_PASSWORD_EDITED ) response = self.get(viewname='user_management:current_user_details') @@ -142,7 +327,7 @@ class UserManagementViewTestCase(UserTestMixin, GenericViewTestCase): def test_user_multiple_set_password_view_with_access(self): self._create_test_user() - self.grant_access(permission=permission_user_edit, obj=self.test_user) + self.grant_access(obj=self.test_user, permission=permission_user_edit) response = self._request_multiple_user_set_password_view( password=TEST_USER_PASSWORD_EDITED @@ -152,30 +337,12 @@ class UserManagementViewTestCase(UserTestMixin, GenericViewTestCase): self.logout() self.login( - username=TEST_USER_2_USERNAME, password=TEST_USER_PASSWORD_EDITED + username=self.test_user.username, password=TEST_USER_PASSWORD_EDITED ) response = self.get(viewname='user_management:current_user_details') self.assertEqual(response.status_code, 200) - def _request_user_delete_view(self): - return self.post( - viewname='user_management:user_delete', args=(self.test_user.pk,) - ) - - def test_user_delete_view_no_access(self): - self._create_test_user() - response = self._request_user_delete_view() - self.assertEqual(response.status_code, 302) - self.assertEqual(get_user_model().objects.count(), 3) - - def test_user_delete_view_with_access(self): - self._create_test_user() - self.grant_access(permission=permission_user_delete, obj=self.test_user) - response = self._request_user_delete_view() - self.assertEqual(response.status_code, 302) - self.assertEqual(get_user_model().objects.count(), 2) - def _request_user_multiple_delete_view(self): return self.post( viewname='user_management:user_multiple_delete', data={ @@ -185,16 +352,24 @@ class UserManagementViewTestCase(UserTestMixin, GenericViewTestCase): def test_user_multiple_delete_view_no_access(self): self._create_test_user() + user_count = get_user_model().objects.count() + response = self._request_user_multiple_delete_view() - self.assertEqual(response.status_code, 302) - self.assertEqual(get_user_model().objects.count(), 3) + self.assertEqual(response.status_code, 404) + + self.assertEqual(get_user_model().objects.count(), user_count) def test_user_multiple_delete_view_with_access(self): self._create_test_user() - self.grant_access(permission=permission_user_delete, obj=self.test_user) + user_count = get_user_model().objects.count() + + self.grant_access( + obj=self.test_user, permission=permission_user_delete + ) response = self._request_user_multiple_delete_view() self.assertEqual(response.status_code, 302) - self.assertEqual(get_user_model().objects.count(), 2) + + self.assertEqual(get_user_model().objects.count(), user_count - 1) class MetadataLookupIntegrationTestCase(GenericDocumentViewTestCase): @@ -212,13 +387,13 @@ class MetadataLookupIntegrationTestCase(GenericDocumentViewTestCase): self.metadata_type.lookup = '{{ users }}' self.metadata_type.save() self.document.metadata.create(metadata_type=self.metadata_type) - self.role.permissions.add( - permission_metadata_document_edit.stored_permission + self.grant_access( + obj=self.document, permission=permission_document_metadata_edit ) response = self.get( viewname='metadata:document_metadata_edit', - kwargs={'pk': self.document.pk} + kwargs={'document_id': self.document.pk} ) self.assertContains( response=response, text=''.format( @@ -230,13 +405,13 @@ class MetadataLookupIntegrationTestCase(GenericDocumentViewTestCase): self.metadata_type.lookup = '{{ groups }}' self.metadata_type.save() self.document.metadata.create(metadata_type=self.metadata_type) - self.role.permissions.add( - permission_metadata_document_edit.stored_permission + self.grant_access( + obj=self.document, permission=permission_document_metadata_edit ) response = self.get( viewname='metadata:document_metadata_edit', - kwargs={'pk': self.document.pk} + kwargs={'document_id': self.document.pk} ) self.assertContains( diff --git a/mayan/apps/user_management/urls.py b/mayan/apps/user_management/urls.py index 20a94f1d7b..a0c103277e 100644 --- a/mayan/apps/user_management/urls.py +++ b/mayan/apps/user_management/urls.py @@ -14,76 +14,93 @@ from .views import ( ) urlpatterns = [ - url(r'^groups/list/$', GroupListView.as_view(), name='group_list'), - url(r'^groups/create/$', GroupCreateView.as_view(), name='group_create'), url( - r'^groups/(?P\d+)/edit/$', GroupEditView.as_view(), - name='group_edit' + regex=r'^groups/$', name='group_list', view=GroupListView.as_view() ), url( - r'^groups/(?P\d+)/delete/$', GroupDeleteView.as_view(), - name='group_delete' + regex=r'^groups/create/$', name='group_create', + view=GroupCreateView.as_view() ), url( - r'^groups/(?P\d+)/members/$', GroupMembersView.as_view(), - name='group_members' - ), - - url( - r'^users/current/$', CurrentUserDetailsView.as_view(), - name='current_user_details' + regex=r'^groups/(?P\d+)/delete/$', name='group_delete', + view=GroupDeleteView.as_view() ), url( - r'^users/current/edit/$', CurrentUserEditView.as_view(), - name='current_user_edit' - ), - url(r'^users/list/$', UserListView.as_view(), name='user_list'), - url(r'^users/create/$', UserCreateView.as_view(), name='user_create'), - url( - r'^users/(?P\d+)/delete/$', UserDeleteView.as_view(), - name='user_delete' - ), - url(r'^users/(?P\d+)/edit/$', UserEditView.as_view(), name='user_edit'), - url( - r'^users/(?P\d+)/$', UserDetailsView.as_view(), - name='user_details' + regex=r'^groups/(?P\d+)/edit/$', name='group_edit', + view=GroupEditView.as_view() ), url( - r'^users/multiple/delete/$', UserDeleteView.as_view(), - name='user_multiple_delete' + regex=r'^groups/(?P\d+)/members/$', name='group_members', + view=GroupMembersView.as_view() ), url( - r'^users/(?P\d+)/set_password/$', UserSetPasswordView.as_view(), - name='user_set_password' + regex=r'^user/$', name='current_user_details', + view=CurrentUserDetailsView.as_view() ), url( - r'^users/multiple/set_password/$', UserSetPasswordView.as_view(), - name='user_multiple_set_password' + regex=r'^user/edit/$', name='current_user_edit', + view=CurrentUserEditView.as_view() ), url( - r'^users/(?P\d+)/groups/$', UserGroupsView.as_view(), - name='user_groups' + regex=r'^users/$', name='user_list', view=UserListView.as_view() ), url( - r'^users/(?P\d+)/options/$', - UserOptionsEditView.as_view(), - name='user_options' + regex=r'^users/create/$', name='user_create', + view=UserCreateView.as_view() ), + url( + regex=r'^users/(?P\d+)/$', name='user_details', + view=UserDetailsView.as_view() + ), + url( + regex=r'^users/(?P\d+)/delete/$', name='user_delete', + view=UserDeleteView.as_view(), + ), + url( + regex=r'^users/(?P\d+)/edit/$', name='user_edit', + view=UserEditView.as_view() + ), + url( + regex=r'^users/(?P\d+)/groups/$', name='user_groups', + view=UserGroupsView.as_view() + ), + url( + regex=r'^users/(?P\d+)/options/$', name='user_options', + view=UserOptionsEditView.as_view() + ), + url( + regex=r'^users/(?P\d+)/set_password/$', + name='user_set_password', view=UserSetPasswordView.as_view() + ), + url( + regex=r'^users/multiple/delete/$', name='user_multiple_delete', + view=UserDeleteView.as_view() + ), + url( + regex=r'^users/multiple/set_password/$', + name='user_multiple_set_password', view=UserSetPasswordView.as_view() + ) ] api_urls = [ - url(r'^groups/$', APIGroupListView.as_view(), name='group-list'), url( - r'^groups/(?P[0-9]+)/$', APIGroupView.as_view(), - name='group-detail' - ), - url(r'^users/$', APIUserListView.as_view(), name='user-list'), - url(r'^users/(?P[0-9]+)/$', APIUserView.as_view(), name='user-detail'), - url( - r'^users/current/$', APICurrentUserView.as_view(), name='user-current' + regex=r'^groups/$', name='group-list', view=APIGroupListView.as_view() ), url( - r'^users/(?P[0-9]+)/groups/$', APIUserGroupList.as_view(), - name='users-group-list' + regex=r'^groups/(?P\d+)/$', name='group-detail', + view=APIGroupView.as_view(), + ), + url( + regex=r'^user/$', name='user-current', + view=APICurrentUserView.as_view() + ), + url(regex=r'^users/$', name='user-list', view=APIUserListView.as_view()), + url( + regex=r'^users/(?P\d+)/$', name='user-detail', + view=APIUserView.as_view() + ), + url( + regex=r'^users/(?P\d+)/groups/$', name='users-group-list', + view=APIUserGroupList.as_view() ), ] diff --git a/mayan/apps/user_management/utils.py b/mayan/apps/user_management/utils.py index 5d5f689609..4c3b74fafe 100644 --- a/mayan/apps/user_management/utils.py +++ b/mayan/apps/user_management/utils.py @@ -5,22 +5,22 @@ from django.contrib.auth import get_user_model from django.utils.translation import ugettext_lazy as _ -def get_groups(): +def get_user_label_text(context): + if not context['request'].user.is_authenticated: + return _('Anonymous') + else: + return context['request'].user.get_full_name() or context['request'].user + + +def lookup_get_groups(): Group = apps.get_model(app_label='auth', model_name='Group') return ','.join([group.name for group in Group.objects.all()]) -def get_users(): +def lookup_get_users(): return ','.join( [ user.get_full_name() or user.username for user in get_user_model().objects.all() ] ) - - -def get_user_label_text(context): - if not context['request'].user.is_authenticated: - return _('Anonymous') - else: - return context['request'].user.get_full_name() or context['request'].user diff --git a/mayan/apps/user_management/views.py b/mayan/apps/user_management/views.py index 14cc8d6536..9cdd9fefd0 100644 --- a/mayan/apps/user_management/views.py +++ b/mayan/apps/user_management/views.py @@ -5,19 +5,21 @@ from django.contrib.auth import get_user_model from django.contrib.auth.forms import SetPasswordForm from django.contrib.auth.models import Group from django.contrib.contenttypes.models import ContentType -from django.core.exceptions import PermissionDenied +from django.db import transaction from django.http import HttpResponseRedirect from django.shortcuts import get_object_or_404 from django.template import RequestContext from django.urls import reverse, reverse_lazy from django.utils.translation import ungettext, ugettext_lazy as _ -from mayan.apps.common.views import ( +from mayan.apps.acls.models import AccessControlList +from mayan.apps.common.generics import ( AssignRemoveView, MultipleObjectConfirmActionView, MultipleObjectFormActionView, SingleObjectCreateView, SingleObjectDeleteView, SingleObjectDetailView, SingleObjectEditView, SingleObjectListView ) +from mayan.apps.common.mixins import ExternalObjectMixin from .events import ( event_group_created, event_group_edited, event_user_created, @@ -52,7 +54,9 @@ class CurrentUserDetailsView(SingleObjectDetailView): class CurrentUserEditView(SingleObjectEditView): extra_context = {'object': None, 'title': _('Edit current user details')} form_class = UserForm - post_action_redirect = reverse_lazy('user_management:current_user_details') + post_action_redirect = reverse_lazy( + viewname='user_management:current_user_details' + ) def get_object(self): return self.request.user @@ -62,38 +66,50 @@ class GroupCreateView(SingleObjectCreateView): extra_context = {'title': _('Create new group')} fields = ('name',) model = Group - post_action_redirect = reverse_lazy('user_management:group_list') + post_action_redirect = reverse_lazy(viewname='user_management:group_list') view_permission = permission_group_create def form_valid(self, form): - group = form.save() + with transaction.atomic(): + result = super(GroupCreateView, self).form_valid(form=form) + event_group_created.commit( + actor=self.request.user, target=self.object + ) + return result - event_group_created.commit( - actor=self.request.user, target=group - ) - messages.success( - self.request, _('Group "%s" created successfully.') % group - ) - return super(GroupCreateView, self).form_valid(form=form) +class GroupDeleteView(SingleObjectDeleteView): + model = Group + object_permission = permission_group_delete + pk_url_kwarg = 'group_id' + post_action_redirect = reverse_lazy(viewname='user_management:group_list') + + def get_extra_context(self): + return { + 'object': self.object, + 'title': _('Delete the group: %s?') % self.object, + } class GroupEditView(SingleObjectEditView): fields = ('name',) model = Group object_permission = permission_group_edit - post_action_redirect = reverse_lazy('user_management:group_list') + pk_url_kwarg = 'group_id' + post_action_redirect = reverse_lazy(viewname='user_management:group_list') def form_valid(self, form): - event_group_edited.commit( - actor=self.request.user, target=self.get_object() - ) - return super(GroupEditView, self).form_valid(form=form) + with transaction.atomic(): + result = super(GroupEditView, self).form_valid(form=form) + event_group_edited.commit( + actor=self.request.user, target=self.object + ) + return result def get_extra_context(self): return { - 'object': self.get_object(), - 'title': _('Edit group: %s') % self.get_object(), + 'object': self.object, + 'title': _('Edit group: %s') % self.object, } @@ -120,29 +136,20 @@ class GroupListView(SingleObjectListView): } -class GroupDeleteView(SingleObjectDeleteView): - model = Group - object_permission = permission_group_delete - post_action_redirect = reverse_lazy('user_management:group_list') - - def get_extra_context(self): - return { - 'object': self.get_object(), - 'title': _('Delete the group: %s?') % self.get_object(), - } - - -class GroupMembersView(AssignRemoveView): +class GroupMembersView(ExternalObjectMixin, AssignRemoveView): decode_content_type = True + external_object_class = Group + external_object_permission = permission_group_edit + external_object_pk_url_kwarg = 'group_id' left_list_title = _('Available users') - right_list_title = _('Users in group') object_permission = permission_group_edit + right_list_title = _('Users in group') @staticmethod def generate_choices(choices): results = [] for choice in choices: - ct = ContentType.objects.get_for_model(choice) + ct = ContentType.objects.get_for_model(model=choice) label = choice.get_full_name() if choice.get_full_name() else choice results.append(('%s,%s' % (ct.model, choice.pk), '%s' % (label))) @@ -151,31 +158,43 @@ class GroupMembersView(AssignRemoveView): return sorted(results, key=lambda x: x[1]) def add(self, item): - self.get_object().user_set.add(item) + self.object.user_set.add(item) + + def dispatch(self, *args, **kwargs): + self.object = self.get_object() + return super(GroupMembersView, self).dispatch(*args, **kwargs) def get_extra_context(self): return { - 'object': self.get_object(), - 'title': _('Users of group: %s') % self.get_object() + 'object': self.object, + 'title': _('Users of group: %s') % self.object } def get_object(self): - return get_object_or_404(klass=Group, pk=self.kwargs['pk']) + return self.get_external_object() def left_list(self): - return GroupMembersView.generate_choices( - get_user_model().objects.exclude( - groups=self.get_object() - ).exclude(is_staff=True).exclude(is_superuser=True) + queryset = AccessControlList.objects.restrict_queryset( + permission=permission_user_edit, + queryset=get_user_model().objects.exclude( + groups=self.object + ).exclude(is_staff=True).exclude(is_superuser=True), + user=self.request.user ) + return GroupMembersView.generate_choices(choices=queryset) + def right_list(self): - return GroupMembersView.generate_choices( - self.get_object().user_set.all() + queryset = AccessControlList.objects.restrict_queryset( + permission=permission_user_edit, + queryset=self.object.user_set.all(), + user=self.request.user ) + return GroupMembersView.generate_choices(choices=queryset) + def remove(self, item): - self.get_object().user_set.remove(item) + self.object.user_set.remove(item) class UserCreateView(SingleObjectCreateView): @@ -186,25 +205,24 @@ class UserCreateView(SingleObjectCreateView): view_permission = permission_user_create def form_valid(self, form): - user = form.save(commit=False) - user.set_unusable_password() - user.save() + with transaction.atomic(): + super(UserCreateView, self).form_valid(form=form) + event_user_created.commit( + actor=self.request.user, target=self.object + ) - event_user_created.commit( - actor=self.request.user, target=user - ) - - messages.success( - self.request, _('User "%s" created successfully.') % user - ) return HttpResponseRedirect( - reverse('user_management:user_set_password', args=(user.pk,)) + reverse( + viewname='user_management:user_set_password', + kwargs={'user_id': self.object.pk} + ) ) class UserDeleteView(MultipleObjectConfirmActionView): object_permission = permission_user_delete - queryset = get_user_model().objects.filter( + pk_url_kwarg = 'user_id' + source_queryset = get_user_model().objects.filter( is_superuser=False, is_staff=False ) success_message = _('User delete request performed on %(count)d user') @@ -213,13 +231,13 @@ class UserDeleteView(MultipleObjectConfirmActionView): ) def get_extra_context(self): - queryset = self.get_queryset() + queryset = self.get_object_list() result = { 'title': ungettext( - 'Delete user', - 'Delete users', - queryset.count() + singular='Delete user', + plural='Delete users', + number=queryset.count() ) } @@ -235,26 +253,18 @@ class UserDeleteView(MultipleObjectConfirmActionView): def object_action(self, form, instance): try: - if instance.is_superuser or instance.is_staff: - messages.error( - self.request, - _( - 'Super user and staff user deleting is not ' - 'allowed, use the admin interface for these cases.' - ) - ) - else: - instance.delete() - messages.success( - self.request, _( - 'User "%s" deleted successfully.' - ) % instance - ) + instance.delete() + messages.success( + message=_( + 'User "%s" deleted successfully.' + ) % instance, request=self.request + ) except Exception as exception: messages.error( - self.request, _( + message=_( 'Error deleting user "%(user)s": %(error)s' - ) % {'user': instance, 'error': exception} + ) % {'user': instance, 'error': exception}, + request=self.request ) @@ -264,7 +274,8 @@ class UserDetailsView(SingleObjectDetailView): 'date_joined', 'groups', ) object_permission = permission_user_view - queryset = get_user_model().objects.filter( + pk_url_kwarg = 'user_id' + source_queryset = get_user_model().objects.filter( is_superuser=False, is_staff=False ) @@ -278,58 +289,72 @@ class UserDetailsView(SingleObjectDetailView): class UserEditView(SingleObjectEditView): fields = ('username', 'first_name', 'last_name', 'email', 'is_active',) object_permission = permission_user_edit - post_action_redirect = reverse_lazy('user_management:user_list') - queryset = get_user_model().objects.filter( + pk_url_kwarg = 'user_id' + post_action_redirect = reverse_lazy(viewname='user_management:user_list') + source_queryset = get_user_model().objects.filter( is_superuser=False, is_staff=False ) def form_valid(self, form): - event_user_edited.commit( - actor=self.request.user, target=self.get_object() - ) - return super(UserEditView, self).form_valid(form=form) + with transaction.atomic(): + result = super(UserEditView, self).form_valid(form=form) + event_user_edited.commit( + actor=self.request.user, target=self.object + ) + + return result def get_extra_context(self): return { - 'object': self.get_object(), - 'title': _('Edit user: %s') % self.get_object(), + 'object': self.object, + 'title': _('Edit user: %s') % self.object, } -class UserGroupsView(AssignRemoveView): +class UserGroupsView(ExternalObjectMixin, AssignRemoveView): decode_content_type = True + external_object_queryset = get_user_model().objects.filter( + is_staff=False, is_superuser=False + ) + external_object_permission = permission_user_edit + external_object_pk_url_kwarg = 'user_id' left_list_title = _('Available groups') right_list_title = _('Groups joined') - object_permission = permission_user_edit def add(self, item): - item.user_set.add(self.get_object()) + item.user_set.add(self.object) + + def dispatch(self, *args, **kwargs): + self.object = self.get_object() + return super(UserGroupsView, self).dispatch(*args, **kwargs) def get_extra_context(self): return { - 'object': self.get_object(), - 'title': _('Groups of user: %s') % self.get_object() + 'object': self.object, + 'title': _('Groups of user: %s') % self.object } def get_object(self): - return get_object_or_404( - klass=get_user_model().objects.filter( - is_superuser=False, is_staff=False - ), pk=self.kwargs['pk'] - ) + return self.get_external_object() def left_list(self): - return AssignRemoveView.generate_choices( - Group.objects.exclude(user=self.get_object()) + queryset = AccessControlList.objects.restrict_queryset( + permission=permission_group_edit, + queryset=Group.objects.exclude(user=self.object), + user=self.request.user ) + return AssignRemoveView.generate_choices(choices=queryset) def right_list(self): - return AssignRemoveView.generate_choices( - Group.objects.filter(user=self.get_object()) + queryset = AccessControlList.objects.restrict_queryset( + permission=permission_group_edit, + queryset=Group.objects.filter(user=self.object), + user=self.request.user ) + return AssignRemoveView.generate_choices(choices=queryset) def remove(self, item): - item.user_set.remove(self.get_object()) + item.user_set.remove(self.object) class UserListView(SingleObjectListView): @@ -350,7 +375,7 @@ class UserListView(SingleObjectListView): 'title': _('Users'), } - def get_object_list(self): + def get_source_queryset(self): return get_user_model().objects.exclude( is_superuser=True ).exclude(is_staff=True).order_by('last_name', 'first_name') @@ -372,13 +397,13 @@ class UserOptionsEditView(SingleObjectEditView): return self.get_user().user_options def get_post_action_redirect(self): - return reverse('user_management:user_list') + return reverse(viewname='user_management:user_list') def get_user(self): return get_object_or_404( klass=get_user_model().objects.filter( is_superuser=False, is_staff=False - ), pk=self.kwargs['pk'] + ), pk=self.kwargs['user_id'] ) @@ -386,66 +411,57 @@ class UserSetPasswordView(MultipleObjectFormActionView): form_class = SetPasswordForm model = get_user_model() object_permission = permission_user_edit + pk_url_kwarg = 'user_id' + source_queryset = get_user_model().objects.filter( + is_superuser=False, is_staff=False + ) success_message = _('Password change request performed on %(count)d user') success_message_plural = _( 'Password change request performed on %(count)d users' ) def get_extra_context(self): - queryset = self.get_queryset() + queryset = self.get_object_list() result = { 'submit_label': _('Submit'), 'title': ungettext( - 'Change user password', - 'Change users passwords', - queryset.count() - ) + singular='Change the password of the %(count)d selected user', + plural='Change the password of the %(count)d selected users', + number=queryset.count() + ) % {'count': queryset.count()} } if queryset.count() == 1: result.update( { 'object': queryset.first(), - 'title': _('Change password for user: %s') % queryset.first() + 'title': _( + 'Change the password of user: %s' + ) % queryset.first() } ) return result def get_form_extra_kwargs(self): - queryset = self.get_queryset() - result = {} - if queryset: - result['user'] = queryset.first() - return result - else: - raise PermissionDenied + queryset = self.get_object_list() + return {'user': queryset.first()} def object_action(self, form, instance): try: - if instance.is_superuser or instance.is_staff: - messages.error( - self.request, - _( - 'Super user and staff user password ' - 'reseting is not allowed, use the admin ' - 'interface for these cases.' - ) - ) - else: - instance.set_password(form.cleaned_data['new_password1']) - instance.save() - messages.success( - self.request, _( - 'Successful password reset for user: %s.' - ) % instance - ) + instance.set_password(form.cleaned_data['new_password1']) + instance.save() + messages.success( + message=_( + 'Successful password reset for user: %s.' + ) % instance, request=self.request + ) except Exception as exception: messages.error( - self.request, _( + message=_( 'Error reseting password for user "%(user)s": %(error)s' ) % { 'user': instance, 'error': exception - } + }, request=self.request ) From 5bab080553d61616085f62e51149923cda225545 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Wed, 30 Jan 2019 03:08:15 -0400 Subject: [PATCH 073/209] Workflows: Update generic view interface Add icons for the workflow runtime proxy views. Fix failing tests. Convert runtime proxy links to use the new list facet menu. Signed-off-by: Roberto Rosario --- mayan/apps/document_states/apps.py | 4 +-- mayan/apps/document_states/icons.py | 10 +++++++ mayan/apps/document_states/links.py | 28 ++++++++++++------- mayan/apps/document_states/models.py | 6 ++-- .../document_states/tests/test_indexing.py | 4 +-- .../views/workflow_instance_views.py | 4 +-- .../views/workflow_proxy_views.py | 18 +++--------- .../document_states/views/workflow_views.py | 10 +++---- 8 files changed, 46 insertions(+), 38 deletions(-) diff --git a/mayan/apps/document_states/apps.py b/mayan/apps/document_states/apps.py index 697ac051f8..d765882dba 100644 --- a/mayan/apps/document_states/apps.py +++ b/mayan/apps/document_states/apps.py @@ -268,13 +268,13 @@ class DocumentStatesApp(MayanAppConfig): link_workflow_instance_transition ), sources=(WorkflowInstance,) ) - menu_object.bind_links( + menu_list_facet.bind_links( links=( link_workflow_runtime_proxy_document_list, link_workflow_runtime_proxy_state_list, ), sources=(WorkflowRuntimeProxy,) ) - menu_object.bind_links( + menu_list_facet.bind_links( links=( link_workflow_runtime_proxy_state_document_list, ), sources=(WorkflowStateRuntimeProxy,) diff --git a/mayan/apps/document_states/icons.py b/mayan/apps/document_states/icons.py index d9f305eebf..c7de8e87d4 100644 --- a/mayan/apps/document_states/icons.py +++ b/mayan/apps/document_states/icons.py @@ -1,6 +1,7 @@ from __future__ import absolute_import, unicode_literals from mayan.apps.appearance.classes import Icon +from mayan.apps.documents.icons import icon_document_list icon_document_workflow_instance_list = Icon( driver_name='fontawesome', symbol='sitemap' @@ -18,6 +19,15 @@ icon_workflow_edit = Icon(driver_name='fontawesome', symbol='pencil-alt') icon_workflow_list = Icon(driver_name='fontawesome', symbol='sitemap') icon_workflow_preview = Icon(driver_name='fontawesome', symbol='eye') +icon_workflow_runtime_proxy_document_list = icon_document_list +icon_workflow_runtime_proxy_list = Icon( + driver_name='fontawesome', symbol='sitemap' +) +icon_workflow_runtime_proxy_state_document_list = icon_document_list +icon_workflow_runtime_proxy_state_list = Icon( + driver_name='fontawesome', symbol='circle' +) + icon_workflow_state_action_delete = Icon( driver_name='fontawesome', symbol='times' ) diff --git a/mayan/apps/document_states/links.py b/mayan/apps/document_states/links.py index e51ccc6d5f..4646327ba6 100644 --- a/mayan/apps/document_states/links.py +++ b/mayan/apps/document_states/links.py @@ -5,16 +5,20 @@ from django.utils.translation import ugettext_lazy as _ from mayan.apps.documents.icons import icon_document_type from mayan.apps.navigation import Link + from .icons import ( icon_document_workflow_instance_list, icon_tool_launch_all_workflows, icon_workflow_create, icon_workflow_delete, icon_workflow_edit, - icon_workflow_list, icon_workflow_preview, icon_workflow_state, - icon_workflow_state_action, icon_workflow_state_action_delete, - icon_workflow_state_action_edit, icon_workflow_state_action_list, - icon_workflow_state_action_selection, icon_workflow_state_create, - icon_workflow_state_delete, icon_workflow_state_edit, - icon_workflow_transition, icon_workflow_transition_create, - icon_workflow_transition_delete, icon_workflow_transition_edit + icon_workflow_list, icon_workflow_preview, + icon_workflow_runtime_proxy_document_list, icon_workflow_runtime_proxy_list, + icon_workflow_runtime_proxy_state_document_list, + icon_workflow_runtime_proxy_state_list, icon_workflow_state, + icon_workflow_state_action_delete, icon_workflow_state_action_edit, + icon_workflow_state_action_list, icon_workflow_state_action_selection, + icon_workflow_state_create, icon_workflow_state_delete, + icon_workflow_state_edit, icon_workflow_transition, + icon_workflow_transition_create, icon_workflow_transition_delete, + icon_workflow_transition_edit ) from .permissions import ( permission_workflow_create, permission_workflow_delete, @@ -152,7 +156,7 @@ link_workflow_transition_edit = Link( ) link_workflow_transition_list = Link( icon_class=icon_workflow_transition, - kwargs={'workflow_transition_id': 'resolved_object.pk'}, + kwargs={'workflow_id': 'resolved_object.pk'}, permission=permission_workflow_view, text=_('Transitions'), view='workflows:workflow_transition_list' ) @@ -165,20 +169,24 @@ link_workflow_transition_triggers = Link( # Workflow runtime proxies link_workflow_runtime_proxy_document_list = Link( + icon_class=icon_workflow_runtime_proxy_document_list, kwargs={'workflow_runtime_proxy_id': 'resolved_object.pk'}, permission=permission_workflow_view, text=_('Workflow documents'), view='workflows:workflow_runtime_proxy_document_list' ) link_workflow_runtime_proxy_list = Link( - icon_class=icon_workflow_list, permission=permission_workflow_view, - text=_('Workflows'), view='workflows:workflow_runtime_proxy_list' + icon_class=icon_workflow_runtime_proxy_list, + permission=permission_workflow_view, text=_('Workflows'), + view='workflows:workflow_runtime_proxy_list' ) link_workflow_runtime_proxy_state_document_list = Link( + icon_class=icon_workflow_runtime_proxy_state_document_list, kwargs={'workflow_runtime_proxy_state_id': 'resolved_object.pk'}, permission=permission_workflow_view, text=_('State documents'), view='workflows:workflow_runtime_proxy_state_document_list' ) link_workflow_runtime_proxy_state_list = Link( + icon_class=icon_workflow_runtime_proxy_state_list, kwargs={'workflow_runtime_proxy_id': 'resolved_object.pk'}, permission=permission_workflow_view, text=_('States'), view='workflows:workflow_runtime_proxy_state_list' diff --git a/mayan/apps/document_states/models.py b/mayan/apps/document_states/models.py index 039f2591bf..e4424031ad 100644 --- a/mayan/apps/document_states/models.py +++ b/mayan/apps/document_states/models.py @@ -485,8 +485,8 @@ class WorkflowInstance(models.Model): all transition options. """ AccessControlList.objects.check_access( - permissions=permission_workflow_transition, - user=_user, obj=self.workflow + obj=self.workflow, + permission=permission_workflow_transition, user=_user ) except PermissionDenied: """ @@ -495,7 +495,7 @@ class WorkflowInstance(models.Model): """ queryset = AccessControlList.objects.restrict_queryset( permission=permission_workflow_transition, - user=_user, queryset=queryset + queryset=queryset, user=_user ) return queryset else: diff --git a/mayan/apps/document_states/tests/test_indexing.py b/mayan/apps/document_states/tests/test_indexing.py index 6142795662..ce79edf389 100644 --- a/mayan/apps/document_states/tests/test_indexing.py +++ b/mayan/apps/document_states/tests/test_indexing.py @@ -94,7 +94,7 @@ class DocumentStateIndexingTestCase(BaseTestCase): self.document.workflows.first().do_transition( transition=self.workflow_transition, - user=self.admin_user + user=self._test_case_user ) self.assertEqual( @@ -111,7 +111,7 @@ class DocumentStateIndexingTestCase(BaseTestCase): self.document.workflows.first().do_transition( transition=self.workflow_transition, - user=self.admin_user + user=self._test_case_user ) self.document.delete(to_trash=False) diff --git a/mayan/apps/document_states/views/workflow_instance_views.py b/mayan/apps/document_states/views/workflow_instance_views.py index cc6a1be8cb..c40ba0c344 100644 --- a/mayan/apps/document_states/views/workflow_instance_views.py +++ b/mayan/apps/document_states/views/workflow_instance_views.py @@ -56,7 +56,7 @@ class DocumentWorkflowInstanceListView(SingleObjectListView): ) % self.get_document(), } - def get_object_list(self): + def get_source_queryset(self): return self.get_document().workflows.all() @@ -89,7 +89,7 @@ class WorkflowInstanceDetailView(SingleObjectListView): 'workflow_instance': self.get_workflow_instance(), } - def get_object_list(self): + def get_source_queryset(self): return self.get_workflow_instance().log_entries.order_by('-datetime') def get_workflow_instance(self): diff --git a/mayan/apps/document_states/views/workflow_proxy_views.py b/mayan/apps/document_states/views/workflow_proxy_views.py index 68cc93fcdc..aa0f53c83f 100644 --- a/mayan/apps/document_states/views/workflow_proxy_views.py +++ b/mayan/apps/document_states/views/workflow_proxy_views.py @@ -1,6 +1,5 @@ from __future__ import absolute_import, unicode_literals -from django.shortcuts import get_object_or_404 from django.template import RequestContext from django.utils.translation import ugettext_lazy as _ @@ -71,14 +70,14 @@ class WorkflowRuntimeProxyListView(SingleObjectListView): 'title': _('Workflows'), } - def get_object_list(self): + def get_source_queryset(self): return WorkflowRuntimeProxy.objects.all() class WorkflowRuntimeProxyStateDocumentListView(ExternalObjectMixin, DocumentListView): - external_object_class = WorkflowRuntimeProxy + external_object_class = WorkflowStateRuntimeProxy external_object_permission = permission_workflow_view - external_object_pk_url_kwarg = 'workflow_runtime_proxy_id' + external_object_pk_url_kwarg = 'workflow_runtime_proxy_state_id' def get_document_queryset(self): return self.get_workflow_state().get_documents() @@ -106,15 +105,6 @@ class WorkflowRuntimeProxyStateDocumentListView(ExternalObjectMixin, DocumentLis return context def get_workflow_state(self): - workflow_state = get_object_or_404( - klass=WorkflowRuntimeProxyStateDocumentListView, - pk=self.kwargs['workflow_instance_state_id'], - workflow_pk=self.get_workflow() - ) - - return workflow_state - - def get_workflow(self): return self.get_external_object() @@ -142,7 +132,7 @@ class WorkflowRuntimeProxyStateListView(ExternalObjectMixin, SingleObjectListVie 'title': _('States of workflow: %s') % self.get_workflow() } - def get_object_list(self): + def get_source_queryset(self): return WorkflowStateRuntimeProxy.objects.filter( workflow=self.get_workflow() ) diff --git a/mayan/apps/document_states/views/workflow_views.py b/mayan/apps/document_states/views/workflow_views.py index b124da1fce..4c22ac4707 100644 --- a/mayan/apps/document_states/views/workflow_views.py +++ b/mayan/apps/document_states/views/workflow_views.py @@ -322,7 +322,7 @@ class WorkflowStateActionListView(SingleObjectListView): def get_form_schema(self): return {'fields': self.get_class().fields} - def get_object_list(self): + def get_source_queryset(self): return self.get_workflow_state().actions.all() def get_workflow_state(self): @@ -380,7 +380,7 @@ class WorkflowStateCreateView(ExternalObjectMixin, SingleObjectCreateView): def get_instance_extra_data(self): return {'workflow': self.get_workflow()} - def get_object_list(self): + def get_source_queryset(self): return self.get_workflow().states.all() def get_success_url(self): @@ -457,7 +457,7 @@ class WorkflowStateListView(ExternalObjectMixin, SingleObjectListView): 'title': _('States of workflow: %s') % self.get_workflow() } - def get_object_list(self): + def get_source_queryset(self): return self.get_workflow().states.all() def get_workflow(self): @@ -488,7 +488,7 @@ class WorkflowTransitionCreateView(ExternalObjectMixin, SingleObjectCreateView): def get_instance_extra_data(self): return {'workflow': self.get_workflow()} - def get_object_list(self): + def get_source_queryset(self): return self.get_workflow().transitions.all() def get_success_url(self): @@ -575,7 +575,7 @@ class WorkflowTransitionListView(ExternalObjectMixin, SingleObjectListView): ) % self.get_workflow() } - def get_object_list(self): + def get_source_queryset(self): return self.get_workflow().transitions.all() def get_workflow(self): From 08fac9fd9de6ba1ea2b8de4d3d0319cd57412ac9 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Wed, 30 Jan 2019 03:11:07 -0400 Subject: [PATCH 074/209] Events: Update generic view interface Signed-off-by: Roberto Rosario --- mayan/apps/events/apps.py | 1 + mayan/apps/events/views.py | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/mayan/apps/events/apps.py b/mayan/apps/events/apps.py index a320a2b454..60c1f562b7 100644 --- a/mayan/apps/events/apps.py +++ b/mayan/apps/events/apps.py @@ -29,6 +29,7 @@ class EventsApp(MayanAppConfig): def ready(self): super(EventsApp, self).ready() + Action = apps.get_model(app_label='actstream', model_name='Action') Notification = self.get_model(model_name='Notification') StoredEventType = self.get_model(model_name='StoredEventType') diff --git a/mayan/apps/events/views.py b/mayan/apps/events/views.py index 49603b33bb..a206c8975c 100644 --- a/mayan/apps/events/views.py +++ b/mayan/apps/events/views.py @@ -33,7 +33,7 @@ class EventListView(SingleObjectListView): 'title': _('Events'), } - def get_object_list(self): + def get_source_queryset(self): return Action.objects.all() @@ -121,7 +121,7 @@ class NotificationListView(SingleObjectListView): 'title': _('Notifications'), } - def get_object_list(self): + def get_source_queryset(self): return self.request.user.notifications.all() @@ -184,7 +184,7 @@ class ObjectEventListView(EventListView): klass=queryset, pk=self.kwargs['object_id'] ) - def get_object_list(self): + def get_source_queryset(self): return any_stream(self.get_object()) @@ -274,5 +274,5 @@ class VerbEventListView(SingleObjectListView): ) % EventType.get(name=self.kwargs['verb']), } - def get_object_list(self): + def get_source_queryset(self): return Action.objects.filter(verb=self.kwargs['verb']) From b4a81ee0bc5cbcbd2980366d76594337d9630cf5 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Wed, 30 Jan 2019 03:29:12 -0400 Subject: [PATCH 075/209] Random ID test mixin: Restore save method Signed-off-by: Roberto Rosario --- mayan/apps/common/tests/mixins.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/mayan/apps/common/tests/mixins.py b/mayan/apps/common/tests/mixins.py index 9f3fa318f7..565a25f569 100644 --- a/mayan/apps/common/tests/mixins.py +++ b/mayan/apps/common/tests/mixins.py @@ -170,22 +170,26 @@ class RandomPrimaryKeyModelMonkeyPatchMixin(object): return primary_key def setUp(self): - original_save = models.Model.save + self.method_save_original = models.Model.save - def new_save(self, *args, **kwargs): - if self.pk: - return original_save(self, *args, **kwargs) + def method_save_new(instance, *args, **kwargs): + if instance.pk: + return self.method_save_original(instance, *args, **kwargs) else: - self.pk = RandomPrimaryKeyModelMonkeyPatchMixin.get_unique_primary_key( - model=self._meta.model + instance.pk = RandomPrimaryKeyModelMonkeyPatchMixin.get_unique_primary_key( + model=instance._meta.model ) - self.id = self.pk + instance.id = instance.pk - return self.save_base(force_insert=True) + return instance.save_base(force_insert=True) - setattr(models.Model, 'save', new_save) + setattr(models.Model, 'save', method_save_new) super(RandomPrimaryKeyModelMonkeyPatchMixin, self).setUp() + def tearDown(self): + models.Model.save = self.method_save_original + super(RandomPrimaryKeyModelMonkeyPatchMixin, self).tearDown() + class TempfileCheckTestCaseMixin(object): # Ignore the jvmstat instrumentation and GitLab's CI .config files From 4ba2d375af841e824ebba2ea20f6bc7366271c68 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Wed, 30 Jan 2019 03:54:10 -0400 Subject: [PATCH 076/209] Update generic view and check access interfaces Signed-off-by: Roberto Rosario --- mayan/apps/acls/tests/test_links.py | 36 ++++++------------- mayan/apps/acls/views.py | 2 +- mayan/apps/cabinets/apps.py | 6 ++-- mayan/apps/cabinets/views.py | 16 ++++----- mayan/apps/converter/views.py | 14 ++++---- .../apps/document_comments/tests/test_api.py | 12 +++---- mayan/apps/document_comments/views.py | 2 +- .../document_indexing/tests/test_views.py | 4 +-- mayan/apps/document_indexing/views.py | 20 +++++------ mayan/apps/document_signatures/views.py | 8 ++--- .../views/workflow_instance_views.py | 8 ++--- .../documents/tests/test_document_views.py | 8 ++--- mayan/apps/dynamic_search/views.py | 2 +- mayan/apps/file_metadata/views.py | 4 +-- mayan/apps/linking/apps.py | 6 ++-- mayan/apps/linking/views.py | 26 +++++++------- mayan/apps/mailer/links.py | 14 ++++---- mayan/apps/mailer/tests/mixins.py | 6 ++-- mayan/apps/mailer/tests/test_views.py | 4 +-- mayan/apps/mailer/urls.py | 12 +++---- mayan/apps/mailer/views.py | 32 ++++++++--------- mayan/apps/metadata/tests/test_events.py | 10 +----- mayan/apps/metadata/tests/test_views.py | 2 +- mayan/apps/permissions/tests/test_views.py | 14 ++++---- mayan/apps/sources/views.py | 8 ++--- mayan/apps/tags/models.py | 2 +- 26 files changed, 128 insertions(+), 150 deletions(-) diff --git a/mayan/apps/acls/tests/test_links.py b/mayan/apps/acls/tests/test_links.py index 231ad6e78d..78d3821685 100644 --- a/mayan/apps/acls/tests/test_links.py +++ b/mayan/apps/acls/tests/test_links.py @@ -14,12 +14,7 @@ from ..permissions import permission_acl_edit, permission_acl_view class ACLsLinksTestCase(GenericDocumentViewTestCase): def test_document_acl_create_link(self): - acl = AccessControlList.objects.create( - content_object=self.document, role=self.role - ) - - acl.permissions.add(permission_acl_edit.stored_permission) - self.login_user() + self.grant_access(obj=self.document, permission=permission_acl_edit) self.add_test_view(test_object=self.document) context = self.get_test_view() @@ -39,12 +34,7 @@ class ACLsLinksTestCase(GenericDocumentViewTestCase): ) def test_document_acl_delete_link(self): - acl = AccessControlList.objects.create( - content_object=self.document, role=self.role - ) - - acl.permissions.add(permission_acl_edit.stored_permission) - self.login_user() + self.grant_access(obj=self.document, permission=permission_acl_edit) self.add_test_view(test_object=acl) context = self.get_test_view() @@ -53,16 +43,13 @@ class ACLsLinksTestCase(GenericDocumentViewTestCase): self.assertNotEqual(resolved_link, None) self.assertEqual( - resolved_link.url, reverse(viewname='acls:acl_delete', kwargs={'acl_pk': acl.pk}) + resolved_link.url, reverse( + viewname='acls:acl_delete', kwargs={'acl_id': acl.pk} + ) ) def test_document_acl_edit_link(self): - acl = AccessControlList.objects.create( - content_object=self.document, role=self.role - ) - - acl.permissions.add(permission_acl_edit.stored_permission) - self.login_user() + self.grant_access(obj=self.document, permission=permission_acl_edit) self.add_test_view(test_object=acl) context = self.get_test_view() @@ -71,16 +58,13 @@ class ACLsLinksTestCase(GenericDocumentViewTestCase): self.assertNotEqual(resolved_link, None) self.assertEqual( - resolved_link.url, reverse(viewname='acls:acl_permissions', kwargs={'acl_pk': acl.pk}) + resolved_link.url, reverse( + viewname='acls:acl_permissions', kwargs={'acl_id': acl.pk} + ) ) def test_document_acl_list_link(self): - acl = AccessControlList.objects.create( - content_object=self.document, role=self.role - ) - - acl.permissions.add(permission_acl_view.stored_permission) - self.login_user() + self.grant_access(obj=self.document, permission=permission_acl_view) self.add_test_view(test_object=self.document) context = self.get_test_view() diff --git a/mayan/apps/acls/views.py b/mayan/apps/acls/views.py index 7e0b7b37b1..b92b7d5f16 100644 --- a/mayan/apps/acls/views.py +++ b/mayan/apps/acls/views.py @@ -132,7 +132,7 @@ class ACLListView(ContentTypeViewMixin, ExternalObjectMixin, SingleObjectListVie ), } - def get_object_list(self): + def get_source_queryset(self): return self.get_external_object().acls.all() diff --git a/mayan/apps/cabinets/apps.py b/mayan/apps/cabinets/apps.py index 4f1d6c4514..3328fcf0e8 100644 --- a/mayan/apps/cabinets/apps.py +++ b/mayan/apps/cabinets/apps.py @@ -75,9 +75,9 @@ class CabinetsApp(MayanAppConfig): permission_cabinet_remove_document ) ) - ModelPermission.register_inheritance( - model=Cabinet, related='get_root', - ) + #ModelPermission.register_inheritance( + # model=Cabinet, related='get_root', + #) SourceColumn( func=lambda context: widget_document_cabinets( diff --git a/mayan/apps/cabinets/views.py b/mayan/apps/cabinets/views.py index c61febaf0f..531db9f063 100644 --- a/mayan/apps/cabinets/views.py +++ b/mayan/apps/cabinets/views.py @@ -64,7 +64,7 @@ class CabinetChildAddView(SingleObjectCreateView): cabinet = super(CabinetChildAddView, self).get_object(*args, **kwargs) AccessControlList.objects.check_access( - permissions=permission_cabinet_edit, obj=cabinet.get_root(), + obj=cabinet.get_root(), permission=permission_cabinet_edit, user=self.request.user, raise_404=True ) @@ -146,7 +146,7 @@ class CabinetDetailView(DocumentListView): permission_object = cabinet.get_root() AccessControlList.objects.check_access( - permissions=permission_cabinet_view, obj=permission_object, + obj=permission_object, permission=permission_cabinet_view, user=self.request.user, raise_404=True ) @@ -187,7 +187,7 @@ class CabinetListView(SingleObjectListView): 'no_results_title': _('No cabinets available'), } - def get_object_list(self): + def get_source_queryset(self): # Add explicit ordering of root nodes since the queryset returned # is not affected by the model's order Meta option. return Cabinet.objects.root_nodes().order_by('label') @@ -200,8 +200,8 @@ class DocumentCabinetListView(CabinetListView): ) AccessControlList.objects.check_access( - permissions=permission_document_view, user=request.user, - obj=self.document, raise_404=True + obj=self.document, permission=permission_document_view, + user=request.user, raise_404=True ) return super(DocumentCabinetListView, self).dispatch( @@ -227,7 +227,7 @@ class DocumentCabinetListView(CabinetListView): 'title': _('Cabinets containing document: %s') % self.document, } - def get_object_list(self): + def get_source_queryset(self): return self.document.get_cabinets().all() @@ -295,7 +295,7 @@ class DocumentAddToCabinetView(MultipleObjectFormActionView): for cabinet in form.cleaned_data['cabinets']: AccessControlList.objects.check_access( - obj=cabinet, permissions=permission_cabinet_add_document, + obj=cabinet, permission=permission_cabinet_add_document, user=self.request.user, raise_404=True ) if cabinet in cabinet_membership: @@ -383,7 +383,7 @@ class DocumentRemoveFromCabinetView(MultipleObjectFormActionView): for cabinet in form.cleaned_data['cabinets']: AccessControlList.objects.check_access( - obj=cabinet, permissions=permission_cabinet_remove_document, + obj=cabinet, permission=permission_cabinet_remove_document, user=self.request.user, raise_404=True ) diff --git a/mayan/apps/converter/views.py b/mayan/apps/converter/views.py index 8aa4b5089c..3a728db467 100644 --- a/mayan/apps/converter/views.py +++ b/mayan/apps/converter/views.py @@ -37,8 +37,8 @@ class TransformationDeleteView(SingleObjectDeleteView): ) AccessControlList.objects.check_access( - permissions=permission_transformation_delete, - obj=self.transformation.content_object, user=request.user + obj=self.transformation.content_object, + permission=permission_transformation_delete, user=request.user ) return super(TransformationDeleteView, self).dispatch( @@ -93,8 +93,8 @@ class TransformationCreateView(SingleObjectCreateView): raise Http404 AccessControlList.objects.check_access( - permissions=permission_transformation_create, - obj=self.content_object, user=request.user + obj=self.content_object, permission=permission_transformation_create, + user=request.user ) return super(TransformationCreateView, self).dispatch( @@ -149,7 +149,7 @@ class TransformationEditView(SingleObjectEditView): AccessControlList.objects.check_access( obj=self.transformation.content_object, - permissions=permission_transformation_edit, user=request.user + permission=permission_transformation_edit, user=request.user ) return super(TransformationEditView, self).dispatch( @@ -206,7 +206,7 @@ class TransformationListView(SingleObjectListView): AccessControlList.objects.check_access( obj=self.content_object, - permissions=permission_transformation_view, + permission=permission_transformation_view, user=request.user ) @@ -235,5 +235,5 @@ class TransformationListView(SingleObjectListView): 'title': _('Transformations for: %s') % self.content_object, } - def get_object_list(self): + def get_source_queryset(self): return Transformation.objects.get_for_model(obj=self.content_object) diff --git a/mayan/apps/document_comments/tests/test_api.py b/mayan/apps/document_comments/tests/test_api.py index 15ef08722a..231313593c 100644 --- a/mayan/apps/document_comments/tests/test_api.py +++ b/mayan/apps/document_comments/tests/test_api.py @@ -19,7 +19,7 @@ class CommentAPITestCase(CommentsTestMixin, DocumentTestMixin, BaseAPITestCase): def _request_api_comment_create_view(self): return self.post( viewname='rest_api:comment-list', - kwargs={'document_pk': self.document.pk}, data={ + kwargs={'document_id': self.document.pk}, data={ 'comment': TEST_COMMENT_TEXT } ) @@ -40,8 +40,8 @@ class CommentAPITestCase(CommentsTestMixin, DocumentTestMixin, BaseAPITestCase): def _request_api_comment_delete_view(self): return self.delete( viewname='rest_api:comment-detail', kwargs={ - 'document_pk': self.document.pk, - 'comment_pk': self.test_comment.pk + 'document_id': self.document.pk, + 'comment_id': self.test_comment.pk } ) @@ -63,8 +63,8 @@ class CommentAPITestCase(CommentsTestMixin, DocumentTestMixin, BaseAPITestCase): def _request_api_comment_detail_view(self): return self.get( viewname='rest_api:comment-detail', kwargs={ - 'document_pk': self.document.pk, - 'comment_pk': self.test_comment.pk + 'document_id': self.document.pk, + 'comment_id': self.test_comment.pk } ) @@ -85,7 +85,7 @@ class CommentAPITestCase(CommentsTestMixin, DocumentTestMixin, BaseAPITestCase): def _request_api_comment_list_view(self): return self.get( viewname='rest_api:comment-list', - kwargs={'document_pk': self.document.pk} + kwargs={'document_id': self.document.pk} ) def test_comment_list_view_no_access(self): diff --git a/mayan/apps/document_comments/views.py b/mayan/apps/document_comments/views.py index f009630156..0881e2b4e1 100644 --- a/mayan/apps/document_comments/views.py +++ b/mayan/apps/document_comments/views.py @@ -100,5 +100,5 @@ class DocumentCommentListView(ExternalObjectMixin, SingleObjectListView): 'title': _('Comments for document: %s') % self.get_document(), } - def get_object_list(self): + def get_source_queryset(self): return self.get_document().comments.all() diff --git a/mayan/apps/document_indexing/tests/test_views.py b/mayan/apps/document_indexing/tests/test_views.py index a38fb13e0e..56fcd49c6f 100644 --- a/mayan/apps/document_indexing/tests/test_views.py +++ b/mayan/apps/document_indexing/tests/test_views.py @@ -56,7 +56,7 @@ class IndexViewTestCase(GenericDocumentViewTestCase): ) response = self._request_index_delete_view(index=index) - self.assertEqual(response.status_code, 403) + self.assertEqual(response.status_code, 404) self.assertEqual(Index.objects.count(), 1) def test_index_delete_view_with_permission(self): @@ -88,7 +88,7 @@ class IndexViewTestCase(GenericDocumentViewTestCase): ) response = self._request_index_edit_view(index=index) - self.assertEqual(response.status_code, 403) + self.assertEqual(response.status_code, 404) index = Index.objects.get(pk=index.pk) self.assertEqual(index.label, TEST_INDEX_LABEL) diff --git a/mayan/apps/document_indexing/views.py b/mayan/apps/document_indexing/views.py index 3729798e6d..0de438b29d 100644 --- a/mayan/apps/document_indexing/views.py +++ b/mayan/apps/document_indexing/views.py @@ -103,8 +103,8 @@ class SetupIndexDocumentTypesView(AssignRemoveView): def get_document_queryset(self): return AccessControlList.objects.restrict_queryset( - permission_document_view, queryset=DocumentType.objects.all(), - user=self.request.user + permission=permission_document_view, + queryset=DocumentType.objects.all(), user=self.request.user ) def get_extra_context(self): @@ -153,7 +153,7 @@ class SetupIndexTreeTemplateListView(SingleObjectListView): def get_index(self): return get_object_or_404(klass=Index, pk=self.kwargs['index_pk']) - def get_object_list(self): + def get_source_queryset(self): return self.get_index().template_root.get_descendants( include_self=True ) @@ -166,7 +166,7 @@ class TemplateNodeCreateView(SingleObjectCreateView): def dispatch(self, request, *args, **kwargs): AccessControlList.objects.check_access( obj=self.get_parent_node().index, - permissions=permission_document_indexing_edit, user=request.user + permission=permission_document_indexing_edit, user=request.user ) return super( @@ -254,7 +254,7 @@ class IndexListView(SingleObjectListView): 'title': _('Indexes'), } - def get_object_list(self): + def get_source_queryset(self): queryset = IndexInstance.objects.filter(enabled=True) return queryset.filter( node_templates__index_instance_nodes__isnull=False @@ -271,7 +271,7 @@ class IndexInstanceNodeView(DocumentListView): AccessControlList.objects.check_access( obj=self.index_instance_node.index(), - permissions=permission_document_indexing_instance_view, + permission=permission_document_indexing_instance_view, user=request.user ) @@ -317,10 +317,10 @@ class IndexInstanceNodeView(DocumentListView): return context - def get_object_list(self): + def get_source_queryset(self): if self.index_instance_node: if self.index_instance_node.index_template_node.link_documents: - return super(IndexInstanceNodeView, self).get_object_list() + return super(IndexInstanceNodeView, self).get_source_queryset() else: self.object_permission = None return self.index_instance_node.get_children().order_by( @@ -339,7 +339,7 @@ class DocumentIndexNodeListView(SingleObjectListView): def dispatch(self, request, *args, **kwargs): AccessControlList.objects.check_access( - obj=self.get_document(), permissions=permission_document_view, + obj=self.get_document(), permission=permission_document_view, user=request.user ) @@ -370,7 +370,7 @@ class DocumentIndexNodeListView(SingleObjectListView): ) % self.get_document(), } - def get_object_list(self): + def get_source_queryset(self): return DocumentIndexInstanceNode.objects.get_for( document=self.get_document() ) diff --git a/mayan/apps/document_signatures/views.py b/mayan/apps/document_signatures/views.py index e77f4aa261..571343db07 100644 --- a/mayan/apps/document_signatures/views.py +++ b/mayan/apps/document_signatures/views.py @@ -217,7 +217,7 @@ class DocumentVersionSignatureDeleteView(SingleObjectDeleteView): kwargs={'document_version_id': self.get_object().document_version.pk} ) - def get_object_list(self): + def get_source_queryset(self): return SignatureBaseModel.objects.select_subclasses() @@ -236,7 +236,7 @@ class DocumentVersionSignatureDetailView(SingleObjectDetailView): ) % self.get_object(), } - def get_object_list(self): + def get_source_queryset(self): return SignatureBaseModel.objects.select_subclasses() @@ -251,7 +251,7 @@ class DocumentVersionSignatureDownloadView(SingleObjectDownloadView): signature.signature_file, name=force_text(signature) ) - def get_object_list(self): + def get_source_queryset(self): return SignatureBaseModel.objects.select_subclasses() @@ -297,7 +297,7 @@ class DocumentVersionSignatureListView(ExternalObjectMixin, SingleObjectListView ) % self.get_document_version(), } - def get_object_list(self): + def get_source_queryset(self): return self.get_document_version().signatures.all() diff --git a/mayan/apps/document_states/views/workflow_instance_views.py b/mayan/apps/document_states/views/workflow_instance_views.py index c40ba0c344..18ef1f2ce0 100644 --- a/mayan/apps/document_states/views/workflow_instance_views.py +++ b/mayan/apps/document_states/views/workflow_instance_views.py @@ -26,8 +26,8 @@ __all__ = ( class DocumentWorkflowInstanceListView(SingleObjectListView): def dispatch(self, request, *args, **kwargs): AccessControlList.objects.check_access( - permissions=permission_workflow_view, user=request.user, - obj=self.get_document() + obj=self.get_document(), permission=permission_workflow_view, + user=request.user, ) return super( @@ -63,8 +63,8 @@ class DocumentWorkflowInstanceListView(SingleObjectListView): class WorkflowInstanceDetailView(SingleObjectListView): def dispatch(self, request, *args, **kwargs): AccessControlList.objects.check_access( - permissions=permission_workflow_view, user=request.user, - obj=self.get_workflow_instance().document + obj=self.get_workflow_instance().document, + permission=permission_workflow_view, user=request.user ) return super( diff --git a/mayan/apps/documents/tests/test_document_views.py b/mayan/apps/documents/tests/test_document_views.py index 189900c18b..24712d2555 100644 --- a/mayan/apps/documents/tests/test_document_views.py +++ b/mayan/apps/documents/tests/test_document_views.py @@ -80,7 +80,7 @@ class DocumentsViewsTestCase(GenericDocumentViewTestCase): document_type=document_type_2 ) - self.assertEqual(response.status_code, 302) + self.assertEqual(response.status_code, 404) self.assertEqual( Document.objects.get(pk=self.document.pk).document_type, @@ -136,7 +136,7 @@ class DocumentsViewsTestCase(GenericDocumentViewTestCase): document_type=document_type_2 ) - self.assertEqual(response.status_code, 302) + self.assertEqual(response.status_code, 404) self.assertEqual( Document.objects.first().document_type, self.document_type @@ -300,7 +300,7 @@ class DocumentsViewsTestCase(GenericDocumentViewTestCase): self.assertEqual(self.document.pages.count(), 0) response = self._request_document_multiple_update_page_count_view() - self.assertEqual(response.status_code, 302) + self.assertEqual(response.status_code, 404) self.assertEqual(self.document.pages.count(), 0) def test_document_multiple_update_page_count_view_with_permission(self): @@ -394,7 +394,7 @@ class DocumentsViewsTestCase(GenericDocumentViewTestCase): self.grant_permission(permission=permission_document_view) response = self._request_document_multiple_transformations_clear() - self.assertEqual(response.status_code, 302) + self.assertEqual(response.status_code, 404) self.assertQuerysetEqual( Transformation.objects.get_for_model(document_page), (repr(transformation),) diff --git a/mayan/apps/dynamic_search/views.py b/mayan/apps/dynamic_search/views.py index 5a475ce8ea..42d091bf17 100644 --- a/mayan/apps/dynamic_search/views.py +++ b/mayan/apps/dynamic_search/views.py @@ -51,7 +51,7 @@ class ResultsView(SearchModelMixin, SingleObjectListView): query_string=self.request.GET ) - def get_object_list(self): + def get_source_queryset(self): self.search_model = self.get_search_model() if self.request.GET: diff --git a/mayan/apps/file_metadata/views.py b/mayan/apps/file_metadata/views.py index 7d87259270..7afdf6ee65 100644 --- a/mayan/apps/file_metadata/views.py +++ b/mayan/apps/file_metadata/views.py @@ -54,7 +54,7 @@ class DocumentDriverListView(SingleObjectListView): ) return document - def get_object_list(self): + def get_source_queryset(self): return self.get_object().latest_version.file_metadata_drivers.all() @@ -84,7 +84,7 @@ class DocumentVersionDriverEntryFileMetadataListView(SingleObjectListView): ) return document_version_driver_entry - def get_object_list(self): + def get_source_queryset(self): return self.get_object().entries.all() diff --git a/mayan/apps/linking/apps.py b/mayan/apps/linking/apps.py index d22d6de8c8..05890f0d6a 100644 --- a/mayan/apps/linking/apps.py +++ b/mayan/apps/linking/apps.py @@ -44,9 +44,9 @@ class LinkingApp(MayanAppConfig): app_label='documents', model_name='Document' ) - ResolvedSmartLink = self.get_model('ResolvedSmartLink') - SmartLink = self.get_model('SmartLink') - SmartLinkCondition = self.get_model('SmartLinkCondition') + ResolvedSmartLink = self.get_model(model_name='ResolvedSmartLink') + SmartLink = self.get_model(model_name='SmartLink') + SmartLinkCondition = self.get_model(model_name='SmartLinkCondition') ModelPermission.register( model=SmartLink, permissions=( diff --git a/mayan/apps/linking/views.py b/mayan/apps/linking/views.py index 238b1c729b..76b4d864e7 100644 --- a/mayan/apps/linking/views.py +++ b/mayan/apps/linking/views.py @@ -40,12 +40,12 @@ class ResolvedSmartLinkView(DocumentListView): ) AccessControlList.objects.check_access( - obj=self.document, permissions=permission_document_view, + obj=self.document, permission=permission_document_view, user=request.user ) AccessControlList.objects.check_access( - obj=self.smart_link, permissions=permission_smart_link_view, + obj=self.smart_link, permission=permission_smart_link_view, user=request.user ) @@ -63,7 +63,7 @@ class ResolvedSmartLinkView(DocumentListView): try: AccessControlList.objects.check_access( - obj=self.smart_link, permissions=permission_smart_link_edit, + obj=self.smart_link, permission=permission_smart_link_edit, user=self.request.user ) except PermissionDenied: @@ -163,12 +163,12 @@ class SmartLinkListView(SingleObjectListView): 'title': _('Smart links'), } - def get_object_list(self): - return self.get_smart_link_queryset() - def get_smart_link_queryset(self): return SmartLink.objects.all() + def get_source_queryset(self): + return self.get_smart_link_queryset() + class DocumentSmartLinkListView(SmartLinkListView): def dispatch(self, request, *args, **kwargs): @@ -177,7 +177,7 @@ class DocumentSmartLinkListView(SmartLinkListView): ) AccessControlList.objects.check_access( - obj=self.document, permissions=permission_document_view, + obj=self.document, permission=permission_document_view, user=request.user ) @@ -268,21 +268,21 @@ class SmartLinkConditionListView(SingleObjectListView): ) % self.get_smart_link(), } - def get_object_list(self): - return self.get_smart_link().conditions.all() - def get_smart_link(self): return get_object_or_404( klass=SmartLink, pk=self.kwargs['smart_link_id'] ) + def get_source_queryset(self): + return self.get_smart_link().conditions.all() + class SmartLinkConditionCreateView(SingleObjectCreateView): form_class = SmartLinkConditionForm def dispatch(self, request, *args, **kwargs): AccessControlList.objects.check_access( - obj=self.get_smart_link(), permissions=permission_smart_link_edit, + obj=self.get_smart_link(), permission=permission_smart_link_edit, user=request.user ) @@ -323,7 +323,7 @@ class SmartLinkConditionEditView(SingleObjectEditView): def dispatch(self, request, *args, **kwargs): AccessControlList.objects.check_access( obj=self.get_object().smart_link, - permissions=permission_smart_link_edit, user=request.user + permission=permission_smart_link_edit, user=request.user ) return super( @@ -351,7 +351,7 @@ class SmartLinkConditionDeleteView(SingleObjectDeleteView): def dispatch(self, request, *args, **kwargs): AccessControlList.objects.check_access( obj=self.get_object().smart_link, - permissions=permission_smart_link_edit, user=request.user + permission=permission_smart_link_edit, user=request.user ) return super( diff --git a/mayan/apps/mailer/links.py b/mayan/apps/mailer/links.py index e9acd5831f..f3220583d0 100644 --- a/mayan/apps/mailer/links.py +++ b/mayan/apps/mailer/links.py @@ -18,12 +18,13 @@ from .permissions import ( ) link_document_send = Link( - args='resolved_object.pk', icon_class=icon_document_send, + icon_class=icon_document_send, kwargs={'document_id': 'resolved_object.pk'}, permission=permission_mailing_send_document, text=_('Email document'), view='mailer:document_send' ) link_document_send_link = Link( - args='resolved_object.pk', icon_class=icon_document_send_link, + icon_class=icon_document_send_link, + kwargs={'document_id': 'resolved_object.pk'}, permission=permission_mailing_link, text=_('Email link'), view='mailer:document_send_link' ) @@ -46,17 +47,18 @@ link_user_mailer_create = Link( view='mailer:user_mailer_backend_selection' ) link_user_mailer_delete = Link( - args='resolved_object.pk', icon_class=icon_user_mailer_delete, + icon_class=icon_user_mailer_delete, + kwargs={'mailer_id': 'resolved_object.pk'}, permission=permission_user_mailer_delete, tags='dangerous', text=_('Delete'), view='mailer:user_mailer_delete' ) link_user_mailer_edit = Link( - args='object.pk', icon_class=icon_user_mailer_edit, + icon_class=icon_user_mailer_edit, kwargs={'mailer_id': 'object.pk'}, permission=permission_user_mailer_edit, text=_('Edit'), view='mailer:user_mailer_edit' ) link_user_mailer_log_list = Link( - args='object.pk', permission=permission_user_mailer_view, + permission=permission_user_mailer_view, kwargs={'mailer_id': 'object.pk'}, text=_('Log'), view='mailer:user_mailer_log' ) link_user_mailer_list = Link( @@ -70,7 +72,7 @@ link_user_mailer_setup = Link( text=_('Mailing profiles'), view='mailer:user_mailer_list' ) link_user_mailer_test = Link( - args='object.pk', icon_class=icon_user_mailer_test, + icon_class=icon_user_mailer_test, kwargs={'mailer_id': 'object.pk'}, permission=permission_user_mailer_use, text=_('Test'), view='mailer:user_mailer_test' ) diff --git a/mayan/apps/mailer/tests/mixins.py b/mayan/apps/mailer/tests/mixins.py index dfa50ead6f..79ad156eb2 100644 --- a/mayan/apps/mailer/tests/mixins.py +++ b/mayan/apps/mailer/tests/mixins.py @@ -37,13 +37,13 @@ class MailerTestMixin(object): def _request_user_mailer_delete(self): return self.post( viewname='mailer:user_mailer_delete', - kwargs={'mailer_pk': self.user_mailer.pk} + kwargs={'mailer_id': self.user_mailer.pk} ) def _request_user_mailer_edit(self): return self.post( viewname='mailer:user_mailer_edit', - kwargs={'mailer_pk': self.user_mailer.pk}, + kwargs={'mailer_id': self.user_mailer.pk}, data={ 'label': TEST_USER_MAILER_LABEL_EDITED } @@ -55,7 +55,7 @@ class MailerTestMixin(object): def _request_user_mailer_test(self): return self.post( viewname='mailer:user_mailer_test', - kwargs={'mailer_pk': self.user_mailer.pk}, + kwargs={'mailer_id': self.user_mailer.pk}, data={ 'email': getattr( self, 'test_email_address', TEST_EMAIL_ADDRESS diff --git a/mayan/apps/mailer/tests/test_views.py b/mayan/apps/mailer/tests/test_views.py index 01ad745069..2906567e64 100644 --- a/mayan/apps/mailer/tests/test_views.py +++ b/mayan/apps/mailer/tests/test_views.py @@ -190,7 +190,7 @@ class DocumentViewsTestCase(MailerTestMixin, GenericDocumentViewTestCase): def _request_document_link_send(self): return self.post( viewname='mailer:document_send_link', - kwargs={'document_pk': self.document.pk}, + kwargs={'document_id': self.document.pk}, data={ 'email': getattr( self, 'test_email_address', TEST_EMAIL_ADDRESS @@ -202,7 +202,7 @@ class DocumentViewsTestCase(MailerTestMixin, GenericDocumentViewTestCase): def _request_document_send(self): return self.post( viewname='mailer:document_send', - kwargs={'document_pk': self.document.pk}, + kwargs={'document_id': self.document.pk}, data={ 'email': getattr( self, 'test_email_address', TEST_EMAIL_ADDRESS diff --git a/mayan/apps/mailer/urls.py b/mayan/apps/mailer/urls.py index 490d88d573..915d0f7798 100644 --- a/mayan/apps/mailer/urls.py +++ b/mayan/apps/mailer/urls.py @@ -11,7 +11,7 @@ from .views import ( urlpatterns = [ url( - regex=r'^documents/(?P\d+)/send/link/$', + regex=r'^documents/(?P\d+)/send/link/$', name='document_send_link', view=MailDocumentLinkView.as_view() ), url( @@ -20,7 +20,7 @@ urlpatterns = [ view=MailDocumentLinkView.as_view() ), url( - regex=r'^documents/(?P\d+)/send/$', name='document_send', + regex=r'^documents/(?P\d+)/send/$', name='document_send', view=MailDocumentView.as_view() ), url( @@ -41,19 +41,19 @@ urlpatterns = [ name='user_mailer_create', view=UserMailingCreateView.as_view() ), url( - regex=r'^user_mailers/(?P\d+)/delete/$', + regex=r'^user_mailers/(?P\d+)/delete/$', name='user_mailer_delete', view=UserMailingDeleteView.as_view() ), url( - regex=r'^user_mailers/(?P\d+)/edit/$', + regex=r'^user_mailers/(?P\d+)/edit/$', name='user_mailer_edit', view=UserMailingEditView.as_view() ), url( - regex=r'^user_mailers/(?P\d+)/log/$', + regex=r'^user_mailers/(?P\d+)/log/$', name='user_mailer_log', view=UserMailerLogEntryListView.as_view() ), url( - regex=r'^user_mailers/(?P\d+)/test/$', + regex=r'^user_mailers/(?P\d+)/test/$', name='user_mailer_test', view=UserMailerTestView.as_view() ), url( diff --git a/mayan/apps/mailer/views.py b/mayan/apps/mailer/views.py index d4e12ef807..e32d5a810b 100644 --- a/mayan/apps/mailer/views.py +++ b/mayan/apps/mailer/views.py @@ -47,7 +47,7 @@ class MailDocumentView(MultipleObjectFormActionView): form_class = DocumentMailForm model = Document object_permission = permission_mailing_send_document - pk_url_kwarg = 'document_pk' + pk_url_kwarg = 'document_id' success_message = _('%(count)d document queued for email delivery') success_message_plural = _( '%(count)d documents queued for email delivery' @@ -63,9 +63,9 @@ class MailDocumentView(MultipleObjectFormActionView): 'submit_icon_class': icon_mail_document_submit, 'submit_label': _('Send'), 'title': ungettext( - self.title, - self.title_plural, - queryset.count() + singular=self.title, + plural=self.title_plural, + number=queryset.count() ) } @@ -87,8 +87,8 @@ class MailDocumentView(MultipleObjectFormActionView): def object_action(self, form, instance): AccessControlList.objects.check_access( - permissions=permission_user_mailer_use, user=self.request.user, - obj=form.cleaned_data['user_mailer'] + obj=form.cleaned_data['user_mailer'], + permission=permission_user_mailer_use, user=self.request.user ) task_send_document.apply_async( @@ -126,7 +126,7 @@ class UserMailerBackendSelectionView(FormView): def form_valid(self, form): backend = form.cleaned_data['backend'] return HttpResponseRedirect( - reverse( + redirect_to=reverse( viewname='mailer:user_mailer_create', kwargs={ 'class_path': backend } @@ -172,7 +172,7 @@ class UserMailingCreateView(SingleObjectDynamicFormCreateView): class UserMailingDeleteView(SingleObjectDeleteView): model = UserMailer object_permission = permission_user_mailer_delete - pk_url_kwarg = 'mailer_pk' + pk_url_kwarg = 'mailer_id' post_action_redirect = reverse_lazy(viewname='mailer:user_mailer_list') def get_extra_context(self): @@ -185,7 +185,7 @@ class UserMailingEditView(SingleObjectDynamicFormEditView): form_class = UserMailerDynamicForm model = UserMailer object_permission = permission_user_mailer_edit - pk_url_kwarg = 'mailer_pk' + pk_url_kwarg = 'mailer_id' def get_extra_context(self): return { @@ -217,11 +217,11 @@ class UserMailerLogEntryListView(SingleObjectListView): ) % self.get_user_mailer(), } - def get_object_list(self): + def get_source_queryset(self): return self.get_user_mailer().error_log.all() def get_user_mailer(self): - return get_object_or_404(klass=UserMailer, pk=self.kwargs['mailer_pk']) + return get_object_or_404(klass=UserMailer, pk=self.kwargs['mailer_id']) class UserMailerListView(SingleObjectListView): @@ -260,15 +260,15 @@ class UserMailerTestView(FormView): obj.test(to=form.cleaned_data['email']) except Exception as exception: messages.error( - request=self.request, message=_( + message=_( 'Error sending test message; %s.' - ) % exception + ) % exception, request=self.request ) else: messages.success( - request=self.request, message=_( + message=_( 'Successfully sent test message.' - ) + ), request=self.request ) return super(UserMailerTestView, self).form_valid(form=form) @@ -283,7 +283,7 @@ class UserMailerTestView(FormView): def get_object(self): return get_object_or_404( - klass=self.get_queryset(), pk=self.kwargs['mailer_pk'] + klass=self.get_queryset(), pk=self.kwargs['mailer_id'] ) def get_queryset(self): diff --git a/mayan/apps/metadata/tests/test_events.py b/mayan/apps/metadata/tests/test_events.py index 6c1611433a..75d8e30ecf 100644 --- a/mayan/apps/metadata/tests/test_events.py +++ b/mayan/apps/metadata/tests/test_events.py @@ -17,8 +17,6 @@ from .mixins import MetadataTestsMixin class MetadataTypeEventsTestCase(MetadataTestsMixin, GenericDocumentViewTestCase): def test_metadata_type_create_event_no_permissions(self): - self.login_user() - Action.objects.all().delete() response = self._request_metadata_type_create_view() @@ -26,8 +24,6 @@ class MetadataTypeEventsTestCase(MetadataTestsMixin, GenericDocumentViewTestCase self.assertEqual(Action.objects.count(), 0) def test_metadata_type_create_event_with_permissions(self): - self.login_user() - Action.objects.all().delete() self.grant_permission(permission=permission_metadata_type_create) @@ -47,19 +43,15 @@ class MetadataTypeEventsTestCase(MetadataTestsMixin, GenericDocumentViewTestCase def test_metadata_type_edit_event_no_permissions(self): self._create_metadata_type() - self.login_user() - Action.objects.all().delete() response = self._request_metadata_type_edit_view() - self.assertEqual(response.status_code, 403) + self.assertEqual(response.status_code, 404) self.assertEqual(Action.objects.count(), 0) def test_metadata_type_edit_event_with_access(self): self._create_metadata_type() - self.login_user() - Action.objects.all().delete() self.grant_access( diff --git a/mayan/apps/metadata/tests/test_views.py b/mayan/apps/metadata/tests/test_views.py index b76afbbdf4..f483319b92 100644 --- a/mayan/apps/metadata/tests/test_views.py +++ b/mayan/apps/metadata/tests/test_views.py @@ -163,7 +163,7 @@ class DocumentMetadataTestCase(MetadataTestsMixin, GenericDocumentViewTestCase): ) response = self._request_post_document_document_metadata_remove_view() - self.assertEqual(response.status_code, 302) + self.assertEqual(response.status_code, 404) self.assertEqual(len(self.document.metadata.all()), 1) diff --git a/mayan/apps/permissions/tests/test_views.py b/mayan/apps/permissions/tests/test_views.py index 2ec8ecfe2a..a4a6252999 100644 --- a/mayan/apps/permissions/tests/test_views.py +++ b/mayan/apps/permissions/tests/test_views.py @@ -49,10 +49,10 @@ class PermissionsViewsTestCase(GroupTestMixin, RoleTestMixin, GenericViewTestCas kwargs={'role_id': self.test_role.pk} ) - def test_role_delete_view_no_access(self): + def test_role_delete_view_no_permission(self): self._create_test_role() response = self._request_role_delete_view() - self.assertEqual(response.status_code, 403) + self.assertEqual(response.status_code, 404) self.assertEqual(Role.objects.count(), 2) self.assertTrue( TEST_ROLE_LABEL in Role.objects.values_list('label', flat=True) @@ -76,11 +76,11 @@ class PermissionsViewsTestCase(GroupTestMixin, RoleTestMixin, GenericViewTestCas } ) - def test_role_edit_view_no_access(self): + def test_role_edit_view_no_permission(self): self._create_test_role() response = self._request_role_edit_view() - self.assertEqual(response.status_code, 403) + self.assertEqual(response.status_code, 404) self.test_role.refresh_from_db() self.assertEqual(Role.objects.count(), 2) @@ -100,7 +100,7 @@ class PermissionsViewsTestCase(GroupTestMixin, RoleTestMixin, GenericViewTestCas def _request_role_list_view(self): return self.get(viewname='permissions:role_list') - def test_role_list_view_no_access(self): + def test_role_list_view_no_permission(self): self._create_test_role() response = self._request_role_list_view() self.assertEqual(response.status_code, 200) @@ -141,7 +141,7 @@ class PermissionsViewsTestCase(GroupTestMixin, RoleTestMixin, GenericViewTestCas kwargs={'role_id': self.test_role.pk} ) - def test_role_groups_view_no_access(self): + def test_role_groups_view_no_permission(self): self._create_test_role() response = self._request_role_groups_view() self.assertEqual(response.status_code, 403) @@ -158,7 +158,7 @@ class PermissionsViewsTestCase(GroupTestMixin, RoleTestMixin, GenericViewTestCas kwargs={'group_id': self.test_group.pk} ) - def test_group_roles_view_no_access(self): + def test_group_roles_view_no_permission(self): self._create_test_group() response = self._request_group_roles_view() self.assertEqual(response.status_code, 403) diff --git a/mayan/apps/sources/views.py b/mayan/apps/sources/views.py index 14a02249fe..dc309c5338 100644 --- a/mayan/apps/sources/views.py +++ b/mayan/apps/sources/views.py @@ -146,7 +146,7 @@ class SourceEditView(SingleObjectEditView): class SourceListView(SingleObjectListView): - queryset = Source.objects.select_subclasses() + source_queryset = Source.objects.select_subclasses() view_permission = permission_sources_view def get_extra_context(self): @@ -201,14 +201,14 @@ class SourceLogView(SingleObjectListView): 'title': _('Log entries for source: %s') % self.get_source(), } - def get_object_list(self): - return self.get_source().logs.all() - def get_source(self): return get_object_or_404( klass=Source.objects.select_subclasses(), pk=self.kwargs['source_id'] ) + def get_source_queryset(self): + return self.get_source().logs.all() + class StagingFileDeleteView(ExternalObjectMixin, SingleObjectDeleteView): external_object_class = StagingFolderSource diff --git a/mayan/apps/tags/models.py b/mayan/apps/tags/models.py index 5f2e6ec88b..1662fc1c27 100644 --- a/mayan/apps/tags/models.py +++ b/mayan/apps/tags/models.py @@ -55,7 +55,7 @@ class Tag(models.Model): def get_absolute_url(self): return reverse( - viewname='tags:tag_tagged_item_list', kwargs={ + viewname='tags:tag_document_list', kwargs={ 'tag_id': self.pk } ) From 46812ab3d38991d71c58d9e7d69f2bb60626ba16 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Wed, 30 Jan 2019 17:09:46 -0400 Subject: [PATCH 077/209] Fix ACL filtering case #3 Test case #3: Generic Foreign Key, multiple ContentTypes + object IDs. Signed-off-by: Roberto Rosario --- mayan/apps/acls/managers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mayan/apps/acls/managers.py b/mayan/apps/acls/managers.py index 9a3f378450..8b55a5af1c 100644 --- a/mayan/apps/acls/managers.py +++ b/mayan/apps/acls/managers.py @@ -61,8 +61,6 @@ class AccessControlListManager(models.Manager): ) ).values('ct_fk_combination') - field_lookup = 'pk__in' - acl_filter = self.annotate( ct_fk_combination=Concat( 'content_type', V('-'), 'object_id', output_field=CharField() @@ -72,6 +70,8 @@ class AccessControlListManager(models.Manager): ct_fk_combination__in=content_type_object_id_queryset ).values('object_id') + field_lookup = 'object_id__in' + result.append(Q(**{field_lookup: acl_filter})) else: # Case 2: Related field of a single type, single ContentType, From 65d75dafde044bbed76ab9337d025f0825f0851f Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Wed, 30 Jan 2019 17:11:15 -0400 Subject: [PATCH 078/209] Fix and improve test for the ACL app Signed-off-by: Roberto Rosario --- mayan/apps/acls/tests/mixins.py | 27 +++++++++- mayan/apps/acls/tests/test_links.py | 62 ++++++++++----------- mayan/apps/acls/tests/test_views.py | 83 ++++++++++++++++------------- 3 files changed, 100 insertions(+), 72 deletions(-) diff --git a/mayan/apps/acls/tests/mixins.py b/mayan/apps/acls/tests/mixins.py index 6f4c9af305..7ea4237294 100644 --- a/mayan/apps/acls/tests/mixins.py +++ b/mayan/apps/acls/tests/mixins.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals +from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ImproperlyConfigured from mayan.apps.permissions.tests.mixins import RoleTestCaseMixin @@ -21,6 +22,30 @@ class ACLTestCaseMixin(RoleTestCaseMixin, UserTestCaseMixin): 'in order to enable the usage of ACLs in tests.' ) - return AccessControlList.objects.grant( + self._test_case_acl = AccessControlList.objects.grant( obj=obj, permission=permission, role=self._test_case_role ) + + +class ACLTestMixin(object): + auto_create_test_role = True + + def _create_test_acl(self): + self.test_acl = AccessControlList.objects.create( + content_object=self.test_object, role=self.test_role + ) + + def setUp(self): + super(ACLTestMixin, self).setUp() + if self.auto_create_test_role: + self._create_test_role() + + self.test_object = self.document + + content_type = ContentType.objects.get_for_model(self.test_object) + + self.test_content_object_view_kwargs = { + 'app_label': content_type.app_label, + 'model': content_type.model, + 'object_id': self.test_object.pk + } diff --git a/mayan/apps/acls/tests/test_links.py b/mayan/apps/acls/tests/test_links.py index 78d3821685..582e9b45c8 100644 --- a/mayan/apps/acls/tests/test_links.py +++ b/mayan/apps/acls/tests/test_links.py @@ -1,6 +1,5 @@ from __future__ import unicode_literals -from django.contrib.contenttypes.models import ContentType from django.urls import reverse from mayan.apps.documents.tests import GenericDocumentViewTestCase @@ -8,35 +7,34 @@ from mayan.apps.documents.tests import GenericDocumentViewTestCase from ..links import ( link_acl_create, link_acl_delete, link_acl_list, link_acl_permissions ) -from ..models import AccessControlList from ..permissions import permission_acl_edit, permission_acl_view +from .mixins import ACLTestMixin -class ACLsLinksTestCase(GenericDocumentViewTestCase): - def test_document_acl_create_link(self): - self.grant_access(obj=self.document, permission=permission_acl_edit) - self.add_test_view(test_object=self.document) +class ACLsLinksTestCase(ACLTestMixin, GenericDocumentViewTestCase): + auto_create_test_role = False + + def test_object_acl_create_link(self): + self.grant_access(obj=self.test_object, permission=permission_acl_edit) + + self.add_test_view(test_object=self.test_object) context = self.get_test_view() resolved_link = link_acl_create.resolve(context=context) self.assertNotEqual(resolved_link, None) - content_type = ContentType.objects.get_for_model(self.document) - kwargs = { - 'app_label': content_type.app_label, - 'model': content_type.model, - 'object_id': self.document.pk - } - self.assertEqual( - resolved_link.url, reverse(viewname='acls:acl_create', kwargs=kwargs) + resolved_link.url, reverse( + viewname='acls:acl_create', + kwargs=self.test_content_object_view_kwargs + ) ) - def test_document_acl_delete_link(self): - self.grant_access(obj=self.document, permission=permission_acl_edit) + def test_object_acl_delete_link(self): + self.grant_access(obj=self.test_object, permission=permission_acl_edit) - self.add_test_view(test_object=acl) + self.add_test_view(test_object=self._test_case_acl) context = self.get_test_view() resolved_link = link_acl_delete.resolve(context=context) @@ -44,14 +42,15 @@ class ACLsLinksTestCase(GenericDocumentViewTestCase): self.assertEqual( resolved_link.url, reverse( - viewname='acls:acl_delete', kwargs={'acl_id': acl.pk} + viewname='acls:acl_delete', + kwargs={'acl_id': self._test_case_acl.pk} ) ) - def test_document_acl_edit_link(self): - self.grant_access(obj=self.document, permission=permission_acl_edit) + def test_object_acl_edit_link(self): + self.grant_access(obj=self.test_object, permission=permission_acl_edit) - self.add_test_view(test_object=acl) + self.add_test_view(test_object=self._test_case_acl) context = self.get_test_view() resolved_link = link_acl_permissions.resolve(context=context) @@ -59,26 +58,23 @@ class ACLsLinksTestCase(GenericDocumentViewTestCase): self.assertEqual( resolved_link.url, reverse( - viewname='acls:acl_permissions', kwargs={'acl_id': acl.pk} + viewname='acls:acl_permissions', + kwargs={'acl_id': self._test_case_acl.pk} ) ) - def test_document_acl_list_link(self): - self.grant_access(obj=self.document, permission=permission_acl_view) + def test_object_acl_list_link(self): + self.grant_access(obj=self.test_object, permission=permission_acl_view) - self.add_test_view(test_object=self.document) + self.add_test_view(test_object=self.test_object) context = self.get_test_view() resolved_link = link_acl_list.resolve(context=context) self.assertNotEqual(resolved_link, None) - content_type = ContentType.objects.get_for_model(self.document) - kwargs = { - 'app_label': content_type.app_label, - 'model': content_type.model, - 'object_id': self.document.pk - } - self.assertEqual( - resolved_link.url, reverse(viewname='acls:acl_list', kwargs=kwargs) + resolved_link.url, reverse( + viewname='acls:acl_list', + kwargs=self.test_content_object_view_kwargs + ) ) diff --git a/mayan/apps/acls/tests/test_views.py b/mayan/apps/acls/tests/test_views.py index a41ca75164..7fabba2db8 100644 --- a/mayan/apps/acls/tests/test_views.py +++ b/mayan/apps/acls/tests/test_views.py @@ -1,6 +1,5 @@ from __future__ import absolute_import, unicode_literals -from django.contrib.contenttypes.models import ContentType from django.utils.encoding import force_text from mayan.apps.documents.tests import GenericDocumentViewTestCase @@ -9,70 +8,57 @@ from mayan.apps.permissions.tests.mixins import RoleTestMixin from ..models import AccessControlList from ..permissions import permission_acl_edit, permission_acl_view +from .mixins import ACLTestMixin -class AccessControlListViewTestCase(RoleTestMixin, GenericDocumentViewTestCase): - def setUp(self): - super(AccessControlListViewTestCase, self).setUp() - self.login_user() - self._create_test_role() - self.test_object = self.document - - content_type = ContentType.objects.get_for_model(self.test_object) - - self.view_content_object_arguments = { - 'app_label': content_type.app_label, - 'model': content_type.model, - 'object_id': self.test_object.pk - } - - def _request_get_acl_create_view(self): +class AccessControlListViewTestCase(ACLTestMixin, RoleTestMixin, GenericDocumentViewTestCase): + def _request_acl_create_get_view(self): return self.get( viewname='acls:acl_create', - kwargs=self.view_content_object_arguments, data={ + kwargs=self.test_content_object_view_kwargs, data={ 'role': self.test_role.pk } ) - def test_acl_create_view_get_no_permission(self): - response = self._request_get_acl_create_view() + def test_acl_create_get_view_no_permission(self): + response = self._request_acl_create_get_view() self.assertEqual(response.status_code, 404) self.assertEqual(AccessControlList.objects.count(), 0) - def test_acl_create_view_get_with_document_access(self): + def test_acl_create_get_view_with_document_access(self): self.grant_access(obj=self.test_object, permission=permission_acl_edit) - response = self._request_get_acl_create_view() - + response = self._request_acl_create_get_view() self.assertContains( response=response, text=force_text(self.test_object), status_code=200 ) - def _request_post_acl_create_view(self): + def _request_acl_create_post_view(self): return self.post( viewname='acls:acl_create', - kwargs=self.view_content_object_arguments, data={ + kwargs=self.test_content_object_view_kwargs, data={ 'role': self.test_role.pk } ) def test_acl_create_view_post_no_permission(self): - response = self._request_post_acl_create_view() + response = self._request_acl_create_post_view() self.assertEqual(response.status_code, 404) self.assertEqual(AccessControlList.objects.count(), 0) - def test_acl_create_view_post_with_document_access(self): + def test_acl_create_view_post_with_access(self): self.grant_access(obj=self.test_object, permission=permission_acl_edit) - response = self._request_post_acl_create_view() + response = self._request_acl_create_post_view() self.assertEqual(response.status_code, 302) + # 2 ACLs: 1 created by the test and the other by the self.grant_access self.assertEqual(AccessControlList.objects.count(), 2) - def test_acl_create_duplicate_view_with_permission(self): + def test_acl_create_duplicate_view_with_access(self): """ Test creating a duplicate ACL entry: same object & role Result: Should redirect to existing ACL for object + role combination @@ -81,20 +67,36 @@ class AccessControlListViewTestCase(RoleTestMixin, GenericDocumentViewTestCase): self.grant_access(obj=self.test_object, permission=permission_acl_edit) - response = self._request_post_acl_create_view() + response = self._request_acl_create_post_view() self.assertNotContains( response=response, text=force_text(self.test_acl.role), status_code=200 ) + # 2 ACLs: 1 created by the test and the other by the self.grant_access self.assertEqual(AccessControlList.objects.count(), 2) - self.assertEqual( - AccessControlList.objects.first().pk, self.test_acl.pk + + # Sorted by role PK + expected_results = sorted( + [ + { + # Test role, created and then requested, + # but created only once + 'object_id': self.test_object.pk, + 'role': self.test_role.pk + }, + { + # Test case ACL for the test case role, ignored + 'object_id': self.test_object.pk, + 'role': self._test_case_role.pk + }, + ], key=lambda item: item['role'] ) - def _create_test_acl(self): - self.test_acl = AccessControlList.objects.create( - content_object=self.test_object, role=self.test_role + self.assertQuerysetEqual( + qs=AccessControlList.objects.order_by('role__id').values( + 'object_id', 'role', + ), transform=dict, values=expected_results ) def _request_acl_delete_view(self): @@ -110,6 +112,7 @@ class AccessControlListViewTestCase(RoleTestMixin, GenericDocumentViewTestCase): response=response, text=force_text(self.test_object), status_code=404 ) + # 1 ACL: the test one self.assertQuerysetEqual( qs=AccessControlList.objects.all(), values=(repr(self.test_acl),) @@ -118,19 +121,23 @@ class AccessControlListViewTestCase(RoleTestMixin, GenericDocumentViewTestCase): def test_acl_delete_view_with_access(self): self._create_test_acl() - acl = self.grant_access( + self.grant_access( obj=self.test_object, permission=permission_acl_edit ) + response = self._request_acl_delete_view() self.assertEqual(response.status_code, 302) + # 1 ACL: the one created by the self.grant_access self.assertQuerysetEqual( - qs=AccessControlList.objects.all(), values=(repr(acl),) + qs=AccessControlList.objects.all(), values=( + repr(self._test_case_acl), + ) ) def _request_acl_list_view(self): return self.get( - viewname='acls:acl_list', kwargs=self.view_content_object_arguments + viewname='acls:acl_list', kwargs=self.test_content_object_view_kwargs ) def test_acl_list_view_no_permission(self): From 38c46433022e6e102fe1ecc217cec0f8ead68a28 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Wed, 30 Jan 2019 17:12:01 -0400 Subject: [PATCH 079/209] Simplify RestrictedQuerysetMixin queryset return Signed-off-by: Roberto Rosario --- mayan/apps/common/mixins.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mayan/apps/common/mixins.py b/mayan/apps/common/mixins.py index 9513ddba79..fcc9036426 100644 --- a/mayan/apps/common/mixins.py +++ b/mayan/apps/common/mixins.py @@ -433,12 +433,12 @@ class RestrictedQuerysetMixin(object): queryset = self.get_source_queryset() if self.object_permission: - return AccessControlList.objects.restrict_queryset( + queryset = AccessControlList.objects.restrict_queryset( permission=self.object_permission, queryset=queryset, user=self.request.user ) - else: - return queryset + + return queryset class ViewPermissionCheckMixin(object): From c61f709c1b4daab3f04d9ab0643a8e3591640b6c Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Thu, 31 Jan 2019 01:05:28 -0400 Subject: [PATCH 080/209] Fix authentication app tests Signed-off-by: Roberto Rosario --- mayan/apps/authentication/tests/test_views.py | 46 +++++++++---------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/mayan/apps/authentication/tests/test_views.py b/mayan/apps/authentication/tests/test_views.py index 4304aa45b5..d56dd32523 100644 --- a/mayan/apps/authentication/tests/test_views.py +++ b/mayan/apps/authentication/tests/test_views.py @@ -8,8 +8,7 @@ from django.urls import reverse from mayan.apps.common.tests import GenericViewTestCase from mayan.apps.smart_settings.classes import Namespace from mayan.apps.user_management.tests.literals import ( - TEST_ADMIN_EMAIL, TEST_ADMIN_PASSWORD, TEST_ADMIN_USERNAME, - TEST_USER_PASSWORD_EDITED + TEST_CASE_USER_EMAIL, TEST_CASE_USER_PASSWORD, TEST_CASE_USER_USERNAME, ) from ..settings import setting_maximum_session_length @@ -19,11 +18,12 @@ from .literals import TEST_EMAIL_AUTHENTICATION_BACKEND class UserLoginTestCase(GenericViewTestCase): """ - Test that users can login via the supported authentication methods + Test that users can login using the supported authentication methods """ authenticated_url = '{}?next={}'.format( reverse(settings.LOGIN_URL), reverse(viewname='documents:document_list') ) + auto_login_user = False def setUp(self): super(UserLoginTestCase, self).setUp() @@ -42,7 +42,7 @@ class UserLoginTestCase(GenericViewTestCase): @override_settings(AUTHENTICATION_LOGIN_METHOD='username') def test_username_login(self): logged_in = self.login( - username=TEST_ADMIN_USERNAME, password=TEST_ADMIN_PASSWORD + username=TEST_CASE_USER_USERNAME, password=TEST_CASE_USER_PASSWORD ) self.assertTrue(logged_in) response = self._request_authenticated_view() @@ -53,12 +53,12 @@ class UserLoginTestCase(GenericViewTestCase): def test_email_login(self): with self.settings(AUTHENTICATION_BACKENDS=(TEST_EMAIL_AUTHENTICATION_BACKEND,)): logged_in = self.login( - username=TEST_ADMIN_USERNAME, password=TEST_ADMIN_PASSWORD + username=TEST_CASE_USER_USERNAME, password=TEST_CASE_USER_PASSWORD ) self.assertFalse(logged_in) logged_in = self.login( - email=TEST_ADMIN_EMAIL, password=TEST_ADMIN_PASSWORD + email=TEST_CASE_USER_EMAIL, password=TEST_CASE_USER_PASSWORD ) self.assertTrue(logged_in) @@ -75,8 +75,8 @@ class UserLoginTestCase(GenericViewTestCase): response = self.post( viewname=settings.LOGIN_URL, data={ - 'username': TEST_ADMIN_USERNAME, - 'password': TEST_ADMIN_PASSWORD + 'username': TEST_CASE_USER_USERNAME, + 'password': TEST_CASE_USER_PASSWORD } ) response = self._request_authenticated_view() @@ -93,7 +93,7 @@ class UserLoginTestCase(GenericViewTestCase): response = self.post( viewname=settings.LOGIN_URL, data={ - 'email': TEST_ADMIN_EMAIL, 'password': TEST_ADMIN_PASSWORD + 'email': TEST_CASE_USER_EMAIL, 'password': TEST_CASE_USER_PASSWORD }, follow=True ) self.assertEqual(response.status_code, 200) @@ -106,8 +106,8 @@ class UserLoginTestCase(GenericViewTestCase): def test_username_remember_me(self): response = self.post( viewname=settings.LOGIN_URL, data={ - 'username': TEST_ADMIN_USERNAME, - 'password': TEST_ADMIN_PASSWORD, + 'username': TEST_CASE_USER_USERNAME, + 'password': TEST_CASE_USER_PASSWORD, 'remember_me': True }, follow=True ) @@ -125,8 +125,8 @@ class UserLoginTestCase(GenericViewTestCase): def test_username_dont_remember_me(self): response = self.post( viewname=settings.LOGIN_URL, data={ - 'username': TEST_ADMIN_USERNAME, - 'password': TEST_ADMIN_PASSWORD, + 'username': TEST_CASE_USER_USERNAME, + 'password': TEST_CASE_USER_PASSWORD, 'remember_me': False }, follow=True ) @@ -141,8 +141,8 @@ class UserLoginTestCase(GenericViewTestCase): with self.settings(AUTHENTICATION_BACKENDS=(TEST_EMAIL_AUTHENTICATION_BACKEND,)): response = self.post( viewname=settings.LOGIN_URL, data={ - 'email': TEST_ADMIN_EMAIL, - 'password': TEST_ADMIN_PASSWORD, + 'email': TEST_CASE_USER_EMAIL, + 'password': TEST_CASE_USER_PASSWORD, 'remember_me': True }, follow=True ) @@ -161,8 +161,8 @@ class UserLoginTestCase(GenericViewTestCase): with self.settings(AUTHENTICATION_BACKENDS=(TEST_EMAIL_AUTHENTICATION_BACKEND,)): response = self.post( viewname=settings.LOGIN_URL, data={ - 'email': TEST_ADMIN_EMAIL, - 'password': TEST_ADMIN_PASSWORD, + 'email': TEST_CASE_USER_EMAIL, + 'password': TEST_CASE_USER_PASSWORD, 'remember_me': False } ) @@ -176,7 +176,7 @@ class UserLoginTestCase(GenericViewTestCase): def test_password_reset(self): response = self.post( viewname='authentication:password_reset_view', data={ - 'email': TEST_ADMIN_EMAIL, + 'email': TEST_CASE_USER_EMAIL, } ) @@ -188,15 +188,15 @@ class UserLoginTestCase(GenericViewTestCase): response = self.post( viewname='authentication:password_reset_confirm_view', args=uid_token[-3:-1], data={ - 'new_password1': TEST_USER_PASSWORD_EDITED, - 'new_password2': TEST_USER_PASSWORD_EDITED, + 'new_password1': TEST_CASE_USER_PASSWORD, + 'new_password2': TEST_CASE_USER_PASSWORD, } ) self.assertEqual(response.status_code, 302) self.login( - username=TEST_ADMIN_USERNAME, password=TEST_USER_PASSWORD_EDITED + username=TEST_CASE_USER_USERNAME, password=TEST_CASE_USER_PASSWORD ) response = self._request_authenticated_view() @@ -209,8 +209,8 @@ class UserLoginTestCase(GenericViewTestCase): path='{}?next={}'.format( reverse(settings.LOGIN_URL), TEST_REDIRECT_URL ), data={ - 'username': TEST_ADMIN_USERNAME, - 'password': TEST_ADMIN_PASSWORD, + 'username': TEST_CASE_USER_USERNAME, + 'password': TEST_CASE_USER_PASSWORD, 'remember_me': False }, follow=True ) From a06c633568eb49b58f9ac1d86a68c5767484a1dc Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Thu, 31 Jan 2019 01:08:53 -0400 Subject: [PATCH 081/209] Authentication: Use class based views Update all views to use the new Django authentication class based views. Signed-off-by: Roberto Rosario --- mayan/apps/authentication/forms.py | 12 +- .../password_reset_complete.html | 3 +- mayan/apps/authentication/urls.py | 29 +-- mayan/apps/authentication/views.py | 201 +++++++----------- 4 files changed, 106 insertions(+), 139 deletions(-) diff --git a/mayan/apps/authentication/forms.py b/mayan/apps/authentication/forms.py index ebc7e5233d..dd1c247f7e 100644 --- a/mayan/apps/authentication/forms.py +++ b/mayan/apps/authentication/forms.py @@ -23,8 +23,10 @@ class EmailAuthenticationForm(forms.Form): remember_me = forms.BooleanField(label=_('Remember me'), required=False) error_messages = { - 'invalid_login': _('Please enter a correct email and password. ' - 'Note that the password field is case-sensitive.'), + 'invalid_login': _( + 'Please enter a correct email and password. Note that the ' + 'password field is case-sensitive.' + ), 'inactive': _('This account is inactive.'), } @@ -56,8 +58,10 @@ class EmailAuthenticationForm(forms.Form): return self.cleaned_data def check_for_test_cookie(self): - warnings.warn('check_for_test_cookie is deprecated; ensure your login ' - 'view is CSRF-protected.', DeprecationWarning) + warnings.warn( + 'check_for_test_cookie is deprecated; ensure your login view ' + 'is CSRF-protected.', DeprecationWarning + ) def get_user_id(self): if self.user_cache: diff --git a/mayan/apps/authentication/templates/authentication/password_reset_complete.html b/mayan/apps/authentication/templates/authentication/password_reset_complete.html index 48b778dcb7..5d43c3fd41 100644 --- a/mayan/apps/authentication/templates/authentication/password_reset_complete.html +++ b/mayan/apps/authentication/templates/authentication/password_reset_complete.html @@ -14,8 +14,7 @@
- +
- {% endblock content_plain %} diff --git a/mayan/apps/authentication/urls.py b/mayan/apps/authentication/urls.py index 81e8352c23..a0cfcb375b 100644 --- a/mayan/apps/authentication/urls.py +++ b/mayan/apps/authentication/urls.py @@ -1,43 +1,44 @@ from __future__ import unicode_literals -from django.conf import settings from django.conf.urls import url -from django.contrib.auth.views import logout from .views import ( - login_view, password_change_done, password_change_view, - password_reset_complete_view, password_reset_confirm_view, - password_reset_done_view, password_reset_view + MayanLoginView, MayanLogoutView, MayanPasswordChangeDoneView, + MayanPasswordChangeView, MayanPasswordResetCompleteView, + MayanPasswordResetConfirmView, MayanPasswordResetDoneView, + MayanPasswordResetView ) + urlpatterns = [ - url(regex=r'^login/$', name='login_view', view=login_view), + url(regex=r'^login/$', name='login_view', view=MayanLoginView.as_view()), url( - regex=r'^logout/$', kwargs={'next_page': settings.LOGIN_REDIRECT_URL}, - name='logout_view', view=logout + regex=r'^logout/$', name='logout_view', view=MayanLogoutView.as_view() ), url( regex=r'^password/change/$', name='password_change_view', - view=password_change_view + view=MayanPasswordChangeView.as_view() ), url( regex=r'^password/change/done/$', name='password_change_done', - view=password_change_done + view=MayanPasswordChangeDoneView.as_view() ), url( regex=r'^password/reset/$', name='password_reset_view', - view=password_reset_view + view=MayanPasswordResetView.as_view() ), url( regex=r'^password/reset/confirm/(?P[0-9A-Za-z_\-]+)/(?P[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$', - name='password_reset_confirm_view', view=password_reset_confirm_view + name='password_reset_confirm_view', + view=MayanPasswordResetConfirmView.as_view() ), url( regex=r'^password/reset/complete/$', - name='password_reset_complete_view', view=password_reset_complete_view + name='password_reset_complete_view', + view=MayanPasswordResetCompleteView.as_view() ), url( regex=r'^password/reset/done/$', name='password_reset_done_view', - view=password_reset_done_view + view=MayanPasswordResetDoneView.as_view() ), ] diff --git a/mayan/apps/authentication/views.py b/mayan/apps/authentication/views.py index 0d038d9042..d698417c9e 100644 --- a/mayan/apps/authentication/views.py +++ b/mayan/apps/authentication/views.py @@ -1,19 +1,17 @@ from __future__ import absolute_import, unicode_literals -from django.conf import settings from django.contrib import messages -from django.contrib.auth import REDIRECT_FIELD_NAME from django.contrib.auth.views import ( - login, password_change, password_reset, password_reset_complete, - password_reset_confirm, password_reset_done + LoginView, LogoutView, PasswordChangeDoneView, PasswordChangeView, + PasswordResetCompleteView, PasswordResetConfirmView, PasswordResetDoneView, + PasswordResetView ) from django.http import HttpResponseRedirect -from django.shortcuts import redirect, resolve_url -from django.urls import reverse -from django.utils.http import is_safe_url +from django.shortcuts import redirect +from django.urls import reverse, reverse_lazy from django.utils.translation import ugettext_lazy as _ -from stronghold.decorators import public +from stronghold.views import StrongholdPublicMixin import mayan from mayan.apps.common.settings import ( @@ -24,143 +22,108 @@ from .forms import EmailAuthenticationForm, UsernameAuthenticationForm from .settings import setting_login_method, setting_maximum_session_length -@public -def login_view(request): - """ - Control how the use is to be authenticated, options are 'email' and - 'username' - """ - success_url_allowed_hosts = set() - kwargs = {'template_name': 'authentication/login.html'} +class MayanLoginView(StrongholdPublicMixin, LoginView): + extra_context = { + 'appearance_type': 'plain' + } + template_name = 'authentication/login.html' + redirect_authenticated_user = True - if setting_login_method.value == 'email': - kwargs['authentication_form'] = EmailAuthenticationForm - else: - kwargs['authentication_form'] = UsernameAuthenticationForm + def form_valid(self, form): + result = super(MayanLoginView, self).form_valid(form=form) + remember_me = form.cleaned_data.get('remember_me') - allowed_hosts = {request.get_host()} - allowed_hosts.update(success_url_allowed_hosts) + # remember_me values: + # True - long session + # False - short session + # None - Form has no remember_me value and we let the session + # expiration default. - redirect_to = request.POST.get( - REDIRECT_FIELD_NAME, request.GET.get(REDIRECT_FIELD_NAME, '') - ) - - url_is_safe = is_safe_url( - url=redirect_to, - allowed_hosts=allowed_hosts, - require_https=request.is_secure(), - ) - - url = redirect_to if url_is_safe else '' - - if not request.user.is_authenticated: - extra_context = { - 'appearance_type': 'plain', - REDIRECT_FIELD_NAME: url or resolve_url(settings.LOGIN_REDIRECT_URL) - } - - result = login(request, extra_context=extra_context, **kwargs) - if request.method == 'POST': - form = kwargs['authentication_form'](request, data=request.POST) - if form.is_valid(): - if form.cleaned_data['remember_me']: - request.session.set_expiry( - setting_maximum_session_length.value - ) - else: - request.session.set_expiry(0) - return result - else: - return HttpResponseRedirect(resolve_url(settings.LOGIN_REDIRECT_URL)) - - -def password_change_view(request): - """ - Password change wrapper for better control - """ - extra_context = {'title': _('Current user password change')} - - if request.user.user_options.block_password_change: - messages.error( - request, _( - 'Changing the password is not allowed for this account.' + if remember_me is True: + self.request.session.set_expiry( + setting_maximum_session_length.value ) + elif remember_me is False: + self.request.session.set_expiry(0) + + return result + + def get_form_class(self): + if setting_login_method.value == 'email': + return EmailAuthenticationForm + else: + return UsernameAuthenticationForm + + +class MayanLogoutView(LogoutView): + """No current change or overrides, left here for future expansion""" + + +class MayanPasswordChangeDoneView(PasswordChangeDoneView): + def dispatch(self, *args, **kwargs): + messages.success( + message=_('Your password has been successfully changed.'), + request=self.request ) - return HttpResponseRedirect(reverse(setting_home_view.view)) - - return password_change( - request, extra_context=extra_context, - template_name='appearance/generic_form.html', - post_change_redirect=reverse(viewname='authentication:password_change_done'), - ) + return redirect(to='common:current_user_details') -def password_change_done(request): - """ - View called when the new user password has been accepted - """ - messages.success( - request, _('Your password has been successfully changed.') - ) - return redirect('common:current_user_details') +class MayanPasswordChangeView(PasswordChangeView): + extra_context = {'title': _('Current user password change')} + success_url = reverse_lazy(viewname='authentication:password_change_done') + template_name = 'appearance/generic_form.html' + + def dispatch(self, *args, **kwargs): + if self.request.user.user_options.block_password_change: + messages.error( + message=_( + 'Changing the password is not allowed for this account.' + ), request=self.request + ) + return HttpResponseRedirect( + redirect_to=reverse(viewname=setting_home_view.view) + ) + + return super(MayanPasswordChangeView, self).dispatch(*args, **kwargs) -@public -def password_reset_complete_view(request): +class MayanPasswordResetCompleteView(StrongholdPublicMixin, PasswordResetCompleteView): extra_context = { 'appearance_type': 'plain' } - - return password_reset_complete( - request=request, extra_context=extra_context, - template_name='authentication/password_reset_complete.html' - ) + template_name = 'authentication/password_reset_complete.html' -@public -def password_reset_confirm_view(request, uidb64=None, token=None): +class MayanPasswordResetConfirmView(StrongholdPublicMixin, PasswordResetConfirmView): extra_context = { 'appearance_type': 'plain' } - - return password_reset_confirm( - request=request, extra_context=extra_context, - template_name='authentication/password_reset_confirm.html', - post_reset_redirect=reverse( - 'authentication:password_reset_complete_view' - ), uidb64=uidb64, token=token + success_url = reverse_lazy( + viewname='authentication:password_reset_complete_view' ) + template_name = 'authentication/password_reset_confirm.html' -@public -def password_reset_done_view(request): +class MayanPasswordResetDoneView(StrongholdPublicMixin, PasswordResetDoneView): extra_context = { 'appearance_type': 'plain' } - - return password_reset_done( - request=request, extra_context=extra_context, - template_name='authentication/password_reset_done.html' - ) + template_name = 'authentication/password_reset_done.html' -@public -def password_reset_view(request): +class MayanPasswordResetView(StrongholdPublicMixin, PasswordResetView): + email_template_name = 'authentication/password_reset_email.html' extra_context = { 'appearance_type': 'plain' } - - return password_reset( - request=request, extra_context=extra_context, - email_template_name='authentication/password_reset_email.html', - extra_email_context={ - 'project_title': setting_project_title.value, - 'project_website': setting_project_url.value, - 'project_copyright': mayan.__copyright__, - 'project_license': mayan.__license__, - }, subject_template_name='authentication/password_reset_subject.txt', - template_name='authentication/password_reset_form.html', - post_reset_redirect=reverse( - 'authentication:password_reset_done_view' - ) + extra_email_context = { + 'project_copyright': mayan.__copyright__, + 'project_license': mayan.__license__, + 'project_title': setting_project_title.value, + 'project_website': setting_project_url.value + } + subject_template_name = 'authentication/password_reset_subject.txt' + success_url = reverse_lazy( + viewname='authentication:password_reset_done_view' ) + template_name = 'authentication/password_reset_form.html' From 66670a5d5943d9aa08c67360c93db97b2ee2ed0e Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Thu, 31 Jan 2019 01:10:59 -0400 Subject: [PATCH 082/209] Update fallback to redirect view When there is no HTTP referer fallback to common.settings_home_view instead of LOGIN_REDIRECT_URL. Signed-off-by: Roberto Rosario --- mayan/apps/common/mixins.py | 8 +++++--- mayan/apps/common/views.py | 10 +++++----- mayan/apps/documents/views/document_page_views.py | 4 ++-- mayan/apps/navigation/classes.py | 6 +++--- 4 files changed, 15 insertions(+), 13 deletions(-) diff --git a/mayan/apps/common/mixins.py b/mayan/apps/common/mixins.py index fcc9036426..e0a1144919 100644 --- a/mayan/apps/common/mixins.py +++ b/mayan/apps/common/mixins.py @@ -5,7 +5,8 @@ from django.contrib import messages from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ImproperlyConfigured, PermissionDenied from django.http import Http404, HttpResponseRedirect -from django.shortcuts import get_object_or_404, resolve_url +from django.shortcuts import get_object_or_404 +from django.urls import reverse from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ungettext from django.views.generic.detail import SingleObjectMixin @@ -19,6 +20,7 @@ from .literals import ( PK_LIST_SEPARATOR, TEXT_CHOICE_ITEMS, TEXT_CHOICE_LIST, TEXT_LIST_AS_ITEMS_PARAMETER, TEXT_LIST_AS_ITEMS_VARIABLE_NAME ) +from .settings import setting_home_view __all__ = ( 'DeleteExtraDataMixin', 'DynamicFormViewMixin', 'ExtraContextMixin', @@ -367,14 +369,14 @@ class RedirectionMixin(object): self.next_url = self.request.POST.get( 'next', self.request.GET.get( 'next', post_action_redirect if post_action_redirect else self.request.META.get( - 'HTTP_REFERER', resolve_url(settings.LOGIN_REDIRECT_URL) + 'HTTP_REFERER', reverse(setting_home_view.value) ) ) ) self.previous_url = self.request.POST.get( 'previous', self.request.GET.get( 'previous', action_cancel_redirect if action_cancel_redirect else self.request.META.get( - 'HTTP_REFERER', resolve_url(settings.LOGIN_REDIRECT_URL) + 'HTTP_REFERER', reverse(setting_home_view.value) ) ) ) diff --git a/mayan/apps/common/views.py b/mayan/apps/common/views.py index f7fc56e2ae..6ef6f2389a 100644 --- a/mayan/apps/common/views.py +++ b/mayan/apps/common/views.py @@ -6,9 +6,9 @@ from django.conf import settings from django.contrib import messages from django.contrib.contenttypes.models import ContentType from django.http import HttpResponseRedirect -from django.shortcuts import get_object_or_404, resolve_url +from django.shortcuts import get_object_or_404 from django.template import RequestContext -from django.urls import reverse_lazy +from django.urls import reverse, reverse_lazy from django.utils import timezone, translation from django.utils.http import urlencode from django.utils.translation import ugettext_lazy as _ @@ -282,7 +282,7 @@ def multi_object_action_view(request): next = request.POST.get( 'next', request.GET.get( 'next', request.META.get( - 'HTTP_REFERER', resolve_url(settings.LOGIN_REDIRECT_URL) + 'HTTP_REFERER', reverse(setting_home_view.value) ) ) ) @@ -299,7 +299,7 @@ def multi_object_action_view(request): messages.error(request, _('No action selected.')) return HttpResponseRedirect( request.META.get( - 'HTTP_REFERER', resolve_url(settings.LOGIN_REDIRECT_URL) + 'HTTP_REFERER', reverse(setting_home_view.value) ) ) @@ -307,7 +307,7 @@ def multi_object_action_view(request): messages.error(request, _('Must select at least one item.')) return HttpResponseRedirect( request.META.get( - 'HTTP_REFERER', resolve_url(settings.LOGIN_REDIRECT_URL) + 'HTTP_REFERER', reverse(setting_home_view.value) ) ) diff --git a/mayan/apps/documents/views/document_page_views.py b/mayan/apps/documents/views/document_page_views.py index 928bb852df..de7ab24807 100644 --- a/mayan/apps/documents/views/document_page_views.py +++ b/mayan/apps/documents/views/document_page_views.py @@ -6,7 +6,6 @@ from furl import furl from django.conf import settings from django.contrib import messages -from django.shortcuts import resolve_url from django.urls import reverse from django.utils.encoding import force_text from django.utils.translation import ugettext_lazy as _ @@ -14,6 +13,7 @@ from django.views.generic import RedirectView from mayan.apps.common.generics import SimpleView, SingleObjectListView from mayan.apps.common.mixins import ExternalObjectMixin +from mayan.apps.common.settings import setting_home_view from mayan.apps.common.utils import resolve from mayan.apps.converter.literals import DEFAULT_ROTATION, DEFAULT_ZOOM_LEVEL @@ -104,7 +104,7 @@ class DocumentPageNavigationBase(ExternalObjectMixin, RedirectView): try: previous_url = self.get_object().get_absolute_url() except AttributeError: - previous_url = resolve_url(settings.LOGIN_REDIRECT_URL) + previous_url = reverse(setting_home_view.value) parsed_url = furl(url=previous_url) diff --git a/mayan/apps/navigation/classes.py b/mayan/apps/navigation/classes.py index a009827667..a49bbca026 100644 --- a/mayan/apps/navigation/classes.py +++ b/mayan/apps/navigation/classes.py @@ -10,10 +10,9 @@ from django.conf import settings from django.contrib.admin.utils import label_for_field from django.core.exceptions import FieldDoesNotExist, PermissionDenied from django.db.models.constants import LOOKUP_SEP -from django.shortcuts import resolve_url from django.template import VariableDoesNotExist, Variable from django.template.defaulttags import URLNode -from django.urls import resolve +from django.urls import reverse, resolve from django.utils.encoding import force_str, force_text from django.utils.translation import ugettext_lazy as _ @@ -22,6 +21,7 @@ from mayan.apps.common.literals import ( TEXT_SORT_ORDER_CHOICE_ASCENDING, TEXT_SORT_ORDER_CHOICE_DESCENDING, TEXT_SORT_ORDER_PARAMETER, TEXT_SORT_ORDER_VARIABLE_NAME ) +from mayan.apps.common.settings import setting_home_view from mayan.apps.common.utils import resolve_attribute from mayan.apps.permissions import Permission @@ -170,7 +170,7 @@ class Link(object): parsed_url = furl( force_str( request.get_full_path() or request.META.get( - 'HTTP_REFERER', resolve_url(settings.LOGIN_REDIRECT_URL) + 'HTTP_REFERER', reverse(setting_home_view.value) ) ) ) From 43d79a9d868d7b4b9b88e89e846ba8f9a67c3c37 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Thu, 31 Jan 2019 01:12:55 -0400 Subject: [PATCH 083/209] Django settings: Add defaults, add new setting Add support for LOGOUT_REDIRECT_URL. Signed-off-by: Roberto Rosario --- mayan/apps/common/settings.py | 15 ++++++++++++++- mayan/apps/smart_settings/literals.py | 13 +++++++++++-- mayan/settings/base.py | 23 +++++++++++++++++++---- 3 files changed, 44 insertions(+), 7 deletions(-) diff --git a/mayan/apps/common/settings.py b/mayan/apps/common/settings.py index c6079f42bf..2187fd1db5 100644 --- a/mayan/apps/common/settings.py +++ b/mayan/apps/common/settings.py @@ -355,7 +355,20 @@ setting_django_login_redirect_url = namespace.add_setting( 'for example. This setting also accepts named URL patterns which ' 'can be used to reduce configuration duplication since you don\'t ' 'have to define the URL in two places (settings and URLconf).' - ), + ) +) +setting_django_logout_redirect_url = namespace.add_setting( + global_name='LOGOUT_REDIRECT_URL', + default=settings.LOGOUT_REDIRECT_URL, + help_text=_( + 'Default: None. The URL where requests are redirected after a user ' + 'logs out using LogoutView (if the view doesn\'t get a next_page ' + 'argument). If None, no redirect will be performed and the logout ' + 'view will be rendered. This setting also accepts named URL ' + 'patterns which can be used to reduce configuration duplication ' + 'since you don\'t have to define the URL in two places (settings ' + 'and URLconf).' + ) ) setting_django_static_url = namespace.add_setting( global_name='STATIC_URL', diff --git a/mayan/apps/smart_settings/literals.py b/mayan/apps/smart_settings/literals.py index b8ba171ff9..fdd1cd9d10 100644 --- a/mayan/apps/smart_settings/literals.py +++ b/mayan/apps/smart_settings/literals.py @@ -1,5 +1,14 @@ from __future__ import unicode_literals +DJANGO_SETTINGS_DEFAULTS = { + # Default in YAML format + 'DEBUG': 'false', + 'LOGIN_URL': 'authentication:login_view', + 'LOGIN_REDIRECT_URL': 'common:home', + 'LOGOUT_REDIRECT_URL': 'authentication:login_view', +} + + DJANGO_SETTINGS_LIST = ( 'ALLOWED_HOSTS', 'APPEND_SLASH', 'AUTH_PASSWORD_VALIDATORS', 'DATA_UPLOAD_MAX_MEMORY_SIZE', 'DATABASES', 'DEBUG', 'DEFAULT_FROM_EMAIL', @@ -7,6 +16,6 @@ DJANGO_SETTINGS_LIST = ( 'EMAIL_HOST_PASSWORD', 'EMAIL_HOST_USER', 'EMAIL_PORT', 'EMAIL_TIMEOUT', 'EMAIL_USE_SSL', 'EMAIL_USE_TLS', 'FILE_UPLOAD_MAX_MEMORY_SIZE', 'HOME_VIEW', 'INSTALLED_APPS', 'INTERNAL_IPS', 'LANGUAGES', - 'LANGUAGE_CODE', 'LOGIN_REDIRECT_URL', 'LOGIN_URL', 'STATIC_URL', - 'STATICFILES_STORAGE', 'TIME_ZONE', 'WSGI_APPLICATION', + 'LANGUAGE_CODE', 'LOGIN_REDIRECT_URL', 'LOGIN_URL', 'LOGOUT_REDIRECT_URL', + 'STATIC_URL', 'STATICFILES_STORAGE', 'TIME_ZONE', 'WSGI_APPLICATION' ) diff --git a/mayan/settings/base.py b/mayan/settings/base.py index 9192d17cd2..bcc8bd887b 100644 --- a/mayan/settings/base.py +++ b/mayan/settings/base.py @@ -16,7 +16,9 @@ import sys from django.utils.translation import ugettext_lazy as _ -from mayan.apps.smart_settings.literals import DJANGO_SETTINGS_LIST +from mayan.apps.smart_settings.literals import ( + DJANGO_SETTINGS_DEFAULTS, DJANGO_SETTINGS_LIST +) from mayan.apps.smart_settings.utils import ( get_environment_variables, read_configuration_file, yaml_loads ) @@ -50,7 +52,9 @@ else: # SECURITY WARNING: don't run with debug turned on in production! -DEBUG = yaml_loads(os.environ.get('MAYAN_DEBUG', 'false')) +DEBUG = yaml_loads( + os.environ.get('MAYAN_DEBUG', DJANGO_SETTINGS_DEFAULTS.get('DEBUG')) +) ALLOWED_HOSTS = ['127.0.0.1', 'localhost', '[::1]'] @@ -249,10 +253,21 @@ TEST_RUNNER = 'mayan.apps.common.tests.runner.MayanTestRunner' # --------- Django ------------------- LOGIN_URL = yaml_loads( - os.environ.get('MAYAN_LOGIN_URL', 'authentication:login_view') + os.environ.get( + 'MAYAN_LOGIN_URL', DJANGO_SETTINGS_DEFAULTS.get('LOGIN_URL') + ) ) LOGIN_REDIRECT_URL = yaml_loads( - os.environ.get('MAYAN_LOGIN_REDIRECT_URL', 'common:root') + os.environ.get( + 'MAYAN_LOGIN_REDIRECT_URL', + DJANGO_SETTINGS_DEFAULTS.get('LOGIN_REDIRECT_URL') + ) +) +LOGOUT_REDIRECT_URL = yaml_loads( + os.environ.get( + 'MAYAN_LOGOUT_REDIRECT_URL', + DJANGO_SETTINGS_DEFAULTS.get('LOGOUT_REDIRECT_URL') + ) ) INTERNAL_IPS = ('127.0.0.1',) From 3976766abe42cbf133c21ee5fdf732665a78cc12 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Thu, 31 Jan 2019 01:20:41 -0400 Subject: [PATCH 084/209] Autoadmin: Fix failing test Signed-off-by: Roberto Rosario --- mayan/apps/autoadmin/tests/test_views.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mayan/apps/autoadmin/tests/test_views.py b/mayan/apps/autoadmin/tests/test_views.py index 23cc7e1ca1..a5a90aa977 100644 --- a/mayan/apps/autoadmin/tests/test_views.py +++ b/mayan/apps/autoadmin/tests/test_views.py @@ -12,8 +12,10 @@ from .literals import TEST_FIRST_TIME_LOGIN_TEXT, TEST_MOCK_VIEW_TEXT class AutoAdminViewCase(GenericViewTestCase): auto_create_group = False auto_create_users = False + auto_login_user = False def setUp(self): + super(AutoAdminViewCase, self).setUp() with mute_stdout(): AutoAdminSingleton.objects.create_autoadmin() From e007af6b3ffca56ab9d74ec3db51a8eb33b9394a Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Thu, 31 Jan 2019 05:53:09 -0400 Subject: [PATCH 085/209] Refactor checkouts app Change "checkin" usage to "check_in". Update URL parameters to the "_id" form. Add support to checkout and check in multiple documents. Optimize queries that used an ID list of documents for filtering using values_list('pk', flat=True). These queries now use .values('pk') as a subquery. Add pre save hooks to block new document version uploads. Signed-off-by: Roberto Rosario --- HISTORY.rst | 2 + mayan/apps/checkouts/api_views.py | 6 +- mayan/apps/checkouts/apps.py | 28 ++- mayan/apps/checkouts/dashboard_widgets.py | 2 +- mayan/apps/checkouts/hooks.py | 13 ++ mayan/apps/checkouts/links.py | 39 ++-- mayan/apps/checkouts/managers.py | 74 +++---- mayan/apps/checkouts/models.py | 43 ++-- mayan/apps/checkouts/permissions.py | 4 +- mayan/apps/checkouts/serializers.py | 4 +- mayan/apps/checkouts/tests/test_models.py | 68 +++---- mayan/apps/checkouts/tests/test_views.py | 134 +++++------- mayan/apps/checkouts/urls.py | 30 ++- mayan/apps/checkouts/views.py | 237 ++++++++++------------ 14 files changed, 333 insertions(+), 351 deletions(-) create mode 100644 mayan/apps/checkouts/hooks.py diff --git a/HISTORY.rst b/HISTORY.rst index 3098b6f852..e26036dd7e 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -225,6 +225,8 @@ - Remove the permissions to grant or revoke a permission to a role. The instead the role edit permission is used. - Add a test mixin to generate random model primary keys. +- Add support for checkout and check in multiple documents at + the same time. 3.1.9 (2018-11-01) ================== diff --git a/mayan/apps/checkouts/api_views.py b/mayan/apps/checkouts/api_views.py index ef8fc61d1b..398b29bb5d 100644 --- a/mayan/apps/checkouts/api_views.py +++ b/mayan/apps/checkouts/api_views.py @@ -7,7 +7,7 @@ from mayan.apps.documents.permissions import permission_document_view from .models import DocumentCheckout from .permissions import ( - permission_document_checkin, permission_document_checkin_override, + permission_document_check_in, permission_document_check_in_override, permission_document_checkout_detail_view ) from .serializers import ( @@ -78,12 +78,12 @@ class APICheckedoutDocumentView(generics.RetrieveDestroyAPIView): if document.checkout_info().user == request.user: AccessControlList.objects.check_access( - permissions=permission_document_checkin, user=request.user, + permissions=permission_document_check_in, user=request.user, obj=document ) else: AccessControlList.objects.check_access( - permissions=permission_document_checkin_override, + permissions=permission_document_check_in_override, user=request.user, obj=document ) diff --git a/mayan/apps/checkouts/apps.py b/mayan/apps/checkouts/apps.py index 8d7d425970..9c37c65dc7 100644 --- a/mayan/apps/checkouts/apps.py +++ b/mayan/apps/checkouts/apps.py @@ -10,7 +10,7 @@ from django.utils.translation import ugettext_lazy as _ from mayan.apps.acls import ModelPermission from mayan.apps.common import ( - MayanAppConfig, menu_facet, menu_main, menu_sidebar + MayanAppConfig, menu_facet, menu_main, menu_multi_item, menu_sidebar ) from mayan.apps.dashboards.dashboards import dashboard_main from mayan.apps.events import ModelEventType @@ -23,8 +23,9 @@ from .events import ( ) from .handlers import handler_check_new_version_creation from .links import ( - link_checkin_document, link_checkout_document, link_checkout_info, - link_checkout_list + link_document_check_in, link_document_checkout, link_document_checkout_info, + link_document_checkout_list, link_document_multiple_check_in, + link_document_multiple_checkout ) from .literals import CHECK_EXPIRED_CHECK_OUTS_INTERVAL from .methods import ( @@ -32,7 +33,7 @@ from .methods import ( method_is_checked_out ) from .permissions import ( - permission_document_checkin, permission_document_checkin_override, + permission_document_check_in, permission_document_check_in_override, permission_document_checkout, permission_document_checkout_detail_view ) from .queues import * # NOQA @@ -79,8 +80,8 @@ class CheckoutsApp(MayanAppConfig): ModelPermission.register( model=Document, permissions=( permission_document_checkout, - permission_document_checkin, - permission_document_checkin_override, + permission_document_check_in, + permission_document_check_in_override, permission_document_checkout_detail_view ) ) @@ -115,13 +116,18 @@ class CheckoutsApp(MayanAppConfig): widget=DashboardWidgetTotalCheckouts, order=-1 ) - menu_facet.bind_links(links=(link_checkout_info,), sources=(Document,)) - menu_main.bind_links(links=(link_checkout_list,), position=98) + menu_facet.bind_links(links=(link_document_checkout_info,), sources=(Document,)) + menu_main.bind_links(links=(link_document_checkout_list,), position=98) + menu_multi_item.bind_links( + links=( + link_document_multiple_check_in, link_document_multiple_checkout + ), sources=(Document,) + ) menu_sidebar.bind_links( - links=(link_checkout_document, link_checkin_document), + links=(link_document_checkout, link_document_check_in), sources=( - 'checkouts:checkout_info', 'checkouts:checkout_document', - 'checkouts:checkin_document' + 'checkouts:document_checkout_info', 'checkouts:document_checkout', + 'checkouts:document_check_in' ) ) diff --git a/mayan/apps/checkouts/dashboard_widgets.py b/mayan/apps/checkouts/dashboard_widgets.py index 334e133054..e205e8b38a 100644 --- a/mayan/apps/checkouts/dashboard_widgets.py +++ b/mayan/apps/checkouts/dashboard_widgets.py @@ -14,7 +14,7 @@ from .permissions import permission_document_checkout_detail_view class DashboardWidgetTotalCheckouts(DashboardWidgetNumeric): icon_class = icon_dashboard_checkouts label = _('Checkedout documents') - link = reverse_lazy(viewname='checkouts:checkout_list') + link = reverse_lazy(viewname='checkouts:document_checkout_list') def render(self, request): AccessControlList = apps.get_model( diff --git a/mayan/apps/checkouts/hooks.py b/mayan/apps/checkouts/hooks.py new file mode 100644 index 0000000000..b36a9ccded --- /dev/null +++ b/mayan/apps/checkouts/hooks.py @@ -0,0 +1,13 @@ +from __future__ import unicode_literals + +from django.apps import apps + + +def hook_is_new_version_allowed(document_version): + NewVersionBlock = apps.get_model( + app_label='checkouts', model_name='NewVersionBlock' + ) + + NewVersionBlock.objects.new_versions_allowed( + document_version=document_version.document + ) diff --git a/mayan/apps/checkouts/links.py b/mayan/apps/checkouts/links.py index b6ef58de91..ddf9451f30 100644 --- a/mayan/apps/checkouts/links.py +++ b/mayan/apps/checkouts/links.py @@ -8,8 +8,8 @@ from .icons import ( icon_checkin_document, icon_checkout_document, icon_checkout_info ) from .permissions import ( - permission_document_checkin, permission_document_checkin_override, - permission_document_checkout, permission_document_checkout_detail_view + permission_document_check_in, permission_document_checkout, + permission_document_checkout_detail_view ) @@ -29,23 +29,32 @@ def is_not_checked_out(context): return True -link_checkout_list = Link( +link_document_checkout_list = Link( icon_class=icon_checkout_info, text=_('Checkouts'), - view='checkouts:checkout_list' + view='checkouts:document_checkout_list' ) -link_checkout_document = Link( - args='object.pk', condition=is_not_checked_out, - icon_class=icon_checkout_document, +link_document_checkout = Link( + condition=is_not_checked_out, icon_class=icon_checkout_document, + kwargs={'document_id': 'object.pk'}, permission=permission_document_checkout, text=_('Check out document'), - view='checkouts:checkout_document', + view='checkouts:document_checkout', ) -link_checkin_document = Link( - args='object.pk', condition=is_checked_out, - icon_class=icon_checkin_document, permission=permission_document_checkin, - text=_('Check in document'), view='checkouts:checkin_document', +link_document_multiple_checkout = Link( + icon_class=icon_checkout_document, + permission=permission_document_checkout, text=_('Check out'), + view='checkouts:document_multiple_checkout', ) -link_checkout_info = Link( - args='resolved_object.pk', icon_class=icon_checkout_info, +link_document_check_in = Link( + condition=is_checked_out, icon_class=icon_checkin_document, + kwargs={'document_id': 'object.pk'}, permission=permission_document_check_in, + text=_('Check in document'), view='checkouts:document_check_in', +) +link_document_multiple_check_in = Link( + icon_class=icon_checkin_document, permission=permission_document_check_in, + text=_('Check in'), view='checkouts:document_multiple_check_in', +) +link_document_checkout_info = Link( + icon_class=icon_checkout_info, kwargs={'document_id': 'resolved_object.pk'}, permission=permission_document_checkout_detail_view, - text=_('Check in/out'), view='checkouts:checkout_info', + text=_('Check in/out'), view='checkouts:document_checkout_info', ) diff --git a/mayan/apps/checkouts/managers.py b/mayan/apps/checkouts/managers.py index 6fd071e959..4fb9ba091d 100644 --- a/mayan/apps/checkouts/managers.py +++ b/mayan/apps/checkouts/managers.py @@ -2,48 +2,54 @@ from __future__ import absolute_import, unicode_literals import logging -from django.apps import apps -from django.db import models +from django.core.exceptions import PermissionDenied +from django.db import models, transaction from django.utils.timezone import now +from django.utils.translation import ugettext_lazy as _ +from mayan.apps.acls.models import AccessControlList from mayan.apps.documents.models import Document from .events import ( event_document_auto_check_in, event_document_check_in, event_document_forceful_check_in ) -from .exceptions import DocumentNotCheckedOut +from .exceptions import DocumentNotCheckedOut, NewDocumentVersionNotAllowed from .literals import STATE_CHECKED_IN, STATE_CHECKED_OUT +from .permissions import permission_document_check_in_override logger = logging.getLogger(__name__) class DocumentCheckoutManager(models.Manager): - def are_document_new_versions_allowed(self, document, user=None): - try: - checkout_info = self.document_checkout_info(document=document) - except DocumentNotCheckedOut: - return True - else: - return not checkout_info.block_new_version - def check_in_document(self, document, user=None): try: document_checkout = self.model.objects.get(document=document) except self.model.DoesNotExist: - raise DocumentNotCheckedOut + raise DocumentNotCheckedOut( + _('Document not checked out.') + ) else: - if user: - if self.get_document_checkout_info(document=document).user != user: - event_document_forceful_check_in.commit( - actor=user, target=document - ) + with transaction.atomic(): + if user: + if self.get_document_checkout_info(document=document).user != user: + try: + AccessControlList.objects.check_access( + obj=document, permission=permission_document_check_in_override, + user=user + ) + except PermissionDenied: + return + else: + event_document_forceful_check_in.commit( + actor=user, target=document + ) + else: + event_document_check_in.commit(actor=user, target=document) else: - event_document_check_in.commit(actor=user, target=document) - else: - event_document_auto_check_in.commit(target=document) + event_document_auto_check_in.commit(target=document) - document_checkout.delete() + document_checkout.delete() def check_in_expired_check_outs(self): for document in self.get_expired_check_outs(): @@ -57,15 +63,10 @@ class DocumentCheckoutManager(models.Manager): def checked_out_documents(self): return Document.objects.filter( - pk__in=self.model.objects.all().values_list( - 'document__pk', flat=True - ) + pk__in=self.model.objects.values('document__id') ) def get_by_natural_key(self, document_natural_key): - Document = apps.get_model( - app_label='documents', model_name='Document' - ) try: document = Document.objects.get_by_natural_key(document_natural_key) except Document.DoesNotExist: @@ -87,15 +88,15 @@ class DocumentCheckoutManager(models.Manager): def get_expired_check_outs(self): expired_list = Document.objects.filter( - pk__in=self.model.objects.filter( + pk__in=self.filter( expiration_datetime__lte=now() - ).values_list('document__pk', flat=True) + ).values('document__id') ) logger.debug('expired_list: %s', expired_list) return expired_list def is_document_checked_out(self, document): - if self.model.objects.filter(document=document): + if self.filter(document=document).exists(): return True else: return False @@ -105,13 +106,7 @@ class NewVersionBlockManager(models.Manager): def block(self, document): self.get_or_create(document=document) - def is_blocked(self, document): - return self.filter(document=document).exists() - def get_by_natural_key(self, document_natural_key): - Document = apps.get_model( - app_label='documents', model_name='Document' - ) try: document = Document.objects.get_by_natural_key(document_natural_key) except Document.DoesNotExist: @@ -119,5 +114,12 @@ class NewVersionBlockManager(models.Manager): return self.get(document__pk=document.pk) + def is_blocked(self, document): + return self.filter(document=document).exists() + + def new_versions_allowed(self, document): + if self.filter(document=document).exist(): + raise NewDocumentVersionNotAllowed + def unblock(self, document): self.filter(document=document).delete() diff --git a/mayan/apps/checkouts/models.py b/mayan/apps/checkouts/models.py index fa9e621855..b6c6702707 100644 --- a/mayan/apps/checkouts/models.py +++ b/mayan/apps/checkouts/models.py @@ -4,7 +4,7 @@ import logging from django.conf import settings from django.core.exceptions import ValidationError -from django.db import models +from django.db import models, transaction from django.urls import reverse from django.utils.encoding import python_2_unicode_compatible from django.utils.timezone import now @@ -68,13 +68,14 @@ class DocumentCheckout(models.Model): ) def delete(self, *args, **kwargs): - # TODO: enclose in transaction - NewVersionBlock.objects.unblock(self.document) - super(DocumentCheckout, self).delete(*args, **kwargs) + with transaction.atomic(): + NewVersionBlock.objects.unblock(document=self.document) + super(DocumentCheckout, self).delete(*args, **kwargs) def get_absolute_url(self): return reverse( - viewname='checkout:checkout_info', kwargs={'pk': self.document.pk} + viewname='checkout:checkout_info', + kwargs={'document_id': self.document.pk} ) def natural_key(self): @@ -82,25 +83,27 @@ class DocumentCheckout(models.Model): natural_key.dependencies = ['documents.Document'] def save(self, *args, **kwargs): - # TODO: enclose in transaction new_checkout = not self.pk if not new_checkout or self.document.is_checked_out(): - raise DocumentAlreadyCheckedOut - - result = super(DocumentCheckout, self).save(*args, **kwargs) - if new_checkout: - event_document_check_out.commit( - actor=self.user, target=self.document - ) - if self.block_new_version: - NewVersionBlock.objects.block(self.document) - - logger.info( - 'Document "%s" checked out by user "%s"', - self.document, self.user + raise DocumentAlreadyCheckedOut( + _('Document already checked out.') ) - return result + with transaction.atomic(): + result = super(DocumentCheckout, self).save(*args, **kwargs) + if new_checkout: + event_document_check_out.commit( + actor=self.user, target=self.document + ) + if self.block_new_version: + NewVersionBlock.objects.block(self.document) + + logger.info( + 'Document "%s" checked out by user "%s"', + self.document, self.user + ) + + return result class NewVersionBlock(models.Model): diff --git a/mayan/apps/checkouts/permissions.py b/mayan/apps/checkouts/permissions.py index 882993d6d0..ce8f347798 100644 --- a/mayan/apps/checkouts/permissions.py +++ b/mayan/apps/checkouts/permissions.py @@ -6,10 +6,10 @@ from mayan.apps.permissions import PermissionNamespace namespace = PermissionNamespace(label=_('Document checkout'), name='checkouts') -permission_document_checkin = namespace.add_permission( +permission_document_check_in = namespace.add_permission( label=_('Check in documents'), name='checkin_document' ) -permission_document_checkin_override = namespace.add_permission( +permission_document_check_in_override = namespace.add_permission( label=_('Forcefully check in documents'), name='checkin_document_override' ) permission_document_checkout = namespace.add_permission( diff --git a/mayan/apps/checkouts/serializers.py b/mayan/apps/checkouts/serializers.py index 5e51e1f582..5efc70d1f4 100644 --- a/mayan/apps/checkouts/serializers.py +++ b/mayan/apps/checkouts/serializers.py @@ -42,8 +42,8 @@ class NewDocumentCheckoutSerializer(serializers.ModelSerializer): document = Document.objects.get(pk=validated_data.pop('document_pk')) AccessControlList.objects.check_access( - permissions=permission_document_checkout, - obj=document, user=self.context['request'].user + obj=document, permissions=permission_document_checkout, + user=self.context['request'].user ) validated_data['document'] = document diff --git a/mayan/apps/checkouts/tests/test_models.py b/mayan/apps/checkouts/tests/test_models.py index 6378cee1d6..d763bb63a3 100644 --- a/mayan/apps/checkouts/tests/test_models.py +++ b/mayan/apps/checkouts/tests/test_models.py @@ -23,7 +23,7 @@ class DocumentCheckoutTestCase(DocumentTestMixin, BaseTestCase): DocumentCheckout.objects.checkout_document( document=self.document, expiration_datetime=expiration_datetime, - user=self.admin_user, block_new_version=True + user=self._test_case_user, block_new_version=True ) self.assertTrue(self.document.is_checked_out()) @@ -33,29 +33,12 @@ class DocumentCheckoutTestCase(DocumentTestMixin, BaseTestCase): ) ) - def test_version_creation_blocking(self): - expiration_datetime = now() + datetime.timedelta(days=1) - - # Silence unrelated logging - logging.getLogger('mayan.apps.documents.models').setLevel( - level=logging.CRITICAL - ) - - DocumentCheckout.objects.checkout_document( - document=self.document, expiration_datetime=expiration_datetime, - user=self.admin_user, block_new_version=True - ) - - with self.assertRaises(NewDocumentVersionNotAllowed): - with open(TEST_SMALL_DOCUMENT_PATH, mode='rb') as file_object: - self.document.new_version(file_object=file_object) - def test_checkin_in(self): expiration_datetime = now() + datetime.timedelta(days=1) DocumentCheckout.objects.checkout_document( document=self.document, expiration_datetime=expiration_datetime, - user=self.admin_user, block_new_version=True + user=self._test_case_user, block_new_version=True ) self.document.check_in() @@ -72,13 +55,13 @@ class DocumentCheckoutTestCase(DocumentTestMixin, BaseTestCase): DocumentCheckout.objects.checkout_document( document=self.document, expiration_datetime=expiration_datetime, - user=self.admin_user, block_new_version=True + user=self._test_case_user, block_new_version=True ) with self.assertRaises(DocumentAlreadyCheckedOut): DocumentCheckout.objects.checkout_document( document=self.document, - expiration_datetime=expiration_datetime, user=self.admin_user, + expiration_datetime=expiration_datetime, user=self._test_case_user, block_new_version=True ) @@ -91,7 +74,7 @@ class DocumentCheckoutTestCase(DocumentTestMixin, BaseTestCase): DocumentCheckout.objects.checkout_document( document=self.document, expiration_datetime=expiration_datetime, - user=self.admin_user, block_new_version=True + user=self._test_case_user, block_new_version=True ) time.sleep(.11) @@ -100,18 +83,6 @@ class DocumentCheckoutTestCase(DocumentTestMixin, BaseTestCase): self.assertFalse(self.document.is_checked_out()) - def test_blocking_new_versions(self): - # Silence unrelated logging - logging.getLogger('mayan.apps.documents.models').setLevel( - level=logging.CRITICAL - ) - - NewVersionBlock.objects.block(document=self.document) - - with self.assertRaises(NewDocumentVersionNotAllowed): - with open(TEST_SMALL_DOCUMENT_PATH, mode='rb') as file_object: - self.document.new_version(file_object=file_object) - class NewVersionBlockTestCase(DocumentTestMixin, BaseTestCase): def test_blocking(self): @@ -141,3 +112,32 @@ class NewVersionBlockTestCase(DocumentTestMixin, BaseTestCase): self.assertFalse( NewVersionBlock.objects.is_blocked(document=self.document) ) + + def test_blocking_new_versions(self): + # Silence unrelated logging + logging.getLogger('mayan.apps.documents.models').setLevel( + level=logging.CRITICAL + ) + + NewVersionBlock.objects.block(document=self.document) + + with self.assertRaises(NewDocumentVersionNotAllowed): + with open(TEST_SMALL_DOCUMENT_PATH, mode='rb') as file_object: + self.document.new_version(file_object=file_object) + + def test_version_creation_blocking(self): + expiration_datetime = now() + datetime.timedelta(days=1) + + # Silence unrelated logging + logging.getLogger('mayan.apps.documents.models').setLevel( + level=logging.CRITICAL + ) + + DocumentCheckout.objects.checkout_document( + document=self.document, expiration_datetime=expiration_datetime, + user=self._test_case_user, block_new_version=True + ) + + with self.assertRaises(NewDocumentVersionNotAllowed): + with open(TEST_SMALL_DOCUMENT_PATH, mode='rb') as file_object: + self.document.new_version(file_object=file_object) diff --git a/mayan/apps/checkouts/tests/test_views.py b/mayan/apps/checkouts/tests/test_views.py index 9e49b0cfc1..e6f1d2980f 100644 --- a/mayan/apps/checkouts/tests/test_views.py +++ b/mayan/apps/checkouts/tests/test_views.py @@ -8,62 +8,53 @@ from django.utils.timezone import now from mayan.apps.common.literals import TIME_DELTA_UNIT_DAYS from mayan.apps.documents.tests import GenericDocumentViewTestCase from mayan.apps.sources.links import link_upload_version -from mayan.apps.user_management.tests import ( - TEST_ADMIN_PASSWORD, TEST_ADMIN_USERNAME, TEST_USER_PASSWORD, - TEST_USER_USERNAME -) from ..models import DocumentCheckout from ..permissions import ( - permission_document_checkin, permission_document_checkin_override, + permission_document_check_in, permission_document_check_in_override, permission_document_checkout, permission_document_checkout_detail_view ) class DocumentCheckoutViewTestCase(GenericDocumentViewTestCase): + create_test_case_superuser = True + + def _checkout_document(self): + expiration_datetime = now() + datetime.timedelta(days=1) + + DocumentCheckout.objects.checkout_document( + document=self.document, expiration_datetime=expiration_datetime, + user=self._test_case_user, block_new_version=True + ) + self.assertTrue(self.document.is_checked_out()) + def _request_document_check_in_view(self): return self.post( - viewname='checkouts:checkin_document', - kwargs={'document_pk': self.document.pk} + viewname='checkouts:document_check_in', + kwargs={'document_id': self.document.pk} ) - def test_checkin_document_view_no_permission(self): - self.login_user() - - expiration_datetime = now() + datetime.timedelta(days=1) - - DocumentCheckout.objects.checkout_document( - document=self.document, expiration_datetime=expiration_datetime, - user=self.user, block_new_version=True - ) - - self.assertTrue(self.document.is_checked_out()) + def test_document_check_in_view_no_permission(self): + self._checkout_document() response = self._request_document_check_in_view() - self.assertEquals(response.status_code, 403) + self.assertEquals(response.status_code, 404) + self.assertTrue(self.document.is_checked_out()) - def test_checkin_document_view_with_access(self): - self.login_user() - - expiration_datetime = now() + datetime.timedelta(days=1) - - DocumentCheckout.objects.checkout_document( - document=self.document, expiration_datetime=expiration_datetime, - user=self.user, block_new_version=True - ) - self.assertTrue(self.document.is_checked_out()) + def test_document_check_in_view_with_access(self): + self._checkout_document() self.grant_access( - obj=self.document, permission=permission_document_checkin + obj=self.document, permission=permission_document_check_in ) self.grant_access( obj=self.document, permission=permission_document_checkout_detail_view ) - response = self._request_document_check_in_view() self.assertEquals(response.status_code, 302) + self.assertFalse(self.document.is_checked_out()) self.assertFalse( DocumentCheckout.objects.is_document_checked_out( @@ -73,8 +64,8 @@ class DocumentCheckoutViewTestCase(GenericDocumentViewTestCase): def _request_document_checkout_view(self): return self.post( - viewname='checkouts:checkout_document', - kwargs={'document_pk': self.document.pk}, + viewname='checkouts:document_checkout', + kwargs={'document_id': self.document.pk}, data={ 'expiration_datetime_0': 2, 'expiration_datetime_1': TIME_DELTA_UNIT_DAYS, @@ -83,14 +74,11 @@ class DocumentCheckoutViewTestCase(GenericDocumentViewTestCase): ) def test_checkout_document_view_no_permission(self): - self.login_user() - response = self._request_document_checkout_view() - self.assertEquals(response.status_code, 403) + self.assertEquals(response.status_code, 404) self.assertFalse(self.document.is_checked_out()) def test_checkout_document_view_with_access(self): - self.login_user() self.grant_access( obj=self.document, permission=permission_document_checkout ) @@ -98,9 +86,9 @@ class DocumentCheckoutViewTestCase(GenericDocumentViewTestCase): obj=self.document, permission=permission_document_checkout_detail_view ) - response = self._request_document_checkout_view() self.assertEquals(response.status_code, 302) + self.assertTrue(self.document.is_checked_out()) def test_document_new_version_after_checkout(self): @@ -113,25 +101,15 @@ class DocumentCheckoutViewTestCase(GenericDocumentViewTestCase): - Link to upload version view should not resolve - Upload version view should reject request """ - self.login( - username=TEST_ADMIN_USERNAME, password=TEST_ADMIN_PASSWORD - ) + self.login_superuser() - expiration_datetime = now() + datetime.timedelta(days=1) - - DocumentCheckout.objects.checkout_document( - document=self.document, expiration_datetime=expiration_datetime, - user=self.admin_user, block_new_version=True - ) - - self.assertTrue(self.document.is_checked_out()) + self._checkout_document() response = self.post( viewname='sources:upload_version', - kwargs={'document_pk': self.document.pk}, + kwargs={'document_id': self.document.pk}, follow=True ) - self.assertContains( response, text='blocked from uploading', status_code=200 @@ -139,7 +117,7 @@ class DocumentCheckoutViewTestCase(GenericDocumentViewTestCase): response = self.get( viewname='documents:document_version_list', - kwargs={'document_pk': self.document.pk}, + kwargs={'document_id': self.document.pk}, follow=True ) @@ -163,28 +141,22 @@ class DocumentCheckoutViewTestCase(GenericDocumentViewTestCase): DocumentCheckout.objects.checkout_document( document=self.document, expiration_datetime=expiration_datetime, - user=self.admin_user, block_new_version=True + user=self._test_case_superuser, block_new_version=True ) self.assertTrue(self.document.is_checked_out()) - self.login( - username=TEST_USER_USERNAME, password=TEST_USER_PASSWORD + self.grant_access( + obj=self.document, permission=permission_document_check_in ) - - self.role.permissions.add( - permission_document_checkin.stored_permission + self.grant_access( + obj=self.document, permission=permission_document_checkout ) - self.role.permissions.add( - permission_document_checkout.stored_permission - ) - response = self.post( - viewname='checkouts:checkin_document', - kwargs={'document_pk': self.document.pk}, + viewname='checkouts:document_check_in', + kwargs={'document_id': self.document.pk}, follow=True ) - self.assertContains( response, text='Insufficient permissions', status_code=403 ) @@ -192,34 +164,20 @@ class DocumentCheckoutViewTestCase(GenericDocumentViewTestCase): self.assertTrue(self.document.is_checked_out()) def test_forcefull_check_in_document_view_with_permission(self): - expiration_datetime = now() + datetime.timedelta(days=1) + self._checkout_document() - DocumentCheckout.objects.checkout_document( - document=self.document, expiration_datetime=expiration_datetime, - user=self.admin_user, block_new_version=True + self.grant_access( + obj=self.document, permission=permission_document_check_in ) - - self.assertTrue(self.document.is_checked_out()) - - self.login( - username=TEST_USER_USERNAME, password=TEST_USER_PASSWORD + self.grant_access( + obj=self.document, permission=permission_document_check_in_override ) - - self.role.permissions.add( - permission_document_checkin.stored_permission - ) - self.role.permissions.add( - permission_document_checkin.stored_permission - ) - self.role.permissions.add( - permission_document_checkin_override.stored_permission - ) - self.role.permissions.add( - permission_document_checkout_detail_view.stored_permission + self.grant_access( + obj=self.document, permission=permission_document_checkout_detail_view ) response = self.post( - viewname='checkouts:checkin_document', - kwargs={'document_pk': self.document.pk}, + viewname='checkouts:document_check_in', + kwargs={'document_id': self.document.pk}, follow=True ) diff --git a/mayan/apps/checkouts/urls.py b/mayan/apps/checkouts/urls.py index e3360ce8b3..23673f57d5 100644 --- a/mayan/apps/checkouts/urls.py +++ b/mayan/apps/checkouts/urls.py @@ -4,26 +4,34 @@ from django.conf.urls import url from .api_views import APICheckedoutDocumentListView, APICheckedoutDocumentView from .views import ( - CheckoutDetailView, CheckoutDocumentView, CheckoutListView, - DocumentCheckinView + DocumentCheckinView, DocumentCheckoutView, DocumentCheckoutDetailView, + DocumentCheckoutListView ) urlpatterns = [ url( - regex=r'^documents/$', name='checkout_list', - view=CheckoutListView.as_view() + regex=r'^documents/$', name='document_checkout_list', + view=DocumentCheckoutListView.as_view() ), url( - regex=r'^documents/(?P\d+)/check/out/$', - name='checkout_document', view=CheckoutDocumentView.as_view() + regex=r'^documents/(?P\d+)/check_in/$', + name='document_check_in', view=DocumentCheckinView.as_view() ), url( - regex=r'^documents/(?P\d+)/check/in/$', - name='checkin_document', view=DocumentCheckinView.as_view() + regex=r'^documents/multiple/check_in/$', + name='document_multiple_check_in', view=DocumentCheckinView.as_view() ), url( - regex=r'^documents/(?P\d+)/check/info/$', - name='checkout_info', view=CheckoutDetailView.as_view() + regex=r'^documents/(?P\d+)/checkout/$', + name='document_checkout', view=DocumentCheckoutView.as_view() + ), + url( + regex=r'^documents/multiple/checkout/$', + name='document_multiple_checkout', view=DocumentCheckoutView.as_view() + ), + url( + regex=r'^documents/(?P\d+)/checkout/info/$', + name='document_checkout_info', view=DocumentCheckoutDetailView.as_view() ), ] @@ -33,7 +41,7 @@ api_urls = [ view=APICheckedoutDocumentListView.as_view() ), url( - regex=r'^checkouts/(?P[0-9]+)/checkout_info/$', + regex=r'^checkouts/(?P\d+)/checkout_info/$', name='checkedout-document-view', view=APICheckedoutDocumentView.as_view() ), diff --git a/mayan/apps/checkouts/views.py b/mayan/apps/checkouts/views.py index a3f2ffc475..b336cd9a4f 100644 --- a/mayan/apps/checkouts/views.py +++ b/mayan/apps/checkouts/views.py @@ -1,86 +1,145 @@ from __future__ import absolute_import, unicode_literals -from django.contrib import messages -from django.http import HttpResponseRedirect from django.shortcuts import get_object_or_404 from django.urls import reverse -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import ugettext_lazy as _, ungettext from mayan.apps.acls.models import AccessControlList from mayan.apps.common.generics import ( - ConfirmView, SingleObjectCreateView, SingleObjectDetailView + MultipleObjectConfirmActionView, MultipleObjectFormActionView, + SingleObjectDetailView ) from mayan.apps.common.utils import encapsulate from mayan.apps.documents.models import Document from mayan.apps.documents.views import DocumentListView -from .exceptions import DocumentAlreadyCheckedOut, DocumentNotCheckedOut from .forms import DocumentCheckoutDefailForm, DocumentCheckoutForm from .icons import icon_checkout_info from .models import DocumentCheckout from .permissions import ( - permission_document_checkin, permission_document_checkin_override, - permission_document_checkout, permission_document_checkout_detail_view + permission_document_check_in, permission_document_checkout, + permission_document_checkout_detail_view ) -class CheckoutDocumentView(SingleObjectCreateView): - form_class = DocumentCheckoutForm +class DocumentCheckinView(MultipleObjectConfirmActionView): + error_message = 'Unable to check in document "%(instance)s". %(exception)s' + model = Document + object_permission = permission_document_check_in + pk_url_kwarg = 'document_id' + success_message = '%(count)d document checked in.' + success_message_plural = '%(count)d documents checked in.' - def dispatch(self, request, *args, **kwargs): - self.document = get_object_or_404( - klass=Document, pk=self.kwargs['document_pk'] - ) + def get_extra_context(self): + queryset = self.get_object_list() - AccessControlList.objects.check_access( - obj=self.document, permissions=permission_document_checkout, - user=request.user - ) + result = { + 'title': ungettext( + singular='Check in %(count)d document', + plural='Check in %(count)d documents', + number=queryset.count() + ) % { + 'count': queryset.count(), + } + } - return super( - CheckoutDocumentView, self - ).dispatch(request, *args, **kwargs) - - def form_valid(self, form): - try: - instance = form.save(commit=False) - instance.user = self.request.user - instance.document = self.document - instance.save() - except DocumentAlreadyCheckedOut: - messages.error( - request=self.request, - message=_('Document already checked out.') + if queryset.count() == 1: + result.update( + { + 'object': queryset.first(), + 'title': _( + 'Check in document: %s' + ) % queryset.first() + } ) - except Exception as exception: - messages.error( - request=self.request, - message=_('Error trying to check out document; %s') % exception + + return result + + def get_post_object_action_url(self): + if self.action_count == 1: + return reverse( + viewname='checkouts:document_checkout_info', + kwargs={'document_id': self.action_id_list[0]} ) else: - messages.success( - request=self.request, - message=_( - 'Document "%s" checked out successfully.' - ) % self.document + super(DocumentCheckinView, self).get_post_action_redirect() + + def object_action(self, form, instance): + DocumentCheckout.objects.check_in_document( + document=instance, user=self.request.user + ) + + +class DocumentCheckoutView(MultipleObjectFormActionView): + error_message = 'Unable to checkout document "%(instance)s". %(exception)s' + form_class = DocumentCheckoutForm + model = Document + object_permission = permission_document_checkout + pk_url_kwarg = 'document_id' + success_message = '%(count)d document checked out.' + success_message_plural = '%(count)d documents checked out.' + + def get_extra_context(self): + queryset = self.get_object_list() + + result = { + 'title': ungettext( + singular='Checkout %(count)d document', + plural='Checkout %(count)d documents', + number=queryset.count() + ) % { + 'count': queryset.count(), + } + } + + if queryset.count() == 1: + result.update( + { + 'object': queryset.first(), + 'title': _( + 'Check out document: %s' + ) % queryset.first() + } ) - return HttpResponseRedirect(redirect_to=self.get_success_url()) + return result + + def get_post_object_action_url(self): + if self.action_count == 1: + return reverse( + viewname='checkouts:document_checkout_info', + kwargs={'document_id': self.action_id_list[0]} + ) + else: + super(DocumentCheckoutView, self).get_post_action_redirect() + + def object_action(self, form, instance): + DocumentCheckout.objects.checkout_document( + block_new_version=form.cleaned_data['block_new_version'], + document=instance, + expiration_datetime=form.cleaned_data['expiration_datetime'], + user=self.request.user, + ) + + +class DocumentCheckoutDetailView(SingleObjectDetailView): + form_class = DocumentCheckoutDefailForm + model = Document + object_permission = permission_document_checkout_detail_view def get_extra_context(self): return { - 'object': self.document, - 'title': _('Check out document: %s') % self.document + 'object': self.get_object(), + 'title': _( + 'Check out details for document: %s' + ) % self.get_object() } - def get_post_action_redirect(self): - return reverse( - viewname='checkouts:checkout_info', - kwargs={'document_pk': self.document.pk} - ) + def get_object(self): + return get_object_or_404(klass=Document, pk=self.kwargs['document_id']) -class CheckoutListView(DocumentListView): +class DocumentCheckoutListView(DocumentListView): def get_document_queryset(self): return AccessControlList.objects.restrict_queryset( permission=permission_document_checkout_detail_view, @@ -89,7 +148,7 @@ class CheckoutListView(DocumentListView): ) def get_extra_context(self): - context = super(CheckoutListView, self).get_extra_context() + context = super(DocumentCheckoutListView, self).get_extra_context() context.update( { 'extra_columns': ( @@ -123,81 +182,3 @@ class CheckoutListView(DocumentListView): } ) return context - - -class CheckoutDetailView(SingleObjectDetailView): - form_class = DocumentCheckoutDefailForm - model = Document - object_permission = permission_document_checkout_detail_view - - def get_extra_context(self): - return { - 'object': self.get_object(), - 'title': _( - 'Check out details for document: %s' - ) % self.get_object() - } - - def get_object(self): - return get_object_or_404(klass=Document, pk=self.kwargs['document_pk']) - - -class DocumentCheckinView(ConfirmView): - def get_extra_context(self): - document = self.get_object() - - context = { - 'object': document, - } - - if document.get_checkout_info().user != self.request.user: - context['title'] = _( - 'You didn\'t originally checked out this document. ' - 'Forcefully check in the document: %s?' - ) % document - else: - context['title'] = _('Check in the document: %s?') % document - - return context - - def get_object(self): - return get_object_or_404(klass=Document, pk=self.kwargs['document_pk']) - - def get_post_action_redirect(self): - return reverse( - viewname='checkouts:checkout_info', - kwargs={'document_pk': self.get_object().pk} - ) - - def view_action(self): - document = self.get_object() - - if document.get_checkout_info().user == self.request.user: - AccessControlList.objects.check_access( - obj=document, permissions=permission_document_checkin, - user=self.request.user - ) - else: - AccessControlList.objects.check_access( - obj=document, permissions=permission_document_checkin_override, - user=self.request.user - ) - - try: - document.check_in(user=self.request.user) - except DocumentNotCheckedOut: - messages.error( - request=self.request, message=_( - 'Document has not been checked out.' - ) - ) - except Exception as exception: - messages.error( - request=self.request, - message=_('Error trying to check in document; %s') % exception - ) - else: - messages.success( - request=self.request, - message=_('Document "%s" checked in successfully.') % document - ) From 3c2d2d108707e470598ea9053915b99427a2e072 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Thu, 31 Jan 2019 05:57:18 -0400 Subject: [PATCH 086/209] Update comment Signed-off-by: Roberto Rosario --- mayan/apps/common/tests/base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mayan/apps/common/tests/base.py b/mayan/apps/common/tests/base.py index 2fc36e4599..c3c06672c5 100644 --- a/mayan/apps/common/tests/base.py +++ b/mayan/apps/common/tests/base.py @@ -29,6 +29,6 @@ class BaseTestCase(RandomPrimaryKeyModelMonkeyPatchMixin, DatabaseConversionMixi class GenericViewTestCase(ClientMethodsTestCaseMixin, TestViewTestCaseMixin, BaseTestCase): """ A generic view test case built on top of the base test case providing - single user test view to test object resolution and shorthand HTTP - method functions. + a single, user customizable view to test object resolution and shorthand + HTTP method functions. """ From 495ac8d196473003064ae24f3999ceaaa4cb90cf Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Thu, 31 Jan 2019 05:57:44 -0400 Subject: [PATCH 087/209] Object action mixin Add post_object_action_url property to redirect the view after all items in the queryset have been processed. Add the exception instance in the error message context. Signed-off-by: Roberto Rosario --- mayan/apps/common/mixins.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/mayan/apps/common/mixins.py b/mayan/apps/common/mixins.py index e0a1144919..5eaa001d2b 100644 --- a/mayan/apps/common/mixins.py +++ b/mayan/apps/common/mixins.py @@ -304,9 +304,13 @@ class ObjectActionMixin(object): Mixin that performs an user action to a queryset """ error_message = 'Unable to perform operation on object %(instance)s.' + post_object_action_url = None success_message = 'Operation performed on %(count)d object.' success_message_plural = 'Operation performed on %(count)d objects.' + def get_post_object_action_url(self): + return self.post_object_action_url + def get_success_message(self, count): return ungettext( singular=self.success_message, @@ -322,25 +326,28 @@ class ObjectActionMixin(object): def view_action(self, form=None): self.action_count = 0 + self.action_id_list = [] for instance in self.get_object_list(): try: self.object_action(form=form, instance=instance) - except PermissionDenied: - pass - except ActionError: + except Exception as exception: messages.error( - request=self.request, - message=self.error_message % {'instance': instance} + message=self.error_message % { + 'exception': exception, 'instance': instance + }, request=self.request ) else: self.action_count += 1 + self.action_id_list.append(instance.pk) messages.success( - request=self.request, - message=self.get_success_message(count=self.action_count) + message=self.get_success_message(count=self.action_count), + request=self.request ) + self.success_url = self.get_post_object_action_url() + class ObjectNameMixin(object): def get_object_name(self, context=None): From e97dde5b46d84bed51c94538725521455bd30f96 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Thu, 31 Jan 2019 05:59:11 -0400 Subject: [PATCH 088/209] Enclose document type change in a transaction Signed-off-by: Roberto Rosario --- .../apps/documents/models/document_models.py | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/mayan/apps/documents/models/document_models.py b/mayan/apps/documents/models/document_models.py index 7d1c35ad1c..aa5b41b044 100644 --- a/mayan/apps/documents/models/document_models.py +++ b/mayan/apps/documents/models/document_models.py @@ -6,7 +6,7 @@ import uuid from django.apps import apps from django.conf import settings from django.core.files import File -from django.db import models +from django.db import models, transaction from django.template import Context, Template from django.urls import reverse from django.utils.encoding import force_text, python_2_unicode_compatible @@ -227,15 +227,17 @@ class Document(models.Model): has_changed = self.document_type != document_type self.document_type = document_type - self.save() - if has_changed or force: - post_document_type_change.send( - sender=self.__class__, instance=self - ) - event_document_type_change.commit(actor=_user, target=self) - if _user: - self.add_as_recent_document_for_user(user=_user) + with transaction.atomic(): + self.save() + if has_changed or force: + post_document_type_change.send( + sender=self.__class__, instance=self + ) + + event_document_type_change.commit(actor=_user, target=self) + if _user: + self.add_as_recent_document_for_user(user=_user) @property def size(self): From d5fc50272dc534830cea2d4e196379d61c0120be Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Thu, 31 Jan 2019 06:07:29 -0400 Subject: [PATCH 089/209] Enable pre save hook Signed-off-by: Roberto Rosario --- mayan/apps/checkouts/apps.py | 5 +++++ mayan/apps/checkouts/hooks.py | 2 +- mayan/apps/checkouts/managers.py | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/mayan/apps/checkouts/apps.py b/mayan/apps/checkouts/apps.py index 9c37c65dc7..5d86957907 100644 --- a/mayan/apps/checkouts/apps.py +++ b/mayan/apps/checkouts/apps.py @@ -22,6 +22,7 @@ from .events import ( event_document_check_out, event_document_forceful_check_in ) from .handlers import handler_check_new_version_creation +from .hooks import hook_is_new_version_allowed from .links import ( link_document_check_in, link_document_checkout, link_document_checkout_info, link_document_checkout_list, link_document_multiple_check_in, @@ -70,6 +71,10 @@ class CheckoutsApp(MayanAppConfig): name='is_checked_out', value=method_is_checked_out ) + DocumentVersion.register_pre_save_hook( + func=hook_is_new_version_allowed + ) + ModelEventType.register( model=Document, event_types=( event_document_auto_check_in, event_document_check_in, diff --git a/mayan/apps/checkouts/hooks.py b/mayan/apps/checkouts/hooks.py index b36a9ccded..8a8e3f03bb 100644 --- a/mayan/apps/checkouts/hooks.py +++ b/mayan/apps/checkouts/hooks.py @@ -9,5 +9,5 @@ def hook_is_new_version_allowed(document_version): ) NewVersionBlock.objects.new_versions_allowed( - document_version=document_version.document + document=document_version.document ) diff --git a/mayan/apps/checkouts/managers.py b/mayan/apps/checkouts/managers.py index 4fb9ba091d..1eca803a63 100644 --- a/mayan/apps/checkouts/managers.py +++ b/mayan/apps/checkouts/managers.py @@ -118,7 +118,7 @@ class NewVersionBlockManager(models.Manager): return self.filter(document=document).exists() def new_versions_allowed(self, document): - if self.filter(document=document).exist(): + if self.filter(document=document).exists(): raise NewDocumentVersionNotAllowed def unblock(self, document): From cce6636b0530e4bb9760ae01e3c881778b641a7e Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Thu, 31 Jan 2019 06:07:59 -0400 Subject: [PATCH 090/209] Improve document version hook system Add support for new pre save hooks. Hooks are now lists of functions instead of dictionaries. Signed-off-by: Roberto Rosario --- .../models/document_version_models.py | 67 ++++++++++++++----- 1 file changed, 51 insertions(+), 16 deletions(-) diff --git a/mayan/apps/documents/models/document_version_models.py b/mayan/apps/documents/models/document_version_models.py index 873ec1ad07..a982bfd45d 100644 --- a/mayan/apps/documents/models/document_version_models.py +++ b/mayan/apps/documents/models/document_version_models.py @@ -47,8 +47,9 @@ class DocumentVersion(models.Model): document is modified after upload it's checksum will not match, used for detecting file tampering among other things. """ - _pre_open_hooks = {} - _post_save_hooks = {} + _pre_open_hooks = [] + _pre_save_hooks = [] + _post_save_hooks = [] document = models.ForeignKey( on_delete=models.CASCADE, related_name='versions', to=Document, @@ -103,12 +104,38 @@ class DocumentVersion(models.Model): return self.get_rendered_string() @classmethod - def register_pre_open_hook(cls, order, func): - cls._pre_open_hooks[order] = func + def _execute_hooks(cls, hook_list, instance, **kwargs): + result = None + + for hook in hook_list: + result = hook(document_version=instance, **kwargs) + if result: + kwargs.update(result) + + return result @classmethod - def register_post_save_hook(cls, order, func): - cls._post_save_hooks[order] = func + def _insert_hook_entry(cls, hook_list, func, order=None): + order = order or len(hook_list) + hook_list.insert(order, func) + + @classmethod + def register_pre_open_hook(cls, func, order=None): + cls._insert_hook_entry( + hook_list=cls._pre_open_hooks, func=func, order=order + ) + + @classmethod + def register_post_save_hook(cls, func, order=None): + cls._insert_hook_entry( + hook_list=cls._post_save_hooks, func=func, order=order + ) + + @classmethod + def register_pre_save_hook(cls, func, order=None): + cls._insert_hook_entry( + hook_list=cls._pre_save_hooks, func=func, order=order + ) @cached_property def cache(self): @@ -130,6 +157,11 @@ class DocumentVersion(models.Model): return super(DocumentVersion, self).delete(*args, **kwargs) + def execute_pre_save_hooks(self): + DocumentVersion._execute_hooks( + hook_list=DocumentVersion._pre_save_hooks, instance=self + ) + def exists(self): """ Returns a boolean value that indicates if the document's file @@ -231,13 +263,14 @@ class DocumentVersion(models.Model): if raw: return self.file.storage.open(self.file.name) else: - result = self.file.storage.open(self.file.name) - for key in sorted(DocumentVersion._pre_open_hooks): - result = DocumentVersion._pre_open_hooks[key]( - file_object=result, document_version=self - ) + file_object = self.file.storage.open(self.file.name) - return result + result = DocumentVersion._execute_hooks( + hook_list=DocumentVersion._pre_open_hooks, + instance=self, file_object=file_object + ) + + return result['file_object'] @property def page_count(self): @@ -282,12 +315,14 @@ class DocumentVersion(models.Model): try: with transaction.atomic(): + self.execute_pre_save_hooks() + super(DocumentVersion, self).save(*args, **kwargs) - for key in sorted(DocumentVersion._post_save_hooks): - DocumentVersion._post_save_hooks[key]( - document_version=self - ) + DocumentVersion._execute_hooks( + hook_list=DocumentVersion._post_save_hooks, + instance=self + ) if new_document_version: # Only do this for new documents From 9328a3e26ede67cf7e4092874580a10aabb697f8 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Thu, 31 Jan 2019 06:09:11 -0400 Subject: [PATCH 091/209] Make new version upload link smarter Use the new document pre save hooks to disable the new version upload link via external functions. Signed-off-by: Roberto Rosario --- mayan/apps/sources/links.py | 19 ++++++++++++------- mayan/apps/sources/views.py | 34 +++++++++++++++++----------------- 2 files changed, 29 insertions(+), 24 deletions(-) diff --git a/mayan/apps/sources/links.py b/mayan/apps/sources/links.py index a35c6ca796..f9c1ed8d78 100644 --- a/mayan/apps/sources/links.py +++ b/mayan/apps/sources/links.py @@ -1,5 +1,7 @@ from __future__ import absolute_import, unicode_literals +import logging + from django.apps import apps from django.utils.translation import ugettext_lazy as _ @@ -23,6 +25,8 @@ from .permissions import ( permission_staging_file_delete ) +logger = logging.getLogger(__name__) + def condition_check_document_creation_acls(context): AccessControlList = apps.get_model( @@ -38,12 +42,13 @@ def condition_check_document_creation_acls(context): ).exists() -def document_new_version_not_blocked(context): - NewVersionBlock = apps.get_model( - app_label='checkouts', model_name='NewVersionBlock' - ) - - return not NewVersionBlock.objects.is_blocked(context['object']) +def condition_new_versions_allowed(context): + try: + context['object'].execute_pre_save_hooks() + except Exception as exception: + logger.debug('execute_pre_save_hooks raised and exception: %s', exception) + else: + return True link_document_create_multiple = Link( @@ -120,7 +125,7 @@ link_staging_file_delete = Link( tags='dangerous', text=_('Delete'), view='sources:staging_file_delete' ) link_upload_version = Link( - condition=document_new_version_not_blocked, + condition=condition_new_versions_allowed, kwargs={'document_pk': 'resolved_object.pk'}, permission=permission_document_new_version, text=_('Upload new version'), view='sources:upload_version' diff --git a/mayan/apps/sources/views.py b/mayan/apps/sources/views.py index dc309c5338..21ec981473 100644 --- a/mayan/apps/sources/views.py +++ b/mayan/apps/sources/views.py @@ -351,8 +351,6 @@ class UploadBaseView(ListModeMixin, MultiFormView): class UploadInteractiveView(UploadBaseView): def dispatch(self, request, *args, **kwargs): - self.subtemplates_list = [] - self.document_type = get_object_or_404( klass=DocumentType, pk=self.request.GET.get( @@ -361,8 +359,8 @@ class UploadInteractiveView(UploadBaseView): ) AccessControlList.objects.check_access( - permission=permission_document_create, user=request.user, - obj=self.document_type + obj=self.document_type, permission=permission_document_create, + user=request.user ) self.tab_links = UploadBaseView.get_active_tab_links() @@ -521,30 +519,32 @@ class UploadInteractiveView(UploadBaseView): class UploadInteractiveVersionView(UploadBaseView): def dispatch(self, request, *args, **kwargs): + self.document = get_object_or_404( + klass=Document, pk=kwargs['document_id'] + ) - self.subtemplates_list = [] + AccessControlList.objects.check_access( + obj=self.document, permission=permission_document_new_version, + user=self.request.user + ) - self.document = get_object_or_404(klass=Document, pk=kwargs['document_id']) - - # TODO: Try to remove this new version block check from here - if NewVersionBlock.objects.is_blocked(self.document): + try: + self.document.latest_version.execute_pre_save_hooks() + except Exception as exception: messages.error( message=_( - 'Document "%s" is blocked from uploading new versions.' - ) % self.document, request=self.request + 'Unable to upload new versions for document ' + '"%(document)s". %(exception)s' + ) % {'document': self.document, 'exception': exception}, + request=self.request ) return HttpResponseRedirect( redirect_to=reverse( viewname='documents:document_version_list', - kwargs={'document_version_id': self.document.pk} + kwargs={'document_id': self.document.pk} ) ) - AccessControlList.objects.check_access( - permission=permission_document_new_version, - user=self.request.user, obj=self.document - ) - self.tab_links = UploadBaseView.get_active_tab_links(self.document) return super( From 0919718114bcf316a3304c70d4f36b290d29c4a9 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Thu, 31 Jan 2019 06:10:32 -0400 Subject: [PATCH 092/209] Update app to use new hooks interface Signed-off-by: Roberto Rosario --- mayan/apps/document_signatures/apps.py | 8 ++++++-- mayan/apps/document_signatures/hooks.py | 23 ++++++++++++++++++++++ mayan/apps/document_signatures/managers.py | 2 +- 3 files changed, 30 insertions(+), 3 deletions(-) create mode 100644 mayan/apps/document_signatures/hooks.py diff --git a/mayan/apps/document_signatures/apps.py b/mayan/apps/document_signatures/apps.py index 55dd1dfd57..587fb079f0 100644 --- a/mayan/apps/document_signatures/apps.py +++ b/mayan/apps/document_signatures/apps.py @@ -18,6 +18,10 @@ from mayan.celery import app from .handlers import ( handler_unverify_key_signatures, handler_verify_key_signatures ) +from .hooks import ( + hook_create_embedded_signature, hook_decrypt_document_version +) + from .links import ( link_all_document_version_signature_verify, link_document_signature_list, link_document_version_signature_delete, @@ -68,10 +72,10 @@ class DocumentSignaturesApp(MayanAppConfig): SignatureBaseModel = self.get_model(model_name='SignatureBaseModel') DocumentVersion.register_post_save_hook( - func=EmbeddedSignature.objects.create, order=1 + func=hook_create_embedded_signature, order=1 ) DocumentVersion.register_pre_open_hook( - func=EmbeddedSignature.objects.open_signed, order=1 + func=hook_decrypt_document_version, order=1 ) ModelPermission.register( diff --git a/mayan/apps/document_signatures/hooks.py b/mayan/apps/document_signatures/hooks.py new file mode 100644 index 0000000000..153dfd0f08 --- /dev/null +++ b/mayan/apps/document_signatures/hooks.py @@ -0,0 +1,23 @@ +from __future__ import unicode_literals + +from django.apps import apps + + +def hook_create_embedded_signature(document_version): + EmbeddedSignature = apps.get_model( + app_label='document_signatures', model_name='EmbeddedSignature' + ) + + EmbeddedSignature.objects.create(document_version=document_version) + + +def hook_decrypt_document_version(document_version, file_object): + EmbeddedSignature = apps.get_model( + app_label='document_signatures', model_name='EmbeddedSignature' + ) + + return { + 'file_object': EmbeddedSignature.objects.open_signed( + document_version=document_version, file_object=file_object + ) + } diff --git a/mayan/apps/document_signatures/managers.py b/mayan/apps/document_signatures/managers.py index 0425fa7d18..8f0eb8f161 100644 --- a/mayan/apps/document_signatures/managers.py +++ b/mayan/apps/document_signatures/managers.py @@ -14,7 +14,7 @@ logger = logging.getLogger(__name__) class EmbeddedSignatureManager(models.Manager): - def open_signed(self, file_object, document_version): + def open_signed(self, document_version, file_object): for signature in self.filter(document_version=document_version): try: return self.open_signed( From 0a864c2f07d6517e6e2d6a82d3f141112358467a Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Thu, 31 Jan 2019 06:11:20 -0400 Subject: [PATCH 093/209] Update ADMIN references to SUPERUSER Signed-off-by: Roberto Rosario --- mayan/apps/user_management/tests/__init__.py | 3 +-- mayan/apps/user_management/tests/literals.py | 8 ++++---- mayan/apps/user_management/tests/mixins.py | 16 ++++++++-------- 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/mayan/apps/user_management/tests/__init__.py b/mayan/apps/user_management/tests/__init__.py index 33ff9e78ba..8b13789179 100644 --- a/mayan/apps/user_management/tests/__init__.py +++ b/mayan/apps/user_management/tests/__init__.py @@ -1,2 +1 @@ -from .literals import * # NOQA -from .mixins import * # NOQA + diff --git a/mayan/apps/user_management/tests/literals.py b/mayan/apps/user_management/tests/literals.py index 9b9b7db24a..9998b2f8f0 100644 --- a/mayan/apps/user_management/tests/literals.py +++ b/mayan/apps/user_management/tests/literals.py @@ -5,12 +5,12 @@ __all__ = ( 'TEST_USER_PASSWORD', 'TEST_USER_PASSWORD_EDITED', 'TEST_USER_USERNAME' ) -TEST_CASE_ADMIN_EMAIL = 'case_admin@example.com' -TEST_CASE_ADMIN_PASSWORD = 'test case admin password' -TEST_CASE_ADMIN_USERNAME = 'test_case_admin' - TEST_CASE_GROUP_NAME = 'test case group' +TEST_CASE_SUPERUSER_EMAIL = 'test_case_superuser@example.com' +TEST_CASE_SUPERUSER_PASSWORD = 'test case superuser password' +TEST_CASE_SUPERUSER_USERNAME = 'test_case_superuser' + TEST_CASE_USER_EMAIL = 'test_case_user@example.com' TEST_CASE_USER_PASSWORD = 'test case user password' TEST_CASE_USER_USERNAME = 'test_case_user' diff --git a/mayan/apps/user_management/tests/mixins.py b/mayan/apps/user_management/tests/mixins.py index cd2c26ec43..6a5dde7534 100644 --- a/mayan/apps/user_management/tests/mixins.py +++ b/mayan/apps/user_management/tests/mixins.py @@ -4,7 +4,7 @@ from django.contrib.auth import get_user_model from django.contrib.auth.models import Group from .literals import ( - TEST_CASE_ADMIN_EMAIL, TEST_CASE_ADMIN_PASSWORD, TEST_CASE_ADMIN_USERNAME, + TEST_CASE_SUPERUSER_EMAIL, TEST_CASE_SUPERUSER_PASSWORD, TEST_CASE_SUPERUSER_USERNAME, TEST_CASE_GROUP_NAME, TEST_GROUP_NAME_EDITED, TEST_CASE_USER_EMAIL, TEST_CASE_USER_PASSWORD, TEST_CASE_USER_USERNAME, TEST_GROUP_NAME, TEST_USER_EMAIL, TEST_USER_USERNAME, TEST_USER_USERNAME_EDITED, @@ -41,7 +41,7 @@ class UserTestCaseMixin(object): if self.auto_login_user: self.login_user() - elif self.create_test_case_superuser: + if self.create_test_case_superuser: self._create_test_case_superuser() if self.auto_login_superuser: @@ -56,8 +56,8 @@ class UserTestCaseMixin(object): def _create_test_case_superuser(self): self._test_case_superuser = get_user_model().objects.create_superuser( - username=TEST_CASE_ADMIN_USERNAME, email=TEST_CASE_ADMIN_EMAIL, - password=TEST_CASE_ADMIN_PASSWORD + username=TEST_CASE_SUPERUSER_USERNAME, email=TEST_CASE_SUPERUSER_EMAIL, + password=TEST_CASE_SUPERUSER_PASSWORD ) def _create_test_case_user(self): @@ -73,8 +73,8 @@ class UserTestCaseMixin(object): def login_superuser(self): self.login( - username=TEST_CASE_ADMIN_USERNAME, - password=TEST_CASE_ADMIN_PASSWORD + username=TEST_CASE_SUPERUSER_USERNAME, + password=TEST_CASE_SUPERUSER_PASSWORD ) def login_user(self): @@ -132,8 +132,8 @@ class GroupTestMixin(object): class UserTestMixin(object): def _create_test_superuser(self): self.test_superuser = get_user_model().objects.create_superuser( - username=TEST_CASE_ADMIN_USERNAME, email=TEST_CASE_ADMIN_EMAIL, - password=TEST_CASE_ADMIN_PASSWORD + username=TEST_CASE_SUPERUSER_USERNAME, email=TEST_CASE_SUPERUSER_EMAIL, + password=TEST_CASE_SUPERUSER_PASSWORD ) def _create_test_user(self): From 125a4317f4836af32a0237a882da0ff126d0e69e Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Thu, 31 Jan 2019 22:23:23 -0400 Subject: [PATCH 094/209] Add custom DatabaseWarning This warning is used to categorize the SQLite production usage warning. Signed-off-by: Roberto Rosario --- mayan/apps/common/apps.py | 5 ++++- mayan/apps/common/warnings.py | 6 ++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/mayan/apps/common/apps.py b/mayan/apps/common/apps.py index b7859b62f5..48155efba8 100644 --- a/mayan/apps/common/apps.py +++ b/mayan/apps/common/apps.py @@ -41,6 +41,7 @@ from .settings import ( from .signals import pre_initial_setup, pre_upgrade from .tasks import task_delete_stale_uploads # NOQA - Force task registration from .utils import check_for_sqlite +from .warnings import DatabaseWarning logger = logging.getLogger(__name__) @@ -88,7 +89,9 @@ class CommonApp(MayanAppConfig): def ready(self): super(CommonApp, self).ready() if check_for_sqlite(): - warnings.warn(force_text(MESSAGE_SQLITE_WARNING)) + warnings.warn( + category=DatabaseWarning, message=force_text(MESSAGE_SQLITE_WARNING) + ) Template( name='menu_main', template_name='appearance/menu_main.html' diff --git a/mayan/apps/common/warnings.py b/mayan/apps/common/warnings.py index 5365974f3e..24929d58db 100644 --- a/mayan/apps/common/warnings.py +++ b/mayan/apps/common/warnings.py @@ -1,6 +1,12 @@ from __future__ import absolute_import +class DatabaseWarning(UserWarning): + """ + Warning when using unsupported database backends + """ + + class InterfaceWarning(UserWarning): """ Warning when using obsolete internal interfaces From 8e66eefe7ce1c36bdd60750c50b3599d909bb3ae Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Thu, 31 Jan 2019 22:26:07 -0400 Subject: [PATCH 095/209] Move file and storage code to the storage app The setting COMMON_TEMPORARY_DIRECTORY is now STORAGE_TEMPORARY_DIRECTORY. Signed-off-by: Roberto Rosario --- HISTORY.rst | 2 + mayan/apps/common/classes.py | 10 -- .../common/management/commands/convertdb.py | 2 +- .../migrations/0011_auto_20181229_0738.py | 11 +- mayan/apps/common/mixins.py | 1 - mayan/apps/common/settings.py | 8 -- mayan/apps/common/storages.py | 3 +- mayan/apps/common/tests/mixins.py | 2 +- mayan/apps/common/tests/test_views.py | 15 +-- mayan/apps/common/utils.py | 116 ----------------- mayan/apps/converter/backends/python.py | 2 +- mayan/apps/converter/classes.py | 4 +- mayan/apps/dependencies/javascript.py | 2 +- mayan/apps/django_gpg/classes.py | 2 +- mayan/apps/django_gpg/managers.py | 2 +- mayan/apps/django_gpg/tests/test_models.py | 2 +- mayan/apps/document_parsing/parsers.py | 2 +- mayan/apps/document_signatures/managers.py | 2 +- .../migrations/0009_auto_20181229_0737.py | 12 +- mayan/apps/document_signatures/storages.py | 2 +- mayan/apps/document_signatures/views.py | 2 +- .../migrations/0051_auto_20181229_0745.py | 10 +- mayan/apps/documents/storages.py | 2 +- mayan/apps/file_metadata/drivers/exiftool.py | 2 +- mayan/apps/lock_manager/backends/file_lock.py | 2 +- .../apps/smart_settings/tests/test_classes.py | 2 +- mayan/apps/sources/models/scanner_sources.py | 2 +- mayan/apps/sources/tests/test_classes.py | 2 +- mayan/apps/sources/tests/test_models.py | 2 +- mayan/apps/sources/tests/test_views.py | 2 +- mayan/apps/storage/classes.py | 10 ++ mayan/apps/storage/settings.py | 17 +++ mayan/apps/storage/utils.py | 123 ++++++++++++++++++ 33 files changed, 202 insertions(+), 178 deletions(-) create mode 100644 mayan/apps/storage/classes.py create mode 100644 mayan/apps/storage/settings.py create mode 100644 mayan/apps/storage/utils.py diff --git a/HISTORY.rst b/HISTORY.rst index e26036dd7e..4106ec19a0 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -227,6 +227,8 @@ - Add a test mixin to generate random model primary keys. - Add support for checkout and check in multiple documents at the same time. +- Move file and storage code to the storage app. The setting + COMMON_TEMPORARY_DIRECTORY is now STORAGE_TEMPORARY_DIRECTORY. 3.1.9 (2018-11-01) ================== diff --git a/mayan/apps/common/classes.py b/mayan/apps/common/classes.py index f8958a304e..161def4408 100644 --- a/mayan/apps/common/classes.py +++ b/mayan/apps/common/classes.py @@ -72,16 +72,6 @@ class ErrorLogNamespace(object): return ErrorLogEntry.objects.filter(namespace=self.name) -class FakeStorageSubclass(object): - """ - Placeholder class to allow serializing the real storage subclass to - support migrations. - """ - - def __eq__(self, other): - return True - - class MissingItem(object): _registry = [] diff --git a/mayan/apps/common/management/commands/convertdb.py b/mayan/apps/common/management/commands/convertdb.py index bd822aba4e..3d4fe0c063 100644 --- a/mayan/apps/common/management/commands/convertdb.py +++ b/mayan/apps/common/management/commands/convertdb.py @@ -11,8 +11,8 @@ from django.core.management.base import CommandError from django.utils.encoding import force_text from django.utils.translation import ugettext_lazy as _ -from mayan.apps.common.utils import fs_cleanup from mayan.apps.documents.models import DocumentType +from mayan.apps.storage.utils import fs_cleanup CONVERTDB_FOLDER = 'convertdb' CONVERTDB_OUTPUT_FILENAME = 'migrate.json' diff --git a/mayan/apps/common/migrations/0011_auto_20181229_0738.py b/mayan/apps/common/migrations/0011_auto_20181229_0738.py index b1796669a1..b9668e2b92 100644 --- a/mayan/apps/common/migrations/0011_auto_20181229_0738.py +++ b/mayan/apps/common/migrations/0011_auto_20181229_0738.py @@ -1,14 +1,13 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.11.16 on 2018-12-29 07:38 from __future__ import unicode_literals from django.db import migrations, models + import mayan.apps.common.classes import mayan.apps.common.models +import mayan.apps.storage.classes class Migration(migrations.Migration): - dependencies = [ ('common', '0010_auto_20180403_0702_squashed_0011_auto_20180429_0758'), ] @@ -17,6 +16,10 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='shareduploadedfile', name='file', - field=models.FileField(storage=mayan.apps.common.classes.FakeStorageSubclass(), upload_to=mayan.apps.common.models.upload_to, verbose_name='File'), + field=models.FileField( + storage=mayan.apps.storage.classes.FakeStorageSubclass(), + upload_to=mayan.apps.common.models.upload_to, + verbose_name='File' + ), ), ] diff --git a/mayan/apps/common/mixins.py b/mayan/apps/common/mixins.py index 5eaa001d2b..e2592217e6 100644 --- a/mayan/apps/common/mixins.py +++ b/mayan/apps/common/mixins.py @@ -1,6 +1,5 @@ from __future__ import unicode_literals -from django.conf import settings from django.contrib import messages from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ImproperlyConfigured, PermissionDenied diff --git a/mayan/apps/common/settings.py b/mayan/apps/common/settings.py index 2187fd1db5..1cdae268b7 100644 --- a/mayan/apps/common/settings.py +++ b/mayan/apps/common/settings.py @@ -1,7 +1,6 @@ from __future__ import unicode_literals import os -import tempfile from django.conf import settings from django.utils.translation import ugettext_lazy as _ @@ -76,13 +75,6 @@ setting_shared_storage_arguments = namespace.add_setting( global_name='COMMON_SHARED_STORAGE_ARGUMENTS', default={'location': os.path.join(settings.MEDIA_ROOT, 'shared_files')} ) -setting_temporary_directory = namespace.add_setting( - global_name='COMMON_TEMPORARY_DIRECTORY', default=tempfile.gettempdir(), - help_text=_( - 'Temporary directory used site wide to store thumbnails, previews ' - 'and temporary files.' - ) -) namespace = Namespace(label=_('Django'), name='django') diff --git a/mayan/apps/common/storages.py b/mayan/apps/common/storages.py index 3140cfad49..9af5517b65 100644 --- a/mayan/apps/common/storages.py +++ b/mayan/apps/common/storages.py @@ -1,7 +1,8 @@ from __future__ import unicode_literals +from mayan.apps.storage.utils import get_storage_subclass + from .settings import setting_shared_storage, setting_shared_storage_arguments -from .utils import get_storage_subclass storage_sharedupload = get_storage_subclass( dotted_path=setting_shared_storage.value diff --git a/mayan/apps/common/tests/mixins.py b/mayan/apps/common/tests/mixins.py index 565a25f569..1e70354454 100644 --- a/mayan/apps/common/tests/mixins.py +++ b/mayan/apps/common/tests/mixins.py @@ -13,7 +13,7 @@ from django.template import Context, Template from django.test.utils import ContextList from django.urls import clear_url_caches, reverse -from ..settings import setting_temporary_directory +from mayan.apps.storage.settings import setting_temporary_directory from .literals import TEST_VIEW_NAME, TEST_VIEW_URL from .utils import mute_stdout diff --git a/mayan/apps/common/tests/test_views.py b/mayan/apps/common/tests/test_views.py index 865c7d782e..b6a7bf35ac 100644 --- a/mayan/apps/common/tests/test_views.py +++ b/mayan/apps/common/tests/test_views.py @@ -14,37 +14,33 @@ from .literals import TEST_ERROR_LOG_ENTRY_RESULT class CommonViewTestCase(GenericViewTestCase): def test_about_view(self): - self.login_user() - response = self.get('common:about_view') self.assertContains(response, text='About', status_code=200) def _create_error_log_entry(self): ModelPermission.register( - model=get_user_model(), permission=permission_error_log_view + model=get_user_model(), permissions=(permission_error_log_view,) ) ErrorLogEntry.objects.register(model=get_user_model()) - self.error_log_entry = self.user.error_logs.create( + self.error_log_entry = self._test_case_user.error_logs.create( result=TEST_ERROR_LOG_ENTRY_RESULT ) def _request_object_error_log_list(self): - content_type = ContentType.objects.get_for_model(model=self.user) + content_type = ContentType.objects.get_for_model(model=self._test_case_user) return self.get( 'common:object_error_list', kwargs={ 'app_label': content_type.app_label, 'model': content_type.model, - 'object_id': self.user.pk + 'object_id': self._test_case_user.pk }, follow=True ) def test_object_error_list_view_no_permissions(self): self._create_error_log_entry() - self.login_user() - response = self._request_object_error_log_list() self.assertNotContains( @@ -55,9 +51,8 @@ class CommonViewTestCase(GenericViewTestCase): def test_object_error_list_view_with_access(self): self._create_error_log_entry() - self.login_user() self.grant_access( - obj=self.user, permission=permission_error_log_view + obj=self._test_case_user, permission=permission_error_log_view ) response = self._request_object_error_log_list() diff --git a/mayan/apps/common/utils.py b/mayan/apps/common/utils.py index fb3df43336..ba51d4d455 100644 --- a/mayan/apps/common/utils.py +++ b/mayan/apps/common/utils.py @@ -1,9 +1,6 @@ from __future__ import unicode_literals import logging -import os -import shutil -import tempfile from django.conf import settings from django.core.exceptions import FieldDoesNotExist @@ -13,7 +10,6 @@ from django.urls.base import get_script_prefix from django.utils.datastructures import MultiValueDict from django.utils.http import urlencode as django_urlencode from django.utils.http import urlquote as django_urlquote -from django.utils.module_loading import import_string from django.utils.six.moves import reduce as reduce_function from django.utils.six.moves import xmlrpc_client @@ -21,7 +17,6 @@ import mayan from .exceptions import NotLatestVersion, UnknownLatestVersion from .literals import DJANGO_SQLITE_BACKEND, MAYAN_PYPI_NAME, PYPI_URL -from .settings import setting_temporary_directory logger = logging.getLogger(__name__) @@ -40,27 +35,6 @@ def check_version(): raise NotLatestVersion(upstream_version=versions[0]) -# http://stackoverflow.com/questions/123198/how-do-i-copy-a-file-in-python -def copyfile(source, destination, buffer_size=1024 * 1024): - """ - Copy a file from source to dest. source and dest - can either be strings or any object with a read or - write method, like StringIO for example. - """ - source_descriptor = get_descriptor(source) - destination_descriptor = get_descriptor(destination, read=False) - - while True: - copy_buffer = source_descriptor.read(buffer_size) - if copy_buffer: - destination_descriptor.write(copy_buffer) - else: - break - - source_descriptor.close() - destination_descriptor.close() - - def encapsulate(function): # Workaround Django ticket 15791 # Changeset 16045 @@ -69,25 +43,6 @@ def encapsulate(function): return lambda: function -def fs_cleanup(filename, file_descriptor=None, suppress_exceptions=True): - """ - Tries to remove the given filename. Ignores non-existent files - """ - if file_descriptor: - os.close(file_descriptor) - - try: - os.remove(filename) - except OSError: - try: - shutil.rmtree(filename) - except OSError: - if suppress_exceptions: - pass - else: - raise - - def get_related_field(model, related_field_name): try: local_field_name, remaining_field_path = related_field_name.split( @@ -108,41 +63,6 @@ def get_related_field(model, related_field_name): return related_field -def get_descriptor(file_input, read=True): - try: - # Is it a file like object? - file_input.seek(0) - except AttributeError: - # If not, try open it. - if read: - return open(file_input, mode='rb') - else: - return open(file_input, mode='wb') - else: - return file_input - - -def get_storage_subclass(dotted_path): - """ - Import a storage class and return a subclass that will always return eq - True to avoid creating a new migration when for runtime storage class - changes. - """ - imported_storage_class = import_string(dotted_path=dotted_path) - - class StorageSubclass(imported_storage_class): - def __init__(self, *args, **kwargs): - return super(StorageSubclass, self).__init__(*args, **kwargs) - - def __eq__(self, other): - return True - - def deconstruct(self): - return ('mayan.apps.common.classes.FakeStorageSubclass', (), {}) - - return StorageSubclass - - def introspect_attribute(attribute_name, obj): try: # Try as a related field @@ -168,21 +88,6 @@ def introspect_attribute(attribute_name, obj): return attribute_name, obj -def TemporaryFile(*args, **kwargs): - kwargs.update({'dir': setting_temporary_directory.value}) - return tempfile.TemporaryFile(*args, **kwargs) - - -def mkdtemp(*args, **kwargs): - kwargs.update({'dir': setting_temporary_directory.value}) - return tempfile.mkdtemp(*args, **kwargs) - - -def mkstemp(*args, **kwargs): - kwargs.update({'dir': setting_temporary_directory.value}) - return tempfile.mkstemp(*args, **kwargs) - - def resolve(path, urlconf=None): path = '/{}'.format(path.replace(get_script_prefix(), '', 1)) return django_resolve(path=path, urlconf=urlconf) @@ -275,24 +180,3 @@ def urlquote(link=None, get=None): return '%s%s' % (link, django_urlencode(get, doseq=True)) else: return django_urlquote(link) - - -def validate_path(path): - if not os.path.exists(path): - # If doesn't exist try to create it - try: - os.mkdir(path) - except Exception as exception: - logger.debug('unhandled exception: %s', exception) - return False - - # Check if it is writable - try: - fd, test_filepath = tempfile.mkstemp(dir=path) - os.close(fd) - os.unlink(test_filepath) - except Exception as exception: - logger.debug('unhandled exception: %s', exception) - return False - - return True diff --git a/mayan/apps/converter/backends/python.py b/mayan/apps/converter/backends/python.py index fd6243a48a..5027c54123 100644 --- a/mayan/apps/converter/backends/python.py +++ b/mayan/apps/converter/backends/python.py @@ -11,7 +11,7 @@ import sh from django.utils.encoding import force_text from django.utils.translation import ugettext_lazy as _ -from mayan.apps.common.utils import fs_cleanup, mkstemp +from mayan.apps.storage.utils import fs_cleanup, mkstemp from ..classes import ConverterBase from ..exceptions import PageCountError diff --git a/mayan/apps/converter/classes.py b/mayan/apps/converter/classes.py index d6b7b7a35f..2a19212296 100644 --- a/mayan/apps/converter/classes.py +++ b/mayan/apps/converter/classes.py @@ -9,9 +9,9 @@ import sh from django.utils.translation import ugettext_lazy as _ -from mayan.apps.common.settings import setting_temporary_directory -from mayan.apps.common.utils import fs_cleanup, mkdtemp, mkstemp from mayan.apps.mimetype.api import get_mimetype +from mayan.apps.storage.settings import setting_temporary_directory +from mayan.apps.storage.utils import fs_cleanup, mkdtemp, mkstemp from .exceptions import InvalidOfficeFormat, OfficeConversionError from .literals import ( diff --git a/mayan/apps/dependencies/javascript.py b/mayan/apps/dependencies/javascript.py index 3a879c9ae4..d104e6f361 100644 --- a/mayan/apps/dependencies/javascript.py +++ b/mayan/apps/dependencies/javascript.py @@ -15,7 +15,7 @@ from django.apps import apps from django.utils.encoding import force_bytes, force_text from django.utils.functional import cached_property -from mayan.apps.common.utils import mkdtemp +from mayan.apps.storage.utils import mkdtemp from .exceptions import DependenciesException diff --git a/mayan/apps/django_gpg/classes.py b/mayan/apps/django_gpg/classes.py index 7243b63744..840952eb95 100644 --- a/mayan/apps/django_gpg/classes.py +++ b/mayan/apps/django_gpg/classes.py @@ -8,7 +8,7 @@ import gnupg from django.utils.translation import ugettext_lazy as _ -from mayan.apps.common.utils import mkdtemp +from mayan.apps.storage.utils import mkdtemp class GPGBackend(object): diff --git a/mayan/apps/django_gpg/managers.py b/mayan/apps/django_gpg/managers.py index e6b498f32c..d565e8ae2a 100644 --- a/mayan/apps/django_gpg/managers.py +++ b/mayan/apps/django_gpg/managers.py @@ -6,7 +6,7 @@ import os from django.db import models -from mayan.apps.common.utils import mkstemp +from mayan.apps.storage.utils import mkstemp from .classes import KeyStub, SignatureVerification from .exceptions import ( diff --git a/mayan/apps/django_gpg/tests/test_models.py b/mayan/apps/django_gpg/tests/test_models.py index 9273cfd065..51c94e7a85 100644 --- a/mayan/apps/django_gpg/tests/test_models.py +++ b/mayan/apps/django_gpg/tests/test_models.py @@ -8,7 +8,7 @@ import mock from django.utils.encoding import force_bytes from mayan.apps.common.tests import BaseTestCase -from mayan.apps.common.utils import TemporaryFile +from mayan.apps.storage.utils import TemporaryFile from ..exceptions import ( DecryptionError, KeyDoesNotExist, NeedPassphrase, PassphraseError, diff --git a/mayan/apps/document_parsing/parsers.py b/mayan/apps/document_parsing/parsers.py index fd70926255..ea6ed8dc60 100644 --- a/mayan/apps/document_parsing/parsers.py +++ b/mayan/apps/document_parsing/parsers.py @@ -7,7 +7,7 @@ import subprocess from django.apps import apps from django.utils.translation import ugettext_lazy as _ -from mayan.apps.common.utils import copyfile, fs_cleanup, mkstemp +from mayan.apps.storage.utils import copyfile, fs_cleanup, mkstemp from .exceptions import ParserError from .settings import setting_pdftotext_path diff --git a/mayan/apps/document_signatures/managers.py b/mayan/apps/document_signatures/managers.py index 8f0eb8f161..3c58e69abe 100644 --- a/mayan/apps/document_signatures/managers.py +++ b/mayan/apps/document_signatures/managers.py @@ -5,10 +5,10 @@ import os from django.db import models -from mayan.apps.common.utils import mkstemp from mayan.apps.django_gpg.exceptions import DecryptionError from mayan.apps.django_gpg.models import Key from mayan.apps.documents.models import DocumentVersion +from mayan.apps.storage.utils import mkstemp logger = logging.getLogger(__name__) diff --git a/mayan/apps/document_signatures/migrations/0009_auto_20181229_0737.py b/mayan/apps/document_signatures/migrations/0009_auto_20181229_0737.py index 9f3d388e6b..e354d69d8a 100644 --- a/mayan/apps/document_signatures/migrations/0009_auto_20181229_0737.py +++ b/mayan/apps/document_signatures/migrations/0009_auto_20181229_0737.py @@ -1,14 +1,13 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.11.16 on 2018-12-29 07:37 from __future__ import unicode_literals from django.db import migrations, models + import mayan.apps.common.classes import mayan.apps.document_signatures.models +import mayan.apps.storage.classes class Migration(migrations.Migration): - dependencies = [ ('document_signatures', '0008_auto_20180429_0759'), ] @@ -17,6 +16,11 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='detachedsignature', name='signature_file', - field=models.FileField(blank=True, null=True, storage=mayan.apps.common.classes.FakeStorageSubclass(), upload_to=mayan.apps.document_signatures.models.upload_to, verbose_name='Signature file'), + field=models.FileField( + blank=True, null=True, + storage=mayan.apps.storage.classes.FakeStorageSubclass(), + upload_to=mayan.apps.document_signatures.models.upload_to, + verbose_name='Signature file' + ), ), ] diff --git a/mayan/apps/document_signatures/storages.py b/mayan/apps/document_signatures/storages.py index 1b88c818ca..423f11c1e5 100644 --- a/mayan/apps/document_signatures/storages.py +++ b/mayan/apps/document_signatures/storages.py @@ -1,6 +1,6 @@ from __future__ import unicode_literals -from mayan.apps.common.utils import get_storage_subclass +from mayan.apps.storage.utils import get_storage_subclass from .settings import ( setting_storage_backend, setting_storage_backend_arguments diff --git a/mayan/apps/document_signatures/views.py b/mayan/apps/document_signatures/views.py index 571343db07..b723cbc304 100644 --- a/mayan/apps/document_signatures/views.py +++ b/mayan/apps/document_signatures/views.py @@ -15,9 +15,9 @@ from mayan.apps.common.generics import ( SingleObjectDetailView, SingleObjectDownloadView, SingleObjectListView ) from mayan.apps.common.mixins import ExternalObjectMixin -from mayan.apps.common.utils import TemporaryFile from mayan.apps.django_gpg.exceptions import NeedPassphrase, PassphraseError from mayan.apps.documents.models import DocumentVersion +from mayan.apps.storage.utils import TemporaryFile from .forms import ( DocumentVersionSignatureCreateForm, DocumentVersionSignatureDetailForm diff --git a/mayan/apps/documents/migrations/0051_auto_20181229_0745.py b/mayan/apps/documents/migrations/0051_auto_20181229_0745.py index d95527ab42..e423157903 100644 --- a/mayan/apps/documents/migrations/0051_auto_20181229_0745.py +++ b/mayan/apps/documents/migrations/0051_auto_20181229_0745.py @@ -1,10 +1,10 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.11.16 on 2018-12-29 07:45 from __future__ import unicode_literals from django.db import migrations, models + import mayan.apps.common.classes import mayan.apps.documents.utils +import mayan.apps.storage.classes class Migration(migrations.Migration): @@ -17,6 +17,10 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='documentversion', name='file', - field=models.FileField(storage=mayan.apps.common.classes.FakeStorageSubclass(), upload_to=mayan.apps.documents.utils.document_uuid_function, verbose_name='File'), + field=models.FileField( + storage=mayan.apps.storage.classes.FakeStorageSubclass(), + upload_to=mayan.apps.documents.utils.document_uuid_function, + verbose_name='File' + ), ), ] diff --git a/mayan/apps/documents/storages.py b/mayan/apps/documents/storages.py index a71ba1c097..4bfb1ec4f1 100644 --- a/mayan/apps/documents/storages.py +++ b/mayan/apps/documents/storages.py @@ -2,7 +2,7 @@ from __future__ import unicode_literals from django.utils.module_loading import import_string -from mayan.apps.common.utils import get_storage_subclass +from mayan.apps.storage.utils import get_storage_subclass from .settings import ( setting_documentimagecache_storage, diff --git a/mayan/apps/file_metadata/drivers/exiftool.py b/mayan/apps/file_metadata/drivers/exiftool.py index 8a75b7c0fe..9b60e0c1cc 100644 --- a/mayan/apps/file_metadata/drivers/exiftool.py +++ b/mayan/apps/file_metadata/drivers/exiftool.py @@ -7,7 +7,7 @@ import sh from django.utils.translation import ugettext_lazy as _ -from mayan.apps.common.utils import fs_cleanup, mkstemp +from mayan.apps.storage.utils import fs_cleanup, mkstemp from ..classes import FileMetadataDriver from ..settings import setting_drivers_arguments diff --git a/mayan/apps/lock_manager/backends/file_lock.py b/mayan/apps/lock_manager/backends/file_lock.py index 459d91e044..43b6a743d7 100644 --- a/mayan/apps/lock_manager/backends/file_lock.py +++ b/mayan/apps/lock_manager/backends/file_lock.py @@ -12,7 +12,7 @@ from django.conf import settings from django.core.files import locks from django.utils.encoding import force_bytes, force_text -from mayan.apps.common.settings import setting_temporary_directory +from mayan.apps.storage.settings import setting_temporary_directory from ..exceptions import LockError from ..settings import setting_default_lock_timeout diff --git a/mayan/apps/smart_settings/tests/test_classes.py b/mayan/apps/smart_settings/tests/test_classes.py index 55c595102a..6be2ae1df0 100644 --- a/mayan/apps/smart_settings/tests/test_classes.py +++ b/mayan/apps/smart_settings/tests/test_classes.py @@ -8,7 +8,7 @@ from django.conf import settings from mayan.apps.common.settings import setting_paginate_by from mayan.apps.common.tests import BaseTestCase -from mayan.apps.common.utils import fs_cleanup, mkstemp +from mayan.apps.storage.utils import fs_cleanup, mkstemp from .literals import TEST_SETTING_NAME, TEST_SETTING_VALUE diff --git a/mayan/apps/sources/models/scanner_sources.py b/mayan/apps/sources/models/scanner_sources.py index 099d6ca913..b4ca0bbc63 100644 --- a/mayan/apps/sources/models/scanner_sources.py +++ b/mayan/apps/sources/models/scanner_sources.py @@ -7,7 +7,7 @@ from django.db import models from django.utils.timezone import now from django.utils.translation import ugettext_lazy as _ -from mayan.apps.common.utils import TemporaryFile +from mayan.apps.storage.utils import TemporaryFile from ..classes import PseudoFile, SourceUploadedFile from ..exceptions import SourceException diff --git a/mayan/apps/sources/tests/test_classes.py b/mayan/apps/sources/tests/test_classes.py index 7466b33fad..319c3eeb37 100644 --- a/mayan/apps/sources/tests/test_classes.py +++ b/mayan/apps/sources/tests/test_classes.py @@ -4,8 +4,8 @@ import os import shutil from mayan.apps.common.tests import BaseTestCase -from mayan.apps.common.utils import mkdtemp from mayan.apps.documents.tests import TEST_NON_ASCII_DOCUMENT_PATH +from mayan.apps.storage.utils import mkdtemp from ..classes import StagingFile diff --git a/mayan/apps/sources/tests/test_models.py b/mayan/apps/sources/tests/test_models.py index 97794ecb19..9947edc461 100644 --- a/mayan/apps/sources/tests/test_models.py +++ b/mayan/apps/sources/tests/test_models.py @@ -9,7 +9,6 @@ from pathlib2 import Path from django.utils.encoding import force_text from mayan.apps.common.tests import BaseTestCase -from mayan.apps.common.utils import mkdtemp from mayan.apps.documents.models import Document, DocumentType from mayan.apps.documents.tests import ( TEST_COMPRESSED_DOCUMENT_PATH, TEST_DOCUMENT_TYPE_LABEL, @@ -17,6 +16,7 @@ from mayan.apps.documents.tests import ( TEST_NON_ASCII_DOCUMENT_PATH, DocumentTestMixin ) from mayan.apps.metadata.models import MetadataType +from mayan.apps.storage.utils import mkdtemp from ..literals import SOURCE_UNCOMPRESS_CHOICE_Y from ..models import POP3Email, WatchFolderSource, WebFormSource diff --git a/mayan/apps/sources/tests/test_views.py b/mayan/apps/sources/tests/test_views.py index 7d02866212..5778283ca6 100644 --- a/mayan/apps/sources/tests/test_views.py +++ b/mayan/apps/sources/tests/test_views.py @@ -6,13 +6,13 @@ import shutil from mayan.apps.checkouts.models import NewVersionBlock from mayan.apps.common.tests import GenericViewTestCase -from mayan.apps.common.utils import fs_cleanup, mkdtemp from mayan.apps.documents.models import Document from mayan.apps.documents.permissions import permission_document_create from mayan.apps.documents.tests import ( TEST_DOCUMENT_DESCRIPTION, TEST_SMALL_DOCUMENT_CHECKSUM, TEST_SMALL_DOCUMENT_PATH, GenericDocumentViewTestCase ) +from mayan.apps.storage.utils import fs_cleanup, mkdtemp from ..links import link_upload_version from ..literals import SOURCE_CHOICE_WEB_FORM diff --git a/mayan/apps/storage/classes.py b/mayan/apps/storage/classes.py new file mode 100644 index 0000000000..0c4bdd2cd5 --- /dev/null +++ b/mayan/apps/storage/classes.py @@ -0,0 +1,10 @@ +from __future__ import unicode_literals + + +class FakeStorageSubclass(object): + """ + Placeholder class to allow serializing the real storage subclass to + support migrations. + """ + def __eq__(self, other): + return True diff --git a/mayan/apps/storage/settings.py b/mayan/apps/storage/settings.py new file mode 100644 index 0000000000..e17acbd76c --- /dev/null +++ b/mayan/apps/storage/settings.py @@ -0,0 +1,17 @@ +from __future__ import unicode_literals + +import tempfile + +from django.utils.translation import ugettext_lazy as _ + +from mayan.apps.smart_settings import Namespace + +namespace = Namespace(label=_('Storage'), name='storage') + +setting_temporary_directory = namespace.add_setting( + global_name='STORAGE_TEMPORARY_DIRECTORY', default=tempfile.gettempdir(), + help_text=_( + 'Temporary directory used site wide to store thumbnails, previews ' + 'and temporary files.' + ) +) diff --git a/mayan/apps/storage/utils.py b/mayan/apps/storage/utils.py new file mode 100644 index 0000000000..09ba4f6c22 --- /dev/null +++ b/mayan/apps/storage/utils.py @@ -0,0 +1,123 @@ +from __future__ import unicode_literals + +import logging +import os +import shutil +import tempfile + +from django.utils.module_loading import import_string + +from .settings import setting_temporary_directory + +logger = logging.getLogger(__name__) + + +def TemporaryFile(*args, **kwargs): + kwargs.update({'dir': setting_temporary_directory.value}) + return tempfile.TemporaryFile(*args, **kwargs) + + +# http://stackoverflow.com/questions/123198/how-do-i-copy-a-file-in-python +def copyfile(source, destination, buffer_size=1024 * 1024): + """ + Copy a file from source to dest. source and dest + can either be strings or any object with a read or + write method, like StringIO for example. + """ + source_descriptor = get_descriptor(source) + destination_descriptor = get_descriptor(destination, read=False) + + while True: + copy_buffer = source_descriptor.read(buffer_size) + if copy_buffer: + destination_descriptor.write(copy_buffer) + else: + break + + source_descriptor.close() + destination_descriptor.close() + + +def fs_cleanup(filename, file_descriptor=None, suppress_exceptions=True): + """ + Tries to remove the given filename. Ignores non-existent files + """ + if file_descriptor: + os.close(file_descriptor) + + try: + os.remove(filename) + except OSError: + try: + shutil.rmtree(filename) + except OSError: + if suppress_exceptions: + pass + else: + raise + + +def get_descriptor(file_input, read=True): + try: + # Is it a file like object? + file_input.seek(0) + except AttributeError: + # If not, try open it. + if read: + return open(file_input, mode='rb') + else: + return open(file_input, mode='wb') + else: + return file_input + + +def get_storage_subclass(dotted_path): + """ + Import a storage class and return a subclass that will always return eq + True to avoid creating a new migration when for runtime storage class + changes. + """ + imported_storage_class = import_string(dotted_path=dotted_path) + + class StorageSubclass(imported_storage_class): + def __init__(self, *args, **kwargs): + return super(StorageSubclass, self).__init__(*args, **kwargs) + + def __eq__(self, other): + return True + + def deconstruct(self): + return ('mayan.apps.storage.classes.FakeStorageSubclass', (), {}) + + return StorageSubclass + + +def mkdtemp(*args, **kwargs): + kwargs.update({'dir': setting_temporary_directory.value}) + return tempfile.mkdtemp(*args, **kwargs) + + +def mkstemp(*args, **kwargs): + kwargs.update({'dir': setting_temporary_directory.value}) + return tempfile.mkstemp(*args, **kwargs) + + +def validate_path(path): + if not os.path.exists(path): + # If doesn't exist try to create it + try: + os.mkdir(path) + except Exception as exception: + logger.debug('unhandled exception: %s', exception) + return False + + # Check if it is writable + try: + fd, test_filepath = tempfile.mkstemp(dir=path) + os.close(fd) + os.unlink(test_filepath) + except Exception as exception: + logger.debug('unhandled exception: %s', exception) + return False + + return True From f92d99bd9a56409b3d2a49cfd5e2e8763cc3e1a4 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Fri, 1 Feb 2019 03:55:43 -0400 Subject: [PATCH 096/209] Refactor the converter app Don't cache the entire converter class to lower memory usage. Instead a get_converter_class() function is now provided to load the converter backend class. Add model permission inheritance to transformations to removel custom permission checking code in the views. User keyword arguments. Update URL parameters to the '_id' form. Add missing edit and delete icons. Improve the create icon using composition. Update add to comply with MERCs 5 and 6. Signed-off-by: Roberto Rosario --- HISTORY.rst | 4 + mayan/apps/converter/__init__.py | 1 - mayan/apps/converter/apps.py | 20 +- mayan/apps/converter/icons.py | 7 +- mayan/apps/converter/links.py | 17 +- mayan/apps/converter/models.py | 4 + mayan/apps/converter/tests/test_views.py | 56 ++--- mayan/apps/converter/transformations.py | 1 - mayan/apps/converter/urls.py | 14 +- mayan/apps/converter/{runtime.py => utils.py} | 5 +- mayan/apps/converter/views.py | 226 ++++++------------ 11 files changed, 152 insertions(+), 203 deletions(-) rename mayan/apps/converter/{runtime.py => utils.py} (66%) diff --git a/HISTORY.rst b/HISTORY.rst index 4106ec19a0..22eb460148 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -229,6 +229,10 @@ the same time. - Move file and storage code to the storage app. The setting COMMON_TEMPORARY_DIRECTORY is now STORAGE_TEMPORARY_DIRECTORY. +- To lower memory usage and reduce memory leaks, the entire + entire converter class is no longer cached and instead loaded + on demand. This allows the garbage collector to clear the memory + used. 3.1.9 (2018-11-01) ================== diff --git a/mayan/apps/converter/__init__.py b/mayan/apps/converter/__init__.py index a643e11bef..d5750d6e40 100644 --- a/mayan/apps/converter/__init__.py +++ b/mayan/apps/converter/__init__.py @@ -1,6 +1,5 @@ from __future__ import unicode_literals -from .runtime import converter_class # NOQA from .transformations import ( # NOQA BaseTransformation, TransformationResize, TransformationRotate, TransformationZoom diff --git a/mayan/apps/converter/apps.py b/mayan/apps/converter/apps.py index 0d545877a8..920a73249a 100644 --- a/mayan/apps/converter/apps.py +++ b/mayan/apps/converter/apps.py @@ -1,8 +1,8 @@ from __future__ import unicode_literals -from django.utils.encoding import force_text from django.utils.translation import ugettext_lazy as _ +from mayan.apps.acls.classes import ModelPermission from mayan.apps.common import MayanAppConfig, menu_object, menu_sidebar from mayan.apps.navigation import SourceColumn @@ -25,12 +25,20 @@ class ConverterApp(MayanAppConfig): Transformation = self.get_model(model_name='Transformation') - SourceColumn(attribute='order', source=Transformation) - SourceColumn( - source=Transformation, label=_('Transformation'), - func=lambda context: force_text(context['object']) + ModelPermission.register_inheritance( + model=Transformation, related='content_object' + ) + + SourceColumn( + attribute='order', include_label=True, source=Transformation + ) + SourceColumn( + attribute='get_transformation_label', is_identifier=True, + source=Transformation + ) + SourceColumn( + attribute='arguments', include_label=True, source=Transformation ) - SourceColumn(attribute='arguments', source=Transformation) menu_object.bind_links( links=(link_transformation_edit, link_transformation_delete), diff --git a/mayan/apps/converter/icons.py b/mayan/apps/converter/icons.py index 38285447b4..61803fddc0 100644 --- a/mayan/apps/converter/icons.py +++ b/mayan/apps/converter/icons.py @@ -3,4 +3,9 @@ from __future__ import absolute_import, unicode_literals from mayan.apps.appearance.classes import Icon icon_transformation = Icon(driver_name='fontawesome', symbol='crop') -icon_transformation_create = Icon(driver_name='fontawesome', symbol='plus') +icon_transformation_create = Icon( + driver_name='fontawesome-dual', primary_symbol='crop', + secondary_symbol='plus' +) +icon_transformation_delete = Icon(driver_name='fontawesome', symbol='times') +icon_transformation_edit = Icon(driver_name='fontawesome', symbol='pencil-alt') diff --git a/mayan/apps/converter/links.py b/mayan/apps/converter/links.py index 99ab3cef71..2ff7805914 100644 --- a/mayan/apps/converter/links.py +++ b/mayan/apps/converter/links.py @@ -5,7 +5,10 @@ from django.utils.translation import ugettext_lazy as _ from mayan.apps.navigation import Link -from .icons import icon_transformation, icon_transformation_create +from .icons import ( + icon_transformation, icon_transformation_create, icon_transformation_delete, + icon_transformation_edit +) from .permissions import ( permission_transformation_create, permission_transformation_delete, permission_transformation_edit, permission_transformation_view @@ -37,12 +40,16 @@ link_transformation_create = Link( text=_('Create new transformation'), view='converter:transformation_create' ) link_transformation_delete = Link( - args='resolved_object.pk', permission=permission_transformation_delete, - tags='dangerous', text=_('Delete'), view='converter:transformation_delete' + icon_class=icon_transformation_delete, + kwargs={'transformation_id': 'resolved_object.pk'}, + permission=permission_transformation_delete, tags='dangerous', + text=_('Delete'), view='converter:transformation_delete' ) link_transformation_edit = Link( - args='resolved_object.pk', permission=permission_transformation_edit, - text=_('Edit'), view='converter:transformation_edit' + icon_class=icon_transformation_edit, + kwargs={'transformation_id': 'resolved_object.pk'}, + permission=permission_transformation_edit, text=_('Edit'), + view='converter:transformation_edit' ) link_transformation_list = Link( icon_class=icon_transformation, diff --git a/mayan/apps/converter/models.py b/mayan/apps/converter/models.py index e241049bcd..8b98df45ee 100644 --- a/mayan/apps/converter/models.py +++ b/mayan/apps/converter/models.py @@ -60,7 +60,11 @@ class Transformation(models.Model): verbose_name_plural = _('Transformations') def __str__(self): + return self.get_transformation_label() + + def get_transformation_label(self): return self.get_name_display() + get_transformation_label.short_description = _('Name') def save(self, *args, **kwargs): if not self.order: diff --git a/mayan/apps/converter/tests/test_views.py b/mayan/apps/converter/tests/test_views.py index 06de5fc9da..c298acd068 100644 --- a/mayan/apps/converter/tests/test_views.py +++ b/mayan/apps/converter/tests/test_views.py @@ -17,31 +17,6 @@ from .literals import ( class TransformationViewsTestCase(GenericDocumentViewTestCase): - def setUp(self): - super(TransformationViewsTestCase, self).setUp() - self.login_user() - - def _transformation_list_view(self): - return self.get( - viewname='converter:transformation_list', kwargs={ - 'app_label': 'documents', 'model': 'document', - 'object_id': self.document.pk - } - ) - - def test_transformation_list_view_no_permissions(self): - response = self._transformation_list_view() - - self.assertEqual(response.status_code, 403) - - def test_transformation_list_view_with_permissions(self): - self.grant_permission(permission=permission_transformation_view) - response = self._transformation_list_view() - - self.assertContains( - response, text=self.document.label, status_code=200 - ) - def _transformation_create_view(self): return self.post( viewname='converter:transformation_create', kwargs={ @@ -56,7 +31,7 @@ class TransformationViewsTestCase(GenericDocumentViewTestCase): def test_transformation_create_view_no_permissions(self): response = self._transformation_create_view() - self.assertEqual(response.status_code, 403) + self.assertEqual(response.status_code, 404) self.assertEqual(Transformation.objects.count(), 0) def test_transformation_create_view_with_access(self): @@ -80,7 +55,7 @@ class TransformationViewsTestCase(GenericDocumentViewTestCase): def _transformation_delete_view(self): return self.post( viewname='converter:transformation_delete', kwargs={ - 'transformation_pk': self.transformation.pk + 'transformation_id': self.transformation.pk } ) @@ -88,7 +63,7 @@ class TransformationViewsTestCase(GenericDocumentViewTestCase): self._create_transformation() response = self._transformation_delete_view() - self.assertEqual(response.status_code, 403) + self.assertEqual(response.status_code, 404) self.assertEqual(Transformation.objects.count(), 1) def test_transformation_delete_view_with_access(self): @@ -104,7 +79,7 @@ class TransformationViewsTestCase(GenericDocumentViewTestCase): def _transformation_edit_view(self): return self.post( viewname='converter:transformation_edit', kwargs={ - 'transformation_pk': self.transformation.pk + 'transformation_id': self.transformation.pk }, data={ 'arguments': TEST_TRANSFORMATION_ARGUMENT_EDITED, 'name': self.transformation.name @@ -114,7 +89,7 @@ class TransformationViewsTestCase(GenericDocumentViewTestCase): def test_transformation_edit_view_no_permission(self): self._create_transformation() response = self._transformation_edit_view() - self.assertEqual(response.status_code, 403) + self.assertEqual(response.status_code, 404) self.transformation.refresh_from_db() self.assertNotEqual( @@ -133,3 +108,24 @@ class TransformationViewsTestCase(GenericDocumentViewTestCase): self.assertEqual( self.transformation.arguments, TEST_TRANSFORMATION_ARGUMENT_EDITED ) + + def _transformation_list_view(self): + return self.get( + viewname='converter:transformation_list', kwargs={ + 'app_label': 'documents', 'model': 'document', + 'object_id': self.document.pk + } + ) + + def test_transformation_list_view_no_permissions(self): + response = self._transformation_list_view() + + self.assertEqual(response.status_code, 404) + + def test_transformation_list_view_with_permissions(self): + self.grant_permission(permission=permission_transformation_view) + response = self._transformation_list_view() + + self.assertContains( + response, text=self.document.label, status_code=200 + ) diff --git a/mayan/apps/converter/transformations.py b/mayan/apps/converter/transformations.py index 1caecc9117..f6038a864c 100644 --- a/mayan/apps/converter/transformations.py +++ b/mayan/apps/converter/transformations.py @@ -58,7 +58,6 @@ class BaseTransformation(object): def register(cls, transformation): cls._registry[transformation.name] = transformation - def __init__(self, **kwargs): self.kwargs = {} for argument_name in self.arguments: diff --git a/mayan/apps/converter/urls.py b/mayan/apps/converter/urls.py index 5334d62b33..d744c20c36 100644 --- a/mayan/apps/converter/urls.py +++ b/mayan/apps/converter/urls.py @@ -9,19 +9,19 @@ from .views import ( urlpatterns = [ url( - regex=r'^create_for/(?P[-\w]+)/(?P[-\w]+)/(?P\d+)/$', - name='transformation_create', view=TransformationCreateView.as_view() - ), - url( - regex=r'^list_for/(?P[-\w]+)/(?P[-\w]+)/(?P\d+)/$', + regex=r'^objects/(?P[-\w]+)/(?P[-\w]+)/(?P\d+)/transformations/$', name='transformation_list', view=TransformationListView.as_view() ), url( - regex=r'^delete/(?P\d+)/$', + regex=r'^objects/(?P[-\w]+)/(?P[-\w]+)/(?P\d+)/transformations/create/$', + name='transformation_create', view=TransformationCreateView.as_view() + ), + url( + regex=r'^transformations/delete/(?P\d+)/$', name='transformation_delete', view=TransformationDeleteView.as_view() ), url( - regex=r'^edit/(?P\d+)/$', + regex=r'^transformations/edit/(?P\d+)/$', name='transformation_edit', view=TransformationEditView.as_view() ), ] diff --git a/mayan/apps/converter/runtime.py b/mayan/apps/converter/utils.py similarity index 66% rename from mayan/apps/converter/runtime.py rename to mayan/apps/converter/utils.py index acfc7d1e7f..c82f1a42a2 100644 --- a/mayan/apps/converter/runtime.py +++ b/mayan/apps/converter/utils.py @@ -7,4 +7,7 @@ from django.utils.module_loading import import_string from .settings import setting_graphics_backend logger = logging.getLogger(__name__) -backend = converter_class = import_string(setting_graphics_backend.value) + + +def get_converter_class(): + return import_string(dotted_path=setting_graphics_backend.value) diff --git a/mayan/apps/converter/views.py b/mayan/apps/converter/views.py index 3a728db467..ca8d519d29 100644 --- a/mayan/apps/converter/views.py +++ b/mayan/apps/converter/views.py @@ -2,18 +2,15 @@ from __future__ import absolute_import, unicode_literals import logging -from django.contrib.contenttypes.models import ContentType -from django.http import Http404 -from django.shortcuts import get_object_or_404 from django.template import RequestContext from django.urls import reverse from django.utils.translation import ugettext_lazy as _ -from mayan.apps.acls.models import AccessControlList from mayan.apps.common.generics import ( SingleObjectCreateView, SingleObjectDeleteView, SingleObjectEditView, SingleObjectListView ) +from mayan.apps.common.mixins import ContentTypeViewMixin, ExternalObjectMixin from .forms import TransformationForm from .icons import icon_transformation @@ -27,103 +24,26 @@ from .permissions import ( logger = logging.getLogger(__name__) -class TransformationDeleteView(SingleObjectDeleteView): - model = Transformation - pk_url_kwarg = 'transformation_pk' - - def dispatch(self, request, *args, **kwargs): - self.transformation = get_object_or_404( - klass=Transformation, pk=self.kwargs['transformation_pk'] - ) - - AccessControlList.objects.check_access( - obj=self.transformation.content_object, - permission=permission_transformation_delete, user=request.user - ) - - return super(TransformationDeleteView, self).dispatch( - request, *args, **kwargs - ) - - def get_post_action_redirect(self): - return reverse( - viewname='converter:transformation_list', kwargs={ - 'app_label': self.transformation.content_type.app_label, - 'model': self.transformation.content_type.model, - 'object_id': self.transformation.object_id - } - ) - - def get_extra_context(self): - return { - 'content_object': self.transformation.content_object, - 'navigation_object_list': ('content_object', 'transformation'), - 'previous': reverse( - viewname='converter:transformation_list', kwargs={ - 'app_label': self.transformation.content_type.app_label, - 'model': self.transformation.content_type.model, - 'object_id': self.transformation.object_id - } - ), - 'title': _( - 'Delete transformation "%(transformation)s" for: ' - '%(content_object)s?' - ) % { - 'transformation': self.transformation, - 'content_object': self.transformation.content_object - }, - 'transformation': self.transformation, - } - - -class TransformationCreateView(SingleObjectCreateView): +class TransformationCreateView(ContentTypeViewMixin, ExternalObjectMixin, SingleObjectCreateView): + external_object_permission = permission_transformation_create + external_object_pk_url_kwarg = 'object_id' form_class = TransformationForm - def dispatch(self, request, *args, **kwargs): - content_type = get_object_or_404( - klass=ContentType, app_label=self.kwargs['app_label'], - model=self.kwargs['model'] - ) - - try: - self.content_object = content_type.get_object_for_this_type( - pk=self.kwargs['object_id'] - ) - except content_type.model_class().DoesNotExist: - raise Http404 - - AccessControlList.objects.check_access( - obj=self.content_object, permission=permission_transformation_create, - user=request.user - ) - - return super(TransformationCreateView, self).dispatch( - request, *args, **kwargs - ) - - def form_valid(self, form): - instance = form.save(commit=False) - instance.content_object = self.content_object - try: - instance.full_clean() - instance.save() - except Exception as exception: - logger.debug('Invalid form, exception: %s', exception) - return super(TransformationCreateView, self).form_invalid( - form=form - ) - else: - return super(TransformationCreateView, self).form_valid(form=form) + def get_external_object_queryset(self): + return self.get_content_type().model_class().objects.all() def get_extra_context(self): return { - 'content_object': self.content_object, + 'content_object': self.external_object, 'navigation_object_list': ('content_object',), 'title': _( 'Create new transformation for: %s' - ) % self.content_object, + ) % self.external_object, } + def get_instance_extra_data(self): + return {'content_object': self.external_object} + def get_post_action_redirect(self): return reverse( viewname='converter:transformation_list', kwargs={ @@ -134,96 +54,100 @@ class TransformationCreateView(SingleObjectCreateView): ) def get_queryset(self): - return Transformation.objects.get_for_model(obj=self.content_object) + return Transformation.objects.get_for_model(obj=self.external_object) + + +class TransformationDeleteView(SingleObjectDeleteView): + model = Transformation + object_permission = permission_transformation_delete + pk_url_kwarg = 'transformation_id' + + def get_extra_context(self): + transformation = self.get_object() + + return { + 'content_object': transformation.content_object, + 'navigation_object_list': ('content_object', 'transformation'), + 'previous': reverse( + viewname='converter:transformation_list', kwargs={ + 'app_label': transformation.content_type.app_label, + 'model': transformation.content_type.model, + 'object_id': transformation.object_id + } + ), + 'title': _( + 'Delete transformation "%(transformation)s" for: ' + '%(content_object)s?' + ) % { + 'transformation': transformation, + 'content_object': transformation.content_object + }, + 'transformation': transformation, + } + + def get_post_action_redirect(self): + transformation = self.get_object() + + return reverse( + viewname='converter:transformation_list', kwargs={ + 'app_label': transformation.content_type.app_label, + 'model': transformation.content_type.model, + 'object_id': transformation.object_id + } + ) class TransformationEditView(SingleObjectEditView): form_class = TransformationForm model = Transformation - pk_url_kwarg = 'transformation_pk' - - def dispatch(self, request, *args, **kwargs): - self.transformation = get_object_or_404( - klass=Transformation, pk=self.kwargs['transformation_pk'] - ) - - AccessControlList.objects.check_access( - obj=self.transformation.content_object, - permission=permission_transformation_edit, user=request.user - ) - - return super(TransformationEditView, self).dispatch( - request=request, *args, **kwargs - ) - - def form_valid(self, form): - instance = form.save(commit=False) - try: - instance.full_clean() - instance.save() - except Exception as exception: - logger.debug('Invalid form, exception: %s', exception) - return super(TransformationEditView, self).form_invalid(form=form) - else: - return super(TransformationEditView, self).form_valid(form=form) + object_permission = permission_transformation_edit + pk_url_kwarg = 'transformation_id' def get_extra_context(self): + transformation = self.get_object() + return { - 'content_object': self.transformation.content_object, + 'content_object': transformation.content_object, 'navigation_object_list': ('content_object', 'transformation'), 'title': _( 'Edit transformation "%(transformation)s" for: %(content_object)s' ) % { - 'transformation': self.transformation, - 'content_object': self.transformation.content_object + 'transformation': transformation, + 'content_object': transformation.content_object }, - 'transformation': self.transformation, + 'transformation': transformation, } def get_post_action_redirect(self): + transformation = self.get_object() + return reverse( viewname='converter:transformation_list', kwargs={ - 'app_label': self.transformation.content_type.app_label, - 'model': self.transformation.content_type.model, - 'object_id': self.transformation.object_id + 'app_label': transformation.content_type.app_label, + 'model': transformation.content_type.model, + 'object_id': transformation.object_id } ) -class TransformationListView(SingleObjectListView): - def dispatch(self, request, *args, **kwargs): - content_type = get_object_or_404( - klass=ContentType, app_label=self.kwargs['app_label'], - model=self.kwargs['model'] - ) +class TransformationListView(ContentTypeViewMixin, ExternalObjectMixin, SingleObjectListView): + external_object_permission = permission_transformation_view + external_object_pk_url_kwarg = 'object_id' - try: - self.content_object = content_type.get_object_for_this_type( - pk=self.kwargs['object_id'] - ) - except content_type.model_class().DoesNotExist: - raise Http404 - - AccessControlList.objects.check_access( - obj=self.content_object, - permission=permission_transformation_view, - user=request.user - ) - - return super(TransformationListView, self).dispatch( - request=request, *args, **kwargs - ) + def get_external_object_queryset(self): + return self.get_content_type().model_class().objects.all() def get_extra_context(self): return { - 'content_object': self.content_object, + 'content_object': self.external_object, 'hide_link': True, 'hide_object': True, 'navigation_object_list': ('content_object',), 'no_results_icon': icon_transformation, 'no_results_main_link': link_transformation_create.resolve( context=RequestContext( - self.request, {'content_object': self.content_object} + dict_={'content_object': self.external_object}, + request=self.request ) ), 'no_results_text': _( @@ -232,8 +156,8 @@ class TransformationListView(SingleObjectListView): 'document file themselves.' ), 'no_results_title': _('No transformations'), - 'title': _('Transformations for: %s') % self.content_object, + 'title': _('Transformations for: %s') % self.external_object } def get_source_queryset(self): - return Transformation.objects.get_for_model(obj=self.content_object) + return Transformation.objects.get_for_model(obj=self.external_object) From e5cd5a40c399692cd62798629966a5fe706d00a5 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Fri, 1 Feb 2019 04:15:16 -0400 Subject: [PATCH 097/209] Improve ACL navigation Update the ACL delete icon for uniformity. Insert both the ACL and object in the view to also display the ACL permissions and delete view when viewing the ACL of an object. Signed-off-by: Roberto Rosario --- mayan/apps/acls/apps.py | 6 ++++-- mayan/apps/acls/icons.py | 2 +- mayan/apps/acls/views.py | 18 +++++++++++++----- 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/mayan/apps/acls/apps.py b/mayan/apps/acls/apps.py index ba66f8a458..5d763cd628 100644 --- a/mayan/apps/acls/apps.py +++ b/mayan/apps/acls/apps.py @@ -2,7 +2,9 @@ from __future__ import unicode_literals from django.utils.translation import ugettext_lazy as _ -from mayan.apps.common import MayanAppConfig, menu_object, menu_sidebar +from mayan.apps.common import ( + MayanAppConfig, menu_object, menu_secondary, menu_sidebar +) from mayan.apps.navigation import SourceColumn from .classes import ModelPermission @@ -35,7 +37,7 @@ class ACLsApp(MayanAppConfig): ) menu_object.bind_links( - links=(link_acl_permissions, link_acl_delete), + links=(link_acl_permissions, link_acl_delete,), sources=(AccessControlList,) ) menu_sidebar.bind_links( diff --git a/mayan/apps/acls/icons.py b/mayan/apps/acls/icons.py index 00cda1f009..1ecd7f5bdb 100644 --- a/mayan/apps/acls/icons.py +++ b/mayan/apps/acls/icons.py @@ -2,7 +2,7 @@ from __future__ import absolute_import, unicode_literals from mayan.apps.appearance.classes import Icon -icon_acl_delete = Icon(driver_name='fontawesome', symbol='minus') +icon_acl_delete = Icon(driver_name='fontawesome', symbol='times') icon_acl_list = Icon(driver_name='fontawesome', symbol='lock') icon_acl_new = Icon( driver_name='fontawesome-dual', primary_symbol='lock', diff --git a/mayan/apps/acls/views.py b/mayan/apps/acls/views.py index b92b7d5f16..f4d43ffdd1 100644 --- a/mayan/apps/acls/views.py +++ b/mayan/apps/acls/views.py @@ -81,8 +81,12 @@ class ACLDeleteView(SingleObjectDeleteView): pk_url_kwarg = 'acl_id' def get_extra_context(self): + acl = self.get_object() + return { - 'object': self.get_object().content_object, + 'acl': acl, + 'object': acl.content_object, + 'navigation_object_list': ('object', 'acl'), 'title': _('Delete ACL: %s') % self.get_object(), } @@ -187,12 +191,16 @@ class ACLPermissionsView(AssignRemoveView): ) def get_extra_context(self): + acl = self.get_object() + return { - 'object': self.get_object().content_object, + 'acl': acl, + 'object': acl.content_object, + 'navigation_object_list': ('object', 'acl'), 'title': _('Role "%(role)s" permission\'s for "%(object)s"') % { - 'role': self.get_object().role, - 'object': self.get_object().content_object, - }, + 'role': acl.role, + 'object': acl.content_object, + } } def get_granted_list(self): From 6143cb5155b4163f0775fdb612386a8268e7a481 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Fri, 1 Feb 2019 04:17:26 -0400 Subject: [PATCH 098/209] Sync list header code to row code Add the list display code to display columns marked as identifier. Signed-off-by: Roberto Rosario --- .../appearance/generic_list_subtemplate.html | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/mayan/apps/appearance/templates/appearance/generic_list_subtemplate.html b/mayan/apps/appearance/templates/appearance/generic_list_subtemplate.html index 9eeaafe932..a95d4fe748 100644 --- a/mayan/apps/appearance/templates/appearance/generic_list_subtemplate.html +++ b/mayan/apps/appearance/templates/appearance/generic_list_subtemplate.html @@ -48,10 +48,23 @@ {% if not hide_object %} {% trans 'Identifier' %} + {% else %} + {% get_source_columns source=object_list only_identifier=True as source_column %} + + {% if source_column.is_sortable %} + {{ source_column.label }} + {% if source_column.attribute == sort_field %} + {% if icon_sort %}{{ icon_sort.render }}{% endif %} + {% endif %} + + {% else %} + {{ source_column.label }} + {% endif %} + {% endif %} {% if not hide_columns %} - {% get_source_columns source=object_list as source_columns %} + {% get_source_columns source=object_list exclude_identifier=True as source_columns %} {% for column in source_columns %} {% if column.is_sortable %} @@ -100,7 +113,7 @@ {% endif %} {% endif %} - {% if not hide_columns %} + {% if not hide_columns %} {% get_source_columns source=object exclude_identifier=True as source_columns %} {% for column in source_columns %} From 991bd9df329f6c8ccd8a22f71426b8883dc5585c Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Fri, 1 Feb 2019 04:19:14 -0400 Subject: [PATCH 099/209] Insert the external object into the view Signed-off-by: Roberto Rosario --- mayan/apps/common/mixins.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/mayan/apps/common/mixins.py b/mayan/apps/common/mixins.py index e2592217e6..868052424d 100644 --- a/mayan/apps/common/mixins.py +++ b/mayan/apps/common/mixins.py @@ -80,6 +80,10 @@ class ExternalObjectMixin(object): external_object_pk_url_kwargs = None # Usage: {'pk': 'pk'} external_object_queryset = None + def dispatch(self, *args, **kwargs): + self.external_object = self.get_external_object() + return super(ExternalObjectMixin, self).dispatch(*args, **kwargs) + def get_pk_url_kwargs(self): pk_url_kwargs = {} From 5b6a6bccb242df30d3cf70feaddf5006a747934e Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Fri, 1 Feb 2019 04:19:53 -0400 Subject: [PATCH 100/209] Add columns for duplicated document proxies Signed-off-by: Roberto Rosario --- mayan/apps/documents/apps.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/mayan/apps/documents/apps.py b/mayan/apps/documents/apps.py index 8ce6417d97..601bbbd48f 100644 --- a/mayan/apps/documents/apps.py +++ b/mayan/apps/documents/apps.py @@ -343,6 +343,10 @@ class DocumentsApp(MayanAppConfig): ) # DuplicatedDocument + SourceColumn( + attribute='label', is_absolute_url=True, is_identifier=True, + is_sortable=True, source=DuplicatedDocumentProxy + ) SourceColumn( func=lambda context: document_page_thumbnail_widget.render( instance=context['object'] @@ -350,8 +354,9 @@ class DocumentsApp(MayanAppConfig): ) SourceColumn( func=lambda context: context['object'].get_duplicate_count( - user=context['request'].user, - ), label=_('Duplicates'), source=DuplicatedDocumentProxy + user=context['request'].user + ), include_label=True, label=_('Duplicates'), + source=DuplicatedDocumentProxy ) app.conf.beat_schedule.update( From 4376d76c8a9856ab87a2d7ecfd7c06c376177a71 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Fri, 1 Feb 2019 04:20:47 -0400 Subject: [PATCH 101/209] Load the converter class on demand Signed-off-by: Roberto Rosario --- mayan/apps/documents/models/document_page_models.py | 9 +++++---- mayan/apps/documents/models/document_version_models.py | 7 ++++--- mayan/apps/ocr/classes.py | 4 ++-- mayan/apps/sources/classes.py | 5 +++-- 4 files changed, 14 insertions(+), 11 deletions(-) diff --git a/mayan/apps/documents/models/document_page_models.py b/mayan/apps/documents/models/document_page_models.py index 7c3037e21a..60a3e841e3 100644 --- a/mayan/apps/documents/models/document_page_models.py +++ b/mayan/apps/documents/models/document_page_models.py @@ -12,10 +12,11 @@ from django.utils.translation import ugettext_lazy as _ from mayan.apps.converter import ( BaseTransformation, TransformationResize, TransformationRotate, - TransformationZoom, converter_class + TransformationZoom ) from mayan.apps.converter.literals import DEFAULT_ROTATION, DEFAULT_ZOOM_LEVEL from mayan.apps.converter.models import Transformation +from mayan.apps.converter.utils import get_converter_class from ..managers import DocumentPageManager from ..settings import ( @@ -67,7 +68,7 @@ class DocumentPage(models.Model): def detect_orientation(self): with self.document_version.open() as file_object: - converter = converter_class( + converter = get_converter_class()( file_object=file_object, mime_type=self.document_version.mimetype ) @@ -198,7 +199,7 @@ class DocumentPage(models.Model): cache_file = self.cache_partition.get_file(filename=cache_filename) if not setting_disable_base_image_cache.value and cache_file: logger.debug('Page cache file "%s" found', cache_filename) - converter = converter_class( + converter = get_converter_class()( file_object=cache_file.open() ) @@ -206,7 +207,7 @@ class DocumentPage(models.Model): else: logger.debug('Page cache file "%s" not found', cache_filename) - converter = converter_class( + converter = get_converter_class()( file_object=self.document_version.get_intermidiate_file() ) converter.seek(page_number=self.page_number - 1) diff --git a/mayan/apps/documents/models/document_version_models.py b/mayan/apps/documents/models/document_version_models.py index a982bfd45d..608644ccb0 100644 --- a/mayan/apps/documents/models/document_version_models.py +++ b/mayan/apps/documents/models/document_version_models.py @@ -11,9 +11,10 @@ from django.utils.encoding import force_text, python_2_unicode_compatible from django.utils.functional import cached_property from django.utils.translation import ugettext_lazy as _ -from mayan.apps.converter import TransformationRotate, converter_class +from mayan.apps.converter import TransformationRotate from mayan.apps.converter.exceptions import InvalidOfficeFormat, PageCountError from mayan.apps.converter.models import Transformation +from mayan.apps.converter.utils import get_converter_class from mayan.apps.mimetype.api import get_mimetype from ..events import event_document_new_version, event_document_version_revert @@ -200,7 +201,7 @@ class DocumentVersion(models.Model): logger.debug('Intermidiate file not found.') try: - converter = converter_class(file_object=self.open()) + converter = get_converter_class()(file_object=self.open()) pdf_file_object = converter.to_pdf() with self.cache_partition.create_file(filename='intermediate_file') as file_object: @@ -419,7 +420,7 @@ class DocumentVersion(models.Model): def update_page_count(self, save=True): try: with self.open() as file_object: - converter = converter_class( + converter = get_converter_class()( file_object=file_object, mime_type=self.mimetype ) detected_pages = converter.get_page_count() diff --git a/mayan/apps/ocr/classes.py b/mayan/apps/ocr/classes.py index 487f5994a3..77c158dc8e 100644 --- a/mayan/apps/ocr/classes.py +++ b/mayan/apps/ocr/classes.py @@ -1,6 +1,6 @@ from __future__ import unicode_literals -from mayan.apps.converter import converter_class +from mayan.apps.converter.utils import get_converter_class class OCRBackendBase(object): @@ -10,7 +10,7 @@ class OCRBackendBase(object): if not transformations: transformations = [] - self.converter = converter_class(file_object=file_object) + self.converter = get_converter_class()(file_object=file_object) for transformation in transformations: self.converter.transform(transformation=transformation) diff --git a/mayan/apps/sources/classes.py b/mayan/apps/sources/classes.py index eace9dca0f..a89458a7dc 100644 --- a/mayan/apps/sources/classes.py +++ b/mayan/apps/sources/classes.py @@ -13,7 +13,8 @@ from django.urls import reverse from django.utils.encoding import force_text, python_2_unicode_compatible from django.utils.six.moves.urllib.parse import quote_plus, unquote_plus -from mayan.apps.converter import TransformationResize, converter_class +from mayan.apps.converter import TransformationResize +from mayan.apps.converter.utils import get_converter_class from .storages import storage_staging_file_image_cache @@ -143,7 +144,7 @@ class StagingFile(object): try: file_object = open(self.get_full_path(), mode='rb') - converter = converter_class(file_object=file_object) + converter = get_converter_class()(file_object=file_object) page_image = converter.get_page() From dcea32ae3875280436521f23dca86d04de422f07 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sun, 3 Feb 2019 19:22:49 -0400 Subject: [PATCH 102/209] Refactor file metadata app Add translatable label to the label admin method. Add access association from DocumentVersionDriverEntry to document version. Enclose process method and event commit in a transaction. Update process method to not error out if EXIF tool is not found. Update views and tests to use ExternalObjectMixin and comply with MERCs 5 and 6. Signed-off-by: Roberto Rosario --- mayan/apps/file_metadata/admin.py | 6 +- mayan/apps/file_metadata/apps.py | 3 + mayan/apps/file_metadata/classes.py | 38 +++++----- mayan/apps/file_metadata/drivers/exiftool.py | 20 ++++-- mayan/apps/file_metadata/exceptions.py | 6 +- mayan/apps/file_metadata/handlers.py | 2 +- mayan/apps/file_metadata/tests/test_views.py | 16 ++--- mayan/apps/file_metadata/views.py | 74 ++++++++------------ 8 files changed, 82 insertions(+), 83 deletions(-) diff --git a/mayan/apps/file_metadata/admin.py b/mayan/apps/file_metadata/admin.py index c563ce4c47..056143ec82 100644 --- a/mayan/apps/file_metadata/admin.py +++ b/mayan/apps/file_metadata/admin.py @@ -1,13 +1,15 @@ from __future__ import unicode_literals from django.contrib import admin +from django.utils.translation import ugettext_lazy as _ from .models import StoredDriver @admin.register(StoredDriver) class StoredDriverAdmin(admin.ModelAdmin): - list_display = ('internal_name', 'label', 'driver_path') + list_display = ('internal_name', 'get_label', 'driver_path') - def label(self, instance): + def get_label(self, instance): return instance.driver_label + get_label.short_description = _('Label') diff --git a/mayan/apps/file_metadata/apps.py b/mayan/apps/file_metadata/apps.py index 9a42d789a3..b66244c28c 100644 --- a/mayan/apps/file_metadata/apps.py +++ b/mayan/apps/file_metadata/apps.py @@ -121,6 +121,9 @@ class FileMetadataApp(MayanAppConfig): ModelPermission.register_inheritance( model=DocumentTypeSettings, related='document_type', ) + ModelPermission.register_inheritance( + model=DocumentVersionDriverEntry, related='document_version', + ) SourceColumn(attribute='key', source=FileMetadataEntry) SourceColumn(attribute='value', source=FileMetadataEntry) diff --git a/mayan/apps/file_metadata/classes.py b/mayan/apps/file_metadata/classes.py index 656ab11966..e1a789c812 100644 --- a/mayan/apps/file_metadata/classes.py +++ b/mayan/apps/file_metadata/classes.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals import logging from django.apps import apps +from django.db import transaction from .events import event_file_metadata_document_version_finish from .exceptions import FileMetadataDriverError @@ -14,35 +15,36 @@ logger = logging.getLogger(__name__) class FileMetadataDriver(object): _registry = {} - @classmethod - def register(cls, mimetypes): - for mimetype in mimetypes: - cls._registry.setdefault(mimetype, []).append(cls) - @classmethod def process_document_version(cls, document_version): for driver_class in cls._registry.get(document_version.mimetype, ()): try: driver = driver_class() - driver.process(document_version=document_version) + + with transaction.atomic(): + driver.process(document_version=document_version) + event_file_metadata_document_version_finish.commit( + action_object=document_version.document, + target=document_version + ) + + post_document_version_file_metadata_processing.send( + sender=document_version.__class__, + instance=document_version + ) except FileMetadataDriverError: # If driver raises error, try next in the list pass else: # If driver was successfull there is no need to try # others in the list for this mimetype - - event_file_metadata_document_version_finish.commit( - action_object=document_version.document, - target=document_version - ) - - post_document_version_file_metadata_processing.send( - sender=document_version.__class__, - instance=document_version - ) return + @classmethod + def register(cls, mimetypes): + for mimetype in mimetypes: + cls._registry.setdefault(mimetype, []).append(cls) + def process(self, document_version): logger.info( 'Starting processing document version: %s', document_version @@ -68,7 +70,9 @@ class FileMetadataDriver(object): document_version=document_version ) - for key, value in self._process(document_version=document_version).items(): + results = self._process(document_version=document_version) or {} + + for key, value in results.items(): document_version_driver_entry.entries.create( key=key, value=value ) diff --git a/mayan/apps/file_metadata/drivers/exiftool.py b/mayan/apps/file_metadata/drivers/exiftool.py index 9b60e0c1cc..6c83f20ee4 100644 --- a/mayan/apps/file_metadata/drivers/exiftool.py +++ b/mayan/apps/file_metadata/drivers/exiftool.py @@ -30,14 +30,20 @@ class EXIFToolDriver(FileMetadataDriver): self.command_exiftool = self.command_exiftool.bake('-j') def _process(self, document_version): - new_file_object, temp_filename = mkstemp() + if self.command_exiftool: + new_file_object, temp_filename = mkstemp() - try: - document_version.save_to_file(filepath=temp_filename) - result = self.command_exiftool(temp_filename) - return json.loads(s=result.stdout)[0] - finally: - fs_cleanup(filename=temp_filename) + try: + document_version.save_to_file(filepath=temp_filename) + result = self.command_exiftool(temp_filename) + return json.loads(s=result.stdout)[0] + finally: + fs_cleanup(filename=temp_filename) + else: + logger.warning( + 'EXIFTool binary not found, not processing document version: %s', + document_version + ) EXIFToolDriver.register( diff --git a/mayan/apps/file_metadata/exceptions.py b/mayan/apps/file_metadata/exceptions.py index 1f80c50b0d..a778a2313d 100644 --- a/mayan/apps/file_metadata/exceptions.py +++ b/mayan/apps/file_metadata/exceptions.py @@ -1,5 +1,9 @@ from __future__ import unicode_literals -class FileMetadataDriverError(Exception): +class FileMetadataError(Exception): """Base file metadata driver exception""" + + +class FileMetadataDriverError(FileMetadataError): + """Exception raised when a driver encounters an unexpected error""" diff --git a/mayan/apps/file_metadata/handlers.py b/mayan/apps/file_metadata/handlers.py index 01a95417e2..84d304f975 100644 --- a/mayan/apps/file_metadata/handlers.py +++ b/mayan/apps/file_metadata/handlers.py @@ -12,7 +12,7 @@ def handler_initialize_new_document_type_settings(sender, instance, **kwargs): if kwargs['created']: DocumentTypeSettings.objects.create( - document_type=instance, auto_process=setting_auto_process.value + auto_process=setting_auto_process.value, document_type=instance ) diff --git a/mayan/apps/file_metadata/tests/test_views.py b/mayan/apps/file_metadata/tests/test_views.py index 5971bf657c..d2dbe87add 100644 --- a/mayan/apps/file_metadata/tests/test_views.py +++ b/mayan/apps/file_metadata/tests/test_views.py @@ -14,10 +14,6 @@ from .literals import TEST_FILE_METADATA_KEY @override_settings(FILE_METADATA_AUTO_PROCESS=True) class FileMetadataViewsTestCase(GenericDocumentViewTestCase): - def setUp(self): - super(FileMetadataViewsTestCase, self).setUp() - self.login_user() - def _request_document_version_driver_list_view(self): return self.get( viewname='file_metadata:document_driver_list', @@ -26,7 +22,7 @@ class FileMetadataViewsTestCase(GenericDocumentViewTestCase): def test_document_version_driver_list_view_no_permission(self): response = self._request_document_version_driver_list_view() - self.assertEqual(response.status_code, 403) + self.assertEqual(response.status_code, 404) def test_document_version_driver_list_view_with_access(self): self.grant_access( @@ -46,12 +42,12 @@ class FileMetadataViewsTestCase(GenericDocumentViewTestCase): def test_document_version_file_metadata_list_view_no_permission(self): response = self._request_document_version_file_metadata_list_view() self.assertNotContains( - response=response, text=TEST_FILE_METADATA_KEY, status_code=403 + response=response, text=TEST_FILE_METADATA_KEY, status_code=404 ) def test_document_version_file_metadata_list_view_with_access(self): self.grant_access( - permission=permission_file_metadata_view, obj=self.document + obj=self.document, permission=permission_file_metadata_view ) response = self._request_document_version_file_metadata_list_view() self.assertContains( @@ -67,7 +63,7 @@ class FileMetadataViewsTestCase(GenericDocumentViewTestCase): def test_document_submit_view_no_permission(self): self.document.latest_version.file_metadata_drivers.all().delete() response = self._request_document_submit_view() - self.assertEqual(response.status_code, 302) + self.assertEqual(response.status_code, 404) self.assertEqual( self.document.latest_version.file_metadata_drivers.count(), 0 ) @@ -94,7 +90,7 @@ class FileMetadataViewsTestCase(GenericDocumentViewTestCase): def test_multiple_document_submit_view_no_permission(self): self.document.latest_version.file_metadata_drivers.all().delete() response = self._request_multiple_document_submit_view() - self.assertEqual(response.status_code, 302) + self.assertEqual(response.status_code, 404) self.assertEqual( self.document.latest_version.file_metadata_drivers.count(), 0 ) @@ -124,7 +120,7 @@ class DocumentTypeViewsTestCase(GenericDocumentViewTestCase): def test_document_type_settings_view_no_permission(self): response = self._request_document_type_settings_view() - self.assertEqual(response.status_code, 403) + self.assertEqual(response.status_code, 404) def test_document_type_settings_view_with_access(self): self.grant_access( diff --git a/mayan/apps/file_metadata/views.py b/mayan/apps/file_metadata/views.py index 7afdf6ee65..879ad5ba34 100644 --- a/mayan/apps/file_metadata/views.py +++ b/mayan/apps/file_metadata/views.py @@ -12,6 +12,7 @@ from mayan.apps.common.generics import ( FormView, MultipleObjectConfirmActionView, SingleObjectEditView, SingleObjectListView ) +from mayan.apps.common.mixins import ExternalObjectMixin from mayan.apps.documents.forms import DocumentTypeFilteredSelectForm from mayan.apps.documents.models import Document, DocumentType @@ -23,7 +24,11 @@ from .permissions import ( ) -class DocumentDriverListView(SingleObjectListView): +class DocumentDriverListView(ExternalObjectMixin, SingleObjectListView): + external_object_class = Document + external_object_permission = permission_file_metadata_view + external_object_pk_url_kwarg = 'document_id' + def get_extra_context(self): return { 'hide_object': True, @@ -38,54 +43,36 @@ class DocumentDriverListView(SingleObjectListView): 'reside in the database.' ), 'no_results_title': _('No file metadata available.'), - 'object': self.get_object(), + 'object': self.external_object, 'title': _( 'File metadata drivers for: %s' - ) % self.get_object(), + ) % self.external_object, } - def get_object(self): - document = get_object_or_404( - klass=Document, pk=self.kwargs['document_id'] - ) - AccessControlList.objects.check_access( - permissions=permission_file_metadata_view, - user=self.request.user, obj=document - ) - return document - def get_source_queryset(self): - return self.get_object().latest_version.file_metadata_drivers.all() + return self.external_object.latest_version.file_metadata_drivers.all() -class DocumentVersionDriverEntryFileMetadataListView(SingleObjectListView): +class DocumentVersionDriverEntryFileMetadataListView(ExternalObjectMixin, SingleObjectListView): + external_object_class = DocumentVersionDriverEntry + external_object_permission = permission_file_metadata_view + external_object_pk_url_kwarg = 'document_version_driver_id' + def get_extra_context(self): return { 'hide_object': True, 'no_results_title': _('No file metadata available.'), - 'object': self.get_object().document_version.document, + 'object': self.external_object.document_version.document, 'title': _( 'File metadata attribures for: %(document)s, for driver: %(driver)s' ) % { - 'document': self.get_object().document_version.document, - 'driver': self.get_object().driver + 'document': self.external_object.document_version.document, + 'driver': self.external_object.driver }, } - def get_object(self): - document_version_driver_entry = get_object_or_404( - klass=DocumentVersionDriverEntry, - pk=self.kwargs['document_version_driver_id'] - ) - AccessControlList.objects.check_access( - obj=document_version_driver_entry.document_version, - permissions=permission_file_metadata_view, - user=self.request.user, - ) - return document_version_driver_entry - def get_source_queryset(self): - return self.get_object().entries.all() + return self.external_object.entries.all() class DocumentSubmitView(MultipleObjectConfirmActionView): @@ -96,13 +83,13 @@ class DocumentSubmitView(MultipleObjectConfirmActionView): success_message_plural = '%(count)d documents submitted to the file metadata queue.' def get_extra_context(self): - queryset = self.get_queryset() + queryset = self.get_object_list() result = { 'title': ungettext( - 'Submit the selected document to the file metadata queue?', - 'Submit the selected documents to the file metadata queue?', - queryset.count() + singular='Submit the selected document to the file metadata queue?', + plural='Submit the selected documents to the file metadata queue?', + number=queryset.count() ) } @@ -112,26 +99,23 @@ class DocumentSubmitView(MultipleObjectConfirmActionView): instance.submit_for_file_metadata_processing() -class DocumentTypeSettingsEditView(SingleObjectEditView): +class DocumentTypeSettingsEditView(ExternalObjectMixin, SingleObjectEditView): + external_object_class = DocumentType + external_object_permission = permission_document_type_file_metadata_setup + external_object_pk_url_kwarg = 'document_type_id' fields = ('auto_process',) - object_permission = permission_document_type_file_metadata_setup post_action_redirect = reverse_lazy(viewname='documents:document_type_list') - def get_document_type(self): - return get_object_or_404( - klass=DocumentType, pk=self.kwargs['document_type_id'] - ) - def get_extra_context(self): return { - 'object': self.get_document_type(), + 'object': self.external_object, 'title': _( 'Edit file metadata settings for document type: %s' - ) % self.get_document_type() + ) % self.external_object } def get_object(self, queryset=None): - return self.get_document_type().file_metadata_settings + return self.external_object.file_metadata_settings class DocumentTypeSubmitView(FormView): From aa95a614510b41b37c35cb3fab3c6a691d108317 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sun, 3 Feb 2019 23:37:52 -0400 Subject: [PATCH 103/209] Refactor metadata app Update permission variable name from "permission_document_metadata_" to "permission_metadata_". Fix failing tests. Add test for same metadata type mixin. Split metadata add and remove test into test for GET and POST requests. Remove use of urlencode and instead use furl. Simplify view using self.action_count and self.action_id_list. Use ExternalObjectMixin to remove repeated code. Move the repeated code to test for all documents to be of the same type into its own mixin. Signed-off-by: Roberto Rosario --- mayan/apps/metadata/api_views.py | 16 +- mayan/apps/metadata/apps.py | 12 +- mayan/apps/metadata/links.py | 12 +- mayan/apps/metadata/permissions.py | 8 +- mayan/apps/metadata/tests/test_api.py | 18 +- mayan/apps/metadata/tests/test_events.py | 4 +- mayan/apps/metadata/tests/test_views.py | 194 +++++++++----- mayan/apps/metadata/views.py | 311 ++++++++--------------- 8 files changed, 266 insertions(+), 309 deletions(-) diff --git a/mayan/apps/metadata/api_views.py b/mayan/apps/metadata/api_views.py index f23a518bca..042b8a903e 100644 --- a/mayan/apps/metadata/api_views.py +++ b/mayan/apps/metadata/api_views.py @@ -14,8 +14,8 @@ from mayan.apps.rest_api.permissions import MayanPermission from .models import MetadataType from .permissions import ( - permission_document_metadata_add, permission_document_metadata_remove, - permission_document_metadata_edit, permission_document_metadata_view, + permission_metadata_add, permission_metadata_remove, + permission_metadata_edit, permission_metadata_view, permission_metadata_type_create, permission_metadata_type_delete, permission_metadata_type_edit, permission_metadata_type_view ) @@ -34,9 +34,9 @@ class APIDocumentMetadataListView(generics.ListCreateAPIView): """ def get_document(self): if self.request.method == 'GET': - permission_required = permission_document_metadata_view + permission_required = permission_metadata_view else: - permission_required = permission_document_metadata_add + permission_required = permission_metadata_add document = get_object_or_404( klass=Document, pk=self.kwargs['document_pk'] @@ -90,13 +90,13 @@ class APIDocumentMetadataView(generics.RetrieveUpdateDestroyAPIView): def get_document(self): if self.request.method == 'GET': - permission_required = permission_document_metadata_view + permission_required = permission_metadata_view elif self.request.method == 'PUT': - permission_required = permission_document_metadata_edit + permission_required = permission_metadata_edit elif self.request.method == 'PATCH': - permission_required = permission_document_metadata_edit + permission_required = permission_metadata_edit elif self.request.method == 'DELETE': - permission_required = permission_document_metadata_remove + permission_required = permission_metadata_remove document = get_object_or_404( klass=Document, pk=self.kwargs['document_pk'] diff --git a/mayan/apps/metadata/apps.py b/mayan/apps/metadata/apps.py index 6058d3b977..99d227ccd8 100644 --- a/mayan/apps/metadata/apps.py +++ b/mayan/apps/metadata/apps.py @@ -48,8 +48,8 @@ from .links import ( ) from .methods import method_get_metadata from .permissions import ( - permission_document_metadata_add, permission_document_metadata_edit, - permission_document_metadata_remove, permission_document_metadata_view, + permission_metadata_add, permission_metadata_edit, + permission_metadata_remove, permission_metadata_view, permission_metadata_type_delete, permission_metadata_type_edit, permission_metadata_type_view ) @@ -130,10 +130,10 @@ class MetadataApp(MayanAppConfig): ModelPermission.register( model=Document, permissions=( - permission_document_metadata_add, - permission_document_metadata_edit, - permission_document_metadata_remove, - permission_document_metadata_view, + permission_metadata_add, + permission_metadata_edit, + permission_metadata_remove, + permission_metadata_view, ) ) ModelPermission.register( diff --git a/mayan/apps/metadata/links.py b/mayan/apps/metadata/links.py index b2b187578e..a93fea2e1a 100644 --- a/mayan/apps/metadata/links.py +++ b/mayan/apps/metadata/links.py @@ -15,32 +15,32 @@ from .icons import ( icon_metadata_type_edit, icon_metadata_type_list ) from .permissions import ( - permission_document_metadata_add, permission_document_metadata_edit, - permission_document_metadata_remove, permission_document_metadata_view, + permission_metadata_add, permission_metadata_edit, + permission_metadata_remove, permission_metadata_view, permission_metadata_type_create, permission_metadata_type_delete, permission_metadata_type_edit, permission_metadata_type_view ) link_document_metadata_add = Link( icon_class=icon_document_metadata_add, kwargs={'document_id': 'object.pk'}, - permission=permission_document_metadata_add, text=_('Add metadata'), + permission=permission_metadata_add, text=_('Add metadata'), view='metadata:document_metadata_add', ) link_document_metadata_edit = Link( icon_class=icon_document_metadata_edit, kwargs={'document_id': 'object.pk'}, - permission=permission_document_metadata_edit, text=_('Edit metadata'), + permission=permission_metadata_edit, text=_('Edit metadata'), view='metadata:document_metadata_edit' ) link_document_metadata_remove = Link( icon_class=icon_document_metadata_remove, kwargs={'document_id': 'object.pk'}, - permission=permission_document_metadata_remove, + permission=permission_metadata_remove, text=_('Remove metadata'), view='metadata:document_metadata_remove' ) link_document_metadata_view = Link( icon_class=icon_document_metadata_view, kwargs={'document_id': 'resolved_object.pk'}, - permission=permission_document_metadata_view, text=_('Metadata'), + permission=permission_metadata_view, text=_('Metadata'), view='metadata:document_metadata_view' ) link_document_multiple_metadata_add = Link( diff --git a/mayan/apps/metadata/permissions.py b/mayan/apps/metadata/permissions.py index e1c35fa460..d1ec76f894 100644 --- a/mayan/apps/metadata/permissions.py +++ b/mayan/apps/metadata/permissions.py @@ -6,16 +6,16 @@ from mayan.apps.permissions import PermissionNamespace namespace = PermissionNamespace(label=_('Metadata'), name='metadata') -permission_document_metadata_add = namespace.add_permission( +permission_metadata_add = namespace.add_permission( label=_('Add metadata to a document'), name='metadata_document_add' ) -permission_document_metadata_edit = namespace.add_permission( +permission_metadata_edit = namespace.add_permission( label=_('Edit a document\'s metadata'), name='metadata_document_edit' ) -permission_document_metadata_remove = namespace.add_permission( +permission_metadata_remove = namespace.add_permission( label=_('Remove metadata from a document'), name='metadata_document_remove' ) -permission_document_metadata_view = namespace.add_permission( +permission_metadata_view = namespace.add_permission( label=_('View metadata from a document'), name='metadata_document_view' ) diff --git a/mayan/apps/metadata/tests/test_api.py b/mayan/apps/metadata/tests/test_api.py index 9092c0c3ad..860d09f7ab 100644 --- a/mayan/apps/metadata/tests/test_api.py +++ b/mayan/apps/metadata/tests/test_api.py @@ -11,8 +11,8 @@ from mayan.apps.rest_api.tests import BaseAPITestCase from ..models import DocumentTypeMetadataType, MetadataType from ..permissions import ( - permission_document_metadata_add, permission_document_metadata_edit, - permission_document_metadata_remove, permission_document_metadata_view, + permission_metadata_add, permission_metadata_edit, + permission_metadata_remove, permission_metadata_view, permission_metadata_type_create, permission_metadata_type_delete, permission_metadata_type_edit, permission_metadata_type_view ) @@ -377,7 +377,7 @@ class DocumentMetadataAPITestCase(BaseAPITestCase): self.assertEqual(self.document.metadata.count(), 0) def test_document_metadata_create_view_with_access(self): - self.grant_access(permission=permission_document_metadata_add, obj=self.document) + self.grant_access(permission=permission_metadata_add, obj=self.document) response = self._request_document_metadata_create_view() self.assertEqual(response.status_code, status.HTTP_201_CREATED) document_metadata = self.document.metadata.first() @@ -387,7 +387,7 @@ class DocumentMetadataAPITestCase(BaseAPITestCase): def test_document_metadata_create_duplicate_view(self): self._create_document_metadata() - self.grant_permission(permission=permission_document_metadata_add) + self.grant_permission(permission=permission_metadata_add) response = self._request_document_metadata_create_view() self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual(list(response.data.keys())[0], 'non_field_errors') @@ -395,7 +395,7 @@ class DocumentMetadataAPITestCase(BaseAPITestCase): def test_document_metadata_create_invalid_lookup_value_view(self): self.metadata_type.lookup = 'invalid,lookup,values,on,purpose' self.metadata_type.save() - self.grant_permission(permission=permission_document_metadata_add) + self.grant_permission(permission=permission_metadata_add) response = self._request_document_metadata_create_view() self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual(list(response.data.keys())[0], 'non_field_errors') @@ -418,7 +418,7 @@ class DocumentMetadataAPITestCase(BaseAPITestCase): def test_document_metadata_delete_view_with_access(self): self._create_document_metadata() self.grant_access( - permission=permission_document_metadata_remove, obj=self.document + permission=permission_metadata_remove, obj=self.document ) response = self._request_document_metadata_delete_view() self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) @@ -438,7 +438,7 @@ class DocumentMetadataAPITestCase(BaseAPITestCase): def test_document_metadata_list_view_with_access(self): self._create_document_metadata() self.grant_access( - permission=permission_document_metadata_view, obj=self.document + permission=permission_metadata_view, obj=self.document ) response = self._request_document_metadata_list_view() self.assertEqual(response.status_code, status.HTTP_200_OK) @@ -477,7 +477,7 @@ class DocumentMetadataAPITestCase(BaseAPITestCase): def test_document_metadata_patch_view_with_access(self): self._create_document_metadata() self.grant_access( - permission=permission_document_metadata_edit, obj=self.document + permission=permission_metadata_edit, obj=self.document ) response = self._request_document_metadata_edit_view_via_patch() self.assertEqual(response.status_code, status.HTTP_200_OK) @@ -510,7 +510,7 @@ class DocumentMetadataAPITestCase(BaseAPITestCase): def test_document_metadata_put_view_with_access(self): self._create_document_metadata() self.grant_access( - permission=permission_document_metadata_edit, obj=self.document + permission=permission_metadata_edit, obj=self.document ) response = self._request_document_metadata_edit_view_via_put() self.assertEqual(response.status_code, status.HTTP_200_OK) diff --git a/mayan/apps/metadata/tests/test_events.py b/mayan/apps/metadata/tests/test_events.py index 75d8e30ecf..70027cd36c 100644 --- a/mayan/apps/metadata/tests/test_events.py +++ b/mayan/apps/metadata/tests/test_events.py @@ -38,7 +38,7 @@ class MetadataTypeEventsTestCase(MetadataTestsMixin, GenericDocumentViewTestCase self.assertEqual(event.verb, event_metadata_type_created.id) self.assertEqual(event.target, metadata_type) - self.assertEqual(event.actor, self.user) + self.assertEqual(event.actor, self._test_case_user) def test_metadata_type_edit_event_no_permissions(self): self._create_metadata_type() @@ -66,4 +66,4 @@ class MetadataTypeEventsTestCase(MetadataTestsMixin, GenericDocumentViewTestCase self.assertEqual(event.verb, event_metadata_type_edited.id) self.assertEqual(event.target, self.metadata_type) - self.assertEqual(event.actor, self.user) + self.assertEqual(event.actor, self._test_case_user) diff --git a/mayan/apps/metadata/tests/test_views.py b/mayan/apps/metadata/tests/test_views.py index f483319b92..a4a457ced4 100644 --- a/mayan/apps/metadata/tests/test_views.py +++ b/mayan/apps/metadata/tests/test_views.py @@ -2,6 +2,8 @@ from __future__ import unicode_literals import logging +from django.utils.encoding import force_text + from mayan.apps.common.tests import GenericViewTestCase from mayan.apps.documents.models import DocumentType from mayan.apps.documents.permissions import ( @@ -13,8 +15,8 @@ from mayan.apps.documents.tests import ( from ..models import MetadataType from ..permissions import ( - permission_document_metadata_add, permission_document_metadata_remove, - permission_document_metadata_edit, permission_metadata_type_create, + permission_metadata_add, permission_metadata_remove, + permission_metadata_edit, permission_metadata_type_create, permission_metadata_type_delete, permission_metadata_type_edit, permission_metadata_type_view ) @@ -28,51 +30,114 @@ from .literals import ( from .mixins import MetadataTestsMixin -class DocumentMetadataTestCase(MetadataTestsMixin, GenericDocumentViewTestCase): +class DocumentMetadataViewTestCase(MetadataTestsMixin, GenericDocumentViewTestCase): def setUp(self): - super(DocumentMetadataTestCase, self).setUp() - self.login_user() + super(DocumentMetadataViewTestCase, self).setUp() self._create_metadata_type() self.document_type.metadata.create(metadata_type=self.metadata_type) - def _request_get_document_metadata_add_view(self): + def _request_document_metadata_add_get_view(self): return self.get( viewname='metadata:document_metadata_add', - kwargs={'document_id': self.document.pk}, + kwargs={'document_id': self.test_document.pk}, ) - def _request_post_document_metadata_add_view(self): + def _request_document_multiple_metadata_add_get_view(self): + return self.get( + viewname='metadata:document_multiple_metadata_add', + data={'id_list': ','.join([force_text(pk) for pk in self.id_list])}, + ) + + def _request_document_metadata_add_post_view(self): return self.post( viewname='metadata:document_metadata_add', - kwargs={'document_id': self.document.pk}, + kwargs={'document_id': self.test_document.pk}, data={'metadata_type': self.metadata_type.pk} ) - def test_document_metadata_add_view_no_permission(self): - response = self._request_get_document_metadata_add_view() + def test_document_metadata_add_get_view_no_permission(self): + response = self._request_document_metadata_add_get_view() self.assertNotContains( - response, text=self.metadata_type.label, status_code=200 + response, text=self.metadata_type.label, status_code=404 ) - response = self._request_post_document_metadata_add_view() - self.assertEqual(response.status_code, 200) - - self.assertEqual(len(self.document.metadata.all()), 0) - - def test_document_metadata_add_view_with_document_access(self): + def test_document_metadata_add_get_view_with_full_access(self): self.grant_access( - permission=permission_document_metadata_add, obj=self.document + obj=self.test_document, permission=permission_metadata_add ) - response = self._request_get_document_metadata_add_view() + response = self._request_document_metadata_add_get_view() self.assertContains( response, text=self.metadata_type.label, status_code=200 ) - response = self._request_post_document_metadata_add_view() + def test_document_individual_metadata_same_type_mixin_with_access(self): + self.grant_access( + obj=self.test_document, permission=permission_metadata_add + ) + + self._create_document_type_random() + self.test_document_type.metadata.create(metadata_type=self.metadata_type) + self._create_document() + self.grant_access( + obj=self.test_document, permission=permission_metadata_add + ) + + response = self._request_document_metadata_add_get_view() + self.assertContains( + response, text=self.metadata_type.label, status_code=200 + ) + + def test_document_single_metadata_same_type_mixin_with_access(self): + self.id_list = [self.test_document.pk] + self.grant_access( + obj=self.test_document, permission=permission_metadata_add + ) + + self._create_document_type_random() + self.test_document_type.metadata.create(metadata_type=self.metadata_type) + self._create_document() + self.grant_access( + obj=self.test_document, permission=permission_metadata_add + ) + + response = self._request_document_multiple_metadata_add_get_view() + self.assertContains( + response, text=self.metadata_type.label, status_code=200 + ) + + def test_document_multiple_metadata_same_type_mixin_with_access(self): + self.id_list = [self.test_document.pk] + self.grant_access( + obj=self.test_document, permission=permission_metadata_add + ) + + self._create_document_type_random() + self.test_document_type.metadata.create(metadata_type=self.metadata_type) + self._create_document() + self.id_list.append(self.test_document.pk) + self.grant_access( + obj=self.test_document, permission=permission_metadata_add + ) + + response = self._request_document_multiple_metadata_add_get_view() + self.assertNotContains( + response, text=self.metadata_type.label, status_code=302 + ) + + def test_document_metadata_add_post_view_no_permission(self): + response = self._request_document_metadata_add_post_view() + self.assertEqual(response.status_code, 404) + + self.assertEqual(len(self.document.metadata.all()), 0) + + def test_document_metadata_add_post_view_with_full_access(self): + self.grant_access( + obj=self.test_document, permission=permission_metadata_add + ) + response = self._request_document_metadata_add_post_view() self.assertEqual(response.status_code, 302) self.assertEqual(len(self.document.metadata.all()), 1) - self.assertQuerysetEqual( qs=self.document.metadata.values('metadata_type',), values=[ @@ -86,13 +151,6 @@ class DocumentMetadataTestCase(MetadataTestsMixin, GenericDocumentViewTestCase): # Gitlab issue #204 # Problems to add required metadata after changing the document type - self.grant_access( - permission=permission_document_properties_edit, obj=self.document - ) - self.grant_access( - permission=permission_document_metadata_edit, obj=self.document - ) - document_type_2 = DocumentType.objects.create( label=TEST_DOCUMENT_TYPE_2_LABEL ) @@ -105,6 +163,12 @@ class DocumentMetadataTestCase(MetadataTestsMixin, GenericDocumentViewTestCase): metadata_type=metadata_type_2, required=True ) + self.grant_access( + obj=self.document, permission=permission_document_properties_edit + ) + self.grant_access( + obj=self.document, permission=permission_metadata_edit + ) self.document.set_document_type(document_type=document_type_2) response = self.get( @@ -133,13 +197,13 @@ class DocumentMetadataTestCase(MetadataTestsMixin, GenericDocumentViewTestCase): TEST_DOCUMENT_METADATA_VALUE_2 ) - def _request_get_document_document_metadata_remove_view(self): + def _request_document_document_metadata_remove_get_view(self): return self.get( viewname='metadata:document_metadata_remove', kwargs={'document_id': self.document.pk} ) - def _request_post_document_document_metadata_remove_view(self): + def _request_document_document_metadata_remove_post_view(self): return self.post( viewname='metadata:document_metadata_remove', kwargs={'document_id': self.document.pk}, data={ @@ -151,38 +215,24 @@ class DocumentMetadataTestCase(MetadataTestsMixin, GenericDocumentViewTestCase): } ) - def test_document_metadata_remove_view_no_permission(self): + def test_document_metadata_remove_get_view_no_permission(self): self.document_metadata = self.document.metadata.create( metadata_type=self.metadata_type, value='' ) - response = self._request_get_document_document_metadata_remove_view() + response = self._request_document_document_metadata_remove_get_view() self.assertNotContains( - response, text=self.metadata_type.label, status_code=200 + response=response, text=self.metadata_type.label, status_code=404 ) - response = self._request_post_document_document_metadata_remove_view() - self.assertEqual(response.status_code, 404) - - self.assertEqual(len(self.document.metadata.all()), 1) - - self.assertQuerysetEqual( - qs=self.document.metadata.values('metadata_type',), - values=[ - { - 'metadata_type': self.metadata_type.pk, - } - ], transform=dict - ) - - def test_document_metadata_remove_view_with_document_access(self): + def test_document_metadata_remove_get_view_with_full_access(self): self.document_metadata = self.document.metadata.create( metadata_type=self.metadata_type, value='' ) self.grant_access( - permission=permission_document_metadata_remove, obj=self.document + obj=self.document, permission=permission_metadata_remove, ) # Silence unrelated logging @@ -190,14 +240,32 @@ class DocumentMetadataTestCase(MetadataTestsMixin, GenericDocumentViewTestCase): level=logging.CRITICAL ) - response = self._request_get_document_document_metadata_remove_view() - + response = self._request_document_document_metadata_remove_get_view() self.assertContains( response, text=self.metadata_type.label, status_code=200 ) - response = self._request_post_document_document_metadata_remove_view() + def test_document_metadata_remove_post_view_no_permission(self): + self.document_metadata = self.document.metadata.create( + metadata_type=self.metadata_type, value='' + ) + response = self._request_document_document_metadata_remove_get_view() + self.assertNotContains( + response=response, text=self.metadata_type.label, status_code=404 + ) + + self.assertEqual(len(self.document.metadata.all()), 1) + + def test_document_metadata_remove_post_view_with_access(self): + self.document_metadata = self.document.metadata.create( + metadata_type=self.metadata_type, value='' + ) + + self.grant_access( + obj=self.test_document, permission=permission_metadata_remove + ) + response = self._request_document_document_metadata_remove_post_view() self.assertEqual(response.status_code, 302) self.assertEqual(len(self.document.metadata.all()), 0) @@ -227,10 +295,10 @@ class DocumentMetadataTestCase(MetadataTestsMixin, GenericDocumentViewTestCase): self.document_2 = self.upload_document() self.grant_access( - permission=permission_document_metadata_edit, obj=self.document + permission=permission_metadata_edit, obj=self.document ) self.grant_access( - permission=permission_document_metadata_edit, obj=self.document_2 + permission=permission_metadata_edit, obj=self.document_2 ) self.document_metadata = self.document.metadata.create( @@ -277,10 +345,10 @@ class DocumentMetadataTestCase(MetadataTestsMixin, GenericDocumentViewTestCase): def test_document_multiple_metadata_remove_with_access(self): self.document_2 = self.upload_document() self.grant_access( - permission=permission_document_metadata_remove, obj=self.document + permission=permission_metadata_remove, obj=self.document ) self.grant_access( - permission=permission_document_metadata_remove, obj=self.document_2 + permission=permission_metadata_remove, obj=self.document_2 ) self.document_metadata = self.document.metadata.create( @@ -311,10 +379,10 @@ class DocumentMetadataTestCase(MetadataTestsMixin, GenericDocumentViewTestCase): self.document_2 = self.upload_document() self.grant_access( - permission=permission_document_metadata_add, obj=self.document + permission=permission_metadata_add, obj=self.document ) self.grant_access( - permission=permission_document_metadata_add, obj=self.document_2 + permission=permission_metadata_add, obj=self.document_2 ) response = self._request_post_document_document_metadata_add_view() @@ -326,7 +394,7 @@ class DocumentMetadataTestCase(MetadataTestsMixin, GenericDocumentViewTestCase): def test_single_document_multiple_metadata_add_view_with_access(self): self.grant_access( - permission=permission_document_metadata_add, obj=self.document + permission=permission_metadata_add, obj=self.document ) metadata_type_2 = MetadataType.objects.create( name=TEST_METADATA_TYPE_NAME_2, label=TEST_METADATA_TYPE_LABEL_2 @@ -351,14 +419,10 @@ class DocumentMetadataTestCase(MetadataTestsMixin, GenericDocumentViewTestCase): ) -class MetadataTypeViewTestCase(DocumentTestMixin, MetadataTestsMixin, GenericViewTestCase): +class MetadataTypeViewViewTestCase(DocumentTestMixin, MetadataTestsMixin, GenericViewTestCase): auto_create_document_type = False auto_upload_document = False - def setUp(self): - super(MetadataTypeViewTestCase, self).setUp() - self.login_user() - def test_metadata_type_create_view_no_permission(self): response = self._request_metadata_type_create_view() diff --git a/mayan/apps/metadata/views.py b/mayan/apps/metadata/views.py index ca7846a875..ab5533b993 100644 --- a/mayan/apps/metadata/views.py +++ b/mayan/apps/metadata/views.py @@ -1,5 +1,7 @@ from __future__ import absolute_import, unicode_literals +from furl import furl + from django.conf import settings from django.contrib import messages from django.core.exceptions import ValidationError @@ -8,7 +10,6 @@ from django.shortcuts import get_object_or_404 from django.template import RequestContext from django.urls import reverse, reverse_lazy from django.utils.encoding import force_text -from django.utils.http import urlencode from django.utils.translation import ugettext_lazy as _, ungettext from mayan.apps.acls.models import AccessControlList @@ -16,6 +17,7 @@ from mayan.apps.common.generics import ( FormView, MultipleObjectFormActionView, SingleObjectCreateView, SingleObjectDeleteView, SingleObjectEditView, SingleObjectListView ) +from mayan.apps.common.mixins import ExternalObjectMixin from mayan.apps.documents.models import Document, DocumentType from mayan.apps.documents.permissions import ( permission_document_type_edit @@ -34,29 +36,20 @@ from .links import ( ) from .models import DocumentMetadata, MetadataType from .permissions import ( - permission_document_metadata_add, permission_document_metadata_edit, - permission_document_metadata_remove, permission_document_metadata_view, + permission_metadata_add, permission_metadata_edit, + permission_metadata_remove, permission_metadata_view, permission_metadata_type_create, permission_metadata_type_delete, permission_metadata_type_edit, permission_metadata_type_view ) -class DocumentMetadataAddView(MultipleObjectFormActionView): - form_class = DocumentMetadataAddForm - model = Document - object_permission = permission_document_metadata_add - pk_url_kwarg = 'document_id' - success_message = _('Metadata add request performed on %(count)d document') - success_message_plural = _( - 'Metadata add request performed on %(count)d documents' - ) - +class DocumentMetadataSameTypeMixin(object): def dispatch(self, request, *args, **kwargs): - result = super( - DocumentMetadataAddView, self - ).dispatch(request, *args, **kwargs) + result = super(DocumentMetadataSameTypeMixin, self).dispatch( + request, *args, **kwargs + ) - queryset = self.get_queryset() + queryset = self.get_object_list() for document in queryset: document.add_as_recent_document_for_user(request.user) @@ -71,40 +64,35 @@ class DocumentMetadataAddView(MultipleObjectFormActionView): return result - def form_valid(self, form): - result = super(DocumentMetadataAddView, self).form_valid(form=form) - queryset = self.get_queryset() +class DocumentMetadataAddView(DocumentMetadataSameTypeMixin, MultipleObjectFormActionView): + form_class = DocumentMetadataAddForm + model = Document + object_permission = permission_metadata_add + pk_url_kwarg = 'document_id' + success_message = _('Metadata add request performed on %(count)d document') + success_message_plural = _( + 'Metadata add request performed on %(count)d documents' + ) + def get_post_object_action_url(self): if self.action_count == 1: - return HttpResponseRedirect( - redirect_to=reverse( - viewname='metadata:document_metadata_edit', - kwargs={'document_id': queryset.first().pk} - ) - ) - elif self.action_count > 1: - return HttpResponseRedirect( - redirect_to='%s?%s' % ( - reverse( - viewname='metadata:document_multiple_metadata_edit' - ), urlencode( - { - 'id_list': ','.join( - map( - force_text, - queryset.values_list('pk', flat=True) - ) - ) - } - ) - ) + return reverse( + viewname='metadata:document_metadata_edit', + kwargs={'document_id': self.action_id_list[0]} ) - return result + elif self.action_count > 1: + url = furl( + path=reverse( + viewname='metadata:document_multiple_metadata_edit' + ), args={'id_list': self.action_id_list} + ) + + return url.tostr() def get_extra_context(self): - queryset = self.get_queryset() + queryset = self.get_object_list() result = { 'submit_label': _('Add'), @@ -128,7 +116,7 @@ class DocumentMetadataAddView(MultipleObjectFormActionView): return result def get_form_extra_kwargs(self): - queryset = self.get_queryset() + queryset = self.get_object_list() result = {} @@ -209,10 +197,10 @@ class DocumentMetadataAddView(MultipleObjectFormActionView): ) -class DocumentMetadataEditView(MultipleObjectFormActionView): +class DocumentMetadataEditView(DocumentMetadataSameTypeMixin, MultipleObjectFormActionView): form_class = DocumentMetadataFormSet model = Document - object_permission = permission_document_metadata_edit + object_permission = permission_metadata_edit pk_url_kwarg = 'document_id' success_message = _( 'Metadata edit request performed on %(count)d document' @@ -221,61 +209,8 @@ class DocumentMetadataEditView(MultipleObjectFormActionView): 'Metadata edit request performed on %(count)d documents' ) - def dispatch(self, request, *args, **kwargs): - result = super( - DocumentMetadataEditView, self - ).dispatch(request, *args, **kwargs) - - queryset = self.get_queryset() - - for document in queryset: - document.add_as_recent_document_for_user(request.user) - - if len(set([document.document_type.pk for document in queryset])) > 1: - messages.error( - message=_( - 'Selected documents must be of the same type.' - ), request=request - ) - return HttpResponseRedirect(redirect_to=self.previous_url) - - return result - - def form_valid(self, form): - result = super(DocumentMetadataEditView, self).form_valid(form=form) - - queryset = self.get_queryset() - - if self.action_count == 1: - return HttpResponseRedirect( - redirect_to=reverse( - viewname='metadata:document_metadata_edit', - kwargs={'document_id': queryset.first().pk} - ) - ) - elif self.action_count > 1: - return HttpResponseRedirect( - redirect_to='%s?%s' % ( - reverse( - viewname='metadata:document_multiple_metadata_edit' - ), urlencode( - { - 'id_list': ','.join( - map( - force_text, queryset.values_list( - 'pk', flat=True - ) - ) - ) - } - ) - ) - ) - - return result - def get_extra_context(self): - queryset = self.get_queryset() + queryset = self.get_object_list() id_list = ','.join( map( @@ -328,7 +263,7 @@ class DocumentMetadataEditView(MultipleObjectFormActionView): return result def get_initial(self): - queryset = self.get_queryset() + queryset = self.get_object_list() metadata_dict = {} initial = [] @@ -355,6 +290,22 @@ class DocumentMetadataEditView(MultipleObjectFormActionView): return initial + def get_post_object_action_url(self): + if self.action_count == 1: + return reverse( + viewname='metadata:document_metadata_edit', + kwargs={'document_id': self.action_id_list[0]} + ) + + elif self.action_count > 1: + url = furl( + path=reverse( + viewname='metadata:document_multiple_metadata_edit' + ), args={'id_list': self.action_id_list} + ) + + return url.tostr() + def object_action(self, form, instance): errors = [] for form in form.forms: @@ -393,25 +344,20 @@ class DocumentMetadataEditView(MultipleObjectFormActionView): ) -class DocumentMetadataListView(SingleObjectListView): - def get_document(self): - queryset = AccessControlList.objects.restrict_queryset( - permission=permission_document_metadata_view, - queryset=Document.objects.all(), - user=self.request.user - ) - - return get_object_or_404(klass=queryset, pk=self.kwargs['document_id']) +class DocumentMetadataListView(ExternalObjectMixin, SingleObjectListView): + external_object_class = Document + external_object_permission = permission_metadata_view + external_object_pk_url_kwarg = 'document_id' def get_extra_context(self): - document = self.get_document() + return { 'hide_object': True, - 'object': document, + 'object': self.external_object, 'no_results_icon': icon_metadata, 'no_results_main_link': link_document_metadata_add.resolve( context=RequestContext( - request=self.request, dict_={'object': document} + request=self.request, dict_={'object': self.external_object} ) ), 'no_results_text': _( @@ -421,17 +367,17 @@ class DocumentMetadataListView(SingleObjectListView): 'values.' ), 'no_results_title': _('This document doesn\'t have any metadata'), - 'title': _('Metadata for document: %s') % document, + 'title': _('Metadata for document: %s') % self.external_object, } - def get_object_list(self): - return self.get_document().metadata.all() + def get_source_queryset(self): + return self.external_object.metadata.all() -class DocumentMetadataRemoveView(MultipleObjectFormActionView): +class DocumentMetadataRemoveView(DocumentMetadataSameTypeMixin, MultipleObjectFormActionView): form_class = DocumentMetadataRemoveFormSet model = Document - object_permission = permission_document_metadata_remove + object_permission = permission_metadata_remove pk_url_kwarg = 'document_id' success_message = _( 'Metadata remove request performed on %(count)d document' @@ -440,60 +386,8 @@ class DocumentMetadataRemoveView(MultipleObjectFormActionView): 'Metadata remove request performed on %(count)d documents' ) - def dispatch(self, request, *args, **kwargs): - result = super( - DocumentMetadataRemoveView, self - ).dispatch(request, *args, **kwargs) - - queryset = self.get_queryset() - - for document in queryset: - document.add_as_recent_document_for_user(request.user) - - if len(set([document.document_type.pk for document in queryset])) > 1: - messages.error( - message=_( - 'Selected documents must be of the same type.' - ), request=request - ) - return HttpResponseRedirect(redirect_to=self.previous_url) - - return result - - def form_valid(self, form): - result = super(DocumentMetadataRemoveView, self).form_valid(form=form) - - queryset = self.get_queryset() - - if self.action_count == 1: - return HttpResponseRedirect( - redirect_to=reverse( - viewname='metadata:document_metadata_edit', - kwargs={'document_id': queryset.first().pk} - ) - ) - elif self.action_count > 1: - return HttpResponseRedirect( - redirect_to='%s?%s' % ( - reverse( - viewname='metadata:document_multiple_metadata_edit' - ), urlencode( - { - 'id_list': ','.join( - map( - force_text, - queryset.values_list('pk', flat=True) - ) - ) - } - ) - ) - ) - - return result - def get_extra_context(self): - queryset = self.get_queryset() + queryset = self.get_object_list() result = { 'form_display_mode_table': True, @@ -518,7 +412,7 @@ class DocumentMetadataRemoveView(MultipleObjectFormActionView): return result def get_initial(self): - queryset = self.get_queryset() + queryset = self.get_object_list() metadata = {} for document in queryset: @@ -545,6 +439,21 @@ class DocumentMetadataRemoveView(MultipleObjectFormActionView): ) return initial + def get_post_object_action_url(self): + if self.action_count == 1: + return reverse( + viewname='metadata:document_metadata_edit', + kwargs={'document_id': self.action_id_list[0]} + ) + elif self.action_count > 1: + url = furl( + path=reverse( + viewname='metadata:document_multiple_metadata_edit' + ), args={'id_list': self.action_id_list} + ) + + return url.tostr() + def object_action(self, form, instance): for form in form.forms: if form.cleaned_data['update']: @@ -651,15 +560,18 @@ class MetadataTypeListView(SingleObjectListView): 'title': _('Metadata types'), } - def get_object_list(self): + def get_source_queryset(self): return MetadataType.objects.all() -class SetupDocumentTypeMetadataTypes(FormView): +class SetupDocumentTypeMetadataTypes(ExternalObjectMixin, FormView): + external_object_class = DocumentType + external_object_permission = permission_metadata_type_edit + external_object_pk_url_kwarg = 'document_type_id' form_class = DocumentTypeMetadataTypeRelationshipFormSet main_model = 'document_type' - model = DocumentType submodel = MetadataType + submodel_permission = permission_metadata_type_edit def form_valid(self, form): try: @@ -693,10 +605,10 @@ class SetupDocumentTypeMetadataTypes(FormView): 'to this document type.' ), 'no_results_title': _('There are no metadata types available'), - 'object': self.get_object(), + 'object': self.external_object, 'title': _( 'Metadata types for document type: %s' - ) % self.get_object() + ) % self.external_object } def get_form_extra_kwargs(self): @@ -705,44 +617,36 @@ class SetupDocumentTypeMetadataTypes(FormView): } def get_initial(self): - obj = self.get_object() initial = [] - for element in self.get_queryset(): + for element in self.get_object_list(): initial.append( { - 'document_type': obj, + 'document_type': self.external_object, 'main_model': self.main_model, 'metadata_type': element, } ) return initial - def get_object(self): - queryset = AccessControlList.objects.restrict_queryset( - permission=permission_metadata_type_edit, - queryset=self.model.objects.all(), + def get_object_list(self): + return AccessControlList.objects.restrict_queryset( + permission=self.submodel_permission, + queryset=self.submodel._meta.default_manager.all(), user=self.request.user ) - return get_object_or_404( - klass=queryset, pk=self.kwargs['document_type_id'] - ) def get_post_action_redirect(self): return reverse(viewname='documents:document_type_list') - def get_queryset(self): - queryset = self.submodel.objects.all() - return AccessControlList.objects.restrict_queryset( - permission=permission_document_type_edit, - user=self.request.user, queryset=queryset - ) - class SetupMetadataTypesDocumentTypes(SetupDocumentTypeMetadataTypes): + external_object_class = MetadataType + external_object_permission = permission_metadata_type_edit + external_object_pk_url_kwarg = 'metadata_type_id' main_model = 'metadata_type' - model = MetadataType submodel = DocumentType + submodel_permission = permission_document_type_edit def get_extra_context(self): return { @@ -754,28 +658,17 @@ class SetupMetadataTypesDocumentTypes(SetupDocumentTypeMetadataTypes): } def get_initial(self): - obj = self.get_object() initial = [] - for element in self.get_queryset(): + for element in self.get_object_list(): initial.append( { 'document_type': element, 'main_model': self.main_model, - 'metadata_type': obj, + 'metadata_type': self.external_object, } ) return initial - def get_object(self): - queryset = AccessControlList.objects.restrict_queryset( - permission=permission_metadata_type_edit, - queryset=self.model.objects.all(), - user=self.request.user - ) - return get_object_or_404( - klass=queryset, pk=self.kwargs['metadata_type_id'] - ) - def get_post_action_redirect(self): return reverse(viewname='metadata:metadata_type_list') From 0918931713903fd3555de6c09dba0f532c943c32 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sun, 3 Feb 2019 23:42:41 -0400 Subject: [PATCH 104/209] Add test mixin to generate random document types Signed-off-by: Roberto Rosario --- mayan/apps/documents/tests/literals.py | 1 + mayan/apps/documents/tests/mixins.py | 19 ++++++++++++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/mayan/apps/documents/tests/literals.py b/mayan/apps/documents/tests/literals.py index 5b4f3edcf6..b4ed159c7f 100644 --- a/mayan/apps/documents/tests/literals.py +++ b/mayan/apps/documents/tests/literals.py @@ -34,6 +34,7 @@ TEST_DOCUMENT_TYPE_2_LABEL = 'test document type 2' TEST_DOCUMENT_TYPE_LABEL_EDITED = 'test document type edited label' TEST_DOCUMENT_TYPE_QUICK_LABEL = 'test quick label' TEST_DOCUMENT_TYPE_QUICK_LABEL_EDITED = 'test quick label edited' +TEST_DOCUMENT_TYPE_RANDOM_LABEL_LENGTH = 16 TEST_DOCUMENT_VERSION_COMMENT_EDITED = 'test document version comment edited' TEST_HYBRID_DOCUMENT = 'hybrid_text_and_image.pdf' TEST_MULTI_PAGE_TIFF = 'multi_page.tiff' diff --git a/mayan/apps/documents/tests/mixins.py b/mayan/apps/documents/tests/mixins.py index 52d423578d..f85d5b9c96 100644 --- a/mayan/apps/documents/tests/mixins.py +++ b/mayan/apps/documents/tests/mixins.py @@ -1,6 +1,8 @@ from __future__ import unicode_literals import os +import random +import string import time from django.conf import settings @@ -9,7 +11,7 @@ from ..models import DocumentType from .literals import ( TEST_DOCUMENT_TYPE_LABEL, TEST_DOCUMENT_TYPE_QUICK_LABEL, - TEST_SMALL_DOCUMENT_FILENAME + TEST_DOCUMENT_TYPE_RANDOM_LABEL_LENGTH, TEST_SMALL_DOCUMENT_FILENAME ) __all__ = ('DocumentTestMixin',) @@ -22,10 +24,25 @@ class DocumentTestMixin(object): test_document_path = None use_document_stub = False + @staticmethod + def _get_random_alphanumeric_string(length): + return ''.join( + [random.choice(string.ascii_letters + string.digits) for _ in range(length)] + ) + def _create_document_type(self): self.document_type = DocumentType.objects.create( label=TEST_DOCUMENT_TYPE_LABEL ) + self.test_document_type = self.document_type + + def _create_document_type_random(self): + self.document_type = DocumentType.objects.create( + label=DocumentTestMixin._get_random_alphanumeric_string( + length=TEST_DOCUMENT_TYPE_RANDOM_LABEL_LENGTH + ) + ) + self.test_document_type = self.document_type def _create_document(self, *args, **kwargs): """ From f93ae2f395d59220f36a80393fc4363812d9caff Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sun, 3 Feb 2019 23:43:34 -0400 Subject: [PATCH 105/209] Don't override success_url everytime Only override success_url if self.get_post_object_action_url() provides an alternative. Signed-off-by: Roberto Rosario --- mayan/apps/common/mixins.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/mayan/apps/common/mixins.py b/mayan/apps/common/mixins.py index 868052424d..0f62a23919 100644 --- a/mayan/apps/common/mixins.py +++ b/mayan/apps/common/mixins.py @@ -349,7 +349,11 @@ class ObjectActionMixin(object): request=self.request ) - self.success_url = self.get_post_object_action_url() + # Allow get_post_object_action_url to override the redirect URL with a + # calculated URL after all objects are processed. + success_url = self.get_post_object_action_url() + if success_url: + self.success_url = success_url class ObjectNameMixin(object): From 67cd01f5aed84121c5b4a06cf5543d691008ab04 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sun, 3 Feb 2019 23:44:29 -0400 Subject: [PATCH 106/209] Update permission variable name Signed-off-by: Roberto Rosario --- mayan/apps/user_management/tests/test_views.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mayan/apps/user_management/tests/test_views.py b/mayan/apps/user_management/tests/test_views.py index a3bb41ddaa..44e6214138 100644 --- a/mayan/apps/user_management/tests/test_views.py +++ b/mayan/apps/user_management/tests/test_views.py @@ -6,7 +6,7 @@ from django.contrib.auth.models import Group from mayan.apps.common.tests import GenericViewTestCase from mayan.apps.documents.tests import GenericDocumentViewTestCase from mayan.apps.metadata.models import MetadataType -from mayan.apps.metadata.permissions import permission_document_metadata_edit +from mayan.apps.metadata.permissions import permission_metadata_edit from mayan.apps.metadata.tests.literals import ( TEST_METADATA_TYPE_LABEL, TEST_METADATA_TYPE_NAME, ) @@ -388,7 +388,7 @@ class MetadataLookupIntegrationTestCase(GenericDocumentViewTestCase): self.metadata_type.save() self.document.metadata.create(metadata_type=self.metadata_type) self.grant_access( - obj=self.document, permission=permission_document_metadata_edit + obj=self.document, permission=permission_metadata_edit ) response = self.get( @@ -406,7 +406,7 @@ class MetadataLookupIntegrationTestCase(GenericDocumentViewTestCase): self.metadata_type.save() self.document.metadata.create(metadata_type=self.metadata_type) self.grant_access( - obj=self.document, permission=permission_document_metadata_edit + obj=self.document, permission=permission_metadata_edit ) response = self.get( From bd12d587ee8aff8a8f09940748fea14d8add1e90 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Tue, 5 Feb 2019 05:40:01 -0400 Subject: [PATCH 107/209] Refactor document indexing app Convert half the widget to HTML widgets. Rename links and views to use the nomeclature _template_ and _instance_ to differenciate between index instances and index templates. Update URL parameters to use the "_id" form. Add more tests. Add model permission inheritance to the IndexTemplateNode, and IndexInstanceNode models. Remove the level and document count display from the instance node. Display instead the total items. Use a FilteredSelectionForm subclass to display the list of index templates to rebuild. Add missing icons. Add keyword arguments to links. Modernize tests to use the document test mixin. Update the permission requirements for the index template document type selection screen. The document type view permission is now required in addition to the index template edit permission. Use ExternalObjectMixin to reduce the code in all views. Signed-off-by: Roberto Rosario --- HISTORY.rst | 4 + mayan/apps/document_indexing/apps.py | 102 ++-- mayan/apps/document_indexing/forms.py | 27 +- .../{widgets.py => html_widgets.py} | 70 +-- mayan/apps/document_indexing/icons.py | 35 +- mayan/apps/document_indexing/links.py | 103 ++-- mayan/apps/document_indexing/models.py | 6 +- mayan/apps/document_indexing/tasks.py | 4 +- .../index_instance_node.html | 13 + .../index_template_node_indentation.html | 11 + .../document_indexing/node_details.html | 1 - .../apps/document_indexing/tests/literals.py | 2 + mayan/apps/document_indexing/tests/mixins.py | 29 +- .../document_indexing/tests/test_models.py | 111 ++-- .../document_indexing/tests/test_views.py | 463 +++++++++++---- mayan/apps/document_indexing/urls.py | 120 ++-- mayan/apps/document_indexing/views.py | 526 ++++++++---------- 17 files changed, 947 insertions(+), 680 deletions(-) rename mayan/apps/document_indexing/{widgets.py => html_widgets.py} (52%) create mode 100644 mayan/apps/document_indexing/templates/document_indexing/index_instance_node.html create mode 100644 mayan/apps/document_indexing/templates/document_indexing/index_template_node_indentation.html diff --git a/HISTORY.rst b/HISTORY.rst index 22eb460148..4adcf98872 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -233,6 +233,10 @@ entire converter class is no longer cached and instead loaded on demand. This allows the garbage collector to clear the memory used. +- Update the permission requirements for the index template + document type selection screen. The document type view + permission is now required in addition to the index + template edit permission. 3.1.9 (2018-11-01) ================== diff --git a/mayan/apps/document_indexing/apps.py b/mayan/apps/document_indexing/apps.py index b6dc7a35b8..b252b699f7 100644 --- a/mayan/apps/document_indexing/apps.py +++ b/mayan/apps/document_indexing/apps.py @@ -27,23 +27,25 @@ from .handlers import ( handler_index_document, handler_post_save_index_document, handler_remove_document ) +from .html_widgets import ( + IndexInstanceNodeWidget, IndexTemplateNodeIndentationWidget, + get_instance_link +) from .licenses import * # NOQA from .links import ( - link_document_index_list, link_index_main_menu, link_index_setup, - link_index_setup_create, link_index_setup_delete, - link_index_setup_document_types, link_index_setup_edit, - link_index_setup_list, link_index_setup_view, link_rebuild_index_instances, - link_template_node_create, link_template_node_delete, - link_template_node_edit + link_document_index_instance_list, link_index_instances_rebuild, + link_index_main_menu, link_index_setup, link_index_template_create, + link_index_template_delete, link_index_template_document_types, + link_index_template_edit, link_index_template_list, + link_index_template_view, link_index_template_node_create, + link_index_template_node_delete, link_index_template_node_edit ) from .permissions import ( permission_document_indexing_create, permission_document_indexing_delete, - permission_document_indexing_edit, - permission_document_indexing_instance_view, + permission_document_indexing_edit, permission_document_indexing_instance_view, permission_document_indexing_rebuild, permission_document_indexing_view ) from .queues import * # NOQA -from .widgets import get_instance_link, index_instance_item_link, node_level class DocumentIndexingApp(MayanAppConfig): @@ -86,53 +88,63 @@ class DocumentIndexingApp(MayanAppConfig): ) ) - SourceColumn(attribute='label', is_identifier=True, source=Index) - SourceColumn(attribute='slug', source=Index) + ModelPermission.register_inheritance( + model=IndexTemplateNode, related='index' + ) + + ModelPermission.register_inheritance( + model=IndexInstanceNode, related='index_template_node__index' + ) + SourceColumn( - attribute='enabled', source=Index, widget=TwoStateWidget + attribute='label', is_identifier=True, is_sortable=True, source=Index + ) + SourceColumn( + attribute='slug', include_label=True, is_sortable=True, source=Index + ) + SourceColumn( + attribute='enabled', include_label=True, is_sortable=True, + source=Index, widget=TwoStateWidget ) SourceColumn( func=lambda context: context[ 'object' - ].instance_root.get_descendants_count(), label=_('Total levels'), - source=IndexInstance, + ].instance_root.get_descendants_count(), include_label=True, + label=_('Total levels'), source=IndexInstance, ) SourceColumn( func=lambda context: context[ 'object' ].instance_root.get_descendants_document_count( user=context['request'].user - ), label=_('Total documents'), source=IndexInstance + ), include_label=True, label=_('Total documents'), source=IndexInstance + ) + SourceColumn( + label=_('Level'), is_identifier=True, source=IndexTemplateNode, + widget=IndexTemplateNodeIndentationWidget + ) + SourceColumn( + attribute='enabled', include_label=True, is_sortable=True, + source=IndexTemplateNode, widget=TwoStateWidget + ) + SourceColumn( + attribute='link_documents', include_label=True, + is_sortable=True, source=IndexTemplateNode, widget=TwoStateWidget ) SourceColumn( - func=lambda context: node_level(context['object']), - label=_('Level'), source=IndexTemplateNode - ) - SourceColumn( - attribute='enabled', source=IndexTemplateNode, - widget=TwoStateWidget - ) - SourceColumn( - attribute='link_documents', source=IndexTemplateNode, - widget=TwoStateWidget + is_identifier=True, is_sortable=True, label=_('Level'), + sort_field='value', source=IndexInstanceNode, + widget=IndexInstanceNodeWidget ) - SourceColumn( - func=lambda context: index_instance_item_link(context['object']), - label=_('Level'), source=IndexInstanceNode - ) - SourceColumn( - func=lambda context: context['object'].get_descendants_count(), - label=_('Levels'), source=IndexInstanceNode - ) SourceColumn( func=lambda context: context[ 'object' - ].get_descendants_document_count( + ].get_item_count( user=context['request'].user - ), label=_('Documents'), source=IndexInstanceNode + ), include_label=True, label=_('Items'), source=IndexInstanceNode ) SourceColumn( @@ -174,35 +186,35 @@ class DocumentIndexingApp(MayanAppConfig): ) menu_facet.bind_links( - links=(link_document_index_list,), sources=(Document,) + links=(link_document_index_instance_list,), sources=(Document,) ) menu_list_facet.bind_links( links=( - link_acl_list, link_index_setup_document_types, - link_index_setup_view, + link_acl_list, link_index_template_document_types, + link_index_template_view, ), sources=(Index,) ) menu_object.bind_links( links=( - link_index_setup_edit, link_index_setup_delete + link_index_template_edit, link_index_template_delete ), sources=(Index,) ) menu_object.bind_links( links=( - link_template_node_create, link_template_node_edit, - link_template_node_delete + link_index_template_node_create, link_index_template_node_edit, + link_index_template_node_delete ), sources=(IndexTemplateNode,) ) menu_main.bind_links(links=(link_index_main_menu,), position=98) menu_secondary.bind_links( - links=(link_index_setup_list, link_index_setup_create), + links=(link_index_template_list, link_index_template_create), sources=( - Index, 'indexing:index_setup_list', - 'indexing:index_setup_create' + Index, 'indexing:index_template_list', + 'indexing:index_template_create' ) ) menu_setup.bind_links(links=(link_index_setup,)) - menu_tools.bind_links(links=(link_rebuild_index_instances,)) + menu_tools.bind_links(links=(link_index_instances_rebuild,)) post_delete.connect( dispatch_uid='document_indexing_handler_delete_empty', diff --git a/mayan/apps/document_indexing/forms.py b/mayan/apps/document_indexing/forms.py index c9e2455d7f..790a5e6e2e 100644 --- a/mayan/apps/document_indexing/forms.py +++ b/mayan/apps/document_indexing/forms.py @@ -4,30 +4,23 @@ from django import forms from django.utils.encoding import force_text from django.utils.translation import ugettext_lazy as _ -from mayan.apps.acls.models import AccessControlList from mayan.apps.common.classes import ModelProperty +from mayan.apps.common.forms import FilteredSelectionForm from mayan.apps.documents.models import Document from .models import Index, IndexTemplateNode from .permissions import permission_document_indexing_rebuild -class IndexListForm(forms.Form): - indexes = forms.ModelMultipleChoiceField( - help_text=_('Indexes to be queued for rebuilding.'), - label=_('Indexes'), queryset=Index.objects.none(), - required=False, widget=forms.widgets.CheckboxSelectMultiple() - ) - - def __init__(self, *args, **kwargs): - user = kwargs.pop('user') - super(IndexListForm, self).__init__(*args, **kwargs) - queryset = AccessControlList.objects.restrict_queryset( - permission=permission_document_indexing_rebuild, - queryset=Index.objects.filter(enabled=True), - user=user - ) - self.fields['indexes'].queryset = queryset +class IndexTemplateFilteredForm(FilteredSelectionForm): + class Meta: + allow_multiple = True + field_name = 'index_templates' + help_text = _('Index templates to be queued for rebuilding.') + label = _('Index templates') + queryset = Index.objects.filter(enabled=True) + permission = permission_document_indexing_rebuild + widget_attributes = {'class': 'select2'} class IndexTemplateNodeForm(forms.ModelForm): diff --git a/mayan/apps/document_indexing/widgets.py b/mayan/apps/document_indexing/html_widgets.py similarity index 52% rename from mayan/apps/document_indexing/widgets.py rename to mayan/apps/document_indexing/html_widgets.py index d55f834c27..803810d295 100644 --- a/mayan/apps/document_indexing/widgets.py +++ b/mayan/apps/document_indexing/html_widgets.py @@ -1,10 +1,33 @@ from __future__ import unicode_literals -from django.apps import apps -from django.utils.encoding import force_text +from django.template.loader import render_to_string from django.utils.html import escape, mark_safe -from .icons import icon_index, icon_index_level_up, icon_node_with_documents +from .icons import ( + icon_index, icon_index_level_up, + icon_index_instance_node_with_documents +) + + +class IndexInstanceNodeWidget(object): + def render(self, name, value): + return render_to_string( + template_name='document_indexing/index_instance_node.html', + context={ + 'index_instance_node': value, + } + ) + + +class IndexTemplateNodeIndentationWidget(object): + def render(self, name, value): + return render_to_string( + template_name='document_indexing/index_template_node_indentation.html', + context={ + 'index_template_node': value, + 'index_template_node_level': range(value.get_level()), + } + ) def get_instance_link(index_instance_node): @@ -19,45 +42,6 @@ def get_instance_link(index_instance_node): ) -def index_instance_item_link(index_instance_item): - #TODO: Replace with a file template - IndexInstanceNode = apps.get_model( - app_label='document_indexing', model_name='IndexInstanceNode' - ) - - if isinstance(index_instance_item, IndexInstanceNode): - if index_instance_item.index_template_node.link_documents: - icon = icon_node_with_documents.render() - else: - icon = icon_index_level_up.render() - else: - icon = '' - - return mark_safe( - s='%(icon)s %(text)s' % { - 'url': index_instance_item.get_absolute_url(), - 'icon': icon, - 'text': index_instance_item - } - ) - - -def node_level(node): - """ - Render an indented tree like output for a specific node - """ - #TODO: Replace with a file template - return mark_safe( - s=''.join( - [ - '     ' * node.get_level(), - '' if node.is_root_node() else icon_index_level_up.render(), - force_text(node) - ] - ) - ) - - def node_tree(node, user): #TODO: Replace with a file template result = [] @@ -71,7 +55,7 @@ def node_tree(node, user): else: element = ancestor if element.index_template_node.link_documents: - icon = icon_node_with_documents + icon = icon_index_instance_node_with_documents else: icon = icon_index_level_up diff --git a/mayan/apps/document_indexing/icons.py b/mayan/apps/document_indexing/icons.py index 9c6724aabb..8041935456 100644 --- a/mayan/apps/document_indexing/icons.py +++ b/mayan/apps/document_indexing/icons.py @@ -2,14 +2,37 @@ from __future__ import absolute_import, unicode_literals from mayan.apps.appearance.classes import Icon -icon_document_index_list = Icon(driver_name='fontawesome', symbol='list-ul') +icon_document_index_instance_list = Icon( + driver_name='fontawesome', symbol='list-ul' +) +icon_index = Icon(driver_name='fontawesome', symbol='list-ul') icon_index_level_up = Icon( driver_name='fontawesomecss', css_classes='fa-level-up-alt fa-rotate-90' ) -icon_index = Icon(driver_name='fontawesome', symbol='list-ul') -icon_index_create = Icon(driver_name='fontawesome', symbol='plus') -icon_index_setup_view = Icon(driver_name='fontawesome', symbol='folder-open') -icon_node_with_documents = Icon(driver_name='fontawesome', symbol='folder') -icon_rebuild_index_instances = Icon( +icon_index_instance_node_with_documents = Icon( + driver_name='fontawesome', symbol='folder' +) +icon_index_instances_rebuild = Icon( driver_name='fontawesome', symbol='list-ul' ) +icon_index_template_create = Icon( + driver_name='fontawesome-dual', primary_symbol='list-ul', + secondary_symbol='plus' +) + +icon_index_template_delete = Icon(driver_name='fontawesome', symbol='times') +icon_index_template_edit = Icon(driver_name='fontawesome', symbol='pencil-alt') +icon_index_template_list = Icon(driver_name='fontawesome', symbol='list-ul') + +icon_index_template_node_create = Icon( + driver_name='fontawesomecss', css_classes='fa-level-up-alt fa-rotate-90' +) + +icon_index_template_node_delete = Icon( + driver_name='fontawesome', symbol='times' +) +icon_index_template_node_edit = Icon( + driver_name='fontawesome', symbol='pencil-alt' +) + +icon_index_template_view = Icon(driver_name='fontawesome', symbol='folder-open') diff --git a/mayan/apps/document_indexing/links.py b/mayan/apps/document_indexing/links.py index cde0f1ab43..0ccce73e73 100644 --- a/mayan/apps/document_indexing/links.py +++ b/mayan/apps/document_indexing/links.py @@ -6,8 +6,11 @@ from mayan.apps.documents.icons import icon_document_type from mayan.apps.navigation import Link, get_cascade_condition from .icons import ( - icon_document_index_list, icon_index, icon_index_create, - icon_index_setup_view, icon_rebuild_index_instances + icon_document_index_instance_list, icon_index, icon_index_template_create, + icon_index_template_delete, icon_index_template_edit, + icon_index_template_list, icon_index_template_node_create, + icon_index_template_node_delete, icon_index_template_node_edit, + icon_index_template_view, icon_index_instances_rebuild ) from .permissions import ( permission_document_indexing_create, permission_document_indexing_delete, @@ -17,20 +20,30 @@ from .permissions import ( ) -def is_not_root_node(context): +def condition_is_not_root_node(context): return not context['resolved_object'].is_root_node() -link_document_index_list = Link( - args='resolved_object.pk', icon_class=icon_document_index_list, - text=_('Indexes'), view='indexing:document_index_list', +link_document_index_instance_list = Link( + icon_class=icon_document_index_instance_list, + kwargs={'document_id': 'resolved_object.pk'}, text=_('Indexes'), + view='indexing:document_index_instace_list', +) +link_index_instances_rebuild = Link( + condition=get_cascade_condition( + app_label='document_indexing', model_name='Index', + object_permission=permission_document_indexing_rebuild, + ), icon_class=icon_index_instances_rebuild, + description=_( + 'Deletes and creates from scratch all the document indexes.' + ), text=_('Rebuild indexes'), view='indexing:index_instances_rebuild' ) link_index_main_menu = Link( condition=get_cascade_condition( app_label='document_indexing', model_name='Index', object_permission=permission_document_indexing_instance_view, ), icon_class=icon_index, text=_('Indexes'), - view='indexing:index_list' + view='indexing:index_instance_list' ) link_index_setup = Link( condition=get_cascade_condition( @@ -38,55 +51,53 @@ link_index_setup = Link( object_permission=permission_document_indexing_view, view_permission=permission_document_indexing_create, ), icon_class=icon_index, text=_('Indexes'), - view='indexing:index_setup_list' + view='indexing:index_template_list' ) -link_index_setup_list = Link( - text=_('Indexes'), view='indexing:index_setup_list' +link_index_template_list = Link( + icon_class=icon_index_template_list, + text=_('Indexes'), view='indexing:index_template_list' ) -link_index_setup_create = Link( - icon_class=icon_index_create, +link_index_template_create = Link( + icon_class=icon_index_template_create, permission=permission_document_indexing_create, text=_('Create index'), - view='indexing:index_setup_create' + view='indexing:index_template_create' ) -link_index_setup_edit = Link( - args='resolved_object.pk', - permission=permission_document_indexing_edit, text=_('Edit'), - view='indexing:index_setup_edit', -) -link_index_setup_delete = Link( - args='resolved_object.pk', +link_index_template_delete = Link( + icon_class=icon_index_template_delete, + kwargs={'index_template_id': 'resolved_object.pk'}, permission=permission_document_indexing_delete, tags='dangerous', - text=_('Delete'), view='indexing:index_setup_delete', + text=_('Delete'), view='indexing:index_template_delete' ) -link_index_setup_view = Link( - args='resolved_object.pk', icon_class=icon_index_setup_view, +link_index_template_edit = Link( + icon_class=icon_index_template_edit, + kwargs={'index_template_id': 'resolved_object.pk'}, + permission=permission_document_indexing_edit, text=_('Edit'), + view='indexing:index_template_edit' +) +link_index_template_view = Link( + kwargs={'index_template_id': 'resolved_object.pk'}, icon_class=icon_index_template_view, permission=permission_document_indexing_edit, text=_('Tree template'), - view='indexing:index_setup_view', + view='indexing:index_template_view' ) -link_index_setup_document_types = Link( - args='resolved_object.pk', icon_class=icon_document_type, +link_index_template_document_types = Link( + kwargs={'index_template_id': 'resolved_object.pk'}, icon_class=icon_document_type, permission=permission_document_indexing_edit, text=_('Document types'), - view='indexing:index_setup_document_types', + view='indexing:index_template_document_types' ) -link_rebuild_index_instances = Link( - condition=get_cascade_condition( - app_label='document_indexing', model_name='Index', - object_permission=permission_document_indexing_rebuild, - ), icon_class=icon_rebuild_index_instances, - description=_( - 'Deletes and creates from scratch all the document indexes.' - ), - text=_('Rebuild indexes'), view='indexing:rebuild_index_instances' +link_index_template_node_create = Link( + icon_class=icon_index_template_node_create, + kwargs={'index_template_node_id': 'resolved_object.pk'}, + text=_('New node'), view='indexing:index_template_node_create' ) -link_template_node_create = Link( - args='resolved_object.pk', text=_('New child node'), - view='indexing:template_node_create', +link_index_template_node_delete = Link( + condition=condition_is_not_root_node, + icon_class=icon_index_template_node_delete, + kwargs={'index_template_node_id': 'resolved_object.pk'}, tags='dangerous', + text=_('Delete'), view='indexing:index_template_node_delete' ) -link_template_node_edit = Link( - args='resolved_object.pk', condition=is_not_root_node, text=_('Edit'), - view='indexing:template_node_edit', -) -link_template_node_delete = Link( - args='resolved_object.pk', condition=is_not_root_node, tags='dangerous', - text=_('Delete'), view='indexing:template_node_delete', +link_index_template_node_edit = Link( + condition=condition_is_not_root_node, + icon_class=icon_index_template_node_edit, + kwargs={'index_template_node_id': 'resolved_object.pk'}, text=_('Edit'), + view='indexing:index_template_node_edit' ) diff --git a/mayan/apps/document_indexing/models.py b/mayan/apps/document_indexing/models.py index ee339d71a1..d5f9fdc5cf 100644 --- a/mayan/apps/document_indexing/models.py +++ b/mayan/apps/document_indexing/models.py @@ -66,7 +66,7 @@ class Index(models.Model): try: return reverse( viewname='indexing:index_instance_node_view', - kwargs={'index_instance_node_pk': self.instance_root.pk} + kwargs={'index_instance_node_id': self.instance_root.pk} ) except IndexInstanceNode.DoesNotExist: return '#' @@ -183,7 +183,7 @@ class IndexTemplateNode(MPTTModel): ) index = models.ForeignKey( on_delete=models.CASCADE, related_name='node_templates', to=Index, - verbose_name=_('Index') + verbose_name=_('Index template') ) expression = models.TextField( help_text=_( @@ -356,7 +356,7 @@ class IndexInstanceNode(MPTTModel): def get_absolute_url(self): return reverse( viewname='indexing:index_instance_node_view', - kwargs={'index_instance_node_pk': self.pk} + kwargs={'index_instance_node_id': self.pk} ) def get_children_count(self): diff --git a/mayan/apps/document_indexing/tasks.py b/mayan/apps/document_indexing/tasks.py index 506c6e243b..f14db26da6 100644 --- a/mayan/apps/document_indexing/tasks.py +++ b/mayan/apps/document_indexing/tasks.py @@ -58,13 +58,13 @@ def task_index_document(self, document_id): @app.task(bind=True, default_retry_delay=RETRY_DELAY, ignore_result=True) -def task_rebuild_index(self, index_id): +def task_rebuild_index(self, index_template_id): Index = apps.get_model( app_label='document_indexing', model_name='Index' ) try: - index = Index.objects.get(pk=index_id) + index = Index.objects.get(pk=index_template_id) index.rebuild() except LockError as exception: # This index is being rebuilt by another task, retry later diff --git a/mayan/apps/document_indexing/templates/document_indexing/index_instance_node.html b/mayan/apps/document_indexing/templates/document_indexing/index_instance_node.html new file mode 100644 index 0000000000..edd1b16155 --- /dev/null +++ b/mayan/apps/document_indexing/templates/document_indexing/index_instance_node.html @@ -0,0 +1,13 @@ +{% load appearance_tags %} + +{% get_icon 'mayan.apps.document_indexing.icons.icon_index_instance_node_with_documents' as icon_index_instance_node_with_documents %} +{% get_icon 'mayan.apps.document_indexing.icons.icon_index_level_up' as icon_index_level_up %} + + + {% if index_instance_node.index_template_node.link_documents %} + {{ icon_index_instance_node_with_documents }} + {% else %} + {{ icon_index_level_up }} + {% endif %} + {{ index_instance_node }} + diff --git a/mayan/apps/document_indexing/templates/document_indexing/index_template_node_indentation.html b/mayan/apps/document_indexing/templates/document_indexing/index_template_node_indentation.html new file mode 100644 index 0000000000..53bd36a9e8 --- /dev/null +++ b/mayan/apps/document_indexing/templates/document_indexing/index_template_node_indentation.html @@ -0,0 +1,11 @@ +{% load appearance_tags %} + +{% get_icon 'mayan.apps.document_indexing.icons.icon_index_level_up' as icon_index_level_up %} + +{% for level in index_template_node_level %} +       +{% endfor %} +{% if not index_template_node.is_root_node %} + {{ icon_index_level_up }} +{% endif %} +{{ index_template_node }} diff --git a/mayan/apps/document_indexing/templates/document_indexing/node_details.html b/mayan/apps/document_indexing/templates/document_indexing/node_details.html index a1ac26b6bf..31567ea8da 100644 --- a/mayan/apps/document_indexing/templates/document_indexing/node_details.html +++ b/mayan/apps/document_indexing/templates/document_indexing/node_details.html @@ -20,5 +20,4 @@ {% endif %} - {% endblock %} diff --git a/mayan/apps/document_indexing/tests/literals.py b/mayan/apps/document_indexing/tests/literals.py index e58a75cb4b..96cf041a06 100644 --- a/mayan/apps/document_indexing/tests/literals.py +++ b/mayan/apps/document_indexing/tests/literals.py @@ -9,3 +9,5 @@ TEST_METADATA_VALUE = '0001' TEST_INDEX_TEMPLATE_METADATA_EXPRESSION = '{{{{ document.get_metadata("{}").value }}}}'.format(TEST_METADATA_TYPE_NAME) TEST_INDEX_TEMPLATE_DOCUMENT_LABEL_EXPRESSION = '{{ document.label }}' TEST_INDEX_TEMPLATE_DOCUMENT_DESCRIPTION_EXPRESSION = '{{ document.description }}' +TEST_INDEX_TEMPLATE_DOCUMENT_UUID_EXPRESSION = '{{ document.uuid }}' +TEST_INDEX_TEMPLATE_NODE_EXPRESSION_EDITED = 'expression edited' diff --git a/mayan/apps/document_indexing/tests/mixins.py b/mayan/apps/document_indexing/tests/mixins.py index 239b8de7dd..b0396dab28 100644 --- a/mayan/apps/document_indexing/tests/mixins.py +++ b/mayan/apps/document_indexing/tests/mixins.py @@ -2,13 +2,30 @@ from __future__ import unicode_literals from ..models import Index -from .literals import TEST_INDEX_LABEL +from .literals import ( + TEST_INDEX_LABEL, TEST_INDEX_TEMPLATE_DOCUMENT_LABEL_EXPRESSION +) -class DocumentIndexingTestMixin(object): - def _create_index(self): +class IndexTemplateTestMixin(object): + def _create_index_template(self, add_document_type=False): # Create empty index - self.index = Index.objects.create(label=TEST_INDEX_LABEL) + self.test_index_template = Index.objects.create(label=TEST_INDEX_LABEL) - # Add our document type to the new index - self.index.document_types.add(self.document_type) + if add_document_type: + # Add our document type to the new index + self.test_index_template.document_types.add(self.test_document_type) + + def _create_index_template_node(self, expression=None, rebuild=False): + self._create_index_template(add_document_type=True) + + expression = expression or TEST_INDEX_TEMPLATE_DOCUMENT_LABEL_EXPRESSION + + self.test_index_template_node = self.test_index_template.node_templates.create( + parent=self.test_index_template.template_root, + expression=expression, link_documents=True + ) + + # Rebuild indexes + if rebuild: + Index.objects.rebuild() diff --git a/mayan/apps/document_indexing/tests/test_models.py b/mayan/apps/document_indexing/tests/test_models.py index 3077e0dce3..acaff8e9a4 100644 --- a/mayan/apps/document_indexing/tests/test_models.py +++ b/mayan/apps/document_indexing/tests/test_models.py @@ -3,9 +3,7 @@ from __future__ import unicode_literals from django.utils.encoding import force_text from mayan.apps.common.tests import BaseTestCase -from mayan.apps.documents.tests import ( - TEST_SMALL_DOCUMENT_PATH, DocumentTestMixin -) +from mayan.apps.documents.tests import DocumentTestMixin from mayan.apps.documents.tests.literals import ( TEST_DOCUMENT_DESCRIPTION, TEST_DOCUMENT_DESCRIPTION_EDITED, TEST_DOCUMENT_LABEL_EDITED @@ -20,65 +18,54 @@ from .literals import ( TEST_INDEX_TEMPLATE_METADATA_EXPRESSION, TEST_METADATA_TYPE_LABEL, TEST_METADATA_TYPE_NAME, TEST_METADATA_VALUE ) -from .mixins import DocumentIndexingTestMixin +from .mixins import IndexTemplateTestMixin -class IndexTestCase(DocumentIndexingTestMixin, DocumentTestMixin, BaseTestCase): +class IndexTestCase(IndexTemplateTestMixin, DocumentTestMixin, BaseTestCase): def test_document_description_index(self): - self._create_index() - - self.index.node_templates.create( - parent=self.index.template_root, + self.test_document.description = TEST_DOCUMENT_DESCRIPTION + self.test_document.save() + self._create_index_template_node( expression=TEST_INDEX_TEMPLATE_DOCUMENT_DESCRIPTION_EXPRESSION, - link_documents=True + rebuild=True ) - self.document.description = TEST_DOCUMENT_DESCRIPTION - self.document.save() - - self.index.rebuild() - self.assertEqual( - IndexInstanceNode.objects.last().value, self.document.description + IndexInstanceNode.objects.last().value, self.test_document.description ) - self.document.description = TEST_DOCUMENT_DESCRIPTION_EDITED - self.document.save() + self.test_document.description = TEST_DOCUMENT_DESCRIPTION_EDITED + self.test_document.save() self.assertEqual( - IndexInstanceNode.objects.last().value, self.document.description + IndexInstanceNode.objects.last().value, self.test_document.description ) def test_document_label_index(self): - self._create_index() - - self.index.node_templates.create( - parent=self.index.template_root, + self._create_index_template_node( expression=TEST_INDEX_TEMPLATE_DOCUMENT_LABEL_EXPRESSION, - link_documents=True + rebuild=True ) - self.index.rebuild() - self.assertEqual( - IndexInstanceNode.objects.last().value, self.document.label + IndexInstanceNode.objects.last().value, self.test_document.label ) - self.document.label = TEST_DOCUMENT_LABEL_EDITED - self.document.save() + self.test_document.label = TEST_DOCUMENT_LABEL_EDITED + self.test_document.save() self.assertEqual( - IndexInstanceNode.objects.last().value, self.document.label + IndexInstanceNode.objects.last().value, self.test_document.label ) def test_date_based_index(self): - self._create_index() + self._create_index_template(add_document_type=True) - level_year = self.index.node_templates.create( - parent=self.index.template_root, + level_year = self.test_index_template.node_templates.create( + parent=self.test_index_template.template_root, expression='{{ document.date_added.year }}', link_documents=False ) - self.index.node_templates.create( + self.test_index_template.node_templates.create( parent=level_year, expression='{{ document.date_added.month }}', link_documents=True @@ -89,18 +76,18 @@ class IndexTestCase(DocumentIndexingTestMixin, DocumentTestMixin, BaseTestCase): self.document.delete() # Uploading a new should not trigger an error - document = self.upload_document() + self._create_document() self.assertEqual( - [instance.value for instance in IndexInstanceNode.objects.all().order_by('index_instance_node_pk')], + [instance.value for instance in IndexInstanceNode.objects.all()], [ - '', force_text(document.date_added.year), - force_text(document.date_added.month).zfill(2) + '', force_text(self.test_document.date_added.year), + force_text(self.test_document.date_added.month) ] ) self.assertTrue( - document in list(IndexInstanceNode.objects.order_by('index_instance_node_pk').last().documents.all()) + self.test_document in list(IndexInstanceNode.objects.last().documents.all()) ) def test_dual_level_dual_document_index(self): @@ -109,33 +96,33 @@ class IndexTestCase(DocumentIndexingTestMixin, DocumentTestMixin, BaseTestCase): values and two second levels with the same value but as separate children of each of the first levels. GitLab issue #391 """ - with open(TEST_SMALL_DOCUMENT_PATH, mode='rb') as file_object: - self.document_2 = self.document_type.new_document( - file_object=file_object - ) + self.document_2 = self.upload_document() - self._create_index() + self._create_index_template(add_document_type=True) # Create simple index template - root = self.index.template_root - level_1 = self.index.node_templates.create( + root = self.test_index_template.template_root + level_1 = self.test_index_template.node_templates.create( parent=root, expression='{{ document.uuid }}', link_documents=False ) - self.index.node_templates.create( + self.test_index_template.node_templates.create( parent=level_1, expression='{{ document.label }}', link_documents=True ) Index.objects.rebuild() + # Typecast to sets to make sorting irrelevant in the comparison. self.assertEqual( - [instance.value for instance in IndexInstanceNode.objects.all().order_by('index_instance_node_pk')], - [ - '', force_text(self.document_2.uuid), self.document_2.label, - force_text(self.document.uuid), self.document.label - ] + set(IndexInstanceNode.objects.values_list('value', flat=True)), + set( + [ + '', force_text(self.document_2.uuid), self.document_2.label, + force_text(self.document.uuid), self.document.label + ] + ) ) def test_metadata_indexing(self): @@ -146,11 +133,11 @@ class IndexTestCase(DocumentIndexingTestMixin, DocumentTestMixin, BaseTestCase): document_type=self.document_type, metadata_type=metadata_type ) - self._create_index() + self._create_index_template(add_document_type=True) # Create simple index template - root = self.index.template_root - self.index.node_templates.create( + root = self.test_index_template.template_root + self.test_index_template.node_templates.create( parent=root, expression=TEST_INDEX_TEMPLATE_METADATA_EXPRESSION, link_documents=True ) @@ -231,15 +218,15 @@ class IndexTestCase(DocumentIndexingTestMixin, DocumentTestMixin, BaseTestCase): On a two level template if the first level doesn't return a result the indexing should stop. GitLab issue #391. """ - self._create_index() + self._create_index_template(add_document_type=True) - level_1 = self.index.node_templates.create( - parent=self.index.template_root, + level_1 = self.test_index_template.node_templates.create( + parent=self.test_index_template.template_root, expression='', link_documents=True ) - self.index.node_templates.create( + self.test_index_template.node_templates.create( parent=level_1, expression='{{ document.label }}', link_documents=True ) @@ -260,11 +247,11 @@ class IndexTestCase(DocumentIndexingTestMixin, DocumentTestMixin, BaseTestCase): metadata_type=metadata_type, value=TEST_METADATA_VALUE ) - self._create_index() + self._create_index_template(add_document_type=True) # Create simple index template - root = self.index.template_root - self.index.node_templates.create( + root = self.test_index_template.template_root + self.test_index_template.node_templates.create( parent=root, expression=TEST_INDEX_TEMPLATE_METADATA_EXPRESSION, link_documents=True ) diff --git a/mayan/apps/document_indexing/tests/test_views.py b/mayan/apps/document_indexing/tests/test_views.py index 56fcd49c6f..9fee007312 100644 --- a/mayan/apps/document_indexing/tests/test_views.py +++ b/mayan/apps/document_indexing/tests/test_views.py @@ -1,5 +1,8 @@ from __future__ import absolute_import, unicode_literals +from mayan.apps.documents.permissions import ( + permission_document_view, permission_document_type_view +) from mayan.apps.documents.tests import GenericDocumentViewTestCase from ..models import Index @@ -7,23 +10,213 @@ from ..permissions import ( permission_document_indexing_create, permission_document_indexing_delete, permission_document_indexing_edit, permission_document_indexing_instance_view, - permission_document_indexing_rebuild + permission_document_indexing_rebuild, permission_document_indexing_view ) from .literals import ( TEST_INDEX_LABEL, TEST_INDEX_LABEL_EDITED, TEST_INDEX_SLUG, - TEST_INDEX_TEMPLATE_DOCUMENT_LABEL_EXPRESSION + TEST_INDEX_TEMPLATE_DOCUMENT_UUID_EXPRESSION, + TEST_INDEX_TEMPLATE_NODE_EXPRESSION_EDITED ) +from .mixins import IndexTemplateTestMixin -class IndexViewTestCase(GenericDocumentViewTestCase): - def setUp(self): - super(IndexViewTestCase, self).setUp() - self.login_user() +class IndexInstanceViewTestCase(IndexTemplateTestMixin, GenericDocumentViewTestCase): + def _request_index_instance_list_view(self): + return self.get( + viewname='indexing:index_instance_list', + ) + + def test_index_instance_list_view_no_permission(self): + self._create_index_template_node() + + response = self._request_index_instance_list_view() + + self.assertNotContains( + response=response, text=TEST_INDEX_LABEL, status_code=200 + ) + + def test_index_instance_list_view_with_access(self): + self._create_index_template_node(rebuild=True) + + self.grant_access( + obj=self.test_index_template, + permission=permission_document_indexing_instance_view + ) + + response = self._request_index_instance_list_view() + + self.assertContains( + response=response, text=TEST_INDEX_LABEL, status_code=200 + ) + + def _request_index_instance_node_view(self, index_instance_node=None): + index_instance_node = index_instance_node or self.test_index_template.instance_root + + return self.get( + viewname='indexing:index_instance_node_view', + kwargs={ + 'index_instance_node_id': index_instance_node.pk + } + ) + + def test_index_instance_node_view_no_permission(self): + self._create_index_template_node() + + response = self._request_index_instance_node_view() + + self.assertEqual(response.status_code, 404) + + def test_index_instance_node_view_with_access(self): + self._create_index_template_node() + + self.grant_access( + obj=self.test_index_template, + permission=permission_document_indexing_instance_view + ) + + response = self._request_index_instance_node_view() + + self.assertContains( + response=response, text=TEST_INDEX_LABEL, status_code=200 + ) + + def _test_index_instance_node_documents_view_base(self, index_access=False, document_access=False): + self._create_index_template_node( + expression=TEST_INDEX_TEMPLATE_DOCUMENT_UUID_EXPRESSION, + rebuild=True + ) + + if index_access: + self.grant_access( + obj=self.test_index_template, + permission=permission_document_indexing_instance_view + ) + + if document_access: + self.grant_access( + obj=self.test_document, + permission=permission_document_view + ) + + index_instance_node = self.test_index_template.instance_root.get_children().get( + value=self.test_document.uuid + ) + + return self._request_index_instance_node_view( + index_instance_node=index_instance_node + ) + + def test_index_instance_node_documents_view_no_permission(self): + response = self._test_index_instance_node_documents_view_base() + + self.assertNotContains( + response=response, text=TEST_INDEX_LABEL, status_code=404 + ) + self.assertNotContains( + response=response, text=self.test_document.label, status_code=404 + ) + + def test_index_instance_node_documents_view_with_index_access(self): + response = self._test_index_instance_node_documents_view_base( + index_access=True + ) + + self.assertContains( + response=response, text=TEST_INDEX_LABEL, status_code=200 + ) + self.assertNotContains( + response=response, text=self.test_document.label, status_code=200 + ) + + def test_index_instance_node_documents_view_with_document_access(self): + response = self._test_index_instance_node_documents_view_base( + document_access=True + ) + + self.assertNotContains( + response=response, text=TEST_INDEX_LABEL, status_code=404 + ) + self.assertNotContains( + response=response, text=self.test_document.label, status_code=404 + ) + + def test_index_instance_node_documents_view_with_full_access(self): + response = self._test_index_instance_node_documents_view_base( + index_access=True, document_access=True + ) + + self.assertContains( + response=response, text=TEST_INDEX_LABEL, status_code=200 + ) + self.assertContains( + response=response, text=self.test_document.label, status_code=200 + ) + + def _request_index_instances_rebuild_get_view(self): + return self.get( + viewname='indexing:index_instances_rebuild', + ) + + def _request_index_instances_rebuild_post_view(self): + return self.post( + viewname='indexing:index_instances_rebuild', data={ + 'index_templates': self.test_index_template.pk + } + ) + + def test_index_instances_rebuild_get_view_no_permission(self): + self._create_index_template_node(rebuild=False) + + response = self._request_index_instances_rebuild_get_view() + self.assertNotContains( + response=response, text=self.test_index_template.label, status_code=200 + ) + + def test_index_instances_rebuild_get_view_with_access(self): + self._create_index_template_node(rebuild=False) + + self.grant_access( + obj=self.test_index_template, permission=permission_document_indexing_rebuild + ) + + response = self._request_index_instances_rebuild_get_view() + self.assertContains( + response=response, text=self.test_index_template.label, status_code=200 + ) + + def test_index_instances_rebuild_post_view_no_permission(self): + self._create_index_template_node(rebuild=False) + + response = self._request_index_instances_rebuild_post_view() + # No error since we just don't see the index + self.assertEqual(response.status_code, 200) + + self.assertEqual( + self.test_index_template.instance_root.get_children_count(), 0 + ) + + def test_index_instances_rebuild_post_view_with_access(self): + self._create_index_template_node(rebuild=False) + + self.grant_access( + obj=self.test_index_template, permission=permission_document_indexing_rebuild + ) + + response = self._request_index_instances_rebuild_post_view() + self.assertEqual(response.status_code, 302) + + # An instance root exists + self.assertTrue(self.test_index_template.instance_root.pk) + + +class IndexTemplateViewTestCase(IndexTemplateTestMixin, GenericDocumentViewTestCase): + auto_create_document_type = False + auto_upload_document = False def _request_index_create_view(self): return self.post( - 'indexing:index_setup_create', data={ + 'indexing:index_template_create', data={ 'label': TEST_INDEX_LABEL, 'slug': TEST_INDEX_SLUG } ) @@ -31,6 +224,7 @@ class IndexViewTestCase(GenericDocumentViewTestCase): def test_index_create_view_no_permission(self): response = self._request_index_create_view() self.assertEquals(response.status_code, 403) + self.assertEqual(Index.objects.count(), 0) def test_index_create_view_with_permission(self): @@ -41,159 +235,218 @@ class IndexViewTestCase(GenericDocumentViewTestCase): response = self._request_index_create_view() self.assertEqual(response.status_code, 302) + self.assertEqual(Index.objects.count(), 1) self.assertEqual(Index.objects.first().label, TEST_INDEX_LABEL) - def _request_index_delete_view(self, index): + def _request_index_template_delete_view(self): return self.post( - viewname='indexing:index_setup_delete', - kwargs={'index_pk': index.pk} + viewname='indexing:index_template_delete', + kwargs={'index_template_id': self.test_index_template.pk} ) - def test_index_delete_view_no_permission(self): - index = Index.objects.create( - label=TEST_INDEX_LABEL, slug=TEST_INDEX_SLUG - ) + def test_index_template_delete_view_no_permission(self): + self._create_index_template() - response = self._request_index_delete_view(index=index) + response = self._request_index_template_delete_view() self.assertEqual(response.status_code, 404) + self.assertEqual(Index.objects.count(), 1) - def test_index_delete_view_with_permission(self): - self.role.permissions.add( - permission_document_indexing_delete.stored_permission + def test_index_template_delete_view_with_access(self): + self._create_index_template() + + self.grant_access( + obj=self.test_index_template, + permission=permission_document_indexing_delete ) - - index = Index.objects.create( - label=TEST_INDEX_LABEL, slug=TEST_INDEX_SLUG - ) - - response = self._request_index_delete_view(index=index) - + response = self._request_index_template_delete_view() self.assertEqual(response.status_code, 302) + self.assertEqual(Index.objects.count(), 0) - def _request_index_edit_view(self, index): + def _request_index_template_document_types_view(self): return self.post( - viewname='indexing:index_setup_edit', kwargs={ - 'index_pk': index.pk + viewname='indexing:index_template_document_types', + kwargs={'index_template_id': self.test_index_template.pk} + ) + + def test_index_template_document_types_view_no_permission(self): + self._create_document_type() + self._create_index_template() + + response = self._request_index_template_document_types_view() + self.assertEqual(response.status_code, 404) + + def test_index_template_document_types_view_with_index_access(self): + self._create_document_type() + self._create_index_template() + + self.grant_access( + obj=self.test_index_template, + permission=permission_document_indexing_edit + ) + response = self._request_index_template_document_types_view() + self.assertNotContains( + response=response, text=self.document_type.label, status_code=200 + ) + + def test_index_template_document_types_view_with_document_type_access(self): + self._create_document_type() + self._create_index_template() + + self.grant_access( + obj=self.test_document_type, + permission=permission_document_type_view + ) + response = self._request_index_template_document_types_view() + self.assertEqual(response.status_code, 404) + + def test_index_template_document_types_view_with_full_access(self): + self._create_document_type() + self._create_index_template() + + self.grant_access( + obj=self.test_index_template, + permission=permission_document_indexing_edit + ) + self.grant_access( + obj=self.test_document_type, + permission=permission_document_type_view + ) + response = self._request_index_template_document_types_view() + self.assertContains( + response=response, text=self.document_type.label, status_code=200 + ) + + def _request_index_edit_view(self): + return self.post( + viewname='indexing:index_template_edit', kwargs={ + 'index_template_id': self.test_index_template.pk }, data={ 'label': TEST_INDEX_LABEL_EDITED, 'slug': TEST_INDEX_SLUG } ) def test_index_edit_view_no_permission(self): - index = Index.objects.create( - label=TEST_INDEX_LABEL, slug=TEST_INDEX_SLUG - ) + self._create_index_template() - response = self._request_index_edit_view(index=index) + response = self._request_index_edit_view() self.assertEqual(response.status_code, 404) - index = Index.objects.get(pk=index.pk) - self.assertEqual(index.label, TEST_INDEX_LABEL) + + self.test_index_template.refresh_from_db() + self.assertEqual(self.test_index_template.label, TEST_INDEX_LABEL) def test_index_edit_view_with_access(self): - index = Index.objects.create( - label=TEST_INDEX_LABEL, slug=TEST_INDEX_SLUG - ) + self._create_index_template() self.grant_access( - permission=permission_document_indexing_edit, - obj=index + obj=self.test_index_template, + permission=permission_document_indexing_edit ) - response = self._request_index_edit_view(index=index) + response = self._request_index_edit_view() self.assertEqual(response.status_code, 302) - index.refresh_from_db() - self.assertEqual(index.label, TEST_INDEX_LABEL_EDITED) - def _create_index(self, rebuild=True): - # Create empty index - self.index = Index.objects.create(label=TEST_INDEX_LABEL) + self.test_index_template.refresh_from_db() + self.assertEqual(self.test_index_template.label, TEST_INDEX_LABEL_EDITED) - # Add our document type to the new index - self.index.document_types.add(self.document_type) + def _request_index_template_list_view(self): + return self.get(viewname='indexing:index_template_list') - # Create simple index template - root = self.index.template_root - self.index.node_templates.create( - parent=root, expression=TEST_INDEX_TEMPLATE_DOCUMENT_LABEL_EXPRESSION, - link_documents=True + def test_index_template_list_view_no_permission(self): + self._create_index_template() + + response = self._request_index_template_list_view() + self.assertNotContains( + response=response, text=self.test_index_template.label, status_code=200 ) - # Rebuild indexes - if rebuild: - Index.objects.rebuild() - - def _request_index_instance_node_view(self, index_instance_node): - return self.get( - viewname='indexing:index_instance_node_view', - kwargs={'index_instance_node_pk': index_instance_node.pk} - ) - - def test_index_instance_node_view_no_permission(self): - self._create_index() - - response = self._request_index_instance_node_view( - index_instance_node=self.index.instance_root - ) - - self.assertEqual(response.status_code, 403) - - def test_index_instance_node_view_with_access(self): - self._create_index() + def test_index_template_list_view_with_access(self): + self._create_index_template() self.grant_access( - permission=permission_document_indexing_instance_view, - obj=self.index - ) - - response = self._request_index_instance_node_view( - index_instance_node=self.index.instance_root + obj=self.test_index_template, + permission=permission_document_indexing_view ) + response = self._request_index_template_list_view() self.assertContains( - response=response, text=TEST_INDEX_LABEL, status_code=200 + response=response, text=self.test_index_template.label, status_code=200 ) - def _request_index_rebuild_get_view(self): - return self.get( - viewname='indexing:rebuild_index_instances', - ) - def _request_index_rebuild_post_view(self): +class IndexTemplaceNodeViewTestCase(IndexTemplateTestMixin, GenericDocumentViewTestCase): + auto_upload_document = False + + def _request_index_instance_node_delete_view(self): return self.post( - viewname='indexing:rebuild_index_instances', data={ - 'indexes': self.index.pk + viewname='indexing:index_template_node_delete', kwargs={ + 'index_template_node_id': self.test_index_template_node.pk } ) - def test_index_rebuild_no_permission(self): - self._create_index(rebuild=False) + def test_index_template_node_delete_view_no_permission(self): + self._create_index_template_node() - response = self._request_index_rebuild_get_view() - self.assertNotContains(response=response, text=self.index.label, status_code=200) + response = self._request_index_instance_node_delete_view() - response = self._request_index_rebuild_post_view() - # No error since we just don't see the index - self.assertEqual(response.status_code, 200) + self.assertEqual(response.status_code, 404) - self.assertEqual( - self.index.instance_root.get_children_count(), 0 - ) + # Root node plus the defaul test document label node + self.assertEqual(self.test_index_template.node_templates.count(), 2) - def test_index_rebuild_with_access(self): - self._create_index(rebuild=False) + def test_index_template_node_delete_view_with_access(self): + self._create_index_template_node() self.grant_access( - permission=permission_document_indexing_rebuild, obj=self.index + obj=self.test_index_template, + permission=permission_document_indexing_edit ) - response = self._request_index_rebuild_get_view() - self.assertContains(response=response, text=self.index.label, status_code=200) + response = self._request_index_instance_node_delete_view() - response = self._request_index_rebuild_post_view() self.assertEqual(response.status_code, 302) - # An instance root exists - self.assertTrue(self.index.instance_root.pk) + # Root node only + self.assertEqual(self.test_index_template.node_templates.count(), 1) + + def _request_index_instance_node_edit_view(self): + return self.post( + viewname='indexing:index_template_node_edit', kwargs={ + 'index_template_node_id': self.test_index_template_node.pk + }, data={ + 'expression': TEST_INDEX_TEMPLATE_NODE_EXPRESSION_EDITED, + 'index': self.test_index_template.pk, + 'parent': self.test_index_template_node.parent.pk, + + } + ) + + def test_index_template_node_edit_view_no_permission(self): + self._create_index_template_node() + original_expression = self.test_index_template_node.expression + + response = self._request_index_instance_node_edit_view() + self.assertEqual(response.status_code, 404) + + self.test_index_template_node.refresh_from_db() + self.assertEqual( + self.test_index_template_node.expression, original_expression + ) + + def test_index_template_node_edit_view_with_access(self): + self._create_index_template_node() + self.grant_access( + obj=self.test_index_template, + permission=permission_document_indexing_edit + ) + + response = self._request_index_instance_node_edit_view() + self.assertEqual(response.status_code, 302) + + self.test_index_template_node.refresh_from_db() + self.assertEqual( + self.test_index_template_node.expression, + TEST_INDEX_TEMPLATE_NODE_EXPRESSION_EDITED + ) diff --git a/mayan/apps/document_indexing/urls.py b/mayan/apps/document_indexing/urls.py index 5aa96bed20..0d97e64a6e 100644 --- a/mayan/apps/document_indexing/urls.py +++ b/mayan/apps/document_indexing/urls.py @@ -2,101 +2,109 @@ from __future__ import unicode_literals from django.conf.urls import url +""" from .api_views import ( - APIDocumentIndexListView, APIIndexListView, + APIDocumentIndexTemplateListView, APIIndexTemplateListView, APIIndexNodeInstanceDocumentListView, APIIndexTemplateListView, APIIndexTemplateView, APIIndexView ) +""" from .views import ( - DocumentIndexNodeListView, IndexInstanceNodeView, IndexListView, - RebuildIndexesView, SetupIndexCreateView, SetupIndexDeleteView, - SetupIndexDocumentTypesView, SetupIndexEditView, SetupIndexListView, - SetupIndexTreeTemplateListView, TemplateNodeCreateView, - TemplateNodeDeleteView, TemplateNodeEditView + DocumentIndexInstanceNodeListView, IndexInstanceNodeView, IndexInstancesRebuildView, + IndexInstanceView, IndexTemplateCreateView, IndexTemplateDeleteView, + IndexTemplateDocumentTypesView, IndexTemplateEditView, + IndexTemplateListView, IndexTemplateNodeCreateView, + IndexTemplateNodeDeleteView, IndexTemplateNodeEditView, + IndexTemplateNodeListView ) urlpatterns = [ url( - regex=r'^indexes/$', name='index_setup_list', - view=SetupIndexListView.as_view() + regex=r'^documents/(?P\d+)/indexes/$', + name='document_index_instance_list', + view=DocumentIndexInstanceNodeListView.as_view() ), url( - regex=r'^indexes/create/$', name='index_setup_create', - view=SetupIndexCreateView.as_view() + regex=r'^index_instances/$', name='index_instance_list', + view=IndexInstanceView.as_view() ), url( - regex=r'^indexes/(?P\d+)/delete/$', - name='index_setup_delete', view=SetupIndexDeleteView.as_view() - ), - url( - regex=r'^indexes/(?P\d+)/edit/$', - name='index_setup_edit', view=SetupIndexEditView.as_view() - ), - url( - regex=r'^indexes/(?P\d+)/templates/$', - name='index_setup_view', view=SetupIndexTreeTemplateListView.as_view() - ), - url( - regex=r'^indexes/(?P\d+)/document_types/$', - name='index_setup_document_types', - view=SetupIndexDocumentTypesView.as_view() - ), - url( - regex=r'^indexes/templates/nodes/(?P\d+)/create/child/$', - name='template_node_create', view=TemplateNodeCreateView.as_view() - ), - url( - regex=r'^indexes/templates/nodes/(?P\d+)/edit/$', - name='template_node_edit', view=TemplateNodeEditView.as_view() - ), - url( - regex=r'^indexes/templates/nodes/(?P\d+)/delete/$', - name='template_node_delete', view=TemplateNodeDeleteView.as_view() - ), - url( - regex=r'^indexes/instances/list/$', name='index_list', - view=IndexListView.as_view() - ), - url( - regex=r'^indexes/instances/node/(?P\d+)/$', + regex=r'^index_instances/nodes/(?P\d+)/$', name='index_instance_node_view', view=IndexInstanceNodeView.as_view() ), - url( - regex=r'^indexes/rebuild/$', name='rebuild_index_instances', - view=RebuildIndexesView.as_view() + regex=r'^index_instances/rebuild/$', name='index_instances_rebuild', + view=IndexInstancesRebuildView.as_view() ), url( - regex=r'^documents/(?P\d+)/indexes/$', - name='document_index_list', view=DocumentIndexNodeListView.as_view() + regex=r'^index_templates/$', name='index_template_list', + view=IndexTemplateListView.as_view() ), + url( + regex=r'^index_templates/create/$', name='index_template_create', + view=IndexTemplateCreateView.as_view() + ), + url( + regex=r'^index_templates/(?P\d+)/delete/$', + name='index_template_delete', view=IndexTemplateDeleteView.as_view() + ), + url( + regex=r'^index_templates/(?P\d+)/document_types/$', + name='index_template_document_types', + view=IndexTemplateDocumentTypesView.as_view() + ), + url( + regex=r'^index_templates/(?P\d+)/edit/$', + name='index_template_edit', view=IndexTemplateEditView.as_view() + ), + url( + regex=r'^index_templates/(?P\d+)/nodes/$', + name='index_template_view', view=IndexTemplateNodeListView.as_view() + ), + url( + regex=r'^index_templates/nodes/(?P\d+)/create/$', + name='index_template_node_create', + view=IndexTemplateNodeCreateView.as_view() + ), + url( + regex=r'^index_templates/nodes/(?P\d+)/delete/$', + name='index_template_node_delete', + view=IndexTemplateNodeDeleteView.as_view() + ), + url( + regex=r'^index_templates/nodes/(?P\d+)/edit/$', + name='index_template_node_edit', + view=IndexTemplateNodeEditView.as_view() + ) ] +""" api_urls = [ url( - regex=r'^indexes/nodes/(?P[0-9]+)/documents/$', + regex=r'^index_templates/nodes/(?P\d+)/documents/$', name='index-node-documents', view=APIIndexNodeInstanceDocumentListView.as_view(), ), url( - regex=r'^indexes/templates/(?P[0-9]+)/$', + regex=r'^index_templates/templates/(?P\d+)/$', name='index-template-detail', view=APIIndexTemplateView.as_view() ), url( - regex=r'^indexes/(?P\d+)/$', name='index-detail', + regex=r'^index_templates/(?P\d+)/$', name='index-detail', view=APIIndexView.as_view() ), url( - regex=r'^indexes/(?P\d+)/templates/$', + regex=r'^index_templates/(?P\d+)/templates/$', name='index-template-detail', view=APIIndexTemplateListView.as_view() ), url( - regex=r'^indexes/$', name='index-list', - view=APIIndexListView.as_view() + regex=r'^index_templates/$', name='index-list', + view=APIIndexTemplateListView.as_view() ), url( regex=r'^documents/(?P\d+)/indexes/$', name='document-index-list', - view=APIDocumentIndexListView.as_view() + view=APIDocumentIndexTemplateListView.as_view() ), ] +""" diff --git a/mayan/apps/document_indexing/views.py b/mayan/apps/document_indexing/views.py index 0de438b29d..57158d956d 100644 --- a/mayan/apps/document_indexing/views.py +++ b/mayan/apps/document_indexing/views.py @@ -1,7 +1,6 @@ from __future__ import absolute_import, unicode_literals from django.contrib import messages -from django.shortcuts import get_object_or_404 from django.template import RequestContext from django.urls import reverse, reverse_lazy from django.utils.html import mark_safe @@ -13,13 +12,15 @@ from mayan.apps.common.generics import ( AssignRemoveView, FormView, SingleObjectCreateView, SingleObjectDeleteView, SingleObjectEditView, SingleObjectListView ) +from mayan.apps.common.mixins import ExternalObjectMixin from mayan.apps.documents.models import Document, DocumentType -from mayan.apps.documents.permissions import permission_document_view +from mayan.apps.documents.permissions import permission_document_type_view from mayan.apps.documents.views import DocumentListView -from .forms import IndexListForm, IndexTemplateNodeForm +from .forms import IndexTemplateFilteredForm, IndexTemplateNodeForm +from .html_widgets import node_tree from .icons import icon_index -from .links import link_index_setup_create +from .links import link_index_template_create from .models import ( DocumentIndexInstanceNode, Index, IndexInstance, IndexInstanceNode, IndexTemplateNode @@ -31,218 +32,49 @@ from .permissions import ( permission_document_indexing_view ) from .tasks import task_rebuild_index -from .widgets import node_tree -# Setup views -class SetupIndexCreateView(SingleObjectCreateView): - extra_context = {'title': _('Create index')} - fields = ('label', 'slug', 'enabled') - model = Index - post_action_redirect = reverse_lazy(viewname='indexing:index_setup_list') - view_permission = permission_document_indexing_create - - -class SetupIndexDeleteView(SingleObjectDeleteView): - model = Index - object_permission = permission_document_indexing_delete - pk_url_kwarg = 'index_pk' - post_action_redirect = reverse_lazy(viewname='indexing:index_setup_list') - - def get_extra_context(self): - return { - 'object': self.get_object(), - 'title': _('Delete the index: %s?') % self.get_object(), - } - - -class SetupIndexEditView(SingleObjectEditView): - fields = ('label', 'slug', 'enabled') - model = Index - object_permission = permission_document_indexing_edit - pk_url_kwarg = 'index_pk' - post_action_redirect = reverse_lazy(viewname='indexing:index_setup_list') - - def get_extra_context(self): - return { - 'object': self.get_object(), - 'title': _('Edit index: %s') % self.get_object(), - } - - -class SetupIndexListView(SingleObjectListView): - model = Index - object_permission = permission_document_indexing_view +class DocumentIndexInstanceNodeListView(ExternalObjectMixin, SingleObjectListView): + """ + Show a list of indexes where the current document can be found + """ + external_object_class = Document + external_object_permission = permission_document_indexing_instance_view + external_object_pk_url_kwarg = 'document_id' + object_permission = permission_document_indexing_instance_view def get_extra_context(self): return { 'hide_object': True, 'no_results_icon': icon_index, - 'no_results_main_link': link_index_setup_create.resolve( - context=RequestContext(request=self.request) - ), 'no_results_text': _( - 'Indexes group document automatically into levels. Indexe are ' - 'defined using template whose markers are replaced with ' - 'direct properties of documents like label or description, or ' - 'that of extended properties like metadata.' + 'Assign the document type of this document ' + 'to an index to have it appear in instances of ' + 'those indexes organization units. ' ), - 'no_results_title': _('There are no indexes.'), - 'title': _('Indexes'), - } - - -class SetupIndexDocumentTypesView(AssignRemoveView): - decode_content_type = True - left_list_title = _('Available document types') - object_permission = permission_document_indexing_edit - right_list_title = _('Document types linked') - - def add(self, item): - self.get_object().document_types.add(item) - - def get_document_queryset(self): - return AccessControlList.objects.restrict_queryset( - permission=permission_document_view, - queryset=DocumentType.objects.all(), user=self.request.user - ) - - def get_extra_context(self): - return { - 'object': self.get_object(), - 'subtitle': _( - 'Only the documents of the types selected will be shown ' - 'in the index when built. Only the events of the documents ' - 'of the types select will trigger updates in the index.' + 'no_results_title': _( + 'This document is not in any index instance' ), + 'object': self.external_object, 'title': _( - 'Document types linked to index: %s' - ) % self.get_object() + 'Index instance nodes containing document: %s' + ) % self.external_object, } - def get_object(self): - return get_object_or_404(klass=Index, pk=self.kwargs['index_pk']) - - def left_list(self): - return AssignRemoveView.generate_choices( - self.get_document_queryset().exclude( - id__in=self.get_object().document_types.all() - ) - ) - - def remove(self, item): - self.get_object().document_types.remove(item) - - def right_list(self): - return AssignRemoveView.generate_choices( - choices=self.get_document_queryset() & self.get_object().document_types.all() - ) - - -class SetupIndexTreeTemplateListView(SingleObjectListView): - object_permission = permission_document_indexing_edit - - def get_extra_context(self): - return { - 'hide_object': True, - 'index': self.get_index(), - 'navigation_object_list': ('index',), - 'title': _('Tree template nodes for index: %s') % self.get_index(), - } - - def get_index(self): - return get_object_or_404(klass=Index, pk=self.kwargs['index_pk']) - def get_source_queryset(self): - return self.get_index().template_root.get_descendants( - include_self=True + return DocumentIndexInstanceNode.objects.get_for( + document=self.external_object ) -class TemplateNodeCreateView(SingleObjectCreateView): - form_class = IndexTemplateNodeForm - model = IndexTemplateNode - - def dispatch(self, request, *args, **kwargs): - AccessControlList.objects.check_access( - obj=self.get_parent_node().index, - permission=permission_document_indexing_edit, user=request.user - ) - - return super( - TemplateNodeCreateView, self - ).dispatch(request=request, *args, **kwargs) - - def get_extra_context(self): - return { - 'index': self.get_parent_node().index, - 'navigation_object_list': ('index',), - 'title': _('Create child node of: %s') % self.get_parent_node(), - } - - def get_initial(self): - parent_node = self.get_parent_node() - return { - 'index': parent_node.index, 'parent': parent_node - } - - def get_parent_node(self): - return get_object_or_404( - klass=IndexTemplateNode, pk=self.kwargs['index_template_node_pk'] - ) - - -class TemplateNodeDeleteView(SingleObjectDeleteView): - model = IndexTemplateNode - object_permission = permission_document_indexing_edit - - def get_extra_context(self): - return { - 'index': self.get_object().index, - 'navigation_object_list': ('index', 'node'), - 'node': self.get_object(), - 'title': _( - 'Delete the index template node: %s?' - ) % self.get_object(), - } - - def get_post_action_redirect(self): - return reverse( - viewname='indexing:index_setup_view', - kwargs={'index_pk': self.get_object().index.pk} - ) - - -class TemplateNodeEditView(SingleObjectEditView): - form_class = IndexTemplateNodeForm - model = IndexTemplateNode - object_permission = permission_document_indexing_edit - - def get_extra_context(self): - return { - 'index': self.get_object().index, - 'navigation_object_list': ('index', 'node'), - 'node': self.get_object(), - 'title': _( - 'Edit the index template node: %s?' - ) % self.get_object(), - } - - def get_post_action_redirect(self): - return reverse( - viewname='indexing:index_setup_view', - kwargs={'index_pk': self.get_object().index.pk} - ) - - -class IndexListView(SingleObjectListView): +class IndexInstanceView(SingleObjectListView): object_permission = permission_document_indexing_instance_view def get_extra_context(self): return { 'hide_links': True, 'no_results_icon': icon_index, - 'no_results_main_link': link_index_setup_create.resolve( + 'no_results_main_link': link_index_template_create.resolve( context=RequestContext(request=self.request) ), 'no_results_text': _( @@ -261,53 +93,33 @@ class IndexListView(SingleObjectListView): ).distinct() -class IndexInstanceNodeView(DocumentListView): +class IndexInstanceNodeView(ExternalObjectMixin, DocumentListView): + external_object_class = IndexInstanceNode + external_object_permission = permission_document_indexing_instance_view + external_object_pk_url_kwarg = 'index_instance_node_id' template_name = 'document_indexing/node_details.html' - def dispatch(self, request, *args, **kwargs): - self.index_instance_node = get_object_or_404( - klass=IndexInstanceNode, pk=self.kwargs['index_instance_node_pk'] - ) - - AccessControlList.objects.check_access( - obj=self.index_instance_node.index(), - permission=permission_document_indexing_instance_view, - user=request.user - ) - - if self.index_instance_node: - if self.index_instance_node.index_template_node.link_documents: - return super(IndexInstanceNodeView, self).dispatch( - request=request, *args, **kwargs - ) - - return SingleObjectListView.dispatch( - self, request=request, *args, **kwargs - ) - - def get_document_queryset(self): - if self.index_instance_node: - if self.index_instance_node.index_template_node.link_documents: - return self.index_instance_node.documents.all() - def get_extra_context(self): context = super(IndexInstanceNodeView, self).get_extra_context() + if not self.external_object.index_template_node.link_documents: + context.pop('table_cell_container_classes', None) + context.update( { 'column_class': 'col-xs-12 col-sm-6 col-md-4 col-lg-3', - 'object': self.index_instance_node, + 'object': self.external_object, 'navigation': mark_safe( _('Navigation: %s') % node_tree( - node=self.index_instance_node, user=self.request.user + node=self.external_object, user=self.request.user ) ), 'title': _( - 'Contents for index: %s' - ) % self.index_instance_node.get_full_path(), + 'Contents for index instance: %s' + ) % self.external_object.get_full_path(), } ) - if self.index_instance_node and not self.index_instance_node.index_template_node.link_documents: + if not self.external_object.index_template_node.link_documents: context.update( { 'hide_object': True, @@ -318,89 +130,39 @@ class IndexInstanceNodeView(DocumentListView): return context def get_source_queryset(self): - if self.index_instance_node: - if self.index_instance_node.index_template_node.link_documents: - return super(IndexInstanceNodeView, self).get_source_queryset() - else: - self.object_permission = None - return self.index_instance_node.get_children().order_by( - 'value' - ) + if self.external_object.index_template_node.link_documents: + return self.external_object.documents.all() else: - self.object_permission = None - return IndexInstanceNode.objects.none() + return self.external_object.get_children().order_by( + 'value' + ) -class DocumentIndexNodeListView(SingleObjectListView): - """ - Show a list of indexes where the current document can be found - """ - object_permission = permission_document_indexing_instance_view - - def dispatch(self, request, *args, **kwargs): - AccessControlList.objects.check_access( - obj=self.get_document(), permission=permission_document_view, - user=request.user - ) - - return super( - DocumentIndexNodeListView, self - ).dispatch(request=request, *args, **kwargs) - - def get_document(self): - return get_object_or_404( - klass=Document, pk=self.kwargs['document_pk'] - ) - - def get_extra_context(self): - return { - 'hide_object': True, - 'no_results_icon': icon_index, - 'no_results_text': _( - 'Assign the document type of this document ' - 'to an index to have it appear in instances of ' - 'those indexes organization units. ' - ), - 'no_results_title': _( - 'This document is not in any index' - ), - 'object': self.get_document(), - 'title': _( - 'Indexes nodes containing document: %s' - ) % self.get_document(), - } - - def get_source_queryset(self): - return DocumentIndexInstanceNode.objects.get_for( - document=self.get_document() - ) - - -class RebuildIndexesView(FormView): +class IndexInstancesRebuildView(FormView): extra_context = { - 'title': _('Rebuild indexes'), + 'title': _('Rebuild index instances'), } - form_class = IndexListForm + form_class = IndexTemplateFilteredForm def form_valid(self, form): count = 0 - for index in form.cleaned_data['indexes']: + for index_template in form.cleaned_data['index_templates']: task_rebuild_index.apply_async( - kwargs=dict(index_id=index.pk) + kwargs=dict(index_template_id=index_template.pk) ) count += 1 messages.success( request=self.request, message=ungettext( - singular='%(count)d index queued for rebuild.', - plural='%(count)d indexes queued for rebuild.', + singular='%(count)d index template queued for rebuild.', + plural='%(count)d indexes templates queued for rebuild.', number=count ) % { 'count': count, } ) - return super(RebuildIndexesView, self).form_valid(form=form) + return super(IndexInstancesRebuildView, self).form_valid(form=form) def get_form_extra_kwargs(self): return { @@ -409,3 +171,191 @@ class RebuildIndexesView(FormView): def get_post_action_redirect(self): return reverse(viewname='common:tools_list') + + +class IndexTemplateCreateView(SingleObjectCreateView): + extra_context = {'title': _('Create index')} + fields = ('label', 'slug', 'enabled') + model = Index + post_action_redirect = reverse_lazy(viewname='indexing:index_template_list') + view_permission = permission_document_indexing_create + + +class IndexTemplateDeleteView(SingleObjectDeleteView): + model = Index + object_permission = permission_document_indexing_delete + pk_url_kwarg = 'index_template_id' + post_action_redirect = reverse_lazy(viewname='indexing:index_template_list') + + def get_extra_context(self): + return { + 'object': self.object, + 'title': _('Delete the index template: %s?') % self.object, + } + + +class IndexTemplateDocumentTypesView(ExternalObjectMixin, AssignRemoveView): + decode_content_type = True + external_object_class = Index + external_object_permission = permission_document_indexing_edit + external_object_pk_url_kwarg = 'index_template_id' + left_list_title = _('Available document types') + object_permission = permission_document_indexing_edit + right_list_title = _('Document types linked') + + def add(self, item): + self.external_object.document_types.add(item) + + def get_document_type_queryset(self): + return AccessControlList.objects.restrict_queryset( + permission=permission_document_type_view, + queryset=DocumentType.objects.all(), user=self.request.user + ) + + def get_extra_context(self): + return { + 'object': self.external_object, + 'subtitle': _( + 'Only the documents of the types selected will be shown ' + 'in the index when built. Only the events of the documents ' + 'of the types select will trigger updates in the index.' + ), + 'title': _( + 'Document types linked to index template: %s' + ) % self.external_object + } + + def left_list(self): + return AssignRemoveView.generate_choices( + self.get_document_type_queryset().exclude( + id__in=self.external_object.document_types.all() + ) + ) + + def remove(self, item): + self.external_object.document_types.remove(item) + + def right_list(self): + return AssignRemoveView.generate_choices( + choices=self.get_document_type_queryset() & self.external_object.document_types.all() + ) + + +class IndexTemplateEditView(SingleObjectEditView): + fields = ('label', 'slug', 'enabled') + model = Index + object_permission = permission_document_indexing_edit + pk_url_kwarg = 'index_template_id' + post_action_redirect = reverse_lazy(viewname='indexing:index_template_list') + + def get_extra_context(self): + return { + 'object': self.object, + 'title': _('Edit index template: %s') % self.object, + } + + +class IndexTemplateListView(SingleObjectListView): + model = Index + object_permission = permission_document_indexing_view + + def get_extra_context(self): + return { + 'hide_object': True, + 'no_results_icon': icon_index, + 'no_results_main_link': link_index_template_create.resolve( + context=RequestContext(request=self.request) + ), + 'no_results_text': _( + 'Indexes group document automatically into levels. Indexe are ' + 'defined using template whose markers are replaced with ' + 'direct properties of documents like label or description, or ' + 'that of extended properties like metadata.' + ), + 'no_results_title': _('There are no index templates.'), + 'title': _('Index templates'), + } + + +class IndexTemplateNodeCreateView(ExternalObjectMixin, SingleObjectCreateView): + external_object_class = IndexTemplateNode + external_object_permission = permission_document_indexing_edit + external_object_pk_url_kwarg = 'index_template_node_id' + form_class = IndexTemplateNodeForm + model = IndexTemplateNode + + def get_extra_context(self): + return { + 'index': self.external_object.index, + 'navigation_object_list': ('index',), + 'title': _('Create child node of: %s') % self.external_object, + } + + def get_initial(self): + return { + 'index': self.external_object.index, 'parent': self.external_object + } + + +class IndexTemplateNodeDeleteView(SingleObjectDeleteView): + model = IndexTemplateNode + object_permission = permission_document_indexing_edit + pk_url_kwarg = 'index_template_node_id' + + def get_extra_context(self): + return { + 'index': self.object.index, + 'navigation_object_list': ('index', 'node'), + 'node': self.object, + 'title': _( + 'Delete the index template node: %s?' + ) % self.object, + } + + def get_post_action_redirect(self): + return reverse( + viewname='indexing:index_template_view', + kwargs={'index_template_id': self.object.index.pk} + ) + + +class IndexTemplateNodeEditView(SingleObjectEditView): + form_class = IndexTemplateNodeForm + model = IndexTemplateNode + object_permission = permission_document_indexing_edit + pk_url_kwarg = 'index_template_node_id' + + def get_extra_context(self): + return { + 'index': self.object.index, + 'navigation_object_list': ('index', 'node'), + 'node': self.object, + 'title': _( + 'Edit the index template node: %s?' + ) % self.object, + } + + def get_post_action_redirect(self): + return reverse( + viewname='indexing:index_template_view', + kwargs={'index_template_id': self.object.index.pk} + ) + + +class IndexTemplateNodeListView(ExternalObjectMixin, SingleObjectListView): + external_object_class = Index + external_object_permission = permission_document_indexing_edit + external_object_pk_url_kwarg = 'index_template_id' + + def get_extra_context(self): + return { + 'hide_object': True, + 'index': self.external_object, + 'navigation_object_list': ('index',), + 'title': _('Nodes for index template: %s') % self.external_object, + } + + def get_source_queryset(self): + return self.external_object.template_root.get_descendants( + include_self=True + ) From 71c2a7773e60958b9a391a698aaa6c571d7f4154 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Tue, 5 Feb 2019 05:47:40 -0400 Subject: [PATCH 108/209] Support separate sortable fields Add support to sort a model column by a field other than the one being displayed. Fix the missing column issue in the list subtemplate. Signed-off-by: Roberto Rosario --- .../appearance/generic_list_subtemplate.html | 24 ++++++++++--------- mayan/apps/navigation/classes.py | 18 +++++++++++--- 2 files changed, 28 insertions(+), 14 deletions(-) diff --git a/mayan/apps/appearance/templates/appearance/generic_list_subtemplate.html b/mayan/apps/appearance/templates/appearance/generic_list_subtemplate.html index a95d4fe748..c19721fbbe 100644 --- a/mayan/apps/appearance/templates/appearance/generic_list_subtemplate.html +++ b/mayan/apps/appearance/templates/appearance/generic_list_subtemplate.html @@ -50,17 +50,19 @@ {% trans 'Identifier' %} {% else %} {% get_source_columns source=object_list only_identifier=True as source_column %} - - {% if source_column.is_sortable %} - {{ source_column.label }} - {% if source_column.attribute == sort_field %} - {% if icon_sort %}{{ icon_sort.render }}{% endif %} + {% if source_column %} + + {% if source_column.is_sortable %} + {{ source_column.label }} + {% if source_column.get_sort_field == sort_field %} + {% if icon_sort %}{{ icon_sort.render }}{% endif %} + {% endif %} + + {% else %} + {{ source_column.label }} {% endif %} - - {% else %} - {{ source_column.label }} - {% endif %} - + + {% endif %} {% endif %} {% if not hide_columns %} @@ -69,7 +71,7 @@ {% if column.is_sortable %} {{ column.label }} - {% if column.attribute == sort_field %} + {% if column.get_sort_field == sort_field %} {% if icon_sort %}{{ icon_sort.render }}{% endif %} {% endif %} diff --git a/mayan/apps/navigation/classes.py b/mayan/apps/navigation/classes.py index a49bbca026..2f8032eda1 100644 --- a/mayan/apps/navigation/classes.py +++ b/mayan/apps/navigation/classes.py @@ -561,7 +561,12 @@ class SourceColumn(object): return final_result - def __init__(self, source, attribute=None, empty_value=None, func=None, include_label=False, is_absolute_url=False, is_identifier=False, is_sortable=False, kwargs=None, label=None, order=None, views=None, widget=None): + def __init__( + self, source, attribute=None, empty_value=None, func=None, + include_label=False, is_absolute_url=False, is_identifier=False, + is_sortable=False, kwargs=None, label=None, order=None, sort_field=None, + views=None, widget=None + ): self.source = source self._label = label self.attribute = attribute @@ -576,6 +581,7 @@ class SourceColumn(object): self.__class__._registry.setdefault(source, []) self.__class__._registry[source].append(self) self.label = None + self.sort_field = sort_field self.views = views or [] self.widget = widget @@ -603,6 +609,12 @@ class SourceColumn(object): self.label = self._label + def get_sort_field(self): + if self.sort_field: + return self.sort_field + else: + return self.attribute + def get_sort_field_querystring(self, context): # We do this to get an mutable copy we can modify querystring = context.request.GET.copy() @@ -612,7 +624,7 @@ class SourceColumn(object): TEXT_SORT_ORDER_VARIABLE_NAME, TEXT_SORT_ORDER_CHOICE_DESCENDING ) - if previous_sort_field != self.attribute: + if previous_sort_field != self.get_sort_field(): sort_order = TEXT_SORT_ORDER_CHOICE_ASCENDING else: if previous_sort_order == TEXT_SORT_ORDER_CHOICE_DESCENDING: @@ -620,7 +632,7 @@ class SourceColumn(object): else: sort_order = TEXT_SORT_ORDER_CHOICE_DESCENDING - querystring[TEXT_SORT_FIELD_PARAMETER] = self.attribute + querystring[TEXT_SORT_FIELD_PARAMETER] = self.get_sort_field() querystring[TEXT_SORT_ORDER_PARAMETER] = sort_order return '?{}'.format(querystring.urlencode()) From a4ef6b3e8ad1417127aeb70350322b9f3b0bf453 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Tue, 5 Feb 2019 05:49:47 -0400 Subject: [PATCH 109/209] Small code cleanups Add keyword arguments. Replace get_queryset with get_object_list. Signed-off-by: Roberto Rosario --- .../templatetags/appearance_tags.py | 2 +- .../document_comments/tests/test_events.py | 4 +-- mayan/apps/document_comments/views.py | 26 +++++++------------ .../documents/views/document_page_views.py | 2 +- mayan/apps/documents/views/document_views.py | 18 ++++++------- mayan/apps/tags/views.py | 5 ---- 6 files changed, 23 insertions(+), 34 deletions(-) diff --git a/mayan/apps/appearance/templatetags/appearance_tags.py b/mayan/apps/appearance/templatetags/appearance_tags.py index 7fd05f607e..ee58fd6584 100644 --- a/mayan/apps/appearance/templatetags/appearance_tags.py +++ b/mayan/apps/appearance/templatetags/appearance_tags.py @@ -28,4 +28,4 @@ def get_form_media_js(form): @register.simple_tag def get_icon(icon_path): - return import_string(icon_path).render() + return import_string(dotted_path=icon_path).render() diff --git a/mayan/apps/document_comments/tests/test_events.py b/mayan/apps/document_comments/tests/test_events.py index 05c9225655..26a58c9738 100644 --- a/mayan/apps/document_comments/tests/test_events.py +++ b/mayan/apps/document_comments/tests/test_events.py @@ -34,7 +34,7 @@ class CommentEventsTestCase(CommentsTestMixin, GenericDocumentViewTestCase): self.assertEqual(event.verb, event_document_comment_created.id) self.assertEqual(event.action_object, self.document) self.assertEqual(event.target, self.test_comment) - self.assertEqual(event.actor, self.user) + self.assertEqual(event.actor, self._test_case_user) def test_comment_deleted_event_no_permissions(self): self._create_comment() @@ -58,4 +58,4 @@ class CommentEventsTestCase(CommentsTestMixin, GenericDocumentViewTestCase): self.assertEqual(event.verb, event_document_comment_deleted.id) self.assertEqual(event.target, self.document) - self.assertEqual(event.actor, self.user) + self.assertEqual(event.actor, self._test_case_user) diff --git a/mayan/apps/document_comments/views.py b/mayan/apps/document_comments/views.py index 0881e2b4e1..fc2091b896 100644 --- a/mayan/apps/document_comments/views.py +++ b/mayan/apps/document_comments/views.py @@ -26,18 +26,15 @@ class DocumentCommentCreateView(ExternalObjectMixin, SingleObjectCreateView): fields = ('comment',) model = Comment - def get_document(self): - return self.get_external_object() - def get_extra_context(self): return { - 'object': self.get_document(), - 'title': _('Add comment to document: %s') % self.get_document(), + 'object': self.external_object, + 'title': _('Add comment to document: %s') % self.external_object, } def get_instance_extra_data(self): return { - 'document': self.get_document(), 'user': self.request.user, + 'document': self.external_object, 'user': self.request.user, } def get_post_action_redirect(self): @@ -63,14 +60,14 @@ class DocumentCommentDeleteView(SingleObjectDeleteView): def get_extra_context(self): return { - 'object': self.get_object().document, - 'title': _('Delete comment: %s?') % self.get_object(), + 'object': self.object.document, + 'title': _('Delete comment: %s?') % self.object, } def get_post_action_redirect(self): return reverse( viewname='comments:comments_for_document', kwargs={ - 'document_id': self.get_object().document.pk + 'document_id': self.object.document.pk } ) @@ -80,25 +77,22 @@ class DocumentCommentListView(ExternalObjectMixin, SingleObjectListView): external_object_permission = permission_comment_view external_object_pk_url_kwarg = 'document_id' - def get_document(self): - return self.get_external_object() - def get_extra_context(self): return { 'hide_link': True, 'hide_object': True, 'no_results_icon': icon_comments_for_document, 'no_results_external_link': link_comment_add.resolve( - RequestContext(self.request, {'object': self.get_document()}) + RequestContext(self.request, {'object': self.external_object}) ), 'no_results_text': _( 'Document comments are timestamped text entries from users. ' 'They are great for collaboration.' ), 'no_results_title': _('There are no comments'), - 'object': self.get_document(), - 'title': _('Comments for document: %s') % self.get_document(), + 'object': self.external_object, + 'title': _('Comments for document: %s') % self.external_object, } def get_source_queryset(self): - return self.get_document().comments.all() + return self.external_object.comments.all() diff --git a/mayan/apps/documents/views/document_page_views.py b/mayan/apps/documents/views/document_page_views.py index de7ab24807..5ac49ebbc7 100644 --- a/mayan/apps/documents/views/document_page_views.py +++ b/mayan/apps/documents/views/document_page_views.py @@ -104,7 +104,7 @@ class DocumentPageNavigationBase(ExternalObjectMixin, RedirectView): try: previous_url = self.get_object().get_absolute_url() except AttributeError: - previous_url = reverse(setting_home_view.value) + previous_url = reverse(viewname=setting_home_view.value) parsed_url = furl(url=previous_url) diff --git a/mayan/apps/documents/views/document_views.py b/mayan/apps/documents/views/document_views.py index e94b10cae3..8bd379be14 100644 --- a/mayan/apps/documents/views/document_views.py +++ b/mayan/apps/documents/views/document_views.py @@ -387,13 +387,13 @@ class DocumentUpdatePageCountView(MultipleObjectConfirmActionView): ) def get_extra_context(self): - queryset = self.get_queryset() + queryset = self.get_object_list() result = { 'title': ungettext( - 'Recalculate the page count of the selected document?', - 'Recalculate the page count of the selected documents?', - queryset.count() + singular='Recalculate the page count of the selected document?', + plural='Recalculate the page count of the selected documents?', + number=queryset.count() ) } @@ -439,13 +439,13 @@ class DocumentTransformationsClearView(MultipleObjectConfirmActionView): ) def get_extra_context(self): - queryset = self.get_queryset() + queryset = self.get_object_list() result = { 'title': ungettext( - 'Clear all the page transformations for the selected document?', - 'Clear all the page transformations for the selected document?', - queryset.count() + singular='Clear all the page transformations for the selected document?', + plural='Clear all the page transformations for the selected document?', + number=queryset.count() ) } @@ -668,7 +668,7 @@ class FavoriteAddView(MultipleObjectConfirmActionView): ) def get_extra_context(self): - queryset = self.get_queryset() + queryset = self.get_object_list() return { 'submit_label': _('Add'), diff --git a/mayan/apps/tags/views.py b/mayan/apps/tags/views.py index d36149333c..63f4e94dd2 100644 --- a/mayan/apps/tags/views.py +++ b/mayan/apps/tags/views.py @@ -225,12 +225,8 @@ class TagListView(SingleObjectListView): } def get_source_queryset(self): - #return self.get_tag_queryset() return Tag.objects.all() - #def get_tag_queryset(self): - # return Tag.objects.all() - class TagDocumentListView(ExternalObjectMixin, DocumentListView): external_object_class = Tag @@ -279,7 +275,6 @@ class DocumentTagListView(ExternalObjectMixin, TagListView): ) return context - #def get_tag_queryset(self): def get_source_queryset(self): return self.get_external_object().get_tags( permission=permission_tag_view, user=self.request.user From 8284dcf3062d6bfb6d27b55b86f88208e9fff798 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Tue, 5 Feb 2019 05:50:25 -0400 Subject: [PATCH 110/209] Improve next_url and previous_url calculation Instead of calculating these values in the dispatch method, add new methods to calculate and insert the values of next_url and previous_url in the context. Signed-off-by: Roberto Rosario --- mayan/apps/common/mixins.py | 59 +++++++++++++++++++++---------------- 1 file changed, 33 insertions(+), 26 deletions(-) diff --git a/mayan/apps/common/mixins.py b/mayan/apps/common/mixins.py index 0f62a23919..26e9278a78 100644 --- a/mayan/apps/common/mixins.py +++ b/mayan/apps/common/mixins.py @@ -374,31 +374,10 @@ class ObjectNameMixin(object): class RedirectionMixin(object): action_cancel_redirect = None + next_url = None + previous_url = None post_action_redirect = None - def dispatch(self, request, *args, **kwargs): - post_action_redirect = self.get_post_action_redirect() - action_cancel_redirect = self.get_action_cancel_redirect() - - self.next_url = self.request.POST.get( - 'next', self.request.GET.get( - 'next', post_action_redirect if post_action_redirect else self.request.META.get( - 'HTTP_REFERER', reverse(setting_home_view.value) - ) - ) - ) - self.previous_url = self.request.POST.get( - 'previous', self.request.GET.get( - 'previous', action_cancel_redirect if action_cancel_redirect else self.request.META.get( - 'HTTP_REFERER', reverse(setting_home_view.value) - ) - ) - ) - - return super( - RedirectionMixin, self - ).dispatch(request, *args, **kwargs) - def get_action_cancel_redirect(self): return self.action_cancel_redirect @@ -406,8 +385,8 @@ class RedirectionMixin(object): context = super(RedirectionMixin, self).get_context_data(**kwargs) context.update( { - 'next': self.next_url, - 'previous': self.previous_url + 'next': self.get_next_url(), + 'previous': self.get_previous_url() } ) @@ -416,8 +395,36 @@ class RedirectionMixin(object): def get_post_action_redirect(self): return self.post_action_redirect + def get_next_url(self): + if self.next_url: + return self.next_url + else: + post_action_redirect = self.get_post_action_redirect() + + return self.request.POST.get( + 'next', self.request.GET.get( + 'next', post_action_redirect if post_action_redirect else self.request.META.get( + 'HTTP_REFERER', reverse(setting_home_view.value) + ) + ) + ) + + def get_previous_url(self): + if self.previous_url: + return self.previous_url + else: + action_cancel_redirect = self.get_action_cancel_redirect() + + return self.request.POST.get( + 'previous', self.request.GET.get( + 'previous', action_cancel_redirect if action_cancel_redirect else self.request.META.get( + 'HTTP_REFERER', reverse(setting_home_view.value) + ) + ) + ) + def get_success_url(self): - return self.next_url or self.previous_url + return self.get_next_url() or self.get_previous_url() class RestrictedQuerysetMixin(object): From e9cdc958f63cf6eef1462403053dd9ba39cbe9ab Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Tue, 5 Feb 2019 05:54:36 -0400 Subject: [PATCH 111/209] Fix typo in link view Signed-off-by: Roberto Rosario --- mayan/apps/document_indexing/links.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mayan/apps/document_indexing/links.py b/mayan/apps/document_indexing/links.py index 0ccce73e73..18557344e4 100644 --- a/mayan/apps/document_indexing/links.py +++ b/mayan/apps/document_indexing/links.py @@ -27,7 +27,7 @@ def condition_is_not_root_node(context): link_document_index_instance_list = Link( icon_class=icon_document_index_instance_list, kwargs={'document_id': 'resolved_object.pk'}, text=_('Indexes'), - view='indexing:document_index_instace_list', + view='indexing:document_index_instance_list', ) link_index_instances_rebuild = Link( condition=get_cascade_condition( From 27517c04f2aa91439c655e9fc9da32cab7a96b66 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Wed, 6 Feb 2019 00:51:26 -0400 Subject: [PATCH 112/209] Fix ACL action tests Signed-off-by: Roberto Rosario --- mayan/apps/acls/tests/test_actions.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/mayan/apps/acls/tests/test_actions.py b/mayan/apps/acls/tests/test_actions.py index f638fef451..d87eceb24b 100644 --- a/mayan/apps/acls/tests/test_actions.py +++ b/mayan/apps/acls/tests/test_actions.py @@ -9,16 +9,13 @@ from ..workflow_actions import GrantAccessAction, RevokeAccessAction class ACLActionTestCase(ActionTestCase): - def setUp(self): - super(ACLActionTestCase, self).setUp() - def test_grant_access_action(self): action = GrantAccessAction( form_data={ 'content_type': ContentType.objects.get_for_model(model=self.document).pk, 'object_id': self.document.pk, - 'roles': [self.role.pk], - 'permissions': [permission_document_view.uuid], + 'roles': [self._test_case_role.pk], + 'permissions': [permission_document_view.pk], } ) action.execute(context={'entry_log': self.entry_log}) @@ -28,7 +25,7 @@ class ACLActionTestCase(ActionTestCase): list(self.document.acls.first().permissions.all()), [permission_document_view.stored_permission] ) - self.assertEqual(self.document.acls.first().role, self.role) + self.assertEqual(self.document.acls.first().role, self._test_case_role) def test_revoke_access_action(self): self.grant_access( @@ -39,8 +36,8 @@ class ACLActionTestCase(ActionTestCase): form_data={ 'content_type': ContentType.objects.get_for_model(model=self.document).pk, 'object_id': self.document.pk, - 'roles': [self.role.pk], - 'permissions': [permission_document_view.uuid], + 'roles': [self._test_case_role.pk], + 'permissions': [permission_document_view.pk], } ) action.execute(context={'entry_log': self.entry_log}) From 7ba47d5c5f9229f9db0b0a830686d5048e091968 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Wed, 6 Feb 2019 01:08:14 -0400 Subject: [PATCH 113/209] Update mailer app Sort arguments. Fix failing tests. Sort view classes. Replace get_object() with self.object in the delete and edit views. Use ExternalObjectMixin to simplify views. Signed-off-by: Roberto Rosario --- mayan/apps/mailer/permissions.py | 20 +++++------ mayan/apps/mailer/queues.py | 6 ++-- mayan/apps/mailer/settings.py | 2 +- mayan/apps/mailer/tests/test_views.py | 18 ++++------ mayan/apps/mailer/urls.py | 10 +++--- mayan/apps/mailer/views.py | 51 +++++++++++---------------- 6 files changed, 47 insertions(+), 60 deletions(-) diff --git a/mayan/apps/mailer/permissions.py b/mayan/apps/mailer/permissions.py index 614dc71d0a..d0658a1113 100644 --- a/mayan/apps/mailer/permissions.py +++ b/mayan/apps/mailer/permissions.py @@ -7,26 +7,26 @@ from mayan.apps.permissions import PermissionNamespace namespace = PermissionNamespace(label=_('Mailing'), name='mailing') permission_mailing_link = namespace.add_permission( - name='mail_link', label=_('Send document link via email') + label=_('Send document link via email'), name='mail_link' ) permission_mailing_send_document = namespace.add_permission( - name='mail_document', label=_('Send document via email') + label=_('Send document via email'), name='mail_document' ) permission_view_error_log = namespace.add_permission( - name='view_error_log', label=_('View system mailing error log') + label=_('View system mailing error log'), name='view_error_log' ) permission_user_mailer_create = namespace.add_permission( - name='user_mailer_create', label=_('Create a mailing profile') + label=_('Create a mailing profile'), name='user_mailer_create' ) permission_user_mailer_delete = namespace.add_permission( - name='user_mailer_delete', label=_('Delete a mailing profile') + label=_('Delete a mailing profile'), name='user_mailer_delete' ) permission_user_mailer_edit = namespace.add_permission( - name='user_mailer_edit', label=_('Edit a mailing profile') -) -permission_user_mailer_view = namespace.add_permission( - name='user_mailer_view', label=_('View a mailing profile') + label=_('Edit a mailing profile'), name='user_mailer_edit' ) permission_user_mailer_use = namespace.add_permission( - name='user_mailer_use', label=_('Use a mailing profile') + label=_('Use a mailing profile'), name='user_mailer_use' +) +permission_user_mailer_view = namespace.add_permission( + label=_('View a mailing profile'), name='user_mailer_view' ) diff --git a/mayan/apps/mailer/queues.py b/mayan/apps/mailer/queues.py index 749c3b3b84..69477e4b8b 100644 --- a/mayan/apps/mailer/queues.py +++ b/mayan/apps/mailer/queues.py @@ -5,9 +5,9 @@ from django.utils.translation import ugettext_lazy as _ from mayan.apps.task_manager.classes import CeleryQueue queue_mailing = CeleryQueue( - name='mailing', label=_('Mailing') + label=_('Mailing'), name='mailing' ) queue_mailing.add_task_type( - name='mayan.apps.mailer.tasks.task_send_document', - label=_('Send document') + label=_('Send document'), + name='mayan.apps.mailer.tasks.task_send_document' ) diff --git a/mayan/apps/mailer/settings.py b/mayan/apps/mailer/settings.py index 55f042860f..5e6db1b513 100644 --- a/mayan/apps/mailer/settings.py +++ b/mayan/apps/mailer/settings.py @@ -9,7 +9,7 @@ from .literals import ( DEFAULT_LINK_BODY_TEMPLATE, DEFAULT_LINK_SUBJECT_TEMPLATE ) -namespace = Namespace(name='mailer', label=_('Mailing')) +namespace = Namespace(label=_('Mailing'), name='mailer') setting_link_subject_template = namespace.add_setting( default=DEFAULT_LINK_SUBJECT_TEMPLATE, diff --git a/mayan/apps/mailer/tests/test_views.py b/mayan/apps/mailer/tests/test_views.py index 2906567e64..784a882479 100644 --- a/mayan/apps/mailer/tests/test_views.py +++ b/mayan/apps/mailer/tests/test_views.py @@ -26,10 +26,6 @@ from .mixins import MailerTestMixin class MailerViewsTestCase(MailerTestMixin, GenericViewTestCase): - def setUp(self): - super(MailerViewsTestCase, self).setUp() - self.login_user() - def test_user_mailer_create_view_no_permissions(self): response = self._request_user_mailer_create() @@ -216,10 +212,10 @@ class DocumentViewsTestCase(MailerTestMixin, GenericDocumentViewTestCase): response = self._request_document_link_send() self.assertNotContains( - response=response, text=self.user_mailer.label, status_code=200 + response=response, text=self.user_mailer.label, status_code=404 ) self.assertNotContains( - response=response, text=self.document.label, status_code=200 + response=response, text=self.document.label, status_code=404 ) self.assertEqual(len(mail.outbox), 0) @@ -248,10 +244,10 @@ class DocumentViewsTestCase(MailerTestMixin, GenericDocumentViewTestCase): response = self._request_document_link_send() self.assertNotContains( - response=response, text=self.user_mailer.label, status_code=302 + response=response, text=self.user_mailer.label, status_code=404 ) self.assertNotContains( - response=response, text=self.document.label, status_code=302 + response=response, text=self.document.label, status_code=404 ) self.assertEqual(len(mail.outbox), 0) @@ -278,10 +274,10 @@ class DocumentViewsTestCase(MailerTestMixin, GenericDocumentViewTestCase): response = self._request_document_send() self.assertNotContains( - response=response, text=self.user_mailer.label, status_code=200 + response=response, text=self.user_mailer.label, status_code=404 ) self.assertNotContains( - response=response, text=self.document.label, status_code=200 + response=response, text=self.document.label, status_code=404 ) self.assertEqual(len(mail.outbox), 0) @@ -311,7 +307,7 @@ class DocumentViewsTestCase(MailerTestMixin, GenericDocumentViewTestCase): response = self._request_document_send() self.assertNotContains( - response=response, text=self.document.label, status_code=302 + response=response, text=self.document.label, status_code=404 ) self.assertEqual(len(mail.outbox), 0) diff --git a/mayan/apps/mailer/urls.py b/mayan/apps/mailer/urls.py index 915d0f7798..cd2c360cd8 100644 --- a/mayan/apps/mailer/urls.py +++ b/mayan/apps/mailer/urls.py @@ -14,11 +14,6 @@ urlpatterns = [ regex=r'^documents/(?P\d+)/send/link/$', name='document_send_link', view=MailDocumentLinkView.as_view() ), - url( - regex=r'^documents/multiple/send/link/$', - name='document_multiple_send_link', - view=MailDocumentLinkView.as_view() - ), url( regex=r'^documents/(?P\d+)/send/$', name='document_send', view=MailDocumentView.as_view() @@ -27,6 +22,11 @@ urlpatterns = [ regex=r'^documents/multiple/send/document/$', name='document_multiple_send', view=MailDocumentView.as_view() ), + url( + regex=r'^documents/multiple/send/link/$', + name='document_multiple_send_link', + view=MailDocumentLinkView.as_view() + ), url( regex=r'^system_mailer/log/$', name='system_mailer_error_log', view=SystemMailerLogEntryListView.as_view() diff --git a/mayan/apps/mailer/views.py b/mayan/apps/mailer/views.py index e32d5a810b..6ad40a274f 100644 --- a/mayan/apps/mailer/views.py +++ b/mayan/apps/mailer/views.py @@ -14,6 +14,7 @@ from mayan.apps.common.generics import ( SingleObjectDynamicFormCreateView, SingleObjectDynamicFormEditView, SingleObjectListView ) +from mayan.apps.common.mixins import ExternalObjectMixin from mayan.apps.documents.models import Document from .classes import MailerBackend @@ -33,15 +34,6 @@ from .permissions import ( from .tasks import task_send_document -class SystemMailerLogEntryListView(SingleObjectListView): - extra_context = { - 'hide_object': True, - 'title': _('Document mailing error log'), - } - model = LogEntry - view_permission = permission_view_error_log - - class MailDocumentView(MultipleObjectFormActionView): as_attachment = True form_class = DocumentMailForm @@ -57,7 +49,7 @@ class MailDocumentView(MultipleObjectFormActionView): title_document = 'Email document: %s' def get_extra_context(self): - queryset = self.get_queryset() + queryset = self.get_object_list() result = { 'submit_icon_class': icon_mail_document_submit, @@ -116,6 +108,15 @@ class MailDocumentLinkView(MailDocumentView): title_document = 'Email link for document: %s' +class SystemMailerLogEntryListView(SingleObjectListView): + extra_context = { + 'hide_object': True, + 'title': _('Document mailing error log'), + } + model = LogEntry + view_permission = permission_view_error_log + + class UserMailerBackendSelectionView(FormView): extra_context = { 'title': _('New mailing profile backend selection'), @@ -177,7 +178,7 @@ class UserMailingDeleteView(SingleObjectDeleteView): def get_extra_context(self): return { - 'title': _('Delete mailing profile: %s') % self.get_object(), + 'title': _('Delete mailing profile: %s') % self.object } @@ -189,11 +190,11 @@ class UserMailingEditView(SingleObjectDynamicFormEditView): def get_extra_context(self): return { - 'title': _('Edit mailing profile: %s') % self.get_object(), + 'title': _('Edit mailing profile: %s') % self.object } def get_form_schema(self): - backend = self.get_object().get_backend() + backend = self.object.get_backend() result = { 'fields': backend.fields, 'widgets': getattr(backend, 'widgets', {}) @@ -248,16 +249,17 @@ class UserMailerListView(SingleObjectListView): return {'fields': self.get_backend().fields} -class UserMailerTestView(FormView): +class UserMailerTestView(ExternalObjectMixin, FormView): + external_object_class = UserMailer + external_object_permission = permission_user_mailer_use + external_object_pk_url_kwarg = 'mailer_id' form_class = UserMailerTestForm def form_valid(self, form): - obj = self.get_object() - # Separate getting the object from executing the test method to avoid # catching PermissionDenied exception. try: - obj.test(to=form.cleaned_data['email']) + self.external_object.test(to=form.cleaned_data['email']) except Exception as exception: messages.error( message=_( @@ -276,18 +278,7 @@ class UserMailerTestView(FormView): def get_extra_context(self): return { 'hide_object': True, - 'object': self.get_object(), + 'object': self.external_object, 'submit_label': _('Test'), - 'title': _('Test mailing profile: %s') % self.get_object(), + 'title': _('Test mailing profile: %s') % self.external_object, } - - def get_object(self): - return get_object_or_404( - klass=self.get_queryset(), pk=self.kwargs['mailer_id'] - ) - - def get_queryset(self): - return AccessControlList.objects.restrict_queryset( - permission=permission_user_mailer_use, - queryset=UserMailer.objects.all(), user=self.request.user - ) From 627056f1ae8ce71fd38f68727d41da52ae5b4805 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Wed, 6 Feb 2019 05:12:14 -0400 Subject: [PATCH 114/209] Refactor the REST API app Remove the APIRoot view. Remove the Endpoint class. Remove the EndpointSerializer. Move API documentation generation from the root urls module to the app's urls module. Update the app API URL generation to be based on viewsets instead of an custom api_urls list. Remove MayanObjectPermissionsFilter and replace it with MayanViewSetObjectPermissionsFilter which allows mapping a required permission to a specific viewset action. Signed-off-by: Roberto Rosario --- mayan/apps/rest_api/api_views.py | 36 ++++++------------ mayan/apps/rest_api/apps.py | 14 +++++-- mayan/apps/rest_api/classes.py | 10 ----- mayan/apps/rest_api/filters.py | 29 +++++++++----- mayan/apps/rest_api/generics.py | 48 +++++++++++++++++++++++ mayan/apps/rest_api/links.py | 6 +-- mayan/apps/rest_api/mixins.py | 54 ++++++++++++++++++++++++++ mayan/apps/rest_api/permissions.py | 61 +++++++++--------------------- mayan/apps/rest_api/relations.py | 35 +++++++++++++++++ mayan/apps/rest_api/serializers.py | 8 ---- mayan/apps/rest_api/urls.py | 30 +++++++++------ mayan/apps/rest_api/viewsets.py | 11 ++++++ mayan/urls/base.py | 14 ------- 13 files changed, 228 insertions(+), 128 deletions(-) delete mode 100644 mayan/apps/rest_api/classes.py create mode 100644 mayan/apps/rest_api/generics.py create mode 100644 mayan/apps/rest_api/mixins.py create mode 100644 mayan/apps/rest_api/relations.py delete mode 100644 mayan/apps/rest_api/serializers.py create mode 100644 mayan/apps/rest_api/viewsets.py diff --git a/mayan/apps/rest_api/api_views.py b/mayan/apps/rest_api/api_views.py index ee1dd41c94..bad68a2f5f 100644 --- a/mayan/apps/rest_api/api_views.py +++ b/mayan/apps/rest_api/api_views.py @@ -1,31 +1,11 @@ from __future__ import unicode_literals -from rest_framework import renderers +from drf_yasg.views import get_schema_view + +from rest_framework import permissions, renderers from rest_framework.authtoken.views import ObtainAuthToken -from rest_framework.views import APIView -from rest_framework.response import Response -from rest_framework.schemas.generators import EndpointEnumerator -from .classes import Endpoint -from .serializers import EndpointSerializer - - -class APIRoot(APIView): - swagger_schema = None - - def get(self, request, format=None): - """ - get: Return a list of all endpoints. - """ - endpoint_enumerator = EndpointEnumerator() - - endpoints = [] - for url in sorted(set([entry[0].split('/')[2] for entry in endpoint_enumerator.get_api_endpoints()])): - if url: - endpoints.append(Endpoint(label=url)) - - serializer = EndpointSerializer(endpoints, many=True) - return Response(serializer.data) +from .schemas import openapi_info class BrowseableObtainAuthToken(ObtainAuthToken): @@ -33,3 +13,11 @@ class BrowseableObtainAuthToken(ObtainAuthToken): Obtain an API authentication token. """ renderer_classes = (renderers.BrowsableAPIRenderer, renderers.JSONRenderer) + + +schema_view = get_schema_view( + openapi_info, + validators=['flex', 'ssv'], + public=True, + permission_classes=(permissions.AllowAny,), +) diff --git a/mayan/apps/rest_api/apps.py b/mayan/apps/rest_api/apps.py index 88d0b2c61a..d913ccd02c 100644 --- a/mayan/apps/rest_api/apps.py +++ b/mayan/apps/rest_api/apps.py @@ -5,6 +5,8 @@ from django.conf import settings from django.utils.module_loading import import_string from django.utils.translation import ugettext_lazy as _ +from rest_framework import routers + from mayan.apps.common import MayanAppConfig, menu_tools from .links import ( @@ -21,7 +23,7 @@ class RESTAPIApp(MayanAppConfig): def ready(self): super(RESTAPIApp, self).ready() - from .urls import api_urls + from .urls import urlpatterns settings.STRONGHOLD_PUBLIC_URLS += (r'^/%s/.+$' % self.app_url,) menu_tools.bind_links( @@ -29,8 +31,14 @@ class RESTAPIApp(MayanAppConfig): link_api, link_api_documentation, link_api_documentation_redoc ) ) + router = routers.DefaultRouter() for app in apps.get_app_configs(): if getattr(app, 'has_rest_api', False): - app_api_urls = import_string('{}.urls.api_urls'.format(app.name)) - api_urls.extend(app_api_urls) + try: + for entry in import_string('{}.urls.api_router_entries'.format(app.name)): + router.register(**entry) + except ImportError: + pass + + urlpatterns.extend(router.urls) diff --git a/mayan/apps/rest_api/classes.py b/mayan/apps/rest_api/classes.py deleted file mode 100644 index 4cb84795a8..0000000000 --- a/mayan/apps/rest_api/classes.py +++ /dev/null @@ -1,10 +0,0 @@ -from __future__ import unicode_literals - - -class Endpoint(object): - def __init__(self, label): - self.label = label - - @property - def url(self): - return '/api/{}/'.format(self.label) diff --git a/mayan/apps/rest_api/filters.py b/mayan/apps/rest_api/filters.py index 1b561c598e..928c997bc8 100644 --- a/mayan/apps/rest_api/filters.py +++ b/mayan/apps/rest_api/filters.py @@ -5,18 +5,27 @@ from rest_framework.filters import BaseFilterBackend from mayan.apps.acls.models import AccessControlList -class MayanObjectPermissionsFilter(BaseFilterBackend): +class MayanViewSetObjectPermissionsFilter(BaseFilterBackend): def filter_queryset(self, request, queryset, view): - # TODO: fix variable name to make it clear it should be a single - # permission + """ + Filter the API view queryset by access using a permission. + Requires the object_permission_map class attribute which is a dictionary + that matches a view action ('update', 'list', etc) to a single + permission instance. + Example: object_permission_map = { + 'update': permission_..._edit + 'list': permission_..._view + } + """ + object_permission_dictionary = getattr(view, 'object_permission_map', {}) + object_permission = object_permission_dictionary.get( + view.action, None + ) - required_permissions = getattr( - view, 'mayan_object_permissions', {} - ).get(request.method, None) - - if required_permissions: - return AccessControlList.objects.filter_by_access( - required_permissions[0], request.user, queryset=queryset + if object_permission: + return AccessControlList.objects.restrict_queryset( + permission=object_permission, queryset=queryset, + user=request.user ) else: return queryset diff --git a/mayan/apps/rest_api/generics.py b/mayan/apps/rest_api/generics.py new file mode 100644 index 0000000000..ec3cf267d3 --- /dev/null +++ b/mayan/apps/rest_api/generics.py @@ -0,0 +1,48 @@ +from __future__ import absolute_import, unicode_literals + +from rest_framework import generics + +from mayan.apps.rest_api.filters import MayanObjectPermissionsFilter +from mayan.apps.rest_api.permissions import MayanPermission + + +class ListAPIView(generics.ListAPIView): + """ + requires: + object_permission = {'GET': ...} + """ + filter_backends = (MayanObjectPermissionsFilter,) + + +class ListCreateAPIView(generics.ListCreateAPIView): + """ + requires: + object_permission = {'GET': ...} + view_permission = {'POST': ...} + """ + filter_backends = (MayanObjectPermissionsFilter,) + permission_classes = (MayanPermission,) + + +class RetrieveDestroyAPIView(generics.RetrieveDestroyAPIView): + """ + requires: + object_permission = { + 'DELETE': ..., + 'GET': ..., + } + """ + filter_backends = (MayanObjectPermissionsFilter,) + + +class RetrieveUpdateDestroyAPIView(generics.RetrieveUpdateDestroyAPIView): + """ + requires: + object_permission = { + 'DELETE': ..., + 'GET': ..., + 'PATCH': ..., + 'PUT': ... + } + """ + filter_backends = (MayanObjectPermissionsFilter,) diff --git a/mayan/apps/rest_api/links.py b/mayan/apps/rest_api/links.py index efd5443ab3..d181599268 100644 --- a/mayan/apps/rest_api/links.py +++ b/mayan/apps/rest_api/links.py @@ -11,14 +11,14 @@ from .icons import ( link_api = Link( icon_class=icon_api, tags='new_window', text=_('REST API'), - view='rest_api:api_root' + view='rest_api:api-root' ) link_api_documentation = Link( icon_class=icon_api_documentation, tags='new_window', - text=_('API Documentation (Swagger)'), view='schema-swagger-ui' + text=_('API Documentation (Swagger)'), view='rest_api:schema-swagger-ui' ) link_api_documentation_redoc = Link( icon_class=icon_api_documentation_redoc, tags='new_window', - text=_('API Documentation (ReDoc)'), view='schema-redoc' + text=_('API Documentation (ReDoc)'), view='rest_api:schema-redoc' ) diff --git a/mayan/apps/rest_api/mixins.py b/mayan/apps/rest_api/mixins.py new file mode 100644 index 0000000000..efacb23ac1 --- /dev/null +++ b/mayan/apps/rest_api/mixins.py @@ -0,0 +1,54 @@ +from __future__ import absolute_import, unicode_literals + +from django.core.exceptions import ImproperlyConfigured + +from mayan.apps.acls.models import AccessControlList + + +class ExternalObjectListSerializerMixin(object): + class Meta: + external_object_list_model = None + external_object_list_permission = None + external_object_list_queryset = None + external_object_list_pk_field = None + external_object_list_pk_list_field = None + + def get_external_object_list(self): + queryset = self.get_external_object_list_queryset() + + if self.Meta.external_object_list_permission: + queryset = AccessControlList.objects.restrict_queryset( + permission=self.Meta.external_object_list_permission, + queryset=queryset, + user=self.context['request'].user + ) + + if self.Meta.external_object_list_pk_field: + id_list = ( + self.validated_data.get(self.Meta.external_object_list_pk_field), + ) + elif self.Meta.external_object_list_pk_list_field: + id_list = self.validated_data.get( + self.Meta.external_object_list_pk_list_field, '' + ).split(',') + else: + raise ImproperlyConfigured( + 'ExternalObjectListSerializerMixin requires a ' + 'external_object_list__pk_field a ' + 'external_object_list_pk_list_field.' + ) + + return queryset.filter(pk__in=id_list) + + def get_external_object_list_queryset(self): + if self.Meta.external_object_list_model: + queryset = self.Meta.external_object_list_model._meta.default_manager.all() + elif self.Meta.external_object_list_queryset: + return self.Meta.external_object_list_queryset + else: + raise ImproperlyConfigured( + 'ExternalObjectListSerializerMixin requires a ' + 'external_object_list_model or a external_object_list_queryset.' + ) + + return queryset diff --git a/mayan/apps/rest_api/permissions.py b/mayan/apps/rest_api/permissions.py index 476b551a21..830c4e5a07 100644 --- a/mayan/apps/rest_api/permissions.py +++ b/mayan/apps/rest_api/permissions.py @@ -1,26 +1,31 @@ -from __future__ import absolute_import - -from __future__ import unicode_literals +from __future__ import absolute_import, unicode_literals from django.core.exceptions import PermissionDenied -from django.http import Http404 from rest_framework.permissions import BasePermission -from mayan.apps.acls.models import AccessControlList from mayan.apps.permissions import Permission -class MayanPermission(BasePermission): +class MayanViewSetPermission(BasePermission): def has_permission(self, request, view): - required_permission = getattr( - view, 'mayan_view_permissions', {} - ).get(request.method, None) + """ + Block the API view by access using a permission. + Required the view_permission_map class attribute which is a dictionary + that matches a view actions ('create', 'destroy', etc) to a single + permission instance. + Example: view_permission_map = { + 'update': permission_..._edit + 'list': permission_..._view + } + """ + view_permission_dictionary = getattr(view, 'view_permission_map', {}) + view_permission = view_permission_dictionary.get(view.action, None) - if required_permission: + if view_permission: try: - Permission.check_permissions( - requester=request.user, permissions=required_permission + Permission.check_user_permission( + permission=view_permission, user=request.user ) except PermissionDenied: return False @@ -28,35 +33,3 @@ class MayanPermission(BasePermission): return True else: return True - - def has_object_permission(self, request, view, obj): - required_permission = getattr( - view, 'mayan_object_permissions', {} - ).get(request.method, None) - - object_permissions_raise_404 = getattr( - view, 'mayan_object_permissions_raise_404', () - ) - - if required_permission: - try: - if hasattr(view, 'mayan_permission_attribute_check'): - AccessControlList.objects.check_access( - permissions=required_permission, - user=request.user, obj=obj, - related=view.mayan_permission_attribute_check - ) - else: - AccessControlList.objects.check_access( - permissions=required_permission, user=request.user, - obj=obj - ) - except PermissionDenied: - if request.method in object_permissions_raise_404: - raise Http404 - else: - return False - else: - return True - else: - return True diff --git a/mayan/apps/rest_api/relations.py b/mayan/apps/rest_api/relations.py new file mode 100644 index 0000000000..5ff9f24f75 --- /dev/null +++ b/mayan/apps/rest_api/relations.py @@ -0,0 +1,35 @@ +from __future__ import unicode_literals + +from rest_framework.relations import HyperlinkedIdentityField + +from mayan.apps.common.utils import resolve_attribute + + +class MultiKwargHyperlinkedIdentityField(HyperlinkedIdentityField): + def __init__(self, *args, **kwargs): + self.view_kwargs = kwargs.pop('view_kwargs', []) + super(MultiKwargHyperlinkedIdentityField, self).__init__(*args, **kwargs) + + def get_url(self, obj, view_name, request, format): + """ + Extends HyperlinkedRelatedField to allow passing more than one view + keyword argument. + ---- + Given an object, return the URL that hyperlinks to the object. + + May raise a `NoReverseMatch` if the `view_name` and `lookup_field` + attributes are not configured to correctly match the URL conf. + """ + # Unsaved objects will not yet have a valid URL. + if hasattr(obj, 'pk') and obj.pk in (None, ''): + return None + + kwargs = {} + for entry in self.view_kwargs: + kwargs[entry['lookup_url_kwarg']] = resolve_attribute( + obj=obj, attribute=entry['lookup_field'] + ) + + return self.reverse( + viewname=view_name, kwargs=kwargs, request=request, format=format + ) diff --git a/mayan/apps/rest_api/serializers.py b/mayan/apps/rest_api/serializers.py deleted file mode 100644 index 041fc524a4..0000000000 --- a/mayan/apps/rest_api/serializers.py +++ /dev/null @@ -1,8 +0,0 @@ -from __future__ import unicode_literals - -from rest_framework import serializers - - -class EndpointSerializer(serializers.Serializer): - label = serializers.CharField(read_only=True) - url = serializers.URLField(read_only=True) diff --git a/mayan/apps/rest_api/urls.py b/mayan/apps/rest_api/urls.py index 33a3a7bed8..a9101bb273 100644 --- a/mayan/apps/rest_api/urls.py +++ b/mayan/apps/rest_api/urls.py @@ -1,18 +1,24 @@ from __future__ import unicode_literals -from django.conf.urls import include, url +from django.conf.urls import url -from .api_views import APIRoot, BrowseableObtainAuthToken - - -api_urls = [ - url(r'^$', APIRoot.as_view(), name='api_root'), - url( - r'^auth/token/obtain/$', BrowseableObtainAuthToken.as_view(), - name='auth_token_obtain' - ), -] +from .api_views import BrowseableObtainAuthToken, schema_view urlpatterns = [ - url(r'^', include(api_urls)), + url( + regex=r'^auth/token/obtain/$', name='auth_token_obtain', + view=BrowseableObtainAuthToken.as_view() + ), + url( + regex=r'^swagger(?P.json|.yaml)$', name='schema-json', + view=schema_view.without_ui(cache_timeout=None), + ), + url( + regex=r'^swagger/$', name='schema-swagger-ui', + view=schema_view.with_ui('swagger', cache_timeout=None) + ), + url( + regex=r'^redoc/$', name='schema-redoc', + view=schema_view.with_ui('redoc', cache_timeout=None) + ), ] diff --git a/mayan/apps/rest_api/viewsets.py b/mayan/apps/rest_api/viewsets.py new file mode 100644 index 0000000000..3369f1350d --- /dev/null +++ b/mayan/apps/rest_api/viewsets.py @@ -0,0 +1,11 @@ +from __future__ import absolute_import, unicode_literals + +from rest_framework import viewsets + +from .filters import MayanViewSetObjectPermissionsFilter +from .permissions import MayanViewSetPermission + + +class MayanAPIModelViewSet(viewsets.ModelViewSet): + filter_backends = (MayanViewSetObjectPermissionsFilter,) + permission_classes = (MayanViewSetPermission,) diff --git a/mayan/urls/base.py b/mayan/urls/base.py index 7e3a9a143d..6681443287 100644 --- a/mayan/urls/base.py +++ b/mayan/urls/base.py @@ -3,22 +3,8 @@ from __future__ import unicode_literals from django.conf.urls import url from django.contrib import admin -from drf_yasg.views import get_schema_view -from rest_framework import permissions - -from mayan.apps.rest_api.schemas import openapi_info - admin.autodiscover() -schema_view = get_schema_view( - openapi_info, - validators=['flex', 'ssv'], - public=True, - permission_classes=(permissions.AllowAny,), -) urlpatterns = [ url(r'^admin/', admin.site.urls), - url(r'^swagger(?P.json|.yaml)$', schema_view.without_ui(cache_timeout=None), name='schema-json'), - url(r'^swagger/$', schema_view.with_ui('swagger', cache_timeout=None), name='schema-swagger-ui'), - url(r'^redoc/$', schema_view.with_ui('redoc', cache_timeout=None), name='schema-redoc'), ] From ea3ba2c4de20b5be138ebc1250514d5b876b3c90 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Wed, 6 Feb 2019 05:19:31 -0400 Subject: [PATCH 115/209] Complete the MOTD app API views Add per viewset action permissions. Signed-off-by: Roberto Rosario --- mayan/apps/motd/api_views.py | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/mayan/apps/motd/api_views.py b/mayan/apps/motd/api_views.py index 88ad8b4871..30a82ae3f6 100644 --- a/mayan/apps/motd/api_views.py +++ b/mayan/apps/motd/api_views.py @@ -2,8 +2,7 @@ from __future__ import absolute_import, unicode_literals from rest_framework import viewsets -from mayan.apps.rest_api.filters import MayanObjectPermissionsFilter -from mayan.apps.rest_api.permissions import MayanPermission +from mayan.apps.rest_api.viewsets import MayanAPIModelViewSet from .models import Message from .permissions import ( @@ -13,7 +12,7 @@ from .permissions import ( from .serializers import MessageSerializer -class APIMessageViewSet(viewsets.ModelViewSet): +class APIMessageViewSet(MayanAPIModelViewSet): """ create: Create a new message. @@ -30,17 +29,16 @@ class APIMessageViewSet(viewsets.ModelViewSet): retrieve: Return the given message details. """ - filter_backends = (MayanObjectPermissionsFilter,) lookup_url_kwarg = 'message_id' - object_permission = { - 'DELETE': permission_message_delete, - 'GET': permission_message_view, - 'PATCH': permission_message_edit, - 'PUT': permission_message_edit, + object_permission_map = { + 'destroy': permission_message_delete, + 'list': permission_message_view, + 'partial_update': permission_message_edit, + 'retrieve': permission_message_view, + 'update': permission_message_edit, } queryset = Message.objects.all() - permission_classes = (MayanPermission,) serializer_class = MessageSerializer - view_permission = { - 'POST': permission_message_create + view_permission_map = { + 'create': permission_message_create } From 278f97b7e4f60eb69645d352570270a62f612edf Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Wed, 6 Feb 2019 05:20:42 -0400 Subject: [PATCH 116/209] Start tags app API refactor Signed-off-by: Roberto Rosario --- mayan/apps/tags/api_views.py | 129 +++++--------- mayan/apps/tags/models.py | 2 +- mayan/apps/tags/serializers.py | 287 +++--------------------------- mayan/apps/tags/tests/test_api.py | 276 ++++++++++++++++++---------- mayan/apps/tags/urls.py | 27 +-- 5 files changed, 250 insertions(+), 471 deletions(-) diff --git a/mayan/apps/tags/api_views.py b/mayan/apps/tags/api_views.py index 7018c54599..32ccc9a576 100644 --- a/mayan/apps/tags/api_views.py +++ b/mayan/apps/tags/api_views.py @@ -1,7 +1,9 @@ from __future__ import absolute_import, unicode_literals -from rest_framework import generics, status -from rest_framework.decorators import action +from drf_yasg.utils import swagger_auto_schema + +from rest_framework import generics, serializers, status, routers, viewsets +from rest_framework.decorators import action, detail_route from rest_framework.exceptions import ValidationError from rest_framework.response import Response @@ -10,12 +12,7 @@ from mayan.apps.documents.api_views import DocumentViewSet from mayan.apps.documents.models import Document from mayan.apps.documents.permissions import permission_document_view from mayan.apps.documents.serializers import DocumentSerializer -from mayan.apps.rest_api.filters import MayanObjectPermissionsFilter -from mayan.apps.rest_api.generics import ( - ListAPIView, ListCreateAPIView, RetrieveDestroyAPIView, - RetrieveUpdateDestroyAPIView -) -from mayan.apps.rest_api.permissions import MayanPermission +from mayan.apps.rest_api.viewsets import MayanAPIModelViewSet from .models import Tag from .permissions import ( @@ -23,121 +20,84 @@ from .permissions import ( permission_tag_edit, permission_tag_remove, permission_tag_view ) from .serializers import ( - DocumentTagAttachSerializer, DocumentTagSerializer, TagAttachSerializer, - TagRemoveSerializer, TagSerializer, + DocumentTagAttachSerializer, DocumentTagSerializer, + TagDocumentAttachRemoveSerializer, + TagSerializer, + ) - -from django.conf.urls import url, include -from django.contrib.auth.models import User - -from rest_framework import routers, serializers, viewsets -from rest_framework.decorators import detail_route -from rest_framework.response import Response - -from drf_yasg.utils import swagger_auto_schema - - -class TagViewSet(viewsets.ModelViewSet): - filter_backends = (MayanObjectPermissionsFilter,) - lookup_field = 'pk' +class TagViewSet(MayanAPIModelViewSet): lookup_url_kwarg='tag_id' - permission_classes = (MayanPermission,) + object_permission_map = { + 'destroy': permission_tag_delete, + 'document_attach': permission_tag_attach, + 'document_list': permission_tag_view, + 'document_remove': permission_tag_remove, + 'list': permission_tag_view, + 'partial_update': permission_tag_edit, + 'update': permission_tag_edit + } queryset = Tag.objects.all() serializer_class = TagSerializer + view_permission_map = { + 'create': permission_tag_create + } - - #@swagger_auto_schema(operation_description='GET /articles/today/') - @swagger_auto_schema( - operation_description="partial_update description override", responses={200: TagAttachSerializer} - ) @action( - detail=True, lookup_field='pk', lookup_url_kwarg='tag_id', - methods=('post',), serializer_class=TagAttachSerializer, - url_name='document-attach', url_path='attach' + detail=True, lookup_url_kwarg='tag_id', methods=('post',), + serializer_class=TagDocumentAttachRemoveSerializer, + url_name='document-attach', url_path='documents/attach' ) - def attach(self, request, *args, **kwargs): - #print '!!! attach', args, kwargs#, self.context - #return Response({}) + def document_attach(self, request, *args, **kwargs): + instance = self.get_object() serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) - #print '((((((((', serializer.validated_data - #self.perform_attach(serializer=serializer) - serializer.attach(instance=self.get_object()) + serializer.attach(instance=instance) headers = self.get_success_headers(data=serializer.data) return Response( serializer.data, status=status.HTTP_200_OK, headers=headers ) - #def perform_attach(self, serializer): - # #print '!!!!', serializer - # serializer.attach(instance=self.get_object()) - - #def get_success_headers(self, data): - # try: - # return {'Location': str(data[api_settings.URL_FIELD_NAME])} - # except (TypeError, KeyError): - # return {} - @action( - detail=True, lookup_field='pk', lookup_url_kwarg='tag_id', - url_name='document-list', url_path='documents' + detail=True, lookup_url_kwarg='tag_id', + serializer_class=DocumentSerializer, url_name='document-list', + url_path='documents' ) def document_list(self, request, *args, **kwargs): - queryset = self.get_object().documents.all() - - #TODO:Filter queryset - #queryset = self.filter_queryset(self.get_queryset()) - + queryset = self.get_object().get_documents(user=self.request.user) page = self.paginate_queryset(queryset) + + serializer = self.get_serializer( + queryset, many=True, context={'request': request} + ) + if page is not None: - #serializer = self.get_serializer(page, many=True) - serializer = DocumentSerializer(page, many=True, context={'request': request}) return self.get_paginated_response(serializer.data) - #serializer = self.get_serializer(queryset, many=True) - serializer = DocumentSerializer(queryset, many=True, context={'request': request}) return Response(serializer.data) - - #serializer = DocumentSerializer( - # instance=, many=True, - # context={'request': request} - #) - #return Response(serializer.data) - - @action( detail=True, lookup_field='pk', lookup_url_kwarg='tag_id', - methods=('post',), serializer_class=TagRemoveSerializer, - url_name='document-remove', url_path='remove' + methods=('post',), serializer_class=TagDocumentAttachRemoveSerializer, + url_name='document-remove', url_path='documents/remove' ) - def remove(self, request, *args, **kwargs): - #print '!!! attach', args, kwargs#, self.context - #return Response({}) + def document_remove(self, request, *args, **kwargs): + instance = self.get_object() serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) - #print '((((((((', serializer.validated_data - #self.perform_attach(serializer=serializer) - serializer.remove(instance=self.get_object()) + serializer.attach(instance=instance) headers = self.get_success_headers(data=serializer.data) return Response( serializer.data, status=status.HTTP_200_OK, headers=headers ) - #def get_serializer_class(self, *args, **kwargs): - # #if self.action == 'attach': - # print '!!!!get_serializer_class', args, kwargs - # return TagAttachSerializer - - class DocumentTagViewSet(ExternalObjectMixin, viewsets.ReadOnlyModelViewSet): external_object_class = Document external_object_pk_url_kwarg = 'document_id' external_object_permission = permission_tag_view - lookup_field = 'pk' + #lookup_field = 'pk' object_permission = { 'list': permission_document_view, 'retrieve': permission_document_view @@ -145,7 +105,8 @@ class DocumentTagViewSet(ExternalObjectMixin, viewsets.ReadOnlyModelViewSet): serializer_class = DocumentTagSerializer @action( - detail=True, lookup_field='pk', lookup_url_kwarg='document_id', + #detail=True, lookup_field='pk', lookup_url_kwarg='document_id', + detail=True, lookup_url_kwarg='document_id', methods=('post',), serializer_class=DocumentTagAttachSerializer, url_name='tag-attach', url_path='attach' ) diff --git a/mayan/apps/tags/models.py b/mayan/apps/tags/models.py index 1662fc1c27..142f2208af 100644 --- a/mayan/apps/tags/models.py +++ b/mayan/apps/tags/models.py @@ -65,7 +65,7 @@ class Tag(models.Model): Return a filtered queryset documents that have this tag attached. """ return AccessControlList.objects.restrict_queryset( - permission=permission_document_view, queryset=self.documents, + permission=permission_document_view, queryset=self.documents.all(), user=user ) diff --git a/mayan/apps/tags/serializers.py b/mayan/apps/tags/serializers.py index 9f4d5c3012..fe4c62a0d0 100644 --- a/mayan/apps/tags/serializers.py +++ b/mayan/apps/tags/serializers.py @@ -1,5 +1,6 @@ from __future__ import absolute_import, unicode_literals +from django.core.exceptions import ImproperlyConfigured from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers @@ -9,7 +10,7 @@ from rest_framework.reverse import reverse from mayan.apps.acls.models import AccessControlList from mayan.apps.documents.models import Document from mayan.apps.documents.serializers import DocumentSerializer -from mayan.apps.rest_api.relations import MultiKwargHyperlinkedIdentityField +from mayan.apps.rest_api.mixins import ExternalObjectListSerializerMixin from .models import Tag from .permissions import permission_tag_attach @@ -42,55 +43,6 @@ class TagSerializer(serializers.HyperlinkedModelSerializer): model = Tag - -""" -class WritableTagSerializer(serializers.ModelSerializer): - documents_pk_list = serializers.CharField( - help_text=_( - 'Comma separated list of document primary keys to which this tag ' - 'will be attached.' - ), required=False - ) - - class Meta: - fields = ( - 'color', 'documents_pk_list', 'id', 'label', - ) - model = Tag - - def _add_documents(self, documents_pk_list, instance): - instance.documents.add( - *Document.objects.filter(pk__in=documents_pk_list.split(',')) - ) - - def create(self, validated_data): - documents_pk_list = validated_data.pop('documents_pk_list', '') - - instance = super(WritableTagSerializer, self).create(validated_data) - - if documents_pk_list: - self._add_documents( - documents_pk_list=documents_pk_list, instance=instance - ) - - return instance - - def update(self, instance, validated_data): - documents_pk_list = validated_data.pop('documents_pk_list', '') - - instance = super(WritableTagSerializer, self).update( - instance, validated_data - ) - - if documents_pk_list: - instance.documents.clear() - self._add_documents( - documents_pk_list=documents_pk_list, instance=instance - ) - - return instance -""" - class DocumentTagSerializer(TagSerializer): #document_attach_url = serializers.HyperlinkedIdentityField( # lookup_url_kwarg='document_id', view_name='rest_api:document-tag-attach' @@ -165,227 +117,32 @@ class DocumentTagAttachSerializer(serializers.Serializer): ) -class TagAttachSerializer(serializers.Serializer): -#class TagAttachSerializer(TagSerializer): - documents_pk_list = serializers.CharField( +class TagDocumentAttachRemoveSerializer(ExternalObjectListSerializerMixin, serializers.Serializer): + document_id = serializers.CharField( + help_text=_( + 'Primary key of document to which this tag will be attached or ' + 'removed.' + ), required=False, write_only=True + ) + documents_id_list = serializers.CharField( help_text=_( 'Comma separated list of document primary keys to which this ' - 'tag will be attached.' - ), write_only=True + 'tag will be attached or removed.' + ), required=False, write_only=True ) - #class Meta(TagSerializer.Meta): - # fields = TagSerializer.Meta.fields + ('documents_pk_list',) - # read_only_fields = TagSerializer.Meta.fields + class Meta: + external_object_list_model = Document + external_object_list_permission = permission_tag_attach + external_object_list_pk_field = 'document_id' + external_object_list_pk_list_field = 'document_id_list' def attach(self, instance): - queryset = AccessControlList.objects.restrict_queryset( - permission=permission_tag_attach, queryset=Document.objects.all(), - user=self.context['request'].user - ) - - for document in queryset.filter(pk__in=self.validated_data['documents_pk_list'].split(',')): + queryset = self.get_external_object_list() + for document in queryset: instance.attach_to(document=document, user=self.context['request'].user) - #print '@@@@@@@', self.validated_data['document_pk_list'] - #print '@@@@@@@', instance - #print '22222', validated_data - #print '!!!', self.data['document_pk_list'] - - -class TagRemoveSerializer(serializers.Serializer): - documents_pk_list = serializers.CharField( - help_text=_( - 'Comma separated list of document primary keys from which this ' - 'tag will be removed.' - ), write_only=True - ) - def remove(self, instance): - queryset = AccessControlList.objects.restrict_queryset( - permission=permission_tag_attach, queryset=Document.objects.all(), - user=self.context['request'].user - ) - - for document in queryset.filter(pk__in=self.validated_data['documents_pk_list'].split(',')): - instance.remove_from(document=document, user=self.context['request'].user) - - - -class RelatedModel(object): - @classmethod - def generate(cls, serializer, validated_data): - result = [] - - kwargs = getattr(serializer.Meta, 'related_model_kwargs', {}) - kwargs.update({'serializer': serializer}) - - for field_name in getattr(serializer.Meta, 'related_models', []): - kwargs.update({'field_name': field_name}) - related_field = cls(**kwargs) - related_field.pop_pk_list(validated_data=validated_data) - result.append(related_field) - - return result - - def __init__(self, field_name, serializer, pk_list_field=None, model=None, object_permission=None): - self.field_name = field_name - self._pk_list_field = pk_list_field - self.model = model - self.object_permission = object_permission - self.serializer = serializer - - def create(self, instance): - field = self.get_field(instance=instance) - field.clear() - #model = self.get_model() - - queryset = self.get_model().objects.filter(pk__in=self.pk_list.split(',')) - - permission = self.object_permission.get('create') - - if permission: - queryset = AccessControlList.objects.restrict_queryset( - permission=permission, - queryset=queryset, - user=self.serializer.context['request'].user - ) - - self.related_add() - - field.add(*queryset) - #fieldqueryset=queryset) - - #def related_add(self, queryset): - # self.get_field().add(*queryset) - - - #def _get_m2m_field(self, instance): - # getattr(instance, m2m_field_name).all() - - """ - def _add_m2m(self, instance, m2m_pk_list, permission): - m2m_field = self._get_m2m_field() - m2m_field.clear() - - queryset = AccessControlList.objects.restrict_queryset( - permission=permission, - queryset=m2m_model.objects.filter(pk__in=m2m_pk_list.split(',')), - user=self.context['request'].user - ) - - #m2m_field.add(*queryset) - self._m2m_add(m2m_field=m2m_field, queryset=queryset) - """ - - def get_model(self): - return self.model or self.get_field.model - - def get_field(self, instance): - return getattr(instance, self.field_name) - - def get_pk_list_field_name(self): - return self._pk_list_field or '{}_pk_list'.format(self.field_name) - - def pop_pk_list(self, validated_data): - self.pk_list = validated_data.pop(self.get_pk_list_field_name(), '') - - -class RelatedModelSerializerMixin(object): - #m2m_pk_list_name = 'documents_pk_list' - #m2m_field_name = 'documents' - #m2m_model = Document - - """ - class Meta: - extra_kwargs = { - 'url': { - 'lookup_field': 'pk', 'lookup_url_kwarg': 'tag_pk', - 'view_name': 'rest_api:tag-detail' - } - } - fields = ( - 'color', 'documents_count', 'documents_pk_list', 'documents_url', - 'id', 'label', 'url' - ) - model = Tag - related_models = ('documents',) - related_models_kwargs = { - 'documents': { - 'pk_list_field': 'documents_pk_list', 'model': Document, - 'permissions': {'create': permission_tag_attach} - } - } - """ - - def _get_m2m_field(self, instance): - getattr(instance, m2m_field_name).all() - - def _add_m2m(self, instance, m2m_pk_list, permission): - m2m_field = self._get_m2m_field() - m2m_field.clear() - - queryset = AccessControlList.objects.restrict_queryset( - permission=permission, - queryset=m2m_model.objects.filter(pk__in=m2m_pk_list.split(',')), - user=self.context['request'].user - ) - - #m2m_field.add(*queryset) - self._m2m_add(m2m_field=m2m_field, queryset=queryset) - - #def _m2m_add(self, m2m_field, queryset): - # m2m_field.add(*queryset) - - def _m2m_add(self, m2m_field, queryset): - for document in queryset.all(): - m2m_field.add(document=document, user=self.context['request'].user) - - def create(self, validated_data): - related_objects = RelatedModel.generate( - serializer=self, validated_data=validated_data - ) - - instance = super(RelatedModelSerializerMixin, self).create( - validated_data=validated_data - ) - - #TODO: return a container class - #TODO:related_objects.create(instance=instance) - for related_object in related_objects: - related_object.create(instance=instance) - - #if m2m_pk_list: - ## self._add_m2m( - # instance=instance, m2m_pk_list=m2m_pk_list, - # permission=permission_tag_add - # ) - - return instance - - ''' - # Extract the related field data before calling the superclass - # .create() and avoid an error due to unknown field data. - - #related_models = self.Meta.related_models - - #self.Meta.related_models - related_models_dictionary = {} - for related_model in self.Meta.related_models: - - #if self.m2m_pk_list_name: - m2m_pk_list = validated_data.pop(self.get_related_model_pk_list(), '') - - instance = super(RelatedObjectSerializerMixin, self).create( - validated_data=validated_data - ) - - if m2m_pk_list: - self._add_m2m( - instance=instance, m2m_pk_list=m2m_pk_list, - permission=permission_tag_add - ) - - return instance - ''' - + queryset = self.get_external_object_list() + for document in queryset: + instance.attach_from(document=document, user=self.context['request'].user) diff --git a/mayan/apps/tags/tests/test_api.py b/mayan/apps/tags/tests/test_api.py index 4c259f2de9..ac2f8de517 100644 --- a/mayan/apps/tags/tests/test_api.py +++ b/mayan/apps/tags/tests/test_api.py @@ -18,93 +18,7 @@ from .literals import ( TEST_TAG_COLOR, TEST_TAG_COLOR_EDITED, TEST_TAG_LABEL, TEST_TAG_LABEL_EDITED ) -from .mixins import TagTestMixin - - -class TagAPITestCase(TagTestMixin, BaseAPITestCase): - def test_tag_create_view_no_permission(self): - response = self._request_api_tag_create_view() - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - self.assertEqual(Tag.objects.count(), 0) - - def test_tag_create_view_with_permission(self): - self.grant_permission(permission=permission_tag_create) - response = self._request_api_tag_create_view() - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - - tag = Tag.objects.first() - self.assertEqual(response.data['id'], tag.pk) - self.assertEqual(response.data['label'], TEST_TAG_LABEL) - self.assertEqual(response.data['color'], TEST_TAG_COLOR) - - self.assertEqual(Tag.objects.count(), 1) - self.assertEqual(tag.label, TEST_TAG_LABEL) - self.assertEqual(tag.color, TEST_TAG_COLOR) - - def test_tag_delete_view_no_access(self): - self._create_test_tag() - response = self._request_api_tag_delete_view() - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - self.assertTrue(self.test_tag in Tag.objects.all()) - self.assertEqual(Tag.objects.all().count(), 1) - - def test_tag_delete_view_with_access(self): - self._create_test_tag() - self.grant_access(obj=self.test_tag, permission=permission_tag_delete) - response = self._request_api_tag_delete_view() - self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) - self.assertEqual(Tag.objects.all().count(), 0) - - def test_tag_edit_patch_view_no_access(self): - self._create_test_tag() - response = self._request_api_tag_edit_patch_view() - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - self.test_tag.refresh_from_db() - self.assertEqual(self.test_tag.label, TEST_TAG_LABEL) - self.assertEqual(self.test_tag.color, TEST_TAG_COLOR) - self.assertEqual(Tag.objects.all().count(), 1) - - def test_tag_edit_patch_view_with_access(self): - self._create_test_tag() - self.grant_access(obj=self.test_tag, permission=permission_tag_edit) - response = self._request_api_tag_edit_patch_view() - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.test_tag.refresh_from_db() - self.assertEqual(self.test_tag.label, TEST_TAG_LABEL_EDITED) - self.assertEqual(self.test_tag.color, TEST_TAG_COLOR_EDITED) - self.assertEqual(Tag.objects.all().count(), 1) - - def test_tag_edit_put_view_no_access(self): - self._create_test_tag() - response = self._request_api_tag_edit_put_view() - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - self.test_tag.refresh_from_db() - self.assertEqual(self.test_tag.label, TEST_TAG_LABEL) - self.assertEqual(self.test_tag.color, TEST_TAG_COLOR) - self.assertEqual(Tag.objects.all().count(), 1) - - def test_tag_edit_put_view_with_access(self): - self._create_test_tag() - self.grant_access(obj=self.test_tag, permission=permission_tag_edit) - response = self._request_api_tag_edit_put_view() - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.test_tag.refresh_from_db() - self.assertEqual(self.test_tag.label, TEST_TAG_LABEL_EDITED) - self.assertEqual(self.test_tag.color, TEST_TAG_COLOR_EDITED) - self.assertEqual(Tag.objects.all().count(), 1) - - def test_tag_list_view_no_access(self): - self._create_test_tag() - response = self._request_api_tag_list_view() - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data['count'], 0) - - def test_tag_list_view_with_access(self): - self._create_test_tag() - self.grant_access(obj=self.test_tag, permission=permission_tag_view) - response = self._request_api_tag_list_view() - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data['count'], 1) +from .mixins import TagAPITestMixin, TagTestMixin class DocumentTagAPITestCase(TagTestMixin, DocumentTestMixin, BaseAPITestCase): @@ -117,13 +31,12 @@ class DocumentTagAPITestCase(TagTestMixin, DocumentTestMixin, BaseAPITestCase): data={'tag_id': self.test_tag.pk} ) - def test_document_tag_attach_view_no_access(self): + def test_document_tag_attach_view_no_permission(self): self._create_test_tag() self.test_document = self.upload_document() response = self._request_api_document_tag_attach_view() self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) self.assertTrue(self.test_tag not in self.test_document.tags.all()) - self.assertEqual(Tag.objects.all().count(), 1) def test_document_tag_attach_view_with_document_access(self): self._create_test_tag() @@ -132,7 +45,6 @@ class DocumentTagAPITestCase(TagTestMixin, DocumentTestMixin, BaseAPITestCase): response = self._request_api_document_tag_attach_view() self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) self.assertTrue(self.test_tag not in self.test_document.tags.all()) - self.assertEqual(Tag.objects.all().count(), 1) def test_document_tag_attach_view_with_tag_access(self): self._create_test_tag() @@ -141,7 +53,6 @@ class DocumentTagAPITestCase(TagTestMixin, DocumentTestMixin, BaseAPITestCase): response = self._request_api_document_tag_attach_view() self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) self.assertTrue(self.test_tag not in self.test_document.tags.all()) - self.assertEqual(Tag.objects.all().count(), 1) def test_document_tag_attach_view_with_full_access(self): self._create_test_tag() @@ -153,7 +64,6 @@ class DocumentTagAPITestCase(TagTestMixin, DocumentTestMixin, BaseAPITestCase): response = self._request_api_document_tag_attach_view() self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertTrue(self.test_tag in self.test_document.tags.all()) - self.assertEqual(Tag.objects.all().count(), 1) def _request_api_document_tag_detail_view(self): return self.get( @@ -205,7 +115,7 @@ class DocumentTagAPITestCase(TagTestMixin, DocumentTestMixin, BaseAPITestCase): kwargs={'document_id': self.test_document.pk} ) - def test_document_tag_list_view_no_access(self): + def test_document_tag_list_view_no_permission(self): self._create_test_tag() self.test_document = self.upload_document() self.test_tag.documents.add(self.test_document) @@ -248,7 +158,7 @@ class DocumentTagAPITestCase(TagTestMixin, DocumentTestMixin, BaseAPITestCase): } ) - def test_document_tag_remove_view_no_access(self): + def test_document_tag_remove_view_no_permission(self): self._create_test_tag() self.test_document = self.upload_document() self.test_tag.documents.add(self.test_document) @@ -291,16 +201,143 @@ class DocumentTagAPITestCase(TagTestMixin, DocumentTestMixin, BaseAPITestCase): self.assertEqual(Tag.objects.all().count(), 1) +class TagAPITestCase(TagAPITestMixin, TagTestMixin, BaseAPITestCase): + def test_tag_create_view_no_permission(self): + response = self._request_api_tag_create_view() + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(Tag.objects.count(), 0) + + def test_tag_create_view_with_permission(self): + self.grant_permission(permission=permission_tag_create) + response = self._request_api_tag_create_view() + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + tag = Tag.objects.first() + self.assertEqual(response.data['id'], tag.pk) + self.assertEqual(response.data['label'], TEST_TAG_LABEL) + self.assertEqual(response.data['color'], TEST_TAG_COLOR) + + self.assertEqual(Tag.objects.count(), 1) + self.assertEqual(tag.label, TEST_TAG_LABEL) + self.assertEqual(tag.color, TEST_TAG_COLOR) + + def test_tag_delete_view_no_permission(self): + self._create_test_tag() + response = self._request_api_tag_delete_view() + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + self.assertTrue(self.test_tag in Tag.objects.all()) + self.assertEqual(Tag.objects.all().count(), 1) + + def test_tag_delete_view_with_access(self): + self._create_test_tag() + self.grant_access(obj=self.test_tag, permission=permission_tag_delete) + response = self._request_api_tag_delete_view() + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + self.assertEqual(Tag.objects.all().count(), 0) + + def test_tag_edit_patch_view_no_permission(self): + self._create_test_tag() + response = self._request_api_tag_edit_patch_view() + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + self.test_tag.refresh_from_db() + self.assertEqual(self.test_tag.label, TEST_TAG_LABEL) + self.assertEqual(self.test_tag.color, TEST_TAG_COLOR) + self.assertEqual(Tag.objects.all().count(), 1) + + def test_tag_edit_patch_view_with_access(self): + self._create_test_tag() + self.grant_access(obj=self.test_tag, permission=permission_tag_edit) + response = self._request_api_tag_edit_patch_view() + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.test_tag.refresh_from_db() + self.assertEqual(self.test_tag.label, TEST_TAG_LABEL_EDITED) + self.assertEqual(self.test_tag.color, TEST_TAG_COLOR_EDITED) + self.assertEqual(Tag.objects.all().count(), 1) + + def test_tag_edit_put_view_no_permission(self): + self._create_test_tag() + response = self._request_api_tag_edit_put_view() + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + self.test_tag.refresh_from_db() + self.assertEqual(self.test_tag.label, TEST_TAG_LABEL) + self.assertEqual(self.test_tag.color, TEST_TAG_COLOR) + self.assertEqual(Tag.objects.all().count(), 1) + + def test_tag_edit_put_view_with_access(self): + self._create_test_tag() + self.grant_access(obj=self.test_tag, permission=permission_tag_edit) + response = self._request_api_tag_edit_put_view() + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.test_tag.refresh_from_db() + self.assertEqual(self.test_tag.label, TEST_TAG_LABEL_EDITED) + self.assertEqual(self.test_tag.color, TEST_TAG_COLOR_EDITED) + self.assertEqual(Tag.objects.all().count(), 1) + + def test_tag_list_view_no_permission(self): + self._create_test_tag() + response = self._request_api_tag_list_view() + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data['count'], 0) + + def test_tag_list_view_with_access(self): + self._create_test_tag() + self.grant_access(obj=self.test_tag, permission=permission_tag_view) + response = self._request_api_tag_list_view() + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data['count'], 1) + + class TagDocumentAPITestCase(TagTestMixin, DocumentTestMixin, BaseAPITestCase): auto_upload_document = False + def _request_api_tag_document_attach_view(self): + return self.post( + viewname='rest_api:tag-document-attach', + kwargs={'tag_id': self.test_tag.pk}, + data={'document_id': self.test_document.pk} + ) + + def test_tag_document_attach_view_no_permission(self): + self._create_test_tag() + self.test_document = self.upload_document() + response = self._request_api_tag_document_attach_view() + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + self.assertTrue(self.test_tag not in self.test_document.tags.all()) + + def test_tag_document_attach_view_with_document_access(self): + self._create_test_tag() + self.test_document = self.upload_document() + self.grant_access(obj=self.test_document, permission=permission_tag_attach) + response = self._request_api_tag_document_attach_view() + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + self.assertTrue(self.test_tag not in self.test_document.tags.all()) + + def test_tag_document_attach_view_with_tag_access(self): + self._create_test_tag() + self.test_document = self.upload_document() + self.grant_access(obj=self.test_tag, permission=permission_tag_attach) + response = self._request_api_tag_document_attach_view() + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertTrue(self.test_tag not in self.test_document.tags.all()) + + def test_tag_document_attach_view_with_full_access(self): + self._create_test_tag() + self.test_document = self.upload_document() + self.grant_access( + obj=self.test_document, permission=permission_tag_attach + ) + self.grant_access(obj=self.test_tag, permission=permission_tag_attach) + response = self._request_api_tag_document_attach_view() + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertTrue(self.test_tag in self.test_document.tags.all()) + def _request_api_tag_document_list_view(self): return self.get( viewname='rest_api:tag-document-list', kwargs={'tag_id': self.test_tag.pk} ) - def test_tag_document_list_view_no_access(self): + def test_tag_document_list_view_no_permission(self): self._create_test_tag() self.test_document = self.upload_document() self.test_tag.documents.add(self.test_document) @@ -340,3 +377,52 @@ class TagDocumentAPITestCase(TagTestMixin, DocumentTestMixin, BaseAPITestCase): response.data['results'][0]['uuid'], force_text(self.test_document.uuid) ) + + def _request_api_tag_document_remove_view(self): + return self.post( + viewname='rest_api:tag-document-remove', kwargs={ + 'tag_id': self.test_tag.pk + }, data={'document_id': self.test_document.pk} + ) + + def test_tag_document_remove_view_no_permission(self): + self._create_test_tag() + self.test_document = self.upload_document() + self.test_tag.documents.add(self.test_document) + response = self._request_api_tag_document_remove_view() + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + self.assertTrue(self.test_tag in self.test_document.tags.all()) + self.assertEqual(Tag.objects.all().count(), 1) + + def test_tag_document_remove_view_with_document_access(self): + self._create_test_tag() + self.test_document = self.upload_document() + self.test_tag.documents.add(self.test_document) + self.grant_access(obj=self.test_document, permission=permission_tag_remove) + response = self._request_api_tag_document_remove_view() + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + self.assertTrue(self.test_tag in self.test_document.tags.all()) + self.assertEqual(Tag.objects.all().count(), 1) + + def test_tag_document_remove_view_with_tag_access(self): + self._create_test_tag() + self.test_document = self.upload_document() + self.test_tag.documents.add(self.test_document) + self.grant_access(obj=self.test_tag, permission=permission_tag_remove) + response = self._request_api_tag_document_remove_view() + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertTrue(self.test_tag in self.test_document.tags.all()) + self.assertEqual(Tag.objects.all().count(), 1) + + def test_tag_document_remove_view_with_full_access(self): + self._create_test_tag() + self.test_document = self.upload_document() + self.test_tag.documents.add(self.test_document) + self.grant_access( + obj=self.test_document, permission=permission_tag_remove + ) + self.grant_access(obj=self.test_tag, permission=permission_tag_remove) + response = self._request_api_tag_document_remove_view() + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertFalse(self.test_tag not in self.test_document.tags.all()) + self.assertEqual(Tag.objects.all().count(), 1) diff --git a/mayan/apps/tags/urls.py b/mayan/apps/tags/urls.py index 66ac0b2659..89df42fb9e 100644 --- a/mayan/apps/tags/urls.py +++ b/mayan/apps/tags/urls.py @@ -2,10 +2,6 @@ from __future__ import unicode_literals from django.conf.urls import url -#from .api_views import ( -# APIDocumentTagView, APIDocumentTagListView, APITagDocumentListView, -# APITagListView, APITagView -#) from .api_views import DocumentTagViewSet, TagViewSet from .views import ( @@ -61,31 +57,10 @@ urlpatterns = [ ) ] - api_router_entries = ( {'prefix': r'tags', 'viewset': TagViewSet, 'basename': 'tag'}, { 'prefix': r'documents/(?P\d+)/tags', - 'viewset': DocumentTagViewSet, 'basename': 'document_tag' + 'viewset': DocumentTagViewSet, 'basename': 'document-tag' }, ) - -""" - url( - regex=r'^tags/(?P\d+)/documents/$', - name='tag-document-list', view=APITagDocumentListView.as_view(), - ), - url( - regex=r'^tags/(?P\d+)/$', name='tag-detail', - view=APITagView.as_view() - ), - url(regex=r'^tags/$', name='tag-list', view=APITagListView.as_view()), - url( - regex=r'^documents/(?P\d+)/tags/$', - name='document-tag-list', view=APIDocumentTagListView.as_view() - ), - url( - regex=r'^documents/(?P\d+)/tags/(?P[0-9]+)/$', - name='document-tag-detail', view=APIDocumentTagView.as_view() - ), -""" From 999e164c3deae85c5de883ae8901ff39699d4e4d Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Wed, 6 Feb 2019 21:36:27 -0400 Subject: [PATCH 117/209] Refactor the Django GPG app API views Convert the Django GPG app API view to use viewsets. Add key-list API view test. Signed-off-by: Roberto Rosario --- mayan/apps/django_gpg/api_views.py | 36 ++++---------- mayan/apps/django_gpg/serializers.py | 8 ++-- mayan/apps/django_gpg/settings.py | 2 +- mayan/apps/django_gpg/tests/test_api.py | 63 +++++++++++++++---------- mayan/apps/django_gpg/urls.py | 13 ++--- mayan/apps/tags/routers.py | 20 -------- 6 files changed, 58 insertions(+), 84 deletions(-) delete mode 100644 mayan/apps/tags/routers.py diff --git a/mayan/apps/django_gpg/api_views.py b/mayan/apps/django_gpg/api_views.py index dc7fcfa4f7..03da798bce 100644 --- a/mayan/apps/django_gpg/api_views.py +++ b/mayan/apps/django_gpg/api_views.py @@ -1,9 +1,6 @@ from __future__ import absolute_import, unicode_literals -from rest_framework import generics - -from mayan.apps.rest_api.filters import MayanObjectPermissionsFilter -from mayan.apps.rest_api.permissions import MayanPermission +from mayan.apps.rest_api.viewsets import MayanAPIModelViewSet from .models import Key from .permissions import ( @@ -12,30 +9,15 @@ from .permissions import ( from .serializers import KeySerializer -class APIKeyListView(generics.ListCreateAPIView): - """ - get: Returns a list of all the keys. - post: Upload a new key. - """ - filter_backends = (MayanObjectPermissionsFilter,) - mayan_object_permissions = {'GET': (permission_key_view,)} - mayan_view_permissions = {'POST': (permission_key_upload,)} - permission_classes = (MayanPermission,) - queryset = Key.objects.all() - serializer_class = KeySerializer - - -class APIKeyView(generics.RetrieveDestroyAPIView): - """ - delete: Delete the selected key. - get: Return the details of the selected key. - """ - filter_backends = (MayanObjectPermissionsFilter,) - lookup_field = 'pk' +class KeyAPIViewSet(MayanAPIModelViewSet): lookup_url_kwarg = 'key_id' - mayan_object_permissions = { - 'DELETE': (permission_key_delete,), - 'GET': (permission_key_view,), + object_permission_map = { + 'destroy': permission_key_delete, + 'list': permission_key_view, + 'retrieve': permission_key_view, } queryset = Key.objects.all() serializer_class = KeySerializer + view_permission_map = { + 'create': permission_key_upload + } diff --git a/mayan/apps/django_gpg/serializers.py b/mayan/apps/django_gpg/serializers.py index 6191ea6f24..b5a1e8e572 100644 --- a/mayan/apps/django_gpg/serializers.py +++ b/mayan/apps/django_gpg/serializers.py @@ -5,11 +5,13 @@ from rest_framework import serializers from .models import Key -class KeySerializer(serializers.ModelSerializer): +class KeySerializer(serializers.HyperlinkedModelSerializer): class Meta: extra_kwargs = { - 'lookup_url_kwarg': 'key_id', - 'url': {'view_name': 'rest_api:key-detail'}, + 'url': { + 'lookup_url_kwarg': 'key_id', + 'view_name': 'rest_api:key-detail' + }, } fields = ( 'algorithm', 'creation_date', 'expiration_date', 'fingerprint', diff --git a/mayan/apps/django_gpg/settings.py b/mayan/apps/django_gpg/settings.py index 0559a1504f..31bb8741b4 100644 --- a/mayan/apps/django_gpg/settings.py +++ b/mayan/apps/django_gpg/settings.py @@ -8,7 +8,7 @@ from .literals import ( DEFAULT_GPG_PATH, DEFAULT_KEYSERVER, DEFAULT_SETTING_GPG_BACKEND ) -namespace = Namespace(name='django_gpg', label=_('Signatures')) +namespace = Namespace(label=_('Signatures'), name='django_gpg') setting_gpg_backend = namespace.add_setting( default=DEFAULT_SETTING_GPG_BACKEND, diff --git a/mayan/apps/django_gpg/tests/test_api.py b/mayan/apps/django_gpg/tests/test_api.py index 14abdd85f3..71ab51e47e 100644 --- a/mayan/apps/django_gpg/tests/test_api.py +++ b/mayan/apps/django_gpg/tests/test_api.py @@ -10,18 +10,10 @@ from ..permissions import ( ) from .literals import TEST_KEY_DATA, TEST_KEY_FINGERPRINT +from .mixins import KeyTestMixin -class KeyAPITestCase(BaseAPITestCase): - def setUp(self): - super(KeyAPITestCase, self).setUp() - self.login_user() - - def _create_key(self): - return Key.objects.create(key_data=TEST_KEY_DATA) - - # Key creation by upload - +class KeyAPITestCase(KeyTestMixin, BaseAPITestCase): def _request_key_create_view(self): return self.post( viewname='rest_api:key-list', data={ @@ -32,13 +24,13 @@ class KeyAPITestCase(BaseAPITestCase): def test_key_create_view_no_permission(self): response = self._request_key_create_view() self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(Key.objects.count(), 0) def test_key_create_view_with_permission(self): self.grant_permission(permission=permission_key_upload) response = self._request_key_create_view() - self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertEqual(response.data['fingerprint'], TEST_KEY_FINGERPRINT) @@ -46,49 +38,70 @@ class KeyAPITestCase(BaseAPITestCase): self.assertEqual(Key.objects.count(), 1) self.assertEqual(key.fingerprint, TEST_KEY_FINGERPRINT) - # Key deletion - def _request_key_delete_view(self): return self.delete( - viewname='rest_api:key-detail', kwargs={'key_id': self.key.pk} + viewname='rest_api:key-detail', kwargs={'key_id': self.test_key.pk} ) def test_key_delete_view_no_access(self): - self.key = self._create_key() + self._create_test_key() + response = self._request_key_delete_view() self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + self.assertEqual(Key.objects.count(), 1) def test_key_delete_view_with_access(self): - self.key = self._create_key() + self._create_test_key() + self.grant_access( - permission=permission_key_delete, obj=self.key + permission=permission_key_delete, obj=self.test_key ) response = self._request_key_delete_view() self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) - self.assertEqual(Key.objects.count(), 0) - # Key detail + self.assertEqual(Key.objects.count(), 0) def _request_key_detail_view(self): return self.get( - viewname='rest_api:key-detail', kwargs={'key_id': self.key.pk} + viewname='rest_api:key-detail', kwargs={'key_id': self.test_key.pk} ) def test_key_detail_view_no_access(self): - self.key = self._create_key() - response = self._request_key_detail_view() + self._create_test_key() + response = self._request_key_detail_view() self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) def test_key_detail_view_with_access(self): - self.key = self._create_key() + self._create_test_key() + self.grant_access( - permission=permission_key_view, obj=self.key + permission=permission_key_view, obj=self.test_key ) response = self._request_key_detail_view() self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual( - response.data['fingerprint'], self.key.fingerprint + response.data['fingerprint'], self.test_key.fingerprint ) + + def _request_key_list_view(self): + return self.get(viewname='rest_api:key-list') + + def test_key_list_view_no_access(self): + self._create_test_key() + + response = self._request_key_list_view() + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.json()['count'], 0) + + def test_key_list_view_with_access(self): + self._create_test_key() + + self.grant_access( + permission=permission_key_view, obj=self.test_key + ) + response = self._request_key_list_view() + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.json()['count'], 1) diff --git a/mayan/apps/django_gpg/urls.py b/mayan/apps/django_gpg/urls.py index eaad4a617b..915373e5b3 100644 --- a/mayan/apps/django_gpg/urls.py +++ b/mayan/apps/django_gpg/urls.py @@ -2,7 +2,7 @@ from __future__ import unicode_literals from django.conf.urls import url -from .api_views import APIKeyListView, APIKeyView +from .api_views import KeyAPIViewSet from .views import ( KeyDeleteView, KeyDetailView, KeyDownloadView, KeyQueryView, KeyQueryResultView, KeyReceive, KeyUploadView, PrivateKeyListView, @@ -45,10 +45,7 @@ urlpatterns = [ ) ] -api_urls = [ - url( - regex=r'^keys/(?P\d+)/$', name='key-detail', - view=APIKeyView.as_view() - ), - url(regex=r'^keys/$', name='key-list', view=APIKeyListView.as_view()) -] + +api_router_entries = ( + {'prefix': r'keys', 'viewset': KeyAPIViewSet, 'basename': 'key'}, +) diff --git a/mayan/apps/tags/routers.py b/mayan/apps/tags/routers.py deleted file mode 100644 index 04f7371af6..0000000000 --- a/mayan/apps/tags/routers.py +++ /dev/null @@ -1,20 +0,0 @@ -from __future__ import unicode_literals - -#from rest_framework import routers - -#router = routers.SimpleRouter() -#router.register(r'users', UserViewSet) -#router.register(r'accounts', AccountViewSet) -#urlpatterns = router.urls - -#router = routers.DefaultRouter() -#from mayan.apps.rest_api.api_views import router -#from mayan.apps.rest_api.urls import router - -from .api_views import TagViewSet - -router_entries = ( - {'prefix': r'tags', 'viewset': TagViewSet, 'base_name': 'tag'}, -) - -#router.register(prefix=r'tags', viewset=TagViewSet, basename='tag') From 7d3677acfb19aefacc4db5ae4cea862f84cf8746 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Wed, 6 Feb 2019 21:37:46 -0400 Subject: [PATCH 118/209] View name cleanups Signed-off-by: Roberto Rosario --- mayan/apps/motd/api_views.py | 4 +--- mayan/apps/motd/urls.py | 4 ++-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/mayan/apps/motd/api_views.py b/mayan/apps/motd/api_views.py index 30a82ae3f6..10b21b41af 100644 --- a/mayan/apps/motd/api_views.py +++ b/mayan/apps/motd/api_views.py @@ -1,7 +1,5 @@ from __future__ import absolute_import, unicode_literals -from rest_framework import viewsets - from mayan.apps.rest_api.viewsets import MayanAPIModelViewSet from .models import Message @@ -12,7 +10,7 @@ from .permissions import ( from .serializers import MessageSerializer -class APIMessageViewSet(MayanAPIModelViewSet): +class MessageAPIViewSet(MayanAPIModelViewSet): """ create: Create a new message. diff --git a/mayan/apps/motd/urls.py b/mayan/apps/motd/urls.py index 24f26246d6..450713a4ee 100644 --- a/mayan/apps/motd/urls.py +++ b/mayan/apps/motd/urls.py @@ -2,7 +2,7 @@ from __future__ import unicode_literals from django.conf.urls import url -from .api_views import APIMessageViewSet +from .api_views import MessageAPIViewSet from .views import ( MessageCreateView, MessageDeleteView, MessageEditView, MessageListView ) @@ -27,5 +27,5 @@ urlpatterns = [ ] api_router_entries = ( - {'prefix': r'messages', 'viewset': APIMessageViewSet, 'basename': 'message'}, + {'prefix': r'messages', 'viewset': MessageAPIViewSet, 'basename': 'message'}, ) From ee2637dddce6056fe48541f48307c21c24053d14 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Wed, 6 Feb 2019 21:57:02 -0400 Subject: [PATCH 119/209] Common: Improve API view and tests Signed-off-by: Roberto Rosario --- mayan/apps/common/api_views.py | 9 ++++----- mayan/apps/common/classes.py | 2 +- mayan/apps/common/mixins.py | 1 - mayan/apps/common/serializers.py | 6 +++--- mayan/apps/common/tests/test_api.py | 30 ++++++++++++++++++++++------- mayan/apps/common/urls.py | 8 ++++---- 6 files changed, 35 insertions(+), 21 deletions(-) diff --git a/mayan/apps/common/api_views.py b/mayan/apps/common/api_views.py index ab762843a8..a5fcf2dc81 100644 --- a/mayan/apps/common/api_views.py +++ b/mayan/apps/common/api_views.py @@ -9,7 +9,7 @@ from .classes import Template from .serializers import ContentTypeSerializer, TemplateSerializer -class APIContentTypeViewSet(viewsets.ReadOnlyModelViewSet): +class ContentTypeAPIViewSet(viewsets.ReadOnlyModelViewSet): """ list: Return a list of all the available content types. @@ -17,13 +17,12 @@ class APIContentTypeViewSet(viewsets.ReadOnlyModelViewSet): retrieve: Return the given content type details. """ - lookup_field = 'pk' lookup_url_kwarg = 'content_type_id' queryset = ContentType.objects.order_by('app_label', 'model') serializer_class = ContentTypeSerializer -class APITemplateViewSet(viewsets.ReadOnlyModelViewSet): +class TemplateAPIViewSet(viewsets.ReadOnlyModelViewSet): """ list: Return a list of partial templates. @@ -31,12 +30,12 @@ class APITemplateViewSet(viewsets.ReadOnlyModelViewSet): retrieve: Return the given partial template details. """ - lookup_url_kwarg = 'name' + lookup_url_kwarg = 'template_name' permission_classes = (IsAuthenticated,) serializer_class = TemplateSerializer def get_object(self): - return Template.get(name=self.kwargs['name']).render( + return Template.get(name=self.kwargs['template_name']).render( request=self.request ) diff --git a/mayan/apps/common/classes.py b/mayan/apps/common/classes.py index 161def4408..bab726943c 100644 --- a/mayan/apps/common/classes.py +++ b/mayan/apps/common/classes.py @@ -292,7 +292,7 @@ class Template(object): def get_absolute_url(self): return reverse( - viewname='rest_api:template-detail', kwargs={'template_pk': self.name} + viewname='rest_api:template-detail', kwargs={'template_name': self.name} ) def render(self, request): diff --git a/mayan/apps/common/mixins.py b/mayan/apps/common/mixins.py index 26e9278a78..4e761bcd8e 100644 --- a/mayan/apps/common/mixins.py +++ b/mayan/apps/common/mixins.py @@ -13,7 +13,6 @@ from django.views.generic.detail import SingleObjectMixin from mayan.apps.acls.models import AccessControlList from mayan.apps.permissions import Permission -from .exceptions import ActionError from .forms import DynamicForm from .literals import ( PK_LIST_SEPARATOR, TEXT_CHOICE_ITEMS, TEXT_CHOICE_LIST, diff --git a/mayan/apps/common/serializers.py b/mayan/apps/common/serializers.py index 0983e55d9a..7ebe538487 100644 --- a/mayan/apps/common/serializers.py +++ b/mayan/apps/common/serializers.py @@ -9,8 +9,8 @@ class ContentTypeSerializer(serializers.HyperlinkedModelSerializer): class Meta: extra_kwargs = { 'url': { - 'lookup_field': 'pk', 'lookup_url_kwarg': 'content_type_id', - 'view_name': 'rest_api:content_type-detail' + 'lookup_url_kwarg': 'content_type_id', + 'view_name': 'rest_api:content-type-detail' } } fields = ('app_label', 'id', 'model', 'url') @@ -22,6 +22,6 @@ class TemplateSerializer(serializers.Serializer): name = serializers.CharField(read_only=True) html = serializers.CharField(read_only=True) url = serializers.HyperlinkedIdentityField( - lookup_field='name', lookup_url_kwarg='name', + lookup_field='name', lookup_url_kwarg='template_name', view_name='rest_api:template-detail' ) diff --git a/mayan/apps/common/tests/test_api.py b/mayan/apps/common/tests/test_api.py index a0111db300..3200e69b7d 100644 --- a/mayan/apps/common/tests/test_api.py +++ b/mayan/apps/common/tests/test_api.py @@ -1,7 +1,6 @@ from __future__ import unicode_literals from django.test import override_settings -from django.urls import reverse from mayan.apps.rest_api.tests import BaseAPITestCase @@ -11,23 +10,40 @@ TEST_TEMPLATE_RESULT = ' Date: Thu, 7 Feb 2019 20:12:55 -0400 Subject: [PATCH 120/209] Refactor the user management app API Signed-off-by: Roberto Rosario --- mayan/apps/user_management/api_views.py | 254 +++---- mayan/apps/user_management/apps.py | 18 +- mayan/apps/user_management/methods.py | 52 +- mayan/apps/user_management/querysets.py | 7 + mayan/apps/user_management/serializers.py | 143 ++-- mayan/apps/user_management/tests/test_api.py | 667 ++++++++++--------- mayan/apps/user_management/urls.py | 33 +- 7 files changed, 651 insertions(+), 523 deletions(-) create mode 100644 mayan/apps/user_management/querysets.py diff --git a/mayan/apps/user_management/api_views.py b/mayan/apps/user_management/api_views.py index 47b467fe72..09eadbef79 100644 --- a/mayan/apps/user_management/api_views.py +++ b/mayan/apps/user_management/api_views.py @@ -1,15 +1,13 @@ from __future__ import unicode_literals -from django.contrib.auth import get_user_model from django.contrib.auth.models import Group -from django.shortcuts import get_object_or_404 -from rest_framework import generics +from rest_framework import generics, status +from rest_framework.decorators import action +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response -from mayan.apps.acls.models import AccessControlList -from mayan.apps.common.mixins import ExternalObjectMixin -from mayan.apps.rest_api.filters import MayanObjectPermissionsFilter -from mayan.apps.rest_api.permissions import MayanPermission +from mayan.apps.rest_api.viewsets import MayanAPIModelViewSet from .permissions import ( permission_group_create, permission_group_delete, permission_group_edit, @@ -17,141 +15,147 @@ from .permissions import ( permission_user_edit, permission_user_view ) from .serializers import ( - GroupSerializer, UserSerializer#, UserGroupListSerializer + CurrentUserSerializer, GroupUserAddRemoveSerializer, GroupSerializer, + UserGroupAddRemoveSerializer, UserSerializer ) +from .querysets import get_user_queryset -class APICurrentUserView(generics.RetrieveUpdateDestroyAPIView): - """ - delete: Delete the current user. - get: Return the details of the current user. - patch: Partially edit the current user. - put: Edit the current user. - """ - serializer_class = UserSerializer +class CurrentUserAPIView(generics.RetrieveUpdateAPIView): + permission_classes = (IsAuthenticated,) + serializer_class = CurrentUserSerializer def get_object(self): return self.request.user -class APIGroupListView(generics.ListCreateAPIView): - """ - get: Returns a list of all the groups. - post: Create a new group. - """ - filter_backends = (MayanObjectPermissionsFilter,) - mayan_object_permissions = {'GET': (permission_group_view,)} - mayan_view_permissions = {'POST': (permission_group_create,)} - permission_classes = (MayanPermission,) - queryset = Group.objects.order_by('id') +class GroupAPIViewSet(MayanAPIModelViewSet): + lookup_url_kwarg = 'group_id' + object_permission_map = { + 'destroy': permission_group_delete, + 'list': permission_group_view, + 'partial_update': permission_group_edit, + 'retrieve': permission_group_view, + 'update': permission_group_edit, + 'user_add': permission_group_edit, + 'user_list': permission_group_view, + 'user_remove': permission_group_edit + } + queryset = Group.objects.all() serializer_class = GroupSerializer - - -class APIGroupView(generics.RetrieveUpdateDestroyAPIView): - """ - delete: Delete the selected group. - get: Return the details of the selected group. - patch: Partially edit the selected group. - put: Edit the selected group. - """ - lookup_url_kwarg = 'group_pk' - mayan_object_permissions = { - 'GET': (permission_group_view,), - 'PUT': (permission_group_edit,), - 'PATCH': (permission_group_edit,), - 'DELETE': (permission_group_delete,) - } - permission_classes = (MayanPermission,) - queryset = Group.objects.order_by('id') - serializer_class = GroupSerializer - - -class APIUserListView(generics.ListCreateAPIView): - """ - get: Returns a list of all the users. - post: Create a new user. - """ - filter_backends = (MayanObjectPermissionsFilter,) - mayan_object_permissions = {'GET': (permission_user_view,)} - mayan_view_permissions = {'POST': (permission_user_create,)} - permission_classes = (MayanPermission,) - queryset = get_user_model().objects.all() - serializer_class = UserSerializer - - -class APIUserView(generics.RetrieveUpdateDestroyAPIView): - """ - delete: Delete the selected user. - get: Return the details of the selected user. - patch: Partially edit the selected user. - put: Edit the selected user. - """ - lookup_url_kwarg = 'user_pk' - mayan_object_permissions = { - 'GET': (permission_user_view,), - 'PUT': (permission_user_edit,), - 'PATCH': (permission_user_edit,), - 'DELETE': (permission_user_delete,) - } - permission_classes = (MayanPermission,) - queryset = get_user_model().objects.all() - serializer_class = UserSerializer - - -class APIUserGroupList(ExternalObjectMixin, generics.ListCreateAPIView): - """ - get: Returns a list of all the groups to which an user belongs. - post: Add a user to a list of groups. - """ - external_object_pk_url_kwarg = 'user_pk' - filter_backends = (MayanObjectPermissionsFilter,) - mayan_object_permissions = { - 'GET': (permission_group_view,), - 'POST': (permission_group_edit,) + view_permission_map = { + 'create': permission_group_create } - def get_external_object_permission(self): - if self.request.method == 'POST': - return permission_user_edit - else: - return permission_user_view - - def get_external_object_queryset(self): - return get_user_model().objects.exclude(is_staff=True).exclude( - is_superuser=True + @action( + detail=True, lookup_url_kwarg='group_id', methods=('post',), + serializer_class=GroupUserAddRemoveSerializer, + url_name='user-add', url_path='users/add' + ) + def user_add(self, request, *args, **kwargs): + instance = self.get_object() + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + serializer.user_add(instance=instance) + headers = self.get_success_headers(data=serializer.data) + return Response( + serializer.data, status=status.HTTP_200_OK, headers=headers ) - def get_serializer(self, *args, **kwargs): - if not self.request: - return None + @action( + detail=True, lookup_url_kwarg='group_id', + serializer_class=UserSerializer, url_name='user-list', + url_path='users' + ) + def user_list(self, request, *args, **kwargs): + queryset = self.get_object().get_users(_user=self.request.user) + page = self.paginate_queryset(queryset) - return super(APIUserGroupList, self).get_serializer(*args, **kwargs) + serializer = self.get_serializer( + queryset, many=True, context={'request': request} + ) - def get_serializer_class(self): - if self.request.method == 'POST': - return UserSerializer - else: - return GroupSerializer + if page is not None: + return self.get_paginated_response(serializer.data) - def get_serializer_context(self): - """ - Extra context provided to the serializer class. - """ - context = super(APIUserGroupList, self).get_serializer_context() - if self.kwargs: - context.update( - { - 'user': self.get_user(), - } - ) + return Response(serializer.data) - return context + @action( + detail=True, lookup_url_kwarg='group_id', + methods=('post',), serializer_class=GroupUserAddRemoveSerializer, + url_name='user-remove', url_path='users/remove' + ) + def user_remove(self, request, *args, **kwargs): + instance = self.get_object() + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + serializer.user_remove(instance=instance) + headers = self.get_success_headers(data=serializer.data) + return Response( + serializer.data, status=status.HTTP_200_OK, headers=headers + ) - def get_queryset(self): - return self.get_user().groups.order_by('id') - def get_user(self): - return self.get_external_object() +class UserAPIViewSet(MayanAPIModelViewSet): + lookup_url_kwarg = 'user_id' + object_permission_map = { + 'destroy': permission_user_delete, + 'group-list': permission_user_view, + 'list': permission_user_view, + 'partial_update': permission_user_edit, + 'retrieve': permission_user_view, + 'update': permission_user_edit, + } + queryset = get_user_queryset() + serializer_class = UserSerializer + view_permission_map = { + 'create': permission_user_create + } - def perform_create(self, serializer): - return serializer.save(user=self.get_object(), _user=self.request.user) + @action( + detail=True, lookup_url_kwarg='user_id', methods=('post',), + serializer_class=UserGroupAddRemoveSerializer, + url_name='group-add', url_path='group/add' + ) + def group_add(self, request, *args, **kwargs): + instance = self.get_object() + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + serializer.group_add(instance=instance) + headers = self.get_success_headers(data=serializer.data) + return Response( + serializer.data, status=status.HTTP_200_OK, headers=headers + ) + + @action( + detail=True, lookup_url_kwarg='user_id', + serializer_class=GroupSerializer, url_name='group-list', + url_path='groups' + ) + def group_list(self, request, *args, **kwargs): + queryset = self.get_object().get_groups(_user=self.request.user) + page = self.paginate_queryset(queryset) + + serializer = self.get_serializer( + queryset, many=True, context={'request': request} + ) + + if page is not None: + return self.get_paginated_response(serializer.data) + + return Response(serializer.data) + + @action( + detail=True, lookup_url_kwarg='user_id', + methods=('post',), serializer_class=UserGroupAddRemoveSerializer, + url_name='group-remove', url_path='groups/remove' + ) + def group_remove(self, request, *args, **kwargs): + instance = self.get_object() + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + serializer.group_remove(instance=instance) + headers = self.get_success_headers(data=serializer.data) + return Response( + serializer.data, status=status.HTTP_200_OK, headers=headers + ) diff --git a/mayan/apps/user_management/apps.py b/mayan/apps/user_management/apps.py index 2ce6953c21..3398cd0658 100644 --- a/mayan/apps/user_management/apps.py +++ b/mayan/apps/user_management/apps.py @@ -33,7 +33,10 @@ from .links import ( link_user_set_password, link_user_setup, text_user_label, separator_user_label ) -from .methods import method_get_absolute_url +from .methods import ( + method_group_get_users, method_group_user_add, method_group_user_remove, + method_user_get_absolute_url, method_user_get_groups +) from .permissions import ( permission_group_delete, permission_group_edit, permission_group_view, permission_user_delete, permission_user_edit, @@ -63,6 +66,14 @@ class UserManagementApp(MayanAppConfig): serializer_class='mayan.apps.user_management.serializers.UserSerializer' ) + # Silence UnorderedObjectListWarning + # "Pagination may yield inconsistent result" + # Remove on Django 2.x + Group._meta.ordering = ('name',) + Group.add_to_class(name='get_users', value=method_group_get_users) + Group.add_to_class(name='user_add', value=method_group_user_add) + Group.add_to_class(name='user_remove', value=method_group_user_remove) + MetadataLookup( description=_('All the groups.'), name='groups', value=lookup_get_groups @@ -124,7 +135,10 @@ class UserManagementApp(MayanAppConfig): ) User.add_to_class( - name='get_absolute_url', value=method_get_absolute_url + name='get_absolute_url', value=method_user_get_absolute_url + ) + User.add_to_class( + name='get_groups', value=method_user_get_groups ) menu_list_facet.bind_links( diff --git a/mayan/apps/user_management/methods.py b/mayan/apps/user_management/methods.py index 23bd71b0a8..9f572ac68d 100644 --- a/mayan/apps/user_management/methods.py +++ b/mayan/apps/user_management/methods.py @@ -1,9 +1,59 @@ from __future__ import unicode_literals +from django.apps import apps +from django.db import transaction from django.shortcuts import reverse +from .events import event_group_edited, event_user_edited +from .permissions import permission_user_view +from .querysets import get_user_queryset -def method_get_absolute_url(self): + +def method_group_get_users(self, _user): + AccessControlList = apps.get_model( + app_label='acls', model_name='AccessControlList' + ) + + return AccessControlList.objects.restrict_queryset( + permission=permission_user_view, queryset=get_user_queryset(), + user=_user + ) + + +def method_group_user_add(self, user, _user): + with transaction.atomic(): + self.user_set.add(user) + event_group_edited.commit( + actor=_user, target=self + ) + event_user_edited.commit( + actor=_user, target=user + ) + + +def method_group_user_remove(self, user, _user): + with transaction.atomic(): + self.user_set.remove(user) + event_group_edited.commit( + actor=_user, target=self + ) + event_user_edited.commit( + actor=_user, target=user + ) + + +def method_user_get_absolute_url(self): return reverse( viewname='user_management:user_details', kwargs={'user_id': self.pk} ) + + +def method_user_get_groups(self, _user): + AccessControlList = apps.get_model( + app_label='acls', model_name='AccessControlList' + ) + + return AccessControlList.objects.restrict_queryset( + permission=permission_user_view, queryset=self.groups.all(), + user=_user + ) diff --git a/mayan/apps/user_management/querysets.py b/mayan/apps/user_management/querysets.py new file mode 100644 index 0000000000..9816561b43 --- /dev/null +++ b/mayan/apps/user_management/querysets.py @@ -0,0 +1,7 @@ +from __future__ import absolute_import, unicode_literals + +from django.contrib.auth import get_user_model + + +def get_user_queryset(): + return get_user_model().objects.filter(is_superuser=False, is_staff=False) diff --git a/mayan/apps/user_management/serializers.py b/mayan/apps/user_management/serializers.py index 6cd8205d46..b1ff9183a0 100644 --- a/mayan/apps/user_management/serializers.py +++ b/mayan/apps/user_management/serializers.py @@ -3,41 +3,112 @@ from __future__ import unicode_literals from django.contrib.auth import get_user_model from django.contrib.auth.models import Group from django.contrib.auth.password_validation import validate_password -from django.core.exceptions import PermissionDenied from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers -from rest_framework.exceptions import ValidationError -from mayan.apps.acls.models import AccessControlList +from mayan.apps.rest_api.mixins import ExternalObjectListSerializerMixin -from .permissions import permission_group_edit, permission_group_view +from .permissions import permission_group_edit, permission_user_edit +from .querysets import get_user_queryset class GroupSerializer(serializers.HyperlinkedModelSerializer): - users_count = serializers.SerializerMethodField() + user_add_url = serializers.HyperlinkedIdentityField( + lookup_url_kwarg='group_id', view_name='rest_api:group-user-add' + ) + + user_list_url = serializers.HyperlinkedIdentityField( + lookup_url_kwarg='group_id', view_name='rest_api:group-user-list' + ) + + user_remove_url = serializers.HyperlinkedIdentityField( + lookup_url_kwarg='group_id', view_name='rest_api:group-user-remove' + ) class Meta: extra_kwargs = { 'url': { - 'lookup_field': 'pk', 'lookup_url_kwarg': 'group_pk', + 'lookup_url_kwarg': 'group_id', 'view_name': 'rest_api:group-detail' } } - fields = ('id', 'name', 'url', 'users_count') + fields = ( + 'id', 'name', 'url', 'user_add_url', 'user_list_url', + 'user_remove_url' + ) model = Group - def get_users_count(self, instance): - return instance.user_set.count() + +class GroupUserAddRemoveSerializer(ExternalObjectListSerializerMixin, serializers.Serializer): + user_id = serializers.CharField( + help_text=_( + 'Primary key of the user that will be added or removed.' + ), required=False, write_only=True + ) + users_id_list = serializers.CharField( + help_text=_( + 'Comma separated list of user primary keys that will be added or ' + 'removed.' + ), required=False, write_only=True + ) + + class Meta: + external_object_list_queryset = get_user_queryset() + external_object_list_permission = permission_user_edit + external_object_list_pk_field = 'user_id' + external_object_list_pk_list_field = 'user_id_list' + + def user_add(self, instance): + queryset = self.get_external_object_list() + for user in queryset: + instance.user_add(user=user, _user=self.context['request'].user) + + def user_remove(self, instance): + queryset = self.get_external_object_list() + for user in queryset: + instance.user_remove(user=user, _user=self.context['request'].user) + + +class UserGroupAddRemoveSerializer(ExternalObjectListSerializerMixin, serializers.Serializer): + group_id = serializers.CharField( + help_text=_( + 'Primary key of the group that will be added or removed.' + ), required=False, write_only=True + ) + groups_id_list = serializers.CharField( + help_text=_( + 'Comma separated list of group primary keys that will be added or ' + 'removed.' + ), required=False, write_only=True + ) + + class Meta: + external_object_list_queryset = Group.objects.all() + external_object_list_permission = permission_group_edit + external_object_list_pk_field = 'group_id' + external_object_list_pk_list_field = 'group_id_list' + + def group_add(self, instance): + queryset = self.get_external_object_list() + for group in queryset: + instance.group_add(group=group, _group=self.context['request'].group) + + def group_remove(self, instance): + queryset = self.get_external_object_list() + for group in queryset: + instance.group_remove(group=group, _group=self.context['request'].group) class UserSerializer(serializers.HyperlinkedModelSerializer): - groups = GroupSerializer(many=True, read_only=True, required=False) - groups_pk_list = serializers.CharField( - help_text=_( - 'Comma separated list of group primary keys to assign this ' - 'user to.' - ), required=False, write_only=True + group_add_url = serializers.HyperlinkedIdentityField( + lookup_url_kwarg='user_id', view_name='rest_api:user-group-add' + ) + group_list_url = serializers.HyperlinkedIdentityField( + lookup_url_kwarg='user_id', view_name='rest_api:user-group-list' + ) + group_remove_url = serializers.HyperlinkedIdentityField( + lookup_url_kwarg='user_id', view_name='rest_api:user-group-remove' ) password = serializers.CharField( required=False, style={'input_type': 'password'}, write_only=True @@ -46,32 +117,19 @@ class UserSerializer(serializers.HyperlinkedModelSerializer): class Meta: extra_kwargs = { 'url': { - 'lookup_field': 'pk', 'lookup_url_kwarg': 'user_pk', + 'lookup_url_kwarg': 'user_id', 'view_name': 'rest_api:user-detail' } } fields = ( - 'first_name', 'date_joined', 'email', 'groups', 'groups_pk_list', - 'id', 'is_active', 'last_login', 'last_name', 'password', 'url', - 'username' + 'first_name', 'date_joined', 'email', 'group_add_url', + 'group_list_url', 'group_remove_url', 'id', 'is_active', + 'last_login', 'last_name', 'password', 'url', 'username' ) model = get_user_model() - read_only_fields = ('groups', 'is_active', 'last_login', 'date_joined') - write_only_fields = ('password', 'group_pk_list') - - def _add_groups(self, instance, groups_pk_list): - instance.groups.clear() - - queryset = AccessControlList.objects.restrict_queryset( - permission=permission_group_edit, - queryset=Group.objects.filter(pk__in=groups_pk_list.split(',')), - user=self.context['request'].user - ) - - instance.groups.add(*queryset) + read_only_fields = ('is_active', 'last_login', 'date_joined') def create(self, validated_data): - groups_pk_list = validated_data.pop('groups_pk_list', '') password = validated_data.pop('password', None) instance = super(UserSerializer, self).create(validated_data) @@ -79,23 +137,15 @@ class UserSerializer(serializers.HyperlinkedModelSerializer): instance.set_password(password) instance.save() - if groups_pk_list: - self._add_groups(instance=instance, groups_pk_list=groups_pk_list) - return instance def update(self, instance, validated_data): - groups_pk_list = validated_data.pop('groups_pk_list', '') - if 'password' in validated_data: instance.set_password(validated_data['password']) validated_data.pop('password') instance = super(UserSerializer, self).update(instance, validated_data) - if groups_pk_list: - self._add_groups(instance=instance, groups_pk_list=groups_pk_list) - return instance def validate(self, data): @@ -103,3 +153,14 @@ class UserSerializer(serializers.HyperlinkedModelSerializer): validate_password(data['password'], self.instance) return data + + +class CurrentUserSerializer(UserSerializer): + class Meta(UserSerializer.Meta): + # Remove some fields that don't apply to the current user + _field_list = ( + 'group_add_url', 'group_list_url', 'group_remove_url', 'url' + ) + fields = [ + field for field in UserSerializer.Meta.fields if field not in _field_list + ] diff --git a/mayan/apps/user_management/tests/test_api.py b/mayan/apps/user_management/tests/test_api.py index d30e474828..1699b6802b 100644 --- a/mayan/apps/user_management/tests/test_api.py +++ b/mayan/apps/user_management/tests/test_api.py @@ -15,523 +15,530 @@ from ..permissions import ( ) from .literals import ( - TEST_GROUP_2_NAME, TEST_GROUP_2_NAME_EDITED, TEST_USER_2_EMAIL, - TEST_USER_2_PASSWORD, TEST_USER_2_USERNAME, TEST_USER_2_USERNAME_EDITED, - TEST_USER_2_PASSWORD_EDITED + TEST_GROUP_NAME, TEST_GROUP_NAME_EDITED, TEST_USER_EMAIL, TEST_USER_USERNAME, + TEST_USER_USERNAME_EDITED, TEST_USER_PASSWORD, TEST_USER_PASSWORD_EDITED ) -from .mixins import UserTestMixin +from .mixins import GroupTestMixin, UserTestMixin -class UserAPITestCase(UserTestMixin, BaseAPITestCase): - def setUp(self): - super(UserAPITestCase, self).setUp() - self.login_user() - - def _request_api_test_user_create(self): +class GroupAPITestCase(UserTestMixin, GroupTestMixin, BaseAPITestCase): + def _request_test_group_create_api_view(self): return self.post( - viewname='rest_api:user-list', data={ - 'email': TEST_USER_2_EMAIL, 'password': TEST_USER_2_PASSWORD, - 'username': TEST_USER_2_USERNAME, + viewname='rest_api:group-list', data={ + 'name': TEST_GROUP_NAME } ) - def test_user_create_no_permission(self): - response = self._request_api_test_user_create() + def test_group_create_api_view_no_permission(self): + group_count = Group.objects.count() + + response = self._request_test_group_create_api_view() self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - # Default two users, the test admin and the test user - self.assertEqual(get_user_model().objects.count(), 2) - def test_user_create_with_permission(self): - self.grant_permission(permission=permission_user_create) - response = self._request_api_test_user_create() + self.assertEqual(group_count, Group.objects.count()) + + def test_group_create_api_view_with_permission(self): + group_count = Group.objects.count() + + self.grant_permission(permission=permission_group_create) + response = self._request_test_group_create_api_view() self.assertEqual(response.status_code, status.HTTP_201_CREATED) - user = get_user_model().objects.get(pk=response.data['id']) - self.assertEqual(user.username, TEST_USER_2_USERNAME) - self.assertEqual(get_user_model().objects.count(), 3) - def _request_api_create_test_user_with_extra_data(self): - return self.post( - viewname='rest_api:user-list', data={ - 'email': TEST_USER_2_EMAIL, 'password': TEST_USER_2_PASSWORD, - 'username': TEST_USER_2_USERNAME, - 'groups_id_list': self.test_groups_id_list + self.assertNotEqual(group_count, Group.objects.count()) + + def _request_test_group_delete_api_view(self): + return self.delete( + viewname='rest_api:group-detail', + kwargs={'group_id': self.test_group.pk} + ) + + def test_group_delete_api_view_no_permission(self): + self._create_test_group() + + group_count = Group.objects.count() + + response = self._request_test_group_delete_api_view() + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + self.assertEqual(group_count, Group.objects.count()) + + def test_group_delete_api_view_with_access(self): + self._create_test_group() + + group_count = Group.objects.count() + + self.grant_access( + obj=self.test_group, permission=permission_group_delete + ) + response = self._request_test_group_delete_api_view() + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + + self.assertNotEqual(group_count, Group.objects.count()) + + def _request_test_group_detail_api_view(self): + return self.get( + viewname='rest_api:group-detail', + kwargs={'group_id': self.test_group.pk} + ) + + def test_group_detail_api_view_no_permission(self): + self._create_test_group() + + response = self._request_test_group_detail_api_view() + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + self.assertNotEqual( + self.test_group.name, response.data.get('name', None) + ) + + def test_group_detail_api_view_with_access(self): + self._create_test_group() + + self.grant_access( + obj=self.test_group, permission=permission_group_view + ) + response = self._request_test_group_detail_api_view() + self.assertEqual(response.status_code, status.HTTP_200_OK) + + self.assertEqual(self.test_group.name, response.data.get('name', None)) + + def _request_test_group_edit_patch_api_view(self): + return self.patch( + viewname='rest_api:group-detail', + kwargs={'group_id': self.test_group.pk}, + data={ + 'name': TEST_GROUP_NAME_EDITED } ) - """ - def test_user_create_with_group_no_permission(self): + def test_group_edit_patch_api_view_no_permission(self): self._create_test_group() - self.test_groups_id_list = '{}'.format(self.test_group.pk) - response = self._request_api_create_test_user_with_extra_data() - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + group_name = self.test_group.name - def test_user_create_with_group_with_user_access(self): + response = self._request_test_group_edit_patch_api_view() + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + self.test_group.refresh_from_db() + self.assertEqual(group_name, self.test_group.name) + + def test_group_edit_via_patch_with_access(self): self._create_test_group() - self.test_groups_id_list = '{}'.format(self.test_group.pk) + + group_name = self.test_group.name self.grant_access( - obj=self.test_user, permission=permission_user_create + obj=self.test_group, permission=permission_group_edit ) - response = self._request_api_create_test_user_with_extra_data() + response = self._request_test_group_edit_patch_api_view() + self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.test_group.refresh_from_db() + self.assertNotEqual(group_name, self.test_group.name) - user = get_user_model().objects.get(pk=response.data['id']) - self.assertEqual(user.username, TEST_USER_2_USERNAME) - self.assertQuerysetEqual(user.groups.all(), (repr(self.test_group),)) + def _request_test_group_list_api_view(self): + return self.get(viewname='rest_api:group-list') - - def test_user_create_with_group_with_user_access(self): + def test_group_list_api_view_no_permission(self): + self._create_test_group() + + response = self._request_test_group_list_api_view() + self.assertNotContains( + response=response, text=self.test_group.name, + status_code=status.HTTP_200_OK + ) + + def test_group_list_api_view_with_access(self): self._create_test_group() - self.test_groups_id_list = '{}'.format(self.test_group.pk) self.grant_access( - obj=self.test_user, permission=permission_user_create + obj=self.test_group, permission=permission_group_view + ) + response = self._request_test_group_list_api_view() + self.assertContains( + response=response, text=self.test_group.name, + status_code=status.HTTP_200_OK ) - response = self._request_api_create_test_user_with_extra_data() - self.assertEqual(response.status_code, status.HTTP_201_CREATED) + def _request_test_group_user_add_patch_api_view(self): + return self.post( + viewname='rest_api:group-user-add', + kwargs={'group_id': self.test_group.pk}, + data={ + 'user_id': self.test_user.pk + } + ) - user = get_user_model().objects.get(pk=response.data['id']) - self.assertEqual(user.username, TEST_USER_2_USERNAME) - self.assertQuerysetEqual(user.groups.all(), (repr(self.test_group),)) - """ + def _setup_group_user_add(self): + self._create_test_group() + self._create_test_user() - def test_user_create_with_groups_no_permission(self): - group_1 = Group.objects.create(name='test group 1') - group_2 = Group.objects.create(name='test group 2') - self.test_groups_id_list = '{},{}'.format(group_1.pk, group_2.pk) - response = self._request_api_create_test_user_with_extra_data() + def test_group_user_add_api_view_no_permission(self): + self._setup_group_user_add() + + response = self._request_test_group_user_add_patch_api_view() + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + self.test_group.refresh_from_db() + self.assertTrue(self.test_user not in self.test_group.user_set.all()) + + def test_group_user_add_with_group_access(self): + self._setup_group_user_add() + + self.grant_access( + obj=self.test_group, permission=permission_group_edit + ) + response = self._request_test_group_user_add_patch_api_view() + self.assertEqual(response.status_code, status.HTTP_200_OK) + + self.test_group.refresh_from_db() + self.assertTrue(self.test_user not in self.test_group.user_set.all()) + + def test_group_user_add_with_user_access(self): + self._setup_group_user_add() + + self.grant_access( + obj=self.test_user, permission=permission_user_edit + ) + response = self._request_test_group_user_add_patch_api_view() + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + self.test_group.refresh_from_db() + self.assertTrue(self.test_user not in self.test_group.user_set.all()) + + def test_group_user_add_with_full_access(self): + self._setup_group_user_add() + + self.grant_access( + obj=self.test_group, permission=permission_group_edit + ) + self.grant_access( + obj=self.test_user, permission=permission_user_edit + ) + response = self._request_test_group_user_add_patch_api_view() + self.assertEqual(response.status_code, status.HTTP_200_OK) + + self.test_group.refresh_from_db() + self.assertTrue(self.test_user in self.test_group.user_set.all()) + + #TODO: group-user-list tests + + def _request_test_group_user_remove_patch_api_view(self): + return self.post( + viewname='rest_api:group-user-remove', + kwargs={'group_id': self.test_group.pk}, + data={ + 'user_id': self.test_user.pk + } + ) + + def _setup_group_user_remove(self): + self._create_test_group() + self._create_test_user() + self.test_group.user_set.add(self.test_user) + + def test_group_user_remove_api_view_no_permission(self): + self._setup_group_user_remove() + + response = self._request_test_group_user_remove_patch_api_view() + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + self.test_group.refresh_from_db() + self.assertTrue(self.test_user in self.test_group.user_set.all()) + + def test_group_user_remove_with_group_access(self): + self._setup_group_user_remove() + + self.grant_access( + obj=self.test_group, permission=permission_group_edit + ) + response = self._request_test_group_user_remove_patch_api_view() + self.assertEqual(response.status_code, status.HTTP_200_OK) + + self.test_group.refresh_from_db() + self.assertTrue(self.test_user in self.test_group.user_set.all()) + + def test_group_user_remove_with_user_access(self): + self._setup_group_user_remove() + + self.grant_access( + obj=self.test_user, permission=permission_user_edit + ) + response = self._request_test_group_user_remove_patch_api_view() + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + self.test_group.refresh_from_db() + self.assertTrue(self.test_user in self.test_group.user_set.all()) + + def test_group_user_remove_with_full_access(self): + self._setup_group_user_remove() + + self.grant_access( + obj=self.test_group, permission=permission_group_edit + ) + self.grant_access( + obj=self.test_user, permission=permission_user_edit + ) + response = self._request_test_group_user_remove_patch_api_view() + self.assertEqual(response.status_code, status.HTTP_200_OK) + + self.test_group.refresh_from_db() + self.assertTrue(self.test_user not in self.test_group.user_set.all()) + + +class UserAPITestCase(GroupTestMixin, UserTestMixin, BaseAPITestCase): + def _request_test_user_create_api_view_api_view(self): + return self.post( + viewname='rest_api:user-list', data={ + 'email': TEST_USER_EMAIL, 'password': TEST_USER_PASSWORD, + 'username': TEST_USER_USERNAME, + } + ) + + def test_user_create_api_view_no_permission(self): + user_count = get_user_model().objects.count() + + response = self._request_test_user_create_api_view_api_view() self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - def test_user_create_with_groups_with_user_permission(self): - group_1 = Group.objects.create(name='test group 1') - group_2 = Group.objects.create(name='test group 2') - self.test_groups_id_list = '{},{}'.format(group_1.pk, group_2.pk) + self.assertEqual(user_count, get_user_model().objects.count()) + + def test_user_create_api_view_with_permission(self): + user_count = get_user_model().objects.count() + self.grant_permission(permission=permission_user_create) - response = self._request_api_create_test_user_with_extra_data() - + response = self._request_test_user_create_api_view_api_view() self.assertEqual(response.status_code, status.HTTP_201_CREATED) - user = get_user_model().objects.get(pk=response.data['id']) - self.assertEqual(user.username, TEST_USER_2_USERNAME) - #self.assertQuerysetEqual( - # user.groups.all().order_by('name'), (repr(group_1), repr(group_2)) - #) - self.assertEqual(user.groups.count(), 0) - def test_user_create_with_groups_with_full_access(self): - group_1 = Group.objects.create(name='test group 1') - group_2 = Group.objects.create(name='test group 2') - self.test_groups_id_list = '{},{}'.format(group_1.pk, group_2.pk) - self.grant_permission(permission=permission_user_create) - self.grant_access(obj=group_1, permission=permission_group_edit) - self.grant_access(obj=group_2, permission=permission_group_edit) - response = self._request_api_create_test_user_with_extra_data() + self.assertEqual(user.username, TEST_USER_USERNAME) + self.assertEqual(user_count + 1, get_user_model().objects.count()) - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - - user = get_user_model().objects.get(pk=response.data['id']) - self.assertEqual(user.username, TEST_USER_2_USERNAME) - self.assertQuerysetEqual( - user.groups.all().order_by('name'), (repr(group_1), repr(group_2)) - ) - - # User login - - def test_user_create_login(self): + def test_user_create_api_view_login(self): self._create_test_user() self.assertTrue( self.login( - username=TEST_USER_2_USERNAME, password=TEST_USER_2_PASSWORD + username=TEST_USER_USERNAME, password=TEST_USER_PASSWORD ) ) - # User password change - - def _request_api_user_password_change(self): + def _request_user_password_change(self): return self.patch( viewname='rest_api:user-detail', kwargs={'user_id': self.test_user.pk}, data={ - 'password': TEST_USER_2_PASSWORD_EDITED, + 'password': TEST_USER_PASSWORD_EDITED, } ) - def test_user_create_login_password_change_no_access(self): + def test_user_create_login_password_change_api_view_no_permission(self): self._create_test_user() - self._request_api_user_password_change() + self._request_user_password_change() self.assertFalse( self.client.login( - username=TEST_USER_2_USERNAME, - password=TEST_USER_2_PASSWORD_EDITED + username=TEST_USER_USERNAME, + password=TEST_USER_PASSWORD_EDITED ) ) - def test_user_create_login_password_change_with_access(self): + def test_user_create_login_password_change_api_view_with_access(self): self._create_test_user() self.grant_access(obj=self.test_user, permission=permission_user_edit) - self._request_api_user_password_change() + self._request_user_password_change() self.assertTrue( self.client.login( - username=TEST_USER_2_USERNAME, - password=TEST_USER_2_PASSWORD_EDITED + username=TEST_USER_USERNAME, + password=TEST_USER_PASSWORD_EDITED ) ) - # User edit - - def _request_api_test_user_edit_via_put(self): + def _request_test_user_edit_put_api_view(self): return self.put( viewname='rest_api:user-detail', kwargs={'user_id': self.test_user.pk}, - data={'username': TEST_USER_2_USERNAME_EDITED} + data={'username': TEST_USER_USERNAME_EDITED} ) - def test_user_edit_via_put_no_access(self): + def test_user_edit_put_api_view_no_permission(self): self._create_test_user() - response = self._request_api_test_user_edit_via_put() + username = self.test_user.username + response = self._request_test_user_edit_put_api_view() self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) self.test_user.refresh_from_db() - self.assertEqual(self.test_user.username, TEST_USER_2_USERNAME) + self.assertEqual(username, self.test_user.username) - def test_user_edit_via_put_with_access(self): + def test_user_edit_put_api_view_with_access(self): self._create_test_user() - self.grant_access(obj=self.test_user, permission=permission_user_edit) - response = self._request_api_test_user_edit_via_put() + username = self.test_user.username + self.grant_access(obj=self.test_user, permission=permission_user_edit) + response = self._request_test_user_edit_put_api_view() self.assertEqual(response.status_code, status.HTTP_200_OK) self.test_user.refresh_from_db() - self.assertEqual(self.test_user.username, TEST_USER_2_USERNAME_EDITED) + self.assertNotEqual(username, self.test_user.username) - def _request_api_test_user_edit_via_patch(self): + def _request_test_user_edit_patch_api_view(self): return self.patch( viewname='rest_api:user-detail', kwargs={'user_id': self.test_user.pk}, - data={'username': TEST_USER_2_USERNAME_EDITED} + data={'username': TEST_USER_USERNAME_EDITED} ) - def test_user_edit_via_patch_no_access(self): + def test_user_edit_patch_api_view_no_permission(self): self._create_test_user() - response = self._request_api_test_user_edit_via_patch() + username = self.test_user.username + response = self._request_test_user_edit_patch_api_view() self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) self.test_user.refresh_from_db() - self.assertEqual(self.test_user.username, TEST_USER_2_USERNAME) + self.assertEqual(username, self.test_user.username) - def test_user_edit_via_patch_with_access(self): + def test_user_edit_patch_api_view_with_access(self): self._create_test_user() - self.grant_access(obj=self.test_user, permission=permission_user_edit) - response = self._request_api_test_user_edit_via_patch() + username = self.test_user.username + self.grant_access(obj=self.test_user, permission=permission_user_edit) + response = self._request_test_user_edit_patch_api_view() self.assertEqual(response.status_code, status.HTTP_200_OK) self.test_user.refresh_from_db() - self.assertEqual(self.test_user.username, TEST_USER_2_USERNAME_EDITED) + self.assertNotEqual(username, self.test_user.username) - def _request_api_test_user_edit_via_patch_with_extra_data(self): - return self.patch( - viewname='rest_api:user-detail', - kwargs={'user_id': self.test_user.pk}, - data={'groups_id_list': '{}'.format(self.test_group.pk)} - ) - - def test_user_edit_add_groups_via_patch_no_access(self): - self._create_test_group() - self._create_test_user() - - response = self._request_api_test_user_edit_via_patch_with_extra_data() - - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - - self.test_user.refresh_from_db() - self.assertEqual(self.test_user.username, TEST_USER_2_USERNAME) - - self.assertQuerysetEqual( - self.test_user.groups.all(), () - ) - - def test_user_edit_add_groups_via_patch_with_access(self): - self._create_test_group() - self._create_test_user() - self.grant_access(obj=self.test_user, permission=permission_user_edit) - self.grant_access(obj=self.test_group, permission=permission_group_edit) - response = self._request_api_test_user_edit_via_patch_with_extra_data() - - self.assertEqual(response.status_code, status.HTTP_200_OK) - - self.test_user.refresh_from_db() - self.assertEqual(self.test_user.username, TEST_USER_2_USERNAME) - - self.assertQuerysetEqual( - self.test_user.groups.all(), (repr(self.test_group),) - ) - - # User delete - - def _request_api_test_user_delete(self): + def _request_test_user_delete_api_view(self): return self.delete( viewname='rest_api:user-detail', kwargs={'user_id': self.test_user.pk} ) - def test_user_delete_no_access(self): + def test_user_delete_api_view_no_permission(self): self._create_test_user() - response = self._request_api_test_user_delete() + + response = self._request_test_user_delete_api_view() self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) self.assertTrue( get_user_model().objects.filter(pk=self.test_user.pk).exists() ) - def test_user_delete_with_access(self): + def test_user_delete_api_view_with_access(self): self._create_test_user() + self.grant_access( obj=self.test_user, permission=permission_user_delete ) - response = self._request_api_test_user_delete() + response = self._request_test_user_delete_api_view() self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) self.assertFalse( get_user_model().objects.filter(pk=self.test_user.pk).exists() ) - # User group listview - - def _request_api_test_user_group_view(self): + def _request_test_user_group_api_view(self): return self.get( - viewname='rest_api:users-group-list', + viewname='rest_api:user-group-list', kwargs={'user_id': self.test_user.pk} ) - def test_user_group_list_no_access(self): - group = Group.objects.create(name=TEST_GROUP_2_NAME) + def test_user_group_list_api_view_no_permission(self): + self._create_test_group() self._create_test_user() - self.test_user.groups.add(group) - response = self._request_api_test_user_group_view() + self.test_user.groups.add(self.test_group) + + response = self._request_test_user_group_api_view() self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - def test_user_group_list_with_user_access(self): - group = Group.objects.create(name=TEST_GROUP_2_NAME) + def test_user_group_list_api_view_with_user_access(self): + self._create_test_group() self._create_test_user() - self.test_user.groups.add(group) + self.test_user.groups.add(self.test_group) + self.grant_access(obj=self.test_user, permission=permission_user_view) - response = self._request_api_test_user_group_view() + response = self._request_test_user_group_api_view() self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data['count'], 0) - def test_user_group_list_with_group_access(self): + def test_user_group_list_api_view_with_group_access(self): self._create_test_group() self._create_test_user() self.test_user.groups.add(self.test_group) + self.grant_access( obj=self.test_group, permission=permission_group_view ) - response = self._request_api_test_user_group_view() + response = self._request_test_user_group_api_view() self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - def test_user_group_list_with_access(self): + def test_user_group_list_api_view_with_access(self): self._create_test_group() self._create_test_user() self.test_user.groups.add(self.test_group) + self.grant_access(obj=self.test_user, permission=permission_user_view) self.grant_access( obj=self.test_group, permission=permission_group_view ) - response = self._request_api_test_user_group_view() + response = self._request_test_user_group_api_view() self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data['count'], 1) - def _request_api_test_user_group_add(self): + def _request_test_user_group_add_api_view(self): return self.patch( viewname='rest_api:user-detail', kwargs={'user_id': self.test_user.pk}, data={'group_id_list': '{}'.format(self.test_group.pk)} ) - def test_user_group_add_no_access(self): + def test_user_group_add_api_view_no_permission(self): self._create_test_group() self._create_test_user() - response = self._request_api_test_user_group_add() + + response = self._request_test_user_group_add_api_view() self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + self.test_user.refresh_from_db() self.assertEqual(self.test_group.user_set.first(), None) - def test_user_group_add_with_user_access(self): + def test_user_group_add_api_view_with_user_access(self): self._create_test_group() self._create_test_user() + self.grant_access(obj=self.test_user, permission=permission_user_edit) - response = self._request_api_test_user_group_add() + response = self._request_test_user_group_add_api_view() self.assertEqual(response.status_code, status.HTTP_200_OK) + self.test_user.refresh_from_db() self.assertEqual(self.test_group.user_set.first(), None) - def test_user_group_add_with_group_access(self): + def test_user_group_add_api_view_with_group_access(self): self._create_test_group() self._create_test_user() + self.grant_access( obj=self.test_group, permission=permission_group_edit ) - response = self._request_api_test_user_group_add() + response = self._request_test_user_group_add_api_view() self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + self.test_user.refresh_from_db() self.assertEqual(self.test_group.user_set.first(), None) - def test_user_group_add_with_full_access(self): + def test_user_group_add_api_view_with_full_access(self): self._create_test_group() self._create_test_user() + self.grant_access(obj=self.test_user, permission=permission_user_edit) self.grant_access( obj=self.test_group, permission=permission_group_edit ) - response = self._request_api_test_user_group_add() + response = self._request_test_user_group_add_api_view() self.assertEqual(response.status_code, status.HTTP_200_OK) + self.test_user.refresh_from_db() self.assertEqual(self.test_group.user_set.first(), self.test_user) - - -class GroupAPITestCase(UserTestMixin, BaseAPITestCase): - def setUp(self): - super(GroupAPITestCase, self).setUp() - self.login_user() - - def _request_api_test_group_create_view(self): - return self.post( - viewname='rest_api:group-list', data={ - 'name': TEST_GROUP_2_NAME - } - ) - - def test_group_create_no_permission(self): - response = self._request_api_test_group_create_view() - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - self.assertFalse( - TEST_GROUP_2_NAME in list( - Group.objects.values_list('name', flat=True) - ) - ) - - def test_group_create_with_permission(self): - self.grant_permission(permission=permission_group_create) - response = self._request_api_test_group_create_view() - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - self.assertTrue( - TEST_GROUP_2_NAME in list( - Group.objects.values_list('name', flat=True) - ) - ) - - def _request_api_test_group_delete_view(self): - return self.delete( - viewname='rest_api:group-detail', - kwargs={'group_id': self.test_group.pk} - ) - - def test_group_delete_no_access(self): - self._create_test_group() - response = self._request_api_test_group_delete_view() - - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - self.assertTrue( - TEST_GROUP_2_NAME in list( - Group.objects.values_list('name', flat=True) - ) - ) - - def test_group_delete_with_access(self): - self._create_test_group() - self.grant_access( - obj=self.test_group, permission=permission_group_delete - ) - response = self._request_api_test_group_delete_view() - - self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) - self.assertFalse( - TEST_GROUP_2_NAME in list( - Group.objects.values_list('name', flat=True) - ) - ) - - def _request_api_test_group_detail_view(self): - return self.get( - viewname='rest_api:group-detail', - kwargs={'group_id': self.test_group.pk} - ) - - def test_group_detail_no_access(self): - self._create_test_group() - response = self._request_api_test_group_detail_view() - - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - self.assertNotEqual( - self.test_group.name, response.data.get('name', None) - ) - - def test_group_detail_with_access(self): - self._create_test_group() - self.grant_access( - obj=self.test_group, permission=permission_group_view - ) - response = self._request_api_test_group_detail_view() - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(self.test_group.name, response.data.get('name', None)) - - - def _request_api_test_group_edit_via_patch_view(self): - return self.patch( - viewname='rest_api:group-detail', - kwargs={'group_id': self.test_group.pk}, - data={ - 'name': TEST_GROUP_2_NAME_EDITED - } - ) - - def test_group_edit_via_patch_no_access(self): - self._create_test_group() - response = self._request_api_test_group_edit_via_patch_view() - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - - self.test_group.refresh_from_db() - self.assertEqual(self.test_group.name, TEST_GROUP_2_NAME) - - def test_group_edit_via_patch_with_access(self): - self._create_test_group() - self.grant_access( - obj=self.test_group, permission=permission_group_edit - ) - response = self._request_api_test_group_edit_via_patch_view() - self.assertEqual(response.status_code, status.HTTP_200_OK) - - self.test_group.refresh_from_db() - self.assertEqual(self.test_group.name, TEST_GROUP_2_NAME_EDITED) - - def _request_api_test_group_list_view(self): - return self.get(viewname='rest_api:group-list') - - def test_group_list_no_access(self): - self._create_test_group() - response = self._request_api_test_group_list_view() - self.assertNotContains( - response=response, text=self.test_group.name, - status_code=status.HTTP_200_OK - ) - - def test_group_list_with_access(self): - self._create_test_group() - self.grant_access( - obj=self.test_group, permission=permission_group_view - ) - response = self._request_api_test_group_list_view() - self.assertContains( - response=response, text=self.test_group.name, - status_code=status.HTTP_200_OK - ) diff --git a/mayan/apps/user_management/urls.py b/mayan/apps/user_management/urls.py index a0c103277e..f0fd92f299 100644 --- a/mayan/apps/user_management/urls.py +++ b/mayan/apps/user_management/urls.py @@ -2,10 +2,7 @@ from __future__ import unicode_literals from django.conf.urls import url -from .api_views import ( - APICurrentUserView, APIGroupListView, APIGroupView, APIUserGroupList, - APIUserListView, APIUserView -) +from .api_views import CurrentUserAPIView, GroupAPIViewSet, UserAPIViewSet from .views import ( CurrentUserDetailsView, CurrentUserEditView, GroupCreateView, GroupDeleteView, GroupEditView, GroupListView, GroupMembersView, @@ -82,25 +79,13 @@ urlpatterns = [ ) ] -api_urls = [ +api_urlpatterns = [ url( - regex=r'^groups/$', name='group-list', view=APIGroupListView.as_view() - ), - url( - regex=r'^groups/(?P\d+)/$', name='group-detail', - view=APIGroupView.as_view(), - ), - url( - regex=r'^user/$', name='user-current', - view=APICurrentUserView.as_view() - ), - url(regex=r'^users/$', name='user-list', view=APIUserListView.as_view()), - url( - regex=r'^users/(?P\d+)/$', name='user-detail', - view=APIUserView.as_view() - ), - url( - regex=r'^users/(?P\d+)/groups/$', name='users-group-list', - view=APIUserGroupList.as_view() - ), + regex=r'^user/$', name='user-current', view=CurrentUserAPIView.as_view() + ) ] + +api_router_entries = ( + {'prefix': r'groups', 'viewset': GroupAPIViewSet, 'basename': 'group'}, + {'prefix': r'users', 'viewset': UserAPIViewSet, 'basename': 'user'}, +) From 61ebda6e63e4da83f5cb7c033711c1980024bff0 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Thu, 7 Feb 2019 20:13:35 -0400 Subject: [PATCH 121/209] REST API app updates - Add back support for API views but using the api_urlpatterns list. Needed for the current user API until a dynamic route router is implemented that can allow a viewset action to specify its entire URL. - Make sure the user is authenticated before trying to the user permissions. - Improve how external_object_list options are read from the class. - None authenticated users will get a blank queryset if the view doesn't require a permission. Signed-off-by: Roberto Rosario --- mayan/apps/rest_api/apps.py | 17 ++++++++-- mayan/apps/rest_api/filters.py | 3 ++ mayan/apps/rest_api/mixins.py | 52 +++++++++++++++++++----------- mayan/apps/rest_api/permissions.py | 5 ++- 4 files changed, 56 insertions(+), 21 deletions(-) diff --git a/mayan/apps/rest_api/apps.py b/mayan/apps/rest_api/apps.py index d913ccd02c..9cebabfb52 100644 --- a/mayan/apps/rest_api/apps.py +++ b/mayan/apps/rest_api/apps.py @@ -36,9 +36,22 @@ class RESTAPIApp(MayanAppConfig): for app in apps.get_app_configs(): if getattr(app, 'has_rest_api', False): try: - for entry in import_string('{}.urls.api_router_entries'.format(app.name)): - router.register(**entry) + app_api_router_entries = import_string( + dotted_path='{}.urls.api_router_entries'.format(app.name) + ) except ImportError: pass + else: + for entry in app_api_router_entries: + router.register(**entry) + + try: + app_api_urlpatterns = import_string( + dotted_path='{}.urls.api_urlpatterns'.format(app.name) + ) + except ImportError: + pass + else: + urlpatterns.extend(app_api_urlpatterns) urlpatterns.extend(router.urls) diff --git a/mayan/apps/rest_api/filters.py b/mayan/apps/rest_api/filters.py index 928c997bc8..0c1e521188 100644 --- a/mayan/apps/rest_api/filters.py +++ b/mayan/apps/rest_api/filters.py @@ -17,6 +17,9 @@ class MayanViewSetObjectPermissionsFilter(BaseFilterBackend): 'list': permission_..._view } """ + if not request.user or not request.user.is_authenticated: + return queryset.none() + object_permission_dictionary = getattr(view, 'object_permission_map', {}) object_permission = object_permission_dictionary.get( view.action, None diff --git a/mayan/apps/rest_api/mixins.py b/mayan/apps/rest_api/mixins.py index efacb23ac1..3a08e520c6 100644 --- a/mayan/apps/rest_api/mixins.py +++ b/mayan/apps/rest_api/mixins.py @@ -6,12 +6,21 @@ from mayan.apps.acls.models import AccessControlList class ExternalObjectListSerializerMixin(object): - class Meta: - external_object_list_model = None - external_object_list_permission = None - external_object_list_queryset = None - external_object_list_pk_field = None - external_object_list_pk_list_field = None + """ + Mixin to allow serializers to get a restricted object list with minimal code. + This mixin adds the follow class Meta options to a serializer: + external_object_list_model + external_object_list_permission + external_object_list_queryset + external_object_list_pk_field + external_object_list_pk_list_field + + The source queryset can also be provided overriding the + .get_external_object_list() method. + """ + def __init__(self, *args, **kwargs): + super(ExternalObjectListSerializerMixin, self).__init__(*args, **kwargs) + self.external_object_list_options = getattr(self, 'Meta', None) def get_external_object_list(self): queryset = self.get_external_object_list_queryset() @@ -23,14 +32,13 @@ class ExternalObjectListSerializerMixin(object): user=self.context['request'].user ) - if self.Meta.external_object_list_pk_field: - id_list = ( - self.validated_data.get(self.Meta.external_object_list_pk_field), - ) - elif self.Meta.external_object_list_pk_list_field: - id_list = self.validated_data.get( - self.Meta.external_object_list_pk_list_field, '' - ).split(',') + pk_field = self.get_external_object_list_option('pk_field') + pk_list_field = self.get_external_object_list_option('pk_list_field') + + if pk_field: + id_list = (self.validated_data.get(pk_field),) + elif pk_list_field: + id_list = self.validated_data.get(pk_list_field, '').split(',') else: raise ImproperlyConfigured( 'ExternalObjectListSerializerMixin requires a ' @@ -40,11 +48,19 @@ class ExternalObjectListSerializerMixin(object): return queryset.filter(pk__in=id_list) + def get_external_object_list_option(self, option_name): + return getattr( + self.external_object_list_options, 'external_object_list_{}'.format(option_name), None + ) + def get_external_object_list_queryset(self): - if self.Meta.external_object_list_model: - queryset = self.Meta.external_object_list_model._meta.default_manager.all() - elif self.Meta.external_object_list_queryset: - return self.Meta.external_object_list_queryset + model = self.get_external_object_list_option('model') + queryset = self.get_external_object_list_option('queryset') + + if model: + queryset = model._meta.default_manager.all() + elif queryset: + return queryset else: raise ImproperlyConfigured( 'ExternalObjectListSerializerMixin requires a ' diff --git a/mayan/apps/rest_api/permissions.py b/mayan/apps/rest_api/permissions.py index 830c4e5a07..faaf8ebc94 100644 --- a/mayan/apps/rest_api/permissions.py +++ b/mayan/apps/rest_api/permissions.py @@ -2,7 +2,7 @@ from __future__ import absolute_import, unicode_literals from django.core.exceptions import PermissionDenied -from rest_framework.permissions import BasePermission +from rest_framework.permissions import BasePermission, IsAuthenticated from mayan.apps.permissions import Permission @@ -19,6 +19,9 @@ class MayanViewSetPermission(BasePermission): 'list': permission_..._view } """ + if not request.user or not request.user.is_authenticated: + return False + view_permission_dictionary = getattr(view, 'view_permission_map', {}) view_permission = view_permission_dictionary.get(view.action, None) From ae1634c3782b7bdbd43839adecbc61250546e9bc Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Fri, 8 Feb 2019 00:44:26 -0400 Subject: [PATCH 122/209] Users: Finish API refactor - Update groups add, remove and users add, remove methods trigger only one event on the parent method and multiple on the child method. - Add missing group_list, _add, _remove permissions. - Monkey patch Django's User and Group model save method to trigger the creation and edited events. - Monkeypatch user sorting to silence warnings. - Improve test mixins to allow reuse of view and API view requests. - Finish adding all API tests. - Add events test from API view requests. - Remove event commits from views. Signed-off-by: Roberto Rosario --- mayan/apps/user_management/api_views.py | 14 +- mayan/apps/user_management/apps.py | 24 +- mayan/apps/user_management/methods.py | 99 +++++- mayan/apps/user_management/serializers.py | 40 +-- mayan/apps/user_management/tests/__init__.py | 1 - mayan/apps/user_management/tests/mixins.py | 136 +++++++- mayan/apps/user_management/tests/test_api.py | 292 +++++++++--------- .../apps/user_management/tests/test_events.py | 68 +++- .../apps/user_management/tests/test_views.py | 14 +- mayan/apps/user_management/views.py | 37 +-- 10 files changed, 482 insertions(+), 243 deletions(-) diff --git a/mayan/apps/user_management/api_views.py b/mayan/apps/user_management/api_views.py index 09eadbef79..ec2627efba 100644 --- a/mayan/apps/user_management/api_views.py +++ b/mayan/apps/user_management/api_views.py @@ -56,7 +56,7 @@ class GroupAPIViewSet(MayanAPIModelViewSet): instance = self.get_object() serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) - serializer.user_add(instance=instance) + serializer.users_add(instance=instance) headers = self.get_success_headers(data=serializer.data) return Response( serializer.data, status=status.HTTP_200_OK, headers=headers @@ -89,7 +89,7 @@ class GroupAPIViewSet(MayanAPIModelViewSet): instance = self.get_object() serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) - serializer.user_remove(instance=instance) + serializer.users_remove(instance=instance) headers = self.get_success_headers(data=serializer.data) return Response( serializer.data, status=status.HTTP_200_OK, headers=headers @@ -100,7 +100,9 @@ class UserAPIViewSet(MayanAPIModelViewSet): lookup_url_kwarg = 'user_id' object_permission_map = { 'destroy': permission_user_delete, - 'group-list': permission_user_view, + 'group_add': permission_user_edit, + 'group_list': permission_user_view, + 'group_remove': permission_user_edit, 'list': permission_user_view, 'partial_update': permission_user_edit, 'retrieve': permission_user_view, @@ -115,13 +117,13 @@ class UserAPIViewSet(MayanAPIModelViewSet): @action( detail=True, lookup_url_kwarg='user_id', methods=('post',), serializer_class=UserGroupAddRemoveSerializer, - url_name='group-add', url_path='group/add' + url_name='group-add', url_path='groups/add' ) def group_add(self, request, *args, **kwargs): instance = self.get_object() serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) - serializer.group_add(instance=instance) + serializer.groups_add(instance=instance) headers = self.get_success_headers(data=serializer.data) return Response( serializer.data, status=status.HTTP_200_OK, headers=headers @@ -154,7 +156,7 @@ class UserAPIViewSet(MayanAPIModelViewSet): instance = self.get_object() serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) - serializer.group_remove(instance=instance) + serializer.groups_remove(instance=instance) headers = self.get_success_headers(data=serializer.data) return Response( serializer.data, status=status.HTTP_200_OK, headers=headers diff --git a/mayan/apps/user_management/apps.py b/mayan/apps/user_management/apps.py index 3398cd0658..3580a01c74 100644 --- a/mayan/apps/user_management/apps.py +++ b/mayan/apps/user_management/apps.py @@ -34,8 +34,10 @@ from .links import ( separator_user_label ) from .methods import ( - method_group_get_users, method_group_user_add, method_group_user_remove, - method_user_get_absolute_url, method_user_get_groups + get_method_group_save, get_method_user_save, method_group_get_users, + method_group_users_add, method_group_users_remove, + method_user_get_absolute_url, method_user_get_groups, + method_user_groups_add, method_user_groups_remove ) from .permissions import ( permission_group_delete, permission_group_edit, @@ -68,11 +70,12 @@ class UserManagementApp(MayanAppConfig): # Silence UnorderedObjectListWarning # "Pagination may yield inconsistent result" - # Remove on Django 2.x + # TODO: Remove on Django 2.x Group._meta.ordering = ('name',) Group.add_to_class(name='get_users', value=method_group_get_users) - Group.add_to_class(name='user_add', value=method_group_user_add) - Group.add_to_class(name='user_remove', value=method_group_user_remove) + Group.add_to_class(name='save', value=get_method_group_save()) + Group.add_to_class(name='users_add', value=method_group_users_add) + Group.add_to_class(name='users_remove', value=method_group_users_remove) MetadataLookup( description=_('All the groups.'), name='groups', @@ -134,12 +137,23 @@ class UserManagementApp(MayanAppConfig): source=User, widget=TwoStateWidget ) + # Silence UnorderedObjectListWarning + # "Pagination may yield inconsistent result" + # TODO: Remove on Django 2.x + User._meta.ordering = ('pk',) User.add_to_class( name='get_absolute_url', value=method_user_get_absolute_url ) User.add_to_class( name='get_groups', value=method_user_get_groups ) + User.add_to_class( + name='groups_add', value=method_user_groups_add + ) + User.add_to_class( + name='groups_remove', value=method_user_groups_remove + ) + User.add_to_class(name='save', value=get_method_user_save()) menu_list_facet.bind_links( links=( diff --git a/mayan/apps/user_management/methods.py b/mayan/apps/user_management/methods.py index 9f572ac68d..5db28def18 100644 --- a/mayan/apps/user_management/methods.py +++ b/mayan/apps/user_management/methods.py @@ -1,11 +1,15 @@ from __future__ import unicode_literals from django.apps import apps +from django.contrib.auth import get_user_model from django.db import transaction from django.shortcuts import reverse -from .events import event_group_edited, event_user_edited -from .permissions import permission_user_view +from .events import ( + event_group_created, event_group_edited, event_user_created, + event_user_edited +) +from .permissions import permission_group_view, permission_user_view from .querysets import get_user_queryset @@ -20,26 +24,50 @@ def method_group_get_users(self, _user): ) -def method_group_user_add(self, user, _user): +def get_method_group_save(): + Group = apps.get_model(app_label='auth', model_name='Group') + group_save_original = Group.save + + def method_group_save(self, *args, **kwargs): + _group = kwargs.pop('_group', None) + + with transaction.atomic(): + is_new = not self.pk + group_save_original(self, *args, **kwargs) + if is_new: + event_group_created.commit( + actor=_group, target=self + ) + else: + event_group_edited.commit( + actor=_group, target=self + ) + + return method_group_save + + +def method_group_users_add(self, users, _user): with transaction.atomic(): - self.user_set.add(user) event_group_edited.commit( actor=_user, target=self ) - event_user_edited.commit( - actor=_user, target=user - ) + for user in users: + self.user_set.add(user) + event_user_edited.commit( + actor=_user, target=user + ) -def method_group_user_remove(self, user, _user): +def method_group_users_remove(self, users, _user): with transaction.atomic(): - self.user_set.remove(user) event_group_edited.commit( actor=_user, target=self ) - event_user_edited.commit( - actor=_user, target=user - ) + for user in users: + self.user_set.remove(user) + event_user_edited.commit( + actor=_user, target=user + ) def method_user_get_absolute_url(self): @@ -54,6 +82,51 @@ def method_user_get_groups(self, _user): ) return AccessControlList.objects.restrict_queryset( - permission=permission_user_view, queryset=self.groups.all(), + permission=permission_group_view, queryset=self.groups.all(), user=_user ) + + +def method_user_groups_add(self, groups, _user): + with transaction.atomic(): + event_user_edited.commit( + actor=_user, target=self + ) + for group in groups: + self.groups.add(group) + event_group_edited.commit( + actor=_user, target=group + ) + + +def method_user_groups_remove(self, groups, _user): + with transaction.atomic(): + event_user_edited.commit( + actor=_user, target=self + ) + for group in groups: + self.groups.remove(group) + event_group_edited.commit( + actor=_user, target=group + ) + + +def get_method_user_save(): + user_save_original = get_user_model().save + + def method_user_save(self, *args, **kwargs): + _user = kwargs.pop('_user', None) + + with transaction.atomic(): + is_new = not self.pk + user_save_original(self, *args, **kwargs) + if is_new: + event_user_created.commit( + actor=_user, target=self + ) + else: + event_user_edited.commit( + actor=_user, target=self + ) + + return method_user_save diff --git a/mayan/apps/user_management/serializers.py b/mayan/apps/user_management/serializers.py index b1ff9183a0..6c98a01201 100644 --- a/mayan/apps/user_management/serializers.py +++ b/mayan/apps/user_management/serializers.py @@ -46,7 +46,7 @@ class GroupUserAddRemoveSerializer(ExternalObjectListSerializerMixin, serializer 'Primary key of the user that will be added or removed.' ), required=False, write_only=True ) - users_id_list = serializers.CharField( + user_id_list = serializers.CharField( help_text=_( 'Comma separated list of user primary keys that will be added or ' 'removed.' @@ -59,15 +59,17 @@ class GroupUserAddRemoveSerializer(ExternalObjectListSerializerMixin, serializer external_object_list_pk_field = 'user_id' external_object_list_pk_list_field = 'user_id_list' - def user_add(self, instance): - queryset = self.get_external_object_list() - for user in queryset: - instance.user_add(user=user, _user=self.context['request'].user) + def users_add(self, instance): + instance.users_add( + users=self.get_external_object_list(), + _user=self.context['request'].user + ) - def user_remove(self, instance): - queryset = self.get_external_object_list() - for user in queryset: - instance.user_remove(user=user, _user=self.context['request'].user) + def users_remove(self, instance): + instance.users_remove( + users=self.get_external_object_list(), + _user=self.context['request'].user + ) class UserGroupAddRemoveSerializer(ExternalObjectListSerializerMixin, serializers.Serializer): @@ -76,7 +78,7 @@ class UserGroupAddRemoveSerializer(ExternalObjectListSerializerMixin, serializer 'Primary key of the group that will be added or removed.' ), required=False, write_only=True ) - groups_id_list = serializers.CharField( + group_id_list = serializers.CharField( help_text=_( 'Comma separated list of group primary keys that will be added or ' 'removed.' @@ -89,15 +91,17 @@ class UserGroupAddRemoveSerializer(ExternalObjectListSerializerMixin, serializer external_object_list_pk_field = 'group_id' external_object_list_pk_list_field = 'group_id_list' - def group_add(self, instance): - queryset = self.get_external_object_list() - for group in queryset: - instance.group_add(group=group, _group=self.context['request'].group) + def groups_add(self, instance): + instance.groups_add( + groups=self.get_external_object_list(), + _user=self.context['request'].user + ) - def group_remove(self, instance): - queryset = self.get_external_object_list() - for group in queryset: - instance.group_remove(group=group, _group=self.context['request'].group) + def groups_remove(self, instance): + instance.groups_remove( + groups=self.get_external_object_list(), + _user=self.context['request'].user + ) class UserSerializer(serializers.HyperlinkedModelSerializer): diff --git a/mayan/apps/user_management/tests/__init__.py b/mayan/apps/user_management/tests/__init__.py index 8b13789179..e69de29bb2 100644 --- a/mayan/apps/user_management/tests/__init__.py +++ b/mayan/apps/user_management/tests/__init__.py @@ -1 +0,0 @@ - diff --git a/mayan/apps/user_management/tests/mixins.py b/mayan/apps/user_management/tests/mixins.py index 6a5dde7534..625fed83f5 100644 --- a/mayan/apps/user_management/tests/mixins.py +++ b/mayan/apps/user_management/tests/mixins.py @@ -8,7 +8,7 @@ from .literals import ( TEST_CASE_GROUP_NAME, TEST_GROUP_NAME_EDITED, TEST_CASE_USER_EMAIL, TEST_CASE_USER_PASSWORD, TEST_CASE_USER_USERNAME, TEST_GROUP_NAME, TEST_USER_EMAIL, TEST_USER_USERNAME, TEST_USER_USERNAME_EDITED, - TEST_USER_PASSWORD + TEST_USER_PASSWORD, TEST_USER_PASSWORD_EDITED ) __all__ = ('GroupTestMixin', 'UserTestCaseMixin', 'UserTestMixin') @@ -59,12 +59,14 @@ class UserTestCaseMixin(object): username=TEST_CASE_SUPERUSER_USERNAME, email=TEST_CASE_SUPERUSER_EMAIL, password=TEST_CASE_SUPERUSER_PASSWORD ) + self._test_case_superuser.clear_password = TEST_CASE_SUPERUSER_PASSWORD def _create_test_case_user(self): self._test_case_user = get_user_model().objects.create_user( username=TEST_CASE_USER_USERNAME, email=TEST_CASE_USER_EMAIL, password=TEST_CASE_USER_PASSWORD ) + self._test_case_user.clear_password = TEST_CASE_USER_PASSWORD def login(self, *args, **kwargs): logged_in = self.client.login(*args, **kwargs) @@ -86,6 +88,67 @@ class UserTestCaseMixin(object): self.client.logout() +class GroupAPITestMixin(object): + def _request_test_group_create_api_view(self): + result = self.post( + viewname='rest_api:group-list', data={ + 'name': TEST_GROUP_NAME + } + ) + if 'id' in result.json(): + self.test_group = Group.objects.get(pk=result.json()['id']) + + return result + + def _request_test_group_delete_api_view(self): + return self.delete( + viewname='rest_api:group-detail', + kwargs={'group_id': self.test_group.pk} + ) + + def _request_test_group_detail_api_view(self): + return self.get( + viewname='rest_api:group-detail', + kwargs={'group_id': self.test_group.pk} + ) + + def _request_test_group_edit_patch_api_view(self): + return self.patch( + viewname='rest_api:group-detail', + kwargs={'group_id': self.test_group.pk}, + data={ + 'name': TEST_GROUP_NAME_EDITED + } + ) + + def _request_test_group_list_api_view(self): + return self.get(viewname='rest_api:group-list') + + def _request_test_group_user_add_api_view(self): + return self.post( + viewname='rest_api:group-user-add', + kwargs={'group_id': self.test_group.pk}, + data={ + 'user_id': self.test_user.pk + } + ) + + def _request_test_group_user_list_api_view(self): + return self.get( + viewname='rest_api:group-user-list', + kwargs={'group_id': self.test_group.pk}, + ) + + def _request_test_group_user_remove_api_view(self): + return self.post( + viewname='rest_api:group-user-remove', + kwargs={'group_id': self.test_group.pk}, + data={ + 'user_id': self.test_user.pk + } + ) + + class GroupTestMixin(object): def _create_test_group(self): self.test_group = Group.objects.create(name=TEST_GROUP_NAME) @@ -94,6 +157,8 @@ class GroupTestMixin(object): self.test_group.name = TEST_GROUP_NAME_EDITED self.test_group.save() + +class GroupViewTestMixin(object): def _request_test_group_create_view(self): reponse = self.post( viewname='user_management:group_create', data={ @@ -135,13 +200,17 @@ class UserTestMixin(object): username=TEST_CASE_SUPERUSER_USERNAME, email=TEST_CASE_SUPERUSER_EMAIL, password=TEST_CASE_SUPERUSER_PASSWORD ) + self.test_superuser.clear_password = TEST_USER_PASSWORD def _create_test_user(self): - self.test_user = get_user_model().objects.create( + self.test_user = get_user_model().objects.create_user( username=TEST_USER_USERNAME, email=TEST_USER_EMAIL, password=TEST_USER_PASSWORD ) + self.test_user.clear_password = TEST_USER_PASSWORD + +class UserViewTestMixin(object): def _request_test_superuser_delete_view(self): return self.post( viewname='user_management:user_delete', @@ -187,3 +256,66 @@ class UserTestMixin(object): viewname='user_management:user_groups', kwargs={'user_id': self.test_user.pk} ) + + +class UserAPITestMixin(object): + def _request_test_user_create_api_view(self): + result = self.post( + viewname='rest_api:user-list', data={ + 'email': TEST_USER_EMAIL, 'password': TEST_USER_PASSWORD, + 'username': TEST_USER_USERNAME, + } + ) + if 'id' in result.json(): + self.test_user = get_user_model().objects.get(pk=result.json()['id']) + + return result + + def _request_test_user_delete_api_view(self): + return self.delete( + viewname='rest_api:user-detail', + kwargs={'user_id': self.test_user.pk} + ) + + def _request_test_user_edit_patch_api_view(self): + return self.patch( + viewname='rest_api:user-detail', + kwargs={'user_id': self.test_user.pk}, + data={'username': TEST_USER_USERNAME_EDITED} + ) + + def _request_test_user_edit_put_api_view(self): + return self.put( + viewname='rest_api:user-detail', + kwargs={'user_id': self.test_user.pk}, + data={'username': TEST_USER_USERNAME_EDITED} + ) + + def _request_test_user_group_list_api_view(self): + return self.get( + viewname='rest_api:user-group-list', + kwargs={'user_id': self.test_user.pk} + ) + + def _request_test_user_group_add_api_view(self): + return self.post( + viewname='rest_api:user-group-add', + kwargs={'user_id': self.test_user.pk}, + data={'group_id_list': '{}'.format(self.test_group.pk)} + ) + + def _request_test_user_group_remove_api_view(self): + return self.post( + viewname='rest_api:user-group-remove', + kwargs={'user_id': self.test_user.pk}, + data={'group_id_list': '{}'.format(self.test_group.pk)} + ) + + def _request_test_user_password_change_api_view(self): + self.test_user.clear_password = TEST_USER_PASSWORD_EDITED + return self.patch( + viewname='rest_api:user-detail', + kwargs={'user_id': self.test_user.pk}, data={ + 'password': TEST_USER_PASSWORD_EDITED, + } + ) diff --git a/mayan/apps/user_management/tests/test_api.py b/mayan/apps/user_management/tests/test_api.py index 1699b6802b..03f62584c8 100644 --- a/mayan/apps/user_management/tests/test_api.py +++ b/mayan/apps/user_management/tests/test_api.py @@ -14,21 +14,12 @@ from ..permissions import ( permission_user_edit, permission_user_view ) -from .literals import ( - TEST_GROUP_NAME, TEST_GROUP_NAME_EDITED, TEST_USER_EMAIL, TEST_USER_USERNAME, - TEST_USER_USERNAME_EDITED, TEST_USER_PASSWORD, TEST_USER_PASSWORD_EDITED +from .mixins import ( + GroupAPITestMixin, GroupTestMixin, UserAPITestMixin, UserTestMixin ) -from .mixins import GroupTestMixin, UserTestMixin -class GroupAPITestCase(UserTestMixin, GroupTestMixin, BaseAPITestCase): - def _request_test_group_create_api_view(self): - return self.post( - viewname='rest_api:group-list', data={ - 'name': TEST_GROUP_NAME - } - ) - +class GroupAPITestCase(GroupAPITestMixin, GroupTestMixin, UserTestMixin, BaseAPITestCase): def test_group_create_api_view_no_permission(self): group_count = Group.objects.count() @@ -46,12 +37,6 @@ class GroupAPITestCase(UserTestMixin, GroupTestMixin, BaseAPITestCase): self.assertNotEqual(group_count, Group.objects.count()) - def _request_test_group_delete_api_view(self): - return self.delete( - viewname='rest_api:group-detail', - kwargs={'group_id': self.test_group.pk} - ) - def test_group_delete_api_view_no_permission(self): self._create_test_group() @@ -75,12 +60,6 @@ class GroupAPITestCase(UserTestMixin, GroupTestMixin, BaseAPITestCase): self.assertNotEqual(group_count, Group.objects.count()) - def _request_test_group_detail_api_view(self): - return self.get( - viewname='rest_api:group-detail', - kwargs={'group_id': self.test_group.pk} - ) - def test_group_detail_api_view_no_permission(self): self._create_test_group() @@ -102,15 +81,6 @@ class GroupAPITestCase(UserTestMixin, GroupTestMixin, BaseAPITestCase): self.assertEqual(self.test_group.name, response.data.get('name', None)) - def _request_test_group_edit_patch_api_view(self): - return self.patch( - viewname='rest_api:group-detail', - kwargs={'group_id': self.test_group.pk}, - data={ - 'name': TEST_GROUP_NAME_EDITED - } - ) - def test_group_edit_patch_api_view_no_permission(self): self._create_test_group() @@ -122,7 +92,7 @@ class GroupAPITestCase(UserTestMixin, GroupTestMixin, BaseAPITestCase): self.test_group.refresh_from_db() self.assertEqual(group_name, self.test_group.name) - def test_group_edit_via_patch_with_access(self): + def test_group_edit_patch_with_access(self): self._create_test_group() group_name = self.test_group.name @@ -136,9 +106,6 @@ class GroupAPITestCase(UserTestMixin, GroupTestMixin, BaseAPITestCase): self.test_group.refresh_from_db() self.assertNotEqual(group_name, self.test_group.name) - def _request_test_group_list_api_view(self): - return self.get(viewname='rest_api:group-list') - def test_group_list_api_view_no_permission(self): self._create_test_group() @@ -160,15 +127,6 @@ class GroupAPITestCase(UserTestMixin, GroupTestMixin, BaseAPITestCase): status_code=status.HTTP_200_OK ) - def _request_test_group_user_add_patch_api_view(self): - return self.post( - viewname='rest_api:group-user-add', - kwargs={'group_id': self.test_group.pk}, - data={ - 'user_id': self.test_user.pk - } - ) - def _setup_group_user_add(self): self._create_test_group() self._create_test_user() @@ -176,7 +134,7 @@ class GroupAPITestCase(UserTestMixin, GroupTestMixin, BaseAPITestCase): def test_group_user_add_api_view_no_permission(self): self._setup_group_user_add() - response = self._request_test_group_user_add_patch_api_view() + response = self._request_test_group_user_add_api_view() self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) self.test_group.refresh_from_db() @@ -188,7 +146,7 @@ class GroupAPITestCase(UserTestMixin, GroupTestMixin, BaseAPITestCase): self.grant_access( obj=self.test_group, permission=permission_group_edit ) - response = self._request_test_group_user_add_patch_api_view() + response = self._request_test_group_user_add_api_view() self.assertEqual(response.status_code, status.HTTP_200_OK) self.test_group.refresh_from_db() @@ -200,7 +158,7 @@ class GroupAPITestCase(UserTestMixin, GroupTestMixin, BaseAPITestCase): self.grant_access( obj=self.test_user, permission=permission_user_edit ) - response = self._request_test_group_user_add_patch_api_view() + response = self._request_test_group_user_add_api_view() self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) self.test_group.refresh_from_db() @@ -215,22 +173,58 @@ class GroupAPITestCase(UserTestMixin, GroupTestMixin, BaseAPITestCase): self.grant_access( obj=self.test_user, permission=permission_user_edit ) - response = self._request_test_group_user_add_patch_api_view() + response = self._request_test_group_user_add_api_view() self.assertEqual(response.status_code, status.HTTP_200_OK) self.test_group.refresh_from_db() self.assertTrue(self.test_user in self.test_group.user_set.all()) - #TODO: group-user-list tests + def _setup_group_user_list(self): + self._create_test_group() + self._create_test_user() + self.test_group.user_set.add(self.test_user) - def _request_test_group_user_remove_patch_api_view(self): - return self.post( - viewname='rest_api:group-user-remove', - kwargs={'group_id': self.test_group.pk}, - data={ - 'user_id': self.test_user.pk - } + def test_group_user_list_api_view_no_permission(self): + self._setup_group_user_list() + + response = self._request_test_group_user_list_api_view() + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + self.assertTrue('count' not in response.json()) + + def test_group_user_list_with_group_access(self): + self._setup_group_user_list() + + self.grant_access( + obj=self.test_group, permission=permission_group_view ) + response = self._request_test_group_user_list_api_view() + self.assertEqual(response.status_code, status.HTTP_200_OK) + + self.assertEqual(response.json()['count'], 0) + + def test_group_user_list_with_user_access(self): + self._setup_group_user_list() + + self.grant_access( + obj=self.test_user, permission=permission_user_view + ) + response = self._request_test_group_user_list_api_view() + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + self.assertTrue('count' not in response.json()) + + def test_group_user_list_with_full_access(self): + self._setup_group_user_list() + + self.grant_access( + obj=self.test_group, permission=permission_group_view + ) + self.grant_access( + obj=self.test_user, permission=permission_user_view + ) + response = self._request_test_group_user_list_api_view() + self.assertEqual(response.status_code, status.HTTP_200_OK) + + self.assertEqual(response.json()['count'], 1) def _setup_group_user_remove(self): self._create_test_group() @@ -240,7 +234,7 @@ class GroupAPITestCase(UserTestMixin, GroupTestMixin, BaseAPITestCase): def test_group_user_remove_api_view_no_permission(self): self._setup_group_user_remove() - response = self._request_test_group_user_remove_patch_api_view() + response = self._request_test_group_user_remove_api_view() self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) self.test_group.refresh_from_db() @@ -252,7 +246,7 @@ class GroupAPITestCase(UserTestMixin, GroupTestMixin, BaseAPITestCase): self.grant_access( obj=self.test_group, permission=permission_group_edit ) - response = self._request_test_group_user_remove_patch_api_view() + response = self._request_test_group_user_remove_api_view() self.assertEqual(response.status_code, status.HTTP_200_OK) self.test_group.refresh_from_db() @@ -264,7 +258,7 @@ class GroupAPITestCase(UserTestMixin, GroupTestMixin, BaseAPITestCase): self.grant_access( obj=self.test_user, permission=permission_user_edit ) - response = self._request_test_group_user_remove_patch_api_view() + response = self._request_test_group_user_remove_api_view() self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) self.test_group.refresh_from_db() @@ -279,26 +273,18 @@ class GroupAPITestCase(UserTestMixin, GroupTestMixin, BaseAPITestCase): self.grant_access( obj=self.test_user, permission=permission_user_edit ) - response = self._request_test_group_user_remove_patch_api_view() + response = self._request_test_group_user_remove_api_view() self.assertEqual(response.status_code, status.HTTP_200_OK) self.test_group.refresh_from_db() self.assertTrue(self.test_user not in self.test_group.user_set.all()) -class UserAPITestCase(GroupTestMixin, UserTestMixin, BaseAPITestCase): - def _request_test_user_create_api_view_api_view(self): - return self.post( - viewname='rest_api:user-list', data={ - 'email': TEST_USER_EMAIL, 'password': TEST_USER_PASSWORD, - 'username': TEST_USER_USERNAME, - } - ) - +class UserAPITestCase(UserAPITestMixin, GroupTestMixin, UserTestMixin, BaseAPITestCase): def test_user_create_api_view_no_permission(self): user_count = get_user_model().objects.count() - response = self._request_test_user_create_api_view_api_view() + response = self._request_test_user_create_api_view() self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) self.assertEqual(user_count, get_user_model().objects.count()) @@ -307,11 +293,9 @@ class UserAPITestCase(GroupTestMixin, UserTestMixin, BaseAPITestCase): user_count = get_user_model().objects.count() self.grant_permission(permission=permission_user_create) - response = self._request_test_user_create_api_view_api_view() + response = self._request_test_user_create_api_view() self.assertEqual(response.status_code, status.HTTP_201_CREATED) - user = get_user_model().objects.get(pk=response.data['id']) - self.assertEqual(user.username, TEST_USER_USERNAME) self.assertEqual(user_count + 1, get_user_model().objects.count()) def test_user_create_api_view_login(self): @@ -319,26 +303,19 @@ class UserAPITestCase(GroupTestMixin, UserTestMixin, BaseAPITestCase): self.assertTrue( self.login( - username=TEST_USER_USERNAME, password=TEST_USER_PASSWORD + username=self.test_user.username, + password=self.test_user.clear_password ) ) - def _request_user_password_change(self): - return self.patch( - viewname='rest_api:user-detail', - kwargs={'user_id': self.test_user.pk}, data={ - 'password': TEST_USER_PASSWORD_EDITED, - } - ) - def test_user_create_login_password_change_api_view_no_permission(self): self._create_test_user() - self._request_user_password_change() + self._request_test_user_password_change_api_view() self.assertFalse( - self.client.login( - username=TEST_USER_USERNAME, - password=TEST_USER_PASSWORD_EDITED + self.login( + username=self.test_user.username, + password=self.test_user.clear_password ) ) @@ -346,22 +323,15 @@ class UserAPITestCase(GroupTestMixin, UserTestMixin, BaseAPITestCase): self._create_test_user() self.grant_access(obj=self.test_user, permission=permission_user_edit) - self._request_user_password_change() + self._request_test_user_password_change_api_view() self.assertTrue( - self.client.login( - username=TEST_USER_USERNAME, - password=TEST_USER_PASSWORD_EDITED + self.login( + username=self.test_user.username, + password=self.test_user.clear_password ) ) - def _request_test_user_edit_put_api_view(self): - return self.put( - viewname='rest_api:user-detail', - kwargs={'user_id': self.test_user.pk}, - data={'username': TEST_USER_USERNAME_EDITED} - ) - def test_user_edit_put_api_view_no_permission(self): self._create_test_user() username = self.test_user.username @@ -383,13 +353,6 @@ class UserAPITestCase(GroupTestMixin, UserTestMixin, BaseAPITestCase): self.test_user.refresh_from_db() self.assertNotEqual(username, self.test_user.username) - def _request_test_user_edit_patch_api_view(self): - return self.patch( - viewname='rest_api:user-detail', - kwargs={'user_id': self.test_user.pk}, - data={'username': TEST_USER_USERNAME_EDITED} - ) - def test_user_edit_patch_api_view_no_permission(self): self._create_test_user() username = self.test_user.username @@ -411,12 +374,6 @@ class UserAPITestCase(GroupTestMixin, UserTestMixin, BaseAPITestCase): self.test_user.refresh_from_db() self.assertNotEqual(username, self.test_user.username) - def _request_test_user_delete_api_view(self): - return self.delete( - viewname='rest_api:user-detail', - kwargs={'user_id': self.test_user.pk} - ) - def test_user_delete_api_view_no_permission(self): self._create_test_user() @@ -440,85 +397,70 @@ class UserAPITestCase(GroupTestMixin, UserTestMixin, BaseAPITestCase): get_user_model().objects.filter(pk=self.test_user.pk).exists() ) - def _request_test_user_group_api_view(self): - return self.get( - viewname='rest_api:user-group-list', - kwargs={'user_id': self.test_user.pk} - ) - - def test_user_group_list_api_view_no_permission(self): + def _setup_user_group_list(self): self._create_test_group() self._create_test_user() self.test_user.groups.add(self.test_group) - response = self._request_test_user_group_api_view() + def test_user_group_list_api_view_no_permission(self): + self._setup_user_group_list() + + response = self._request_test_user_group_list_api_view() self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) def test_user_group_list_api_view_with_user_access(self): - self._create_test_group() - self._create_test_user() - self.test_user.groups.add(self.test_group) + self._setup_user_group_list() self.grant_access(obj=self.test_user, permission=permission_user_view) - response = self._request_test_user_group_api_view() + response = self._request_test_user_group_list_api_view() self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data['count'], 0) def test_user_group_list_api_view_with_group_access(self): - self._create_test_group() - self._create_test_user() - self.test_user.groups.add(self.test_group) + self._setup_user_group_list() self.grant_access( obj=self.test_group, permission=permission_group_view ) - response = self._request_test_user_group_api_view() + response = self._request_test_user_group_list_api_view() self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - def test_user_group_list_api_view_with_access(self): - self._create_test_group() - self._create_test_user() - self.test_user.groups.add(self.test_group) + def test_user_group_list_api_view_with_full_access(self): + self._setup_user_group_list() self.grant_access(obj=self.test_user, permission=permission_user_view) self.grant_access( obj=self.test_group, permission=permission_group_view ) - response = self._request_test_user_group_api_view() + response = self._request_test_user_group_list_api_view() self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data['count'], 1) - def _request_test_user_group_add_api_view(self): - return self.patch( - viewname='rest_api:user-detail', - kwargs={'user_id': self.test_user.pk}, - data={'group_id_list': '{}'.format(self.test_group.pk)} - ) + def _setup_user_group_add(self): + self._create_test_group() + self._create_test_user() def test_user_group_add_api_view_no_permission(self): - self._create_test_group() - self._create_test_user() + self._setup_user_group_add() response = self._request_test_user_group_add_api_view() self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) self.test_user.refresh_from_db() - self.assertEqual(self.test_group.user_set.first(), None) + self.assertTrue(self.test_group not in self.test_user.groups.all()) def test_user_group_add_api_view_with_user_access(self): - self._create_test_group() - self._create_test_user() + self._setup_user_group_add() self.grant_access(obj=self.test_user, permission=permission_user_edit) response = self._request_test_user_group_add_api_view() self.assertEqual(response.status_code, status.HTTP_200_OK) self.test_user.refresh_from_db() - self.assertEqual(self.test_group.user_set.first(), None) + self.assertTrue(self.test_group not in self.test_user.groups.all()) def test_user_group_add_api_view_with_group_access(self): - self._create_test_group() - self._create_test_user() + self._setup_user_group_add() self.grant_access( obj=self.test_group, permission=permission_group_edit @@ -527,11 +469,10 @@ class UserAPITestCase(GroupTestMixin, UserTestMixin, BaseAPITestCase): self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) self.test_user.refresh_from_db() - self.assertEqual(self.test_group.user_set.first(), None) + self.assertTrue(self.test_group not in self.test_user.groups.all()) def test_user_group_add_api_view_with_full_access(self): - self._create_test_group() - self._create_test_user() + self._setup_user_group_add() self.grant_access(obj=self.test_user, permission=permission_user_edit) self.grant_access( @@ -541,4 +482,53 @@ class UserAPITestCase(GroupTestMixin, UserTestMixin, BaseAPITestCase): self.assertEqual(response.status_code, status.HTTP_200_OK) self.test_user.refresh_from_db() - self.assertEqual(self.test_group.user_set.first(), self.test_user) + self.assertTrue(self.test_group in self.test_user.groups.all()) + + def _setup_user_group_remove(self): + self._create_test_group() + self._create_test_user() + self.test_user.groups.add(self.test_group) + + def test_user_group_remove_api_view_no_permission(self): + self._setup_user_group_remove() + + response = self._request_test_user_group_remove_api_view() + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + self.test_user.refresh_from_db() + self.assertTrue(self.test_group in self.test_user.groups.all()) + + def test_user_group_remove_api_view_with_user_access(self): + self._setup_user_group_remove() + + self.grant_access(obj=self.test_user, permission=permission_user_edit) + response = self._request_test_user_group_remove_api_view() + self.assertEqual(response.status_code, status.HTTP_200_OK) + + self.test_user.refresh_from_db() + self.assertTrue(self.test_group in self.test_user.groups.all()) + + def test_user_group_remove_api_view_with_group_access(self): + self._setup_user_group_remove() + + self.grant_access( + obj=self.test_group, permission=permission_group_edit + ) + response = self._request_test_user_group_remove_api_view() + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + self.test_user.refresh_from_db() + self.assertTrue(self.test_group in self.test_user.groups.all()) + + def test_user_group_remove_api_view_with_full_access(self): + self._setup_user_group_remove() + + self.grant_access(obj=self.test_user, permission=permission_user_edit) + self.grant_access( + obj=self.test_group, permission=permission_group_edit + ) + response = self._request_test_user_group_remove_api_view() + self.assertEqual(response.status_code, status.HTTP_200_OK) + + self.test_user.refresh_from_db() + self.assertTrue(self.test_group not in self.test_user.groups.all()) diff --git a/mayan/apps/user_management/tests/test_events.py b/mayan/apps/user_management/tests/test_events.py index e6d630c89f..d2e35cee05 100644 --- a/mayan/apps/user_management/tests/test_events.py +++ b/mayan/apps/user_management/tests/test_events.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals from actstream.models import Action from mayan.apps.common.tests import GenericViewTestCase +from mayan.apps.rest_api.tests import BaseAPITestCase from ..permissions import ( permission_group_create, permission_group_edit, permission_user_create, @@ -14,10 +15,13 @@ from ..events import ( event_user_edited ) -from .mixins import GroupTestMixin, UserTestMixin +from .mixins import ( + GroupAPITestMixin, GroupTestMixin, GroupViewTestMixin, UserAPITestMixin, + UserTestMixin, UserViewTestMixin +) -class GroupEventsTestCase(GroupTestMixin, UserTestMixin, GenericViewTestCase): +class GroupEventsTestCase(GroupTestMixin, GroupViewTestMixin, UserTestMixin, GenericViewTestCase): def test_group_create_event(self): Action.objects.all().delete() @@ -25,6 +29,7 @@ class GroupEventsTestCase(GroupTestMixin, UserTestMixin, GenericViewTestCase): permission=permission_group_create ) self._request_test_group_create_view() + self.assertEqual(Action.objects.last().target, self.test_group) self.assertEqual(Action.objects.last().verb, event_group_created.id) @@ -36,22 +41,49 @@ class GroupEventsTestCase(GroupTestMixin, UserTestMixin, GenericViewTestCase): obj=self.test_group, permission=permission_group_edit ) self._request_test_group_edit_view() + self.assertEqual(Action.objects.last().target, self.test_group) self.assertEqual(Action.objects.last().verb, event_group_edited.id) -class UserEventsTestCase(UserTestMixin, GenericViewTestCase): - def test_user_create_event(self): +class GroupEventsAPITestCase(GroupAPITestMixin, GroupTestMixin, GroupViewTestMixin, BaseAPITestCase): + def test_group_create_event_from_api_view(self): + Action.objects.all().delete() + + self.grant_permission( + permission=permission_group_create + ) + self._request_test_group_create_api_view() + + self.assertEqual(Action.objects.last().target, self.test_group) + self.assertEqual(Action.objects.last().verb, event_group_created.id) + + def test_group_edit_event_from_api_view(self): + self._create_test_group() + Action.objects.all().delete() + + self.grant_access( + obj=self.test_group, permission=permission_group_edit + ) + self._request_test_group_edit_patch_api_view() + + self.assertEqual(Action.objects.last().target, self.test_group) + self.assertEqual(Action.objects.last().verb, event_group_edited.id) + + +class UserEventsTestCase(UserAPITestMixin, UserTestMixin, UserViewTestMixin, GenericViewTestCase): + def test_user_create_event_from_view(self): Action.objects.all().delete() self.grant_permission( permission=permission_user_create ) self._request_test_user_create_view() + self.assertEqual(Action.objects.last().target, self.test_user) self.assertEqual(Action.objects.last().verb, event_user_created.id) - def test_user_edit_event(self): + def test_user_edit_event_from_view(self): self._create_test_user() Action.objects.all().delete() @@ -59,5 +91,31 @@ class UserEventsTestCase(UserTestMixin, GenericViewTestCase): obj=self.test_user, permission=permission_user_edit ) self._request_test_user_edit_view() + + self.assertEqual(Action.objects.last().target, self.test_user) + self.assertEqual(Action.objects.last().verb, event_user_edited.id) + + +class UserEventsAPITestCase(UserAPITestMixin, UserTestMixin, UserViewTestMixin, BaseAPITestCase): + def test_user_create_event_from_api_view(self): + Action.objects.all().delete() + + self.grant_permission( + permission=permission_user_create + ) + self._request_test_user_create_api_view() + + self.assertEqual(Action.objects.last().target, self.test_user) + self.assertEqual(Action.objects.last().verb, event_user_created.id) + + def test_user_edit_event_from_api_view(self): + self._create_test_user() + Action.objects.all().delete() + + self.grant_access( + obj=self.test_user, permission=permission_user_edit + ) + self._request_test_user_edit_patch_api_view() + self.assertEqual(Action.objects.last().target, self.test_user) self.assertEqual(Action.objects.last().verb, event_user_edited.id) diff --git a/mayan/apps/user_management/tests/test_views.py b/mayan/apps/user_management/tests/test_views.py index 44e6214138..f1b6ff0a8e 100644 --- a/mayan/apps/user_management/tests/test_views.py +++ b/mayan/apps/user_management/tests/test_views.py @@ -21,12 +21,14 @@ from .literals import ( TEST_GROUP_NAME, TEST_GROUP_NAME_EDITED, TEST_USER_PASSWORD_EDITED, TEST_USER_USERNAME ) -from .mixins import GroupTestMixin, UserTestMixin +from .mixins import ( + GroupTestMixin, GroupViewTestMixin, UserTestMixin, UserViewTestMixin +) TEST_USER_TO_DELETE_USERNAME = 'user_to_delete' -class GroupViewsTestCase(GroupTestMixin, UserTestMixin, GenericViewTestCase): +class GroupViewsTestCase(GroupTestMixin, GroupViewTestMixin, UserTestMixin, GenericViewTestCase): def test_group_create_view_no_permission(self): response = self._request_test_group_create_view() self.assertEqual(response.status_code, 403) @@ -136,7 +138,7 @@ class GroupViewsTestCase(GroupTestMixin, UserTestMixin, GenericViewTestCase): ) -class UserViewsTestCase(GroupTestMixin, UserTestMixin, GenericViewTestCase): +class UserViewsTestCase(GroupTestMixin, UserTestMixin, UserViewTestMixin, GenericViewTestCase): def test_user_create_view_no_permission(self): user_count = get_user_model().objects.count() @@ -343,7 +345,7 @@ class UserViewsTestCase(GroupTestMixin, UserTestMixin, GenericViewTestCase): self.assertEqual(response.status_code, 200) - def _request_user_multiple_delete_view(self): + def _request_test_user_multiple_delete_view(self): return self.post( viewname='user_management:user_multiple_delete', data={ 'id_list': self.test_user.pk @@ -354,7 +356,7 @@ class UserViewsTestCase(GroupTestMixin, UserTestMixin, GenericViewTestCase): self._create_test_user() user_count = get_user_model().objects.count() - response = self._request_user_multiple_delete_view() + response = self._request_test_user_multiple_delete_view() self.assertEqual(response.status_code, 404) self.assertEqual(get_user_model().objects.count(), user_count) @@ -366,7 +368,7 @@ class UserViewsTestCase(GroupTestMixin, UserTestMixin, GenericViewTestCase): self.grant_access( obj=self.test_user, permission=permission_user_delete ) - response = self._request_user_multiple_delete_view() + response = self._request_test_user_multiple_delete_view() self.assertEqual(response.status_code, 302) self.assertEqual(get_user_model().objects.count(), user_count - 1) diff --git a/mayan/apps/user_management/views.py b/mayan/apps/user_management/views.py index 9cdd9fefd0..b27f46c342 100644 --- a/mayan/apps/user_management/views.py +++ b/mayan/apps/user_management/views.py @@ -5,7 +5,6 @@ from django.contrib.auth import get_user_model from django.contrib.auth.forms import SetPasswordForm from django.contrib.auth.models import Group from django.contrib.contenttypes.models import ContentType -from django.db import transaction from django.http import HttpResponseRedirect from django.shortcuts import get_object_or_404 from django.template import RequestContext @@ -21,10 +20,6 @@ from mayan.apps.common.generics import ( ) from mayan.apps.common.mixins import ExternalObjectMixin -from .events import ( - event_group_created, event_group_edited, event_user_created, - event_user_edited -) from .forms import UserForm from .icons import icon_group_setup, icon_user_setup from .links import link_group_create, link_user_create @@ -69,14 +64,6 @@ class GroupCreateView(SingleObjectCreateView): post_action_redirect = reverse_lazy(viewname='user_management:group_list') view_permission = permission_group_create - def form_valid(self, form): - with transaction.atomic(): - result = super(GroupCreateView, self).form_valid(form=form) - event_group_created.commit( - actor=self.request.user, target=self.object - ) - return result - class GroupDeleteView(SingleObjectDeleteView): model = Group @@ -98,14 +85,6 @@ class GroupEditView(SingleObjectEditView): pk_url_kwarg = 'group_id' post_action_redirect = reverse_lazy(viewname='user_management:group_list') - def form_valid(self, form): - with transaction.atomic(): - result = super(GroupEditView, self).form_valid(form=form) - event_group_edited.commit( - actor=self.request.user, target=self.object - ) - return result - def get_extra_context(self): return { 'object': self.object, @@ -205,12 +184,7 @@ class UserCreateView(SingleObjectCreateView): view_permission = permission_user_create def form_valid(self, form): - with transaction.atomic(): - super(UserCreateView, self).form_valid(form=form) - event_user_created.commit( - actor=self.request.user, target=self.object - ) - + super(UserCreateView, self).form_valid(form=form) return HttpResponseRedirect( reverse( viewname='user_management:user_set_password', @@ -295,15 +269,6 @@ class UserEditView(SingleObjectEditView): is_superuser=False, is_staff=False ) - def form_valid(self, form): - with transaction.atomic(): - result = super(UserEditView, self).form_valid(form=form) - event_user_edited.commit( - actor=self.request.user, target=self.object - ) - - return result - def get_extra_context(self): return { 'object': self.object, From b63323861005fd9536c201c47c30afc67f00d0f4 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Fri, 8 Feb 2019 00:53:57 -0400 Subject: [PATCH 123/209] Fix pk_list_field processing This field was being ignored. Improved the code to check for values in sequence. Signed-off-by: Roberto Rosario --- mayan/apps/rest_api/mixins.py | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/mayan/apps/rest_api/mixins.py b/mayan/apps/rest_api/mixins.py index 3a08e520c6..2324cb570b 100644 --- a/mayan/apps/rest_api/mixins.py +++ b/mayan/apps/rest_api/mixins.py @@ -35,22 +35,33 @@ class ExternalObjectListSerializerMixin(object): pk_field = self.get_external_object_list_option('pk_field') pk_list_field = self.get_external_object_list_option('pk_list_field') - if pk_field: - id_list = (self.validated_data.get(pk_field),) - elif pk_list_field: - id_list = self.validated_data.get(pk_list_field, '').split(',') - else: + if not pk_field and not pk_list_field: raise ImproperlyConfigured( 'ExternalObjectListSerializerMixin requires a ' - 'external_object_list__pk_field a ' + 'external_object_list_pk_field a ' 'external_object_list_pk_list_field.' ) + + if pk_field: + pk_field_value = self.validated_data.get(pk_field) + + if pk_list_field: + pk_list_field_value = self.validated_data.get(pk_list_field) + + if pk_field_value: + id_list = (pk_field_value,) + elif pk_list_field_value: + id_list = pk_list_field_value or ''.split(',') + else: + id_list = () + return queryset.filter(pk__in=id_list) def get_external_object_list_option(self, option_name): return getattr( - self.external_object_list_options, 'external_object_list_{}'.format(option_name), None + self.external_object_list_options, 'external_object_list_{}'.format(option_name), + None ) def get_external_object_list_queryset(self): From dcd1af685a089a518adaeeef92ab9e030639334d Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Tue, 12 Feb 2019 03:34:14 -0400 Subject: [PATCH 124/209] Add new AddRemoveView view Add a new view based on AssignRemove with extra features and filtering. AddRemoveView also has two new buttons: Add all, Remove all. Signed-off-by: Roberto Rosario --- mayan/apps/common/forms.py | 4 +- mayan/apps/common/generics.py | 278 ++++++++++++++++++++++++++++++++-- mayan/apps/common/icons.py | 12 ++ 3 files changed, 278 insertions(+), 16 deletions(-) diff --git a/mayan/apps/common/forms.py b/mayan/apps/common/forms.py index 4d1c1828a6..88e6ee5d56 100644 --- a/mayan/apps/common/forms.py +++ b/mayan/apps/common/forms.py @@ -39,7 +39,9 @@ class ChoiceForm(forms.Form): } ) - selection = forms.MultipleChoiceField(widget=DisableableSelectWidget()) + selection = forms.MultipleChoiceField( + required=False, widget=DisableableSelectWidget() + ) class FormOptions(object): diff --git a/mayan/apps/common/generics.py b/mayan/apps/common/generics.py index bdd2f04e2a..014dbe7bda 100644 --- a/mayan/apps/common/generics.py +++ b/mayan/apps/common/generics.py @@ -5,6 +5,7 @@ from django.contrib import messages from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ImproperlyConfigured, ValidationError from django.http import HttpResponseRedirect +from django.urls import reverse from django.utils.encoding import force_text from django.utils.translation import ugettext_lazy as _ from django.views.generic import DetailView @@ -19,12 +20,15 @@ from django.views.generic.list import ListView from django_downloadview import ( TextIteratorIO, VirtualDownloadView, VirtualFile ) + +from mayan.apps.acls.models import AccessControlList + from pure_pagination.mixins import PaginationMixin from .forms import ChoiceForm from .icons import ( - icon_assign_remove_add, icon_assign_remove_remove, icon_sort_down, - icon_sort_up + icon_add_all, icon_assign_remove_add, icon_assign_remove_remove, + icon_remove_all, icon_sort_down, icon_sort_up ) from .literals import ( TEXT_SORT_FIELD_PARAMETER, TEXT_SORT_FIELD_VARIABLE_NAME, @@ -32,12 +36,13 @@ from .literals import ( TEXT_SORT_ORDER_VARIABLE_NAME ) from .mixins import ( - DeleteExtraDataMixin, DynamicFormViewMixin, ExtraContextMixin, - FormExtraKwargsMixin, ListModeMixin, MultipleObjectMixin, + DeleteExtraDataMixin, DynamicFormViewMixin, ExternalObjectMixin, + ExtraContextMixin, FormExtraKwargsMixin, ListModeMixin, MultipleObjectMixin, ObjectActionMixin, ObjectNameMixin, RedirectionMixin, RestrictedQuerysetMixin, ViewPermissionCheckMixin ) from .settings import setting_paginate_by +from .utils import resolve __all__ = ( 'AssignRemoveView', 'ConfirmView', 'FormView', @@ -275,6 +280,7 @@ class SingleObjectDownloadView(RestrictedQuerysetMixin, SingleObjectMixin, Downl class MultiFormView(DjangoFormView): prefix = None prefixes = {} + template_name = 'appearance/generic_form.html' def _create_form(self, form_name, klass): form_kwargs = self.get_form_kwargs(form_name) @@ -285,6 +291,14 @@ class MultiFormView(DjangoFormView): form = klass(**form_kwargs) return form + def all_forms_valid(self, forms): + return None + + def dispatch(self, request, *args, **kwargs): + form_classes = self.get_form_classes() + self.forms = self.get_forms(form_classes) + return super(MultiFormView, self).dispatch(request, *args, **kwargs) + def forms_valid(self, forms): for form_name, form in forms.items(): form_valid_method = '%s_form_valid' % form_name @@ -299,11 +313,6 @@ class MultiFormView(DjangoFormView): def forms_invalid(self, forms): return self.render_to_response(self.get_context_data(forms=forms)) - 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 get_context_data(self, **kwargs): """ Insert the form into the context dict. @@ -328,8 +337,13 @@ class MultiFormView(DjangoFormView): 'files': self.request.FILES, }) + kwargs.update(self.get_form_extra_kwargs(form_name=form_name) or {}) + return kwargs + def get_form_extra_kwargs(self, form_name): + return None + def get_forms(self, form_classes): return dict( [ @@ -350,13 +364,247 @@ class MultiFormView(DjangoFormView): return self.prefixes.get(form_name, self.prefix) 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=forms) + if all([form.is_valid() for form in self.forms.values()]): + return self.forms_valid(forms=self.forms) else: - return self.forms_invalid(forms=forms) + return self.forms_invalid(forms=self.forms) + + +class AddRemoveView(ExternalObjectMixin, ExtraContextMixin, ViewPermissionCheckMixin, RestrictedQuerysetMixin, MultiFormView): + form_classes = {'form_available': ChoiceForm, 'form_added': ChoiceForm} + grouped = False + list_added_help_text = _( + 'Select entries to be removed. Hold Control to select multiple ' + 'entries. Once the selection is complete, click the button below ' + 'or double click the list to activate the action.' + ) + list_available_help_text = _( + 'Select entries to be added. Hold Control to select multiple ' + 'entries. Once the selection is complete, click the button below ' + 'or double click the list to activate the action.' + ) + + # Form titles + list_added_title = None + list_available_title = None + + # Attributes to filter the object to which selections will be added or + # remove + main_object_model = None + main_object_permission = None + main_object_pk_url_kwarg = None + main_object_pk_url_kwargs = None + main_object_source_queryset = None + + # Attributes to filter the queryset of the selection + secondary_object_model = None + secondary_object_permission = None + secondary_object_source_queryset = None + + # Main object methods to use to add and remove selections + action_add_method = None + action_remove_method = None + + # If a method is not specified, use this related field to add and remove + # selections + related_field = None + + prefixes = {'form_available': 'available', 'form_added': 'added'} + + def __init__(self, *args, **kwargs): + self.external_object_class = self.main_object_model + self.external_object_permission = self.main_object_permission + self.external_object_pk_url_kwarg = self.main_object_pk_url_kwarg + self.external_object_pk_url_kwargs = self.main_object_pk_url_kwargs + self.external_object_queryset = self.main_object_source_queryset + + super(AddRemoveView, self).__init__(*args, **kwargs) + + def action_add(self, queryset): + if self.action_add_method: + kwargs = {'queryset': queryset} + kwargs.update(self.get_action_add_extra_kwargs()) + kwargs.update(self.get_actions_extra_kwargs()) + getattr(self.main_object, self.action_add_method)(**kwargs) + elif self.related_field: + getattr(self.main_object, self.related_field).add(*queryset) + else: + raise ImproperlyConfigured( + 'View %s must be called with either an action_add_method, a ' + 'related_field.' % self.__class__.__name__ + ) + + def action_remove(self, queryset): + if self.action_remove_method: + kwargs = {'queryset': queryset} + kwargs.update(self.get_action_remove_extra_kwargs()) + kwargs.update(self.get_actions_extra_kwargs()) + getattr(self.main_object, self.action_remove_method)(**kwargs) + elif self.related_field: + getattr(self.main_object, self.related_field).remove(*queryset) + else: + raise ImproperlyConfigured( + 'View %s must be called with either an action_remove_method, a ' + 'related_field.' % self.__class__.__name__ + ) + + def dispatch(self, request, *args, **kwargs): + self.main_object = self.get_external_object() + result = super(AddRemoveView, self).dispatch(request=request, *args, **kwargs) + return result + + def forms_valid(self, forms): + if 'available-add_all' in self.request.POST: + selection_add = self.get_secondary_object_list() + else: + selection_add = self.get_secondary_object_list().filter( + pk__in=forms['form_available'].cleaned_data['selection'] + ) + + self.action_add(queryset=selection_add) + + if 'added-remove_all' in self.request.POST: + selection_remove = self.get_secondary_object_list() + else: + selection_remove = self.get_secondary_object_list().filter( + pk__in=forms['form_added'].cleaned_data['selection'] + ) + + self.action_remove(queryset=selection_remove) + + return super(AddRemoveView, self).forms_valid(forms=forms) + + def generate_choices(self, queryset): + for obj in queryset: + yield (obj.pk, force_text(obj)) + + def get_action_add_extra_kwargs(self): + # Keyword arguments to apply to the add method + return {} + + def get_action_remove_extra_kwargs(self): + # Keyword arguments to apply to the remove method + return {} + + def get_actions_extra_kwargs(self): + # Keyword arguments to apply to both the add and remove methods + return {} + + def get_context_data(self, **kwargs): + # Use get_context_data to leave the get_extra_context for subclasses + context = super(AddRemoveView, self).get_context_data(**kwargs) + context.update( + { + 'subtemplates_list': [ + { + 'name': 'appearance/generic_form_subtemplate.html', + 'column_class': 'col-xs-12 col-sm-6 col-md-6 col-lg-6', + 'context': { + 'extra_buttons': [ + { + 'label': _('Add all'), + 'icon_class': icon_add_all, + 'name': 'add_all', + } + ], + 'form': self.forms['form_available'], + 'form_css_classes': 'form-hotkey-double-click', + 'hide_labels': True, + 'submit_icon_class': icon_assign_remove_add, + 'submit_label': _('Add'), + 'title': self.list_available_title or ' ', + } + }, + { + 'name': 'appearance/generic_form_subtemplate.html', + 'column_class': 'col-xs-12 col-sm-6 col-md-6 col-lg-6', + 'context': { + 'extra_buttons': [ + { + 'label': _('Remove all'), + 'icon_class': icon_remove_all, + 'name': 'remove_all', + } + ], + 'form': self.forms['form_added'], + 'form_css_classes': 'form-hotkey-double-click', + 'hide_labels': True, + 'submit_icon_class': icon_assign_remove_remove, + 'submit_label': _('Remove'), + 'title': self.list_added_title or ' ', + } + } + ] + } + ) + + return context + + def get_disabled_choices(self): + return () + + def get_form_extra_kwargs(self, form_name): + if form_name == 'form_available': + return { + 'choices': self.generate_choices( + queryset=self.get_list_available_queryset() + ), + 'help_text': self.get_list_available_help_text() + } + else: + return { + 'choices': self.generate_choices( + queryset=self.get_list_added_queryset() + ), + 'disabled_choices': self.get_disabled_choices(), + 'help_text': self.get_list_added_help_text() + } + + def get_list_added_help_text(self): + return self.list_added_help_text + + def get_list_added_queryset(self): + if not self.related_field: + raise ImproperlyConfigured( + 'View %s must be called with either a related_field or ' + 'override .get_list_added_queryset().' % self.__class__.__name__ + ) + + return self.get_secondary_object_list().filter( + pk__in=getattr(self.main_object, self.related_field).values('pk') + ) + + def get_list_available_help_text(self): + return self.list_available_help_text + + def get_list_available_queryset(self): + return self.get_secondary_object_list().exclude( + pk__in=self.get_list_added_queryset().values('pk') + ) + + def get_secondary_object_list(self): + queryset = self.get_secondary_object_source_queryset() + + if queryset is None: + queryset = self.secondary_object_model._meta.default_manager.all() + + if self.secondary_object_permission: + return AccessControlList.objects.restrict_queryset( + permission=self.secondary_object_permission, queryset=queryset, + user=self.request.user + ) + else: + return queryset + + def get_secondary_object_source_queryset(self): + return self.secondary_object_source_queryset + + def get_success_url(self): + # Redirect to the same view + return reverse( + viewname=self.request.resolver_match.view_name, + kwargs=self.request.resolver_match.kwargs + ) class MultipleObjectFormActionView(ObjectActionMixin, ViewPermissionCheckMixin, RestrictedQuerysetMixin, MultipleObjectMixin, FormExtraKwargsMixin, ExtraContextMixin, RedirectionMixin, DjangoFormView): diff --git a/mayan/apps/common/icons.py b/mayan/apps/common/icons.py index ca653fbc6c..4e7d7bdc8c 100644 --- a/mayan/apps/common/icons.py +++ b/mayan/apps/common/icons.py @@ -3,6 +3,12 @@ from __future__ import absolute_import, unicode_literals from mayan.apps.appearance.classes import Icon icon_about = Icon(driver_name='fontawesome', symbol='info') +icon_add_all = Icon( + driver_name='fontawesome-layers', data=[ + {'class': 'far fa-circle'}, + {'class': 'fas fa-plus', 'transform': 'shrink-6'} + ] +) icon_assign_remove_add = Icon(driver_name='fontawesome', symbol='plus') icon_assign_remove_remove = Icon(driver_name='fontawesome', symbol='minus') icon_check_version = Icon(driver_name='fontawesome', symbol='sync') @@ -43,6 +49,12 @@ icon_ok = Icon( icon_packages_licenses = Icon( driver_name='fontawesome', symbol='certificate' ) +icon_remove_all = Icon( + driver_name='fontawesome-layers', data=[ + {'class': 'far fa-circle'}, + {'class': 'fas fa-minus', 'transform': 'shrink-6'} + ] +) icon_setup = Icon( driver_name='fontawesome', symbol='cog' ) From 1fee7260e496092b501bdea63369b30471a7e43d Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Tue, 12 Feb 2019 03:35:43 -0400 Subject: [PATCH 125/209] Allow adding extra buttons to forms Signed-off-by: Roberto Rosario --- .../appearance/generic_form_subtemplate.html | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/mayan/apps/appearance/templates/appearance/generic_form_subtemplate.html b/mayan/apps/appearance/templates/appearance/generic_form_subtemplate.html index 2175b885b8..975860aa2a 100644 --- a/mayan/apps/appearance/templates/appearance/generic_form_subtemplate.html +++ b/mayan/apps/appearance/templates/appearance/generic_form_subtemplate.html @@ -90,6 +90,17 @@ {% if cancel_label %}{{ cancel_label }}{% else %}{% trans 'Cancel' %}{% endif %} {% endif %} + + {% for button in extra_buttons %} + + {% endfor %} + + {% endif %} {% endif %} From f3f7b4bb7d25bf99177b9a8b64f0f76ac120a62b Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Tue, 12 Feb 2019 03:36:16 -0400 Subject: [PATCH 126/209] Refactor the permissions app Use the new AddRemove View for the Role's group and permissions views as well as the Group's role views. Convert the API to use viewsets. Add more tests. Add role created and edited events. Add event subscription support to roles. Signed-off-by: Roberto Rosario --- mayan/apps/permissions/api_views.py | 215 ++++++--- mayan/apps/permissions/apps.py | 20 +- mayan/apps/permissions/classes.py | 10 +- mayan/apps/permissions/events.py | 16 + mayan/apps/permissions/methods.py | 43 ++ mayan/apps/permissions/models.py | 67 ++- mayan/apps/permissions/serializers.py | 183 ++++---- mayan/apps/permissions/tests/literals.py | 12 +- mayan/apps/permissions/tests/mixins.py | 19 +- mayan/apps/permissions/tests/test_api.py | 505 +++++++++++--------- mayan/apps/permissions/tests/test_views.py | 507 +++++++++++++++++++-- mayan/apps/permissions/urls.py | 26 +- mayan/apps/permissions/views.py | 167 +++---- 13 files changed, 1269 insertions(+), 521 deletions(-) create mode 100644 mayan/apps/permissions/events.py create mode 100644 mayan/apps/permissions/methods.py diff --git a/mayan/apps/permissions/api_views.py b/mayan/apps/permissions/api_views.py index fb23c78a00..3e0f079551 100644 --- a/mayan/apps/permissions/api_views.py +++ b/mayan/apps/permissions/api_views.py @@ -1,77 +1,180 @@ from __future__ import unicode_literals -from rest_framework import generics +from rest_framework import status, viewsets +from rest_framework.response import Response +from rest_framework.decorators import action -from mayan.apps.rest_api.filters import MayanObjectPermissionsFilter -from mayan.apps.rest_api.permissions import MayanPermission +from mayan.apps.rest_api.viewsets import MayanAPIModelViewSet +from mayan.apps.user_management.permissions import permission_group_view +from mayan.apps.user_management.serializers import GroupSerializer -from .classes import Permission +from .classes import PermissionNamespace from .models import Role from .permissions import ( permission_role_create, permission_role_delete, permission_role_edit, permission_role_view ) from .serializers import ( - PermissionSerializer, RoleSerializer, WritableRoleSerializer + PermissionNamespaceSerializer, PermissionSerializer, RoleGroupAddRemoveSerializer, + RolePermissionAddRemoveSerializer, RoleSerializer ) -class APIPermissionList(generics.ListAPIView): - """ - get: Returns a list of all the available permissions. - """ +class PermissionNamespaceViewSet(viewsets.ReadOnlyModelViewSet): + lookup_field = 'name' + lookup_url_kwarg = 'permission_namespace_name' + serializer_class = PermissionNamespaceSerializer + + def get_object(self): + lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field + filter_kwargs = {self.lookup_field: self.kwargs[lookup_url_kwarg]} + return PermissionNamespace.get(**filter_kwargs) + + @action( + detail=True, serializer_class=PermissionSerializer, + url_name='permission-list', url_path='permissions' + ) + def permission_list(self, request, *args, **kwargs): + queryset = self.get_object().permissions + page = self.paginate_queryset(queryset) + + serializer = self.get_serializer( + queryset, many=True, context={'request': request} + ) + + if page is not None: + return self.get_paginated_response(serializer.data) + + return Response(serializer.data) + + def get_queryset(self): + return PermissionNamespace.all() + + +class PermissionViewSet(viewsets.ReadOnlyModelViewSet): + lookup_field = 'pk' + lookup_url_kwarg = 'permission_name' + lookup_value_regex = r'[\w\.]+' serializer_class = PermissionSerializer - queryset = Permission.all() + + def get_object(self): + namespace = PermissionNamespace.get(name=self.kwargs['permission_namespace_name']) + permissions = namespace.get_permissions() + return permissions.get(self.kwargs['permission_name']) -class APIRoleListView(generics.ListCreateAPIView): - """ - get: Returns a list of all the roles. - post: Create a new role. - """ - filter_backends = (MayanObjectPermissionsFilter,) - mayan_object_permissions = {'GET': (permission_role_view,)} - mayan_view_permissions = {'POST': (permission_role_create,)} - permission_classes = (MayanPermission,) - queryset = Role.objects.all() - - def get_serializer(self, *args, **kwargs): - if not self.request: - return None - - return super(APIRoleListView, self).get_serializer(*args, **kwargs) - - def get_serializer_class(self): - if self.request.method == 'GET': - return RoleSerializer - elif self.request.method == 'POST': - return WritableRoleSerializer - - -class APIRoleView(generics.RetrieveUpdateDestroyAPIView): - """ - delete: Delete the selected role. - get: Return the details of the selected role. - patch: Edit the selected role. - put: Edit the selected role. - """ - mayan_object_permissions = { - 'GET': (permission_role_view,), - 'PUT': (permission_role_edit,), - 'PATCH': (permission_role_edit,), - 'DELETE': (permission_role_delete,) +class RoleAPIViewSet(MayanAPIModelViewSet): + lookup_url_kwarg = 'role_id' + object_permission_map = { + 'destroy': permission_role_delete, + 'group_add': permission_role_edit, + 'group_list': permission_role_view, + 'group_remove': permission_role_edit, + 'list': permission_role_view, + 'partial_update': permission_role_edit, + 'retrieve': permission_role_view, + 'update': permission_role_edit, } - permission_classes = (MayanPermission,) queryset = Role.objects.all() + serializer_class = RoleSerializer + view_permission_map = { + 'create': permission_role_create + } - def get_serializer(self, *args, **kwargs): - if not self.request: - return None + @action( + detail=True, lookup_url_kwarg='role_id', methods=('post',), + serializer_class=RoleGroupAddRemoveSerializer, + url_name='group-add', url_path='groups/add' + ) + def group_add(self, request, *args, **kwargs): + instance = self.get_object() + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + serializer.groups_add(instance=instance) + headers = self.get_success_headers(data=serializer.data) + return Response( + serializer.data, status=status.HTTP_200_OK, headers=headers + ) - return super(APIRoleView, self).get_serializer(*args, **kwargs) + @action( + detail=True, lookup_url_kwarg='role_id', + serializer_class=GroupSerializer, url_name='group-list', + url_path='groups' + ) + def group_list(self, request, *args, **kwargs): + queryset = self.get_object().get_groups( + permission=permission_group_view, user=self.request.user + ) + page = self.paginate_queryset(queryset) - def get_serializer_class(self): - if self.request.method == 'GET': - return RoleSerializer - elif self.request.method in ('PATCH', 'PUT'): - return WritableRoleSerializer + serializer = self.get_serializer( + queryset, many=True, context={'request': request} + ) + + if page is not None: + return self.get_paginated_response(serializer.data) + + return Response(serializer.data) + + @action( + detail=True, lookup_url_kwarg='role_id', + methods=('post',), serializer_class=RoleGroupAddRemoveSerializer, + url_name='group-remove', url_path='groups/remove' + ) + def group_remove(self, request, *args, **kwargs): + instance = self.get_object() + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + serializer.groups_remove(instance=instance) + headers = self.get_success_headers(data=serializer.data) + return Response( + serializer.data, status=status.HTTP_200_OK, headers=headers + ) + + @action( + detail=True, lookup_url_kwarg='role_id', methods=('post',), + serializer_class=RolePermissionAddRemoveSerializer, + url_name='permission-add', url_path='permissions/add' + ) + def permission_add(self, request, *args, **kwargs): + instance = self.get_object() + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + serializer.permissions_add(instance=instance) + headers = self.get_success_headers(data=serializer.data) + return Response( + serializer.data, status=status.HTTP_200_OK, headers=headers + ) + + @action( + detail=True, lookup_url_kwarg='role_id', + serializer_class=PermissionSerializer, url_name='permission-list', + url_path='permissions' + ) + def permission_list(self, request, *args, **kwargs): + queryset = self.get_object().permissions.all() + page = self.paginate_queryset(queryset) + + serializer = self.get_serializer( + queryset, many=True, context={'request': request} + ) + + if page is not None: + return self.get_paginated_response(serializer.data) + + return Response(serializer.data) + + @action( + detail=True, lookup_url_kwarg='role_id', + methods=('post',), serializer_class=RolePermissionAddRemoveSerializer, + url_name='permission-remove', url_path='permissions/remove' + ) + def permission_remove(self, request, *args, **kwargs): + instance = self.get_object() + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + serializer.permissions_remove(instance=instance) + headers = self.get_success_headers(data=serializer.data) + return Response( + serializer.data, status=status.HTTP_200_OK, headers=headers + ) diff --git a/mayan/apps/permissions/apps.py b/mayan/apps/permissions/apps.py index 34ca946cbd..63983adcbb 100644 --- a/mayan/apps/permissions/apps.py +++ b/mayan/apps/permissions/apps.py @@ -13,14 +13,20 @@ from mayan.apps.common import ( menu_secondary, menu_setup ) from mayan.apps.common.signals import perform_upgrade +from mayan.apps.events import ModelEventType +from mayan.apps.events.links import ( + link_events_for_object, link_object_event_types_user_subcriptions_list +) from mayan.apps.navigation import SourceColumn +from .events import event_role_created, event_role_edited from .handlers import handler_purge_permissions from .links import ( link_group_roles, link_permission_grant, link_permission_revoke, link_role_create, link_role_delete, link_role_edit, link_role_groups, link_role_list, link_role_permissions ) +from .methods import method_group_roles_add, method_group_roles_remove from .permissions import ( permission_role_delete, permission_role_edit, permission_role_view ) @@ -37,10 +43,18 @@ class PermissionsApp(MayanAppConfig): def ready(self): super(PermissionsApp, self).ready() + from actstream import registry Role = self.get_model('Role') Group = apps.get_model(app_label='auth', model_name='Group') + Group.add_to_class(name='roles_add', value=method_group_roles_add) + Group.add_to_class(name='roles_remove', value=method_group_roles_remove) + + ModelEventType.register( + event_types=(event_role_created, event_role_edited), model=Role + ) + ModelPermission.register( model=Role, permissions=( permission_acl_edit, permission_acl_view, @@ -53,7 +67,9 @@ class PermissionsApp(MayanAppConfig): menu_list_facet.bind_links( links=( - link_acl_list, link_role_groups, link_role_permissions, + link_acl_list, link_events_for_object, + link_object_event_types_user_subcriptions_list, + link_role_groups, link_role_permissions, ), sources=(Role,) ) menu_list_facet.bind_links( @@ -78,3 +94,5 @@ class PermissionsApp(MayanAppConfig): dispatch_uid='permissions_handler_purge_permissions', receiver=handler_purge_permissions ) + + registry.register(Role) diff --git a/mayan/apps/permissions/classes.py b/mayan/apps/permissions/classes.py index eb75ca8202..08e5a8dbdf 100644 --- a/mayan/apps/permissions/classes.py +++ b/mayan/apps/permissions/classes.py @@ -2,15 +2,12 @@ from __future__ import unicode_literals import itertools import logging -import warnings from django.apps import apps from django.core.exceptions import PermissionDenied from django.utils.encoding import force_text, python_2_unicode_compatible from django.utils.translation import ugettext_lazy as _ -from mayan.apps.common.warnings import InterfaceWarning - from .exceptions import InvalidNamespace logger = logging.getLogger(__name__) @@ -49,6 +46,13 @@ class PermissionNamespace(object): self.permissions.append(permission) return permission + def get_permissions(self): + result = {} + for permission in self.permissions: + result[permission.pk] = permission + + return result + @python_2_unicode_compatible class Permission(object): diff --git a/mayan/apps/permissions/events.py b/mayan/apps/permissions/events.py new file mode 100644 index 0000000000..9b6d20ba96 --- /dev/null +++ b/mayan/apps/permissions/events.py @@ -0,0 +1,16 @@ +from __future__ import absolute_import, unicode_literals + +from django.utils.translation import ugettext_lazy as _ + +from mayan.apps.events import EventTypeNamespace + +namespace = EventTypeNamespace( + label=_('Permissions'), name='permissions' +) + +event_role_created = namespace.add_event_type( + label=_('Role created'), name='role_created' +) +event_role_edited = namespace.add_event_type( + label=_('Role edited'), name='role_edited' +) diff --git a/mayan/apps/permissions/methods.py b/mayan/apps/permissions/methods.py new file mode 100644 index 0000000000..79311d38f1 --- /dev/null +++ b/mayan/apps/permissions/methods.py @@ -0,0 +1,43 @@ +from __future__ import unicode_literals + +from django.apps import apps +from django.db import transaction + +from mayan.apps.user_management.events import event_group_edited + +from .events import event_role_edited + + +def method_group_get_roles(self, permission, _user): + AccessControlList = apps.get_model( + app_label='acls', model_name='AccessControlList' + ) + + return AccessControlList.objects.restrict_queryset( + permission=permission, queryset=self.roles.all(), + user=_user + ) + + +def method_group_roles_add(self, queryset, _user): + with transaction.atomic(): + event_group_edited.commit( + actor=_user, target=self + ) + for role in queryset: + self.roles.add(role) + event_role_edited.commit( + actor=_user, action_object=self, target=role + ) + + +def method_group_roles_remove(self, queryset, _user): + with transaction.atomic(): + event_group_edited.commit( + actor=_user, target=self + ) + for role in queryset: + self.roles.remove(role) + event_role_edited.commit( + actor=_user, action_object=self, target=role + ) diff --git a/mayan/apps/permissions/models.py b/mayan/apps/permissions/models.py index 503df464aa..70c167feb9 100644 --- a/mayan/apps/permissions/models.py +++ b/mayan/apps/permissions/models.py @@ -2,13 +2,17 @@ from __future__ import unicode_literals import logging +from django.apps import apps from django.contrib.auth.models import Group -from django.db import models +from django.db import models, transaction from django.urls import reverse from django.utils.encoding import force_text, python_2_unicode_compatible from django.utils.translation import ugettext_lazy as _ +from mayan.apps.user_management.events import event_group_edited + from .classes import Permission +from .events import event_role_created, event_role_edited from .managers import RoleManager, StoredPermissionManager logger = logging.getLogger(__name__) @@ -118,9 +122,70 @@ class Role(models.Model): def grant(self, permission): self.permissions.add(permission.stored_permission) + def get_groups(self, permission, user): + AccessControlList = apps.get_model( + app_label='acls', model_name='AccessControlList' + ) + + return AccessControlList.objects.restrict_queryset( + permission=permission, queryset=self.groups.all(), + user=user + ) + + def groups_add(self, queryset, _user=None): + with transaction.atomic(): + event_role_edited.commit( + actor=_user, target=self + ) + for obj in queryset: + self.groups.add(obj) + event_group_edited.commit( + actor=_user, action_object=self, target=obj + ) + + def groups_remove(self, queryset, _user=None): + with transaction.atomic(): + event_role_edited.commit( + actor=_user, target=self + ) + for obj in queryset: + self.groups.remove(obj) + event_group_edited.commit( + actor=_user, action_object=self, target=obj + ) + def natural_key(self): return (self.label,) natural_key.dependencies = ['auth.Group', 'permissions.StoredPermission'] + def permissions_add(self, queryset, _user=None): + with transaction.atomic(): + event_role_edited.commit( + actor=_user, target=self + ) + self.permissions.add(*queryset) + + def permissions_remove(self, queryset, _user=None): + with transaction.atomic(): + event_role_edited.commit( + actor=_user, target=self + ) + self.permissions.remove(*queryset) + def revoke(self, permission): self.permissions.remove(permission.stored_permission) + + def save(self, *args, **kwargs): + _user = kwargs.pop('_user', None) + + with transaction.atomic(): + is_new = not self.pk + super(Role, self).save(*args, **kwargs) + if is_new: + event_role_created.commit( + actor=_user, target=self + ) + else: + event_role_edited.commit( + actor=_user, target=self + ) diff --git a/mayan/apps/permissions/serializers.py b/mayan/apps/permissions/serializers.py index 4753bb3867..f4127e7ded 100644 --- a/mayan/apps/permissions/serializers.py +++ b/mayan/apps/permissions/serializers.py @@ -4,18 +4,45 @@ from django.contrib.auth.models import Group from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers -from rest_framework.exceptions import ValidationError -from mayan.apps.user_management.serializers import GroupSerializer +from mayan.apps.rest_api.mixins import ExternalObjectListSerializerMixin +from mayan.apps.rest_api.relations import MultiKwargHyperlinkedIdentityField +from mayan.apps.user_management.permissions import permission_group_edit from .classes import Permission from .models import Role, StoredPermission +class PermissionNamespaceSerializer(serializers.Serializer): + name = serializers.CharField(read_only=True) + label = serializers.CharField(read_only=True) + permissions_url = serializers.HyperlinkedIdentityField( + lookup_field='name', + lookup_url_kwarg='permission_namespace_name', + view_name='rest_api:permission_namespace-permission-list' + ) + url = serializers.HyperlinkedIdentityField( + lookup_field='name', + lookup_url_kwarg='permission_namespace_name', + view_name='rest_api:permission_namespace-detail' + ) + + class PermissionSerializer(serializers.Serializer): namespace = serializers.CharField(read_only=True) pk = serializers.CharField(read_only=True) label = serializers.CharField(read_only=True) + url = MultiKwargHyperlinkedIdentityField( + view_kwargs=( + { + 'lookup_field': 'namespace.name', 'lookup_url_kwarg': 'permission_namespace_name', + }, + { + 'lookup_field': 'pk', 'lookup_url_kwarg': 'permission_name', + } + ), + view_name='rest_api:permission-detail' + ) def to_representation(self, instance): if isinstance(instance, StoredPermission): @@ -28,85 +55,85 @@ class PermissionSerializer(serializers.Serializer): ) +class RoleGroupAddRemoveSerializer(ExternalObjectListSerializerMixin, serializers.Serializer): + group_id_list = serializers.CharField( + help_text=_( + 'Comma separated list of group primary keys that will be added or ' + 'removed.' + ), required=False, write_only=True + ) + + class Meta: + external_object_list_model = Group + external_object_list_permission = permission_group_edit + external_object_list_pk_list_field = 'group_id_list' + + def groups_add(self, instance): + instance.groups_add( + queryset=self.get_external_object_list(), + _user=self.context['request'].user + ) + + def groups_remove(self, instance): + instance.groups_remove( + queryset=self.get_external_object_list(), + _user=self.context['request'].user + ) + + +class RolePermissionAddRemoveSerializer(ExternalObjectListSerializerMixin, serializers.Serializer): + permission_id_list = serializers.CharField( + help_text=_( + 'Comma separated list of permission primary keys that will be added or ' + 'removed.' + ), required=False, write_only=True + ) + + class Meta: + external_object_list_model = Permission + external_object_list_pk_list_field = 'permission_id_list' + + def permissions_add(self, instance): + instance.permissions.add( + *self.get_external_object_list() + ) + + def permissions_remove(self, instance): + instance.permissions.remove( + *self.get_external_object_list() + ) + + class RoleSerializer(serializers.HyperlinkedModelSerializer): - groups = GroupSerializer(many=True, read_only=True) - permissions = PermissionSerializer(many=True, read_only=True) + group_add_url = serializers.HyperlinkedIdentityField( + lookup_url_kwarg='role_id', view_name='rest_api:role-group-add' + ) + group_list_url = serializers.HyperlinkedIdentityField( + lookup_url_kwarg='role_id', view_name='rest_api:role-group-list' + ) + group_remove_url = serializers.HyperlinkedIdentityField( + lookup_url_kwarg='role_id', view_name='rest_api:role-group-remove' + ) + permission_add_url = serializers.HyperlinkedIdentityField( + lookup_url_kwarg='role_id', view_name='rest_api:role-permission-add' + ) + permission_list_url = serializers.HyperlinkedIdentityField( + lookup_url_kwarg='role_id', view_name='rest_api:role-permission-list' + ) + permission_remove_url = serializers.HyperlinkedIdentityField( + lookup_url_kwarg='role_id', view_name='rest_api:role-permission-remove' + ) class Meta: extra_kwargs = { - 'url': {'view_name': 'rest_api:role-detail'}, + 'url': { + 'lookup_url_kwarg': 'role_id', + 'view_name': 'rest_api:role-detail' + } } - fields = ('groups', 'id', 'label', 'permissions', 'url') + fields = ( + 'id', 'label', 'url', 'group_add_url', 'group_list_url', + 'group_remove_url', 'permission_add_url', 'permission_list_url', + 'permission_remove_url' + ) model = Role - - -class WritableRoleSerializer(serializers.HyperlinkedModelSerializer): - groups_pk_list = serializers.CharField( - help_text=_( - 'Comma separated list of groups primary keys to add to, or replace' - ' in this role.' - ), required=False - ) - - permissions_pk_list = serializers.CharField( - help_text=_( - 'Comma separated list of permission primary keys to grant to this ' - 'role.' - ), required=False - ) - - class Meta: - fields = ('groups_pk_list', 'id', 'label', 'permissions_pk_list') - model = Role - - def create(self, validated_data): - self.groups_pk_list = validated_data.pop('groups_pk_list', '') - self.permissions_pk_list = validated_data.pop( - 'permissions_pk_list', '' - ) - - instance = super(WritableRoleSerializer, self).create(validated_data) - - if self.groups_pk_list: - self._add_groups(instance=instance) - - if self.permissions_pk_list: - self._add_permissions(instance=instance) - - return instance - - def _add_groups(self, instance): - instance.groups.add( - *Group.objects.filter(pk__in=self.groups_pk_list.split(',')) - ) - - def _add_permissions(self, instance): - for pk in self.permissions_pk_list.split(','): - try: - stored_permission = Permission.get(pk=pk) - instance.permissions.add(stored_permission) - instance.save() - except KeyError: - raise ValidationError(_('No such permission: %s') % pk) - - def update(self, instance, validated_data): - result = validated_data.copy() - - self.groups_pk_list = validated_data.pop('groups_pk_list', '') - self.permissions_pk_list = validated_data.pop( - 'permissions_pk_list', '' - ) - - result = super(WritableRoleSerializer, self).update( - instance, validated_data - ) - - if self.groups_pk_list: - instance.groups.clear() - self._add_groups(instance=instance) - - if self.permissions_pk_list: - instance.permissions.clear() - self._add_permissions(instance=instance) - - return result diff --git a/mayan/apps/permissions/tests/literals.py b/mayan/apps/permissions/tests/literals.py index 5c392a61ea..f81c7f4a3d 100644 --- a/mayan/apps/permissions/tests/literals.py +++ b/mayan/apps/permissions/tests/literals.py @@ -1,11 +1,11 @@ from __future__ import unicode_literals -TEST_CASE_ROLE_LABEL = 'test case role' +TEST_CASE_ROLE_LABEL = 'test case role label' TEST_INVALID_PERMISSION_NAMESPACE_NAME = 'invalid namespace' TEST_INVALID_PERMISSION_NAME = 'invalid name' -TEST_PERMISSION_NAMESPACE_LABEL = 'test namespace label' -TEST_PERMISSION_NAMESPACE_NAME = 'test namespace' -TEST_PERMISSION_LABEL = 'test name label' -TEST_PERMISSION_NAME = 'test name' -TEST_ROLE_LABEL = 'test role 2' +TEST_PERMISSION_NAMESPACE_LABEL = 'test permission namespace label' +TEST_PERMISSION_NAMESPACE_NAME = 'test_permission_namespace_name' +TEST_PERMISSION_LABEL = 'test permission name label' +TEST_PERMISSION_NAME = '{}.{}'.format(TEST_PERMISSION_NAMESPACE_NAME, 'test_permission_name') +TEST_ROLE_LABEL = 'test role label' TEST_ROLE_LABEL_EDITED = 'test role label edited' diff --git a/mayan/apps/permissions/tests/mixins.py b/mayan/apps/permissions/tests/mixins.py index 212fb554c4..8dd5442da4 100644 --- a/mayan/apps/permissions/tests/mixins.py +++ b/mayan/apps/permissions/tests/mixins.py @@ -1,8 +1,25 @@ from __future__ import unicode_literals +from ..classes import PermissionNamespace from ..models import Role -from .literals import TEST_CASE_ROLE_LABEL, TEST_ROLE_LABEL +from .literals import ( + TEST_CASE_ROLE_LABEL, TEST_PERMISSION_LABEL, TEST_PERMISSION_NAME, + TEST_PERMISSION_NAMESPACE_LABEL, TEST_PERMISSION_NAMESPACE_NAME, + TEST_ROLE_LABEL +) + + +class PermissionTestMixin(object): + def _create_test_permission(self): + self.test_permission_namespace = PermissionNamespace( + label=TEST_PERMISSION_NAMESPACE_LABEL, + name=TEST_PERMISSION_NAMESPACE_NAME + ) + self.test_permission = self.test_permission_namespace.add_permission( + label=TEST_PERMISSION_LABEL, + name=TEST_PERMISSION_NAME + ) class RoleTestCaseMixin(object): diff --git a/mayan/apps/permissions/tests/test_api.py b/mayan/apps/permissions/tests/test_api.py index 2ed28ceffb..54e7c0f292 100644 --- a/mayan/apps/permissions/tests/test_api.py +++ b/mayan/apps/permissions/tests/test_api.py @@ -1,13 +1,14 @@ from __future__ import unicode_literals -from django.contrib.auth.models import Group - from rest_framework import status from mayan.apps.rest_api.tests import BaseAPITestCase -from mayan.apps.user_management.tests.literals import TEST_GROUP_NAME +from mayan.apps.user_management.permissions import ( + permission_group_edit, permission_group_view +) +from mayan.apps.user_management.tests.mixins import GroupTestMixin -from ..classes import Permission +from ..classes import PermissionNamespace from ..models import Role from ..permissions import ( permission_role_create, permission_role_delete, permission_role_edit, @@ -15,239 +16,313 @@ from ..permissions import ( ) from .literals import TEST_ROLE_LABEL, TEST_ROLE_LABEL_EDITED -from .mixins import RoleTestMixin +from .mixins import PermissionTestMixin, RoleTestMixin -class PermissionAPITestCase(RoleTestMixin, BaseAPITestCase): - def test_permissions_list_view(self): - response = self.get(viewname='rest_api:permission-list') +class PermissionNamespaceAPITestCase(PermissionTestMixin, RoleTestMixin, BaseAPITestCase): + def _request_permission_namespace_list_api_view(self): + return self.get(viewname='rest_api:permission_namespace-list') + + def test_permission_namespace_list_api_view(self): + PermissionNamespace._registry = {} + self._create_test_permission() + + response = self._request_permission_namespace_list_api_view() self.assertEqual(response.status_code, status.HTTP_200_OK) - - # Role view - - def test_roles_list_view_no_access(self): - response = self.get(viewname='rest_api:role-list') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data['count'], 0) - - def test_roles_list_view_with_access(self): - self.grant_access( - permission=permission_role_view, obj=self.test_role + self.assertEqual( + self.test_permission_namespace.name, response.json()['results'][0]['name'] ) - response = self.get(viewname='rest_api:role-list') + + def _request_permission_namespace_permission_list_api_view(self): + return self.get( + kwargs={ + 'permission_namespace_name': self.test_permission_namespace.name + }, viewname='rest_api:permission_namespace-permission-list' + ) + + def test_permission_namespace_permission_list_api_view(self): + self._create_test_permission() + + response = self._request_permission_namespace_permission_list_api_view() self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data['count'], 1) - self.assertEqual(response.data['results'][0]['label'], self.test_role.label) + self.assertEqual( + response.json()['results'][0]['pk'], self.test_permission.pk + ) - # Role create - - def _role_create_request(self, extra_data=None): - data = { - 'label': TEST_ROLE_LABEL - } - - if extra_data: - data.update(extra_data) +class RoleAPITestCase(RoleTestMixin, BaseAPITestCase): + def _request_role_create_api_view(self): return self.post( - viewname='rest_api:role-list', data=data - ) - - def test_role_create_view_no_permission(self): - response = self._role_create_request() - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - self.assertEqual(Role.objects.count(), 1) - - def test_role_create_view_with_permission(self): - self.grant_permission(permission=permission_role_create) - response = self._role_create_request() - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - - role = Role.objects.get(label=TEST_ROLE_LABEL) - self.assertEqual(response.data, {'label': role.label, 'id': role.pk}) - self.assertEqual(Role.objects.count(), 2) - self.assertEqual(role.label, TEST_ROLE_LABEL) - - #def _create_group(self): - # self.test_group = Group.objects.create(name=TEST_GROUP_NAME) - - def _request_role_create_with_extra_data(self): - self._create_group() - - return self._role_create_request( - extra_data={ - 'groups_pk_list': '{}'.format(self.test_group.pk), - 'permissions_pk_list': '{}'.format(permission_role_view.pk) + viewname='rest_api:role-list', data={ + 'label': TEST_ROLE_LABEL } ) - def test_role_create_complex_view_no_permission(self): - response = self._request_role_create_with_extra_data() + def test_role_create_api_view_no_permission(self): + role_count = Role.objects.count() + response = self._request_role_create_api_view() self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - self.assertEqual(Role.objects.count(), 1) - self.assertEqual( - list(Role.objects.values_list('label', flat=True)), - [TEST_ROLE_LABEL] - ) - def test_role_create_complex_view_with_permission(self): + self.assertEqual(role_count, Role.objects.count()) + + def test_role_create_api_view_with_permission(self): + role_count = Role.objects.count() + self.grant_permission(permission=permission_role_create) - response = self._request_role_create_with_extra_data() - + response = self._request_role_create_api_view() self.assertEqual(response.status_code, status.HTTP_201_CREATED) - self.assertEqual(Role.objects.count(), 2) - role = Role.objects.get(label=TEST_ROLE_2_LABEL) - self.assertEqual(role.label, TEST_ROLE_2_LABEL) - self.assertQuerysetEqual( - role.groups.all(), (repr(self.test_group),) - ) - self.assertQuerysetEqual( - role.permissions.all(), - (repr(permission_role_view.stored_permission),) - ) - # Role edit + self.assertEqual(role_count + 1, Role.objects.count()) - def _request_role_edit(self, extra_data=None, request_type='patch'): - data = { - 'label': TEST_ROLE_LABEL_EDITED - } - - if extra_data: - data.update(extra_data) - - return getattr(self, request_type)( - viewname='rest_api:role-detail', kwargs={'role_id': self.test_role.pk}, - data=data - ) - - def test_role_edit_via_patch_no_access(self): - self._create_test_role() - response = self._request_role_edit(request_type='patch') - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - self.test_role.refresh_from_db() - self.assertEqual(self.test_role.label, TEST_ROLE_LABEL) - - def test_role_edit_via_patch_with_access(self): - self._create_test_role() - self.grant_access(permission=permission_role_edit, obj=self.test_role) - response = self._request_role_edit(request_type='patch') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.test_role.refresh_from_db() - self.assertEqual(self.test_role.label, TEST_ROLE_LABEL_EDITED) - - def _request_role_edit_via_patch_with_extra_data(self): - self._create_test_role() - self._create_group() - return self._request_role_edit( - extra_data={ - 'groups_pk_list': '{}'.format(self.test_group.pk), - 'permissions_pk_list': '{}'.format(permission_role_view.pk) - }, - request_type='patch' - ) - - def test_role_edit_complex_via_patch_no_access(self): - self._create_test_role() - - response = self._request_role_edit_via_patch_with_extra_data() - - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - self.test_role.refresh_from_db() - self.assertEqual(self.test_role.label, TEST_ROLE_LABEL) - - self.assertQuerysetEqual( - self.test_role.groups.all(), (repr(self.group),) - ) - self.assertQuerysetEqual(self.test_role.permissions.all(), ()) - - def test_role_edit_complex_via_patch_with_access(self): - self._create_test_role() - self.grant_access(permission=permission_role_edit, obj=self.test_role) - response = self._request_role_edit_via_patch_with_extra_data() - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.test_role.refresh_from_db() - self.assertEqual(self.test_role.label, TEST_ROLE_LABEL_EDITED) - self.assertQuerysetEqual( - self.test_role.groups.all(), (repr(self.test_group),) - ) - self.assertQuerysetEqual( - self.test_role.permissions.all(), - (repr(permission_role_view.stored_permission),) - ) - - def test_role_edit_via_put_no_access(self): - self._create_test_role() - response = self._request_role_edit(request_type='put') - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - self.test_role.refresh_from_db() - self.assertEqual(self.test_role.label, TEST_ROLE_LABEL) - - def test_role_edit_via_put_with_access(self): - self._create_test_role() - self.grant_access(permission=permission_role_edit, obj=self.test_role) - response = self._request_role_edit(request_type='put') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.test_role.refresh_from_db() - self.assertEqual(self.test_role.label, TEST_ROLE_LABEL_EDITED) - - def _request_role_edit_via_put_with_extra_data(self): - self._create_test_role() - self._create_group() - - return self._request_role_edit( - extra_data={ - 'groups_pk_list': '{}'.format(self.test_group.pk), - 'permissions_pk_list': '{}'.format(permission_role_view.pk) - }, request_type='put' - ) - - def test_role_edit_complex_via_put_no_access(self): - self._create_test_role() - response = self._request_role_edit_via_put_with_extra_data() - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - self.test_role.refresh_from_db() - self.assertEqual(self.test_role.label, TEST_ROLE_LABEL) - self.assertQuerysetEqual( - self.test_role.groups.all(), (repr(self.group),) - ) - self.assertQuerysetEqual( - self.test_role.permissions.all(), - () - ) - - def test_role_edit_complex_via_put_with_access(self): - self._create_test_role() - self.grant_access(permission=permission_role_edit, obj=self.test_role) - response = self._request_role_edit_via_put_with_extra_data() - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.test_role.refresh_from_db() - self.assertEqual(self.test_role.label, TEST_ROLE_LABEL_EDITED) - self.assertQuerysetEqual( - self.test_role.groups.all(), (repr(self.test_group),) - ) - self.assertQuerysetEqual( - self.test_role.permissions.all(), - (repr(permission_role_view.stored_permission),) - ) - - # Role delete - - def _request_role_delete_view(self): + def _request_role_delete_api_view(self): return self.delete( viewname='rest_api:role-detail', kwargs={'role_id': self.test_role.pk} ) - def test_role_delete_view_no_access(self): - response = self._request_role_delete_view() - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - self.assertEqual(Role.objects.count(), 1) + def test_role_delete_api_view_no_permission(self): + self._create_test_role() + role_count = Role.objects.count() - def test_role_delete_view_with_access(self): - self.grant_access(permission=permission_role_delete, obj=self.test_role) - response = self._request_role_delete_view() + response = self._request_role_delete_api_view() + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + self.assertEqual(role_count, Role.objects.count()) + + def test_role_delete_api_view_with_access(self): + self._create_test_role() + role_count = Role.objects.count() + + self.grant_access(obj=self.test_role, permission=permission_role_delete) + response = self._request_role_delete_api_view() self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) - self.assertEqual(Role.objects.count(), 0) + + self.assertEqual(role_count - 1, Role.objects.count()) + + def _request_role_edit(self, request_type='patch'): + return getattr(self, request_type)( + viewname='rest_api:role-detail', kwargs={'role_id': self.test_role.pk}, + data={ + 'label': TEST_ROLE_LABEL_EDITED + } + ) + + def test_role_edit_patch_api_view_no_permission(self): + self._create_test_role() + role_label = self.test_role.label + + response = self._request_role_edit(request_type='patch') + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + self.test_role.refresh_from_db() + self.assertEqual(self.test_role.label, role_label) + + def test_role_edit_patch_api_view_with_access(self): + self._create_test_role() + role_label = self.test_role.label + + self.grant_access(obj=self.test_role, permission=permission_role_edit) + response = self._request_role_edit(request_type='patch') + self.assertEqual(response.status_code, status.HTTP_200_OK) + + self.test_role.refresh_from_db() + self.assertNotEqual(self.test_role.label, role_label) + + def test_role_edit_put_api_view_no_permission(self): + self._create_test_role() + role_label = self.test_role.label + + response = self._request_role_edit(request_type='put') + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + self.test_role.refresh_from_db() + self.assertEqual(self.test_role.label, role_label) + + def test_role_edit_put_api_view_with_access(self): + self._create_test_role() + role_label = self.test_role.label + + self.grant_access(obj=self.test_role, permission=permission_role_edit) + response = self._request_role_edit(request_type='put') + self.assertEqual(response.status_code, status.HTTP_200_OK) + + self.test_role.refresh_from_db() + self.assertNotEqual(self.test_role.label, role_label) + + def _request_role_list_api_view(self): + return self.get(viewname='rest_api:role-list') + + def test_role_list_api_view_no_permission(self): + self._create_test_role() + + response = self._request_role_list_api_view() + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertFalse(self.test_role.label in response.content) + + def test_role_list_api_view_with_access(self): + self._create_test_role() + + self.grant_access(obj=self.test_role, permission=permission_role_view) + response = self._request_role_list_api_view() + self.assertEqual(response.status_code, status.HTTP_200_OK) + + self.assertTrue(self.test_role.label in response.content) + + +class RoleGroupAPITestCase(GroupTestMixin, RoleTestMixin, BaseAPITestCase): + def _request_role_group_list_api_view(self): + return self.get( + viewname='rest_api:role-group-list', + kwargs={'role_id': self.test_role.pk} + ) + + def _request_role_group_add_api_view(self): + return self.post( + viewname='rest_api:role-group-add', + kwargs={'role_id': self.test_role.pk}, + data={'group_id_list': '{}'.format(self.test_group.pk)} + ) + + def _request_role_group_remove_api_view(self): + return self.post( + viewname='rest_api:role-group-remove', + kwargs={'role_id': self.test_role.pk}, + data={'group_id_list': '{}'.format(self.test_group.pk)} + ) + + def _setup_role_group_list(self): + self._create_test_group() + self._create_test_role() + self.test_role.groups.add(self.test_group) + + def test_role_group_list_api_view_no_permission(self): + self._setup_role_group_list() + + response = self._request_role_group_list_api_view() + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + def test_role_group_list_api_view_with_role_access(self): + self._setup_role_group_list() + + self.grant_access(obj=self.test_role, permission=permission_role_view) + response = self._request_role_group_list_api_view() + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data['count'], 0) + + def test_role_group_list_api_view_with_group_access(self): + self._setup_role_group_list() + + self.grant_access( + obj=self.test_group, permission=permission_group_view + ) + response = self._request_role_group_list_api_view() + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + def test_role_group_list_api_view_with_full_access(self): + self._setup_role_group_list() + + self.grant_access(obj=self.test_role, permission=permission_role_view) + self.grant_access( + obj=self.test_group, permission=permission_group_view + ) + response = self._request_role_group_list_api_view() + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data['count'], 1) + + def _setup_role_group_add(self): + self._create_test_group() + self._create_test_role() + + def test_role_group_add_api_view_no_permission(self): + self._setup_role_group_add() + + response = self._request_role_group_add_api_view() + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + self.test_role.refresh_from_db() + self.assertTrue(self.test_group not in self.test_role.groups.all()) + + def test_role_group_add_api_view_with_role_access(self): + self._setup_role_group_add() + + self.grant_access(obj=self.test_role, permission=permission_role_edit) + response = self._request_role_group_add_api_view() + self.assertEqual(response.status_code, status.HTTP_200_OK) + + self.test_role.refresh_from_db() + self.assertTrue(self.test_group not in self.test_role.groups.all()) + + def test_role_group_add_api_view_with_group_access(self): + self._setup_role_group_add() + + self.grant_access( + obj=self.test_group, permission=permission_group_edit + ) + response = self._request_role_group_add_api_view() + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + self.test_role.refresh_from_db() + self.assertTrue(self.test_group not in self.test_role.groups.all()) + + def test_role_group_add_api_view_with_full_access(self): + self._setup_role_group_add() + + self.grant_access(obj=self.test_role, permission=permission_role_edit) + self.grant_access( + obj=self.test_group, permission=permission_group_edit + ) + response = self._request_role_group_add_api_view() + self.assertEqual(response.status_code, status.HTTP_200_OK) + + self.test_role.refresh_from_db() + self.assertTrue(self.test_group in self.test_role.groups.all()) + + def _setup_role_group_remove(self): + self._create_test_group() + self._create_test_role() + self.test_role.groups.add(self.test_group) + + def test_role_group_remove_api_view_no_permission(self): + self._setup_role_group_remove() + + response = self._request_role_group_remove_api_view() + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + self.test_role.refresh_from_db() + self.assertTrue(self.test_group in self.test_role.groups.all()) + + def test_role_group_remove_api_view_with_role_access(self): + self._setup_role_group_remove() + + self.grant_access(obj=self.test_role, permission=permission_role_edit) + response = self._request_role_group_remove_api_view() + self.assertEqual(response.status_code, status.HTTP_200_OK) + + self.test_role.refresh_from_db() + self.assertTrue(self.test_group in self.test_role.groups.all()) + + def test_role_group_remove_api_view_with_group_access(self): + self._setup_role_group_remove() + + self.grant_access( + obj=self.test_group, permission=permission_group_edit + ) + response = self._request_role_group_remove_api_view() + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + self.test_role.refresh_from_db() + self.assertTrue(self.test_group in self.test_role.groups.all()) + + def test_role_group_remove_api_view_with_full_access(self): + self._setup_role_group_remove() + + self.grant_access(obj=self.test_role, permission=permission_role_edit) + self.grant_access( + obj=self.test_group, permission=permission_group_edit + ) + response = self._request_role_group_remove_api_view() + self.assertEqual(response.status_code, status.HTTP_200_OK) + + self.test_role.refresh_from_db() + self.assertTrue(self.test_group not in self.test_role.groups.all()) diff --git a/mayan/apps/permissions/tests/test_views.py b/mayan/apps/permissions/tests/test_views.py index a4a6252999..a1565da404 100644 --- a/mayan/apps/permissions/tests/test_views.py +++ b/mayan/apps/permissions/tests/test_views.py @@ -2,7 +2,7 @@ from __future__ import unicode_literals from mayan.apps.common.tests import GenericViewTestCase from mayan.apps.user_management.permissions import permission_group_edit -from mayan.apps.user_management.tests import GroupTestMixin +from mayan.apps.user_management.tests.mixins import GroupTestMixin from ..models import Role from ..permissions import ( @@ -11,14 +11,10 @@ from ..permissions import ( ) from .literals import TEST_ROLE_LABEL, TEST_ROLE_LABEL_EDITED -from .mixins import RoleTestMixin +from .mixins import PermissionTestMixin, RoleTestMixin -class PermissionsViewsTestCase(GroupTestMixin, RoleTestMixin, GenericViewTestCase): - def setUp(self): - super(PermissionsViewsTestCase, self).setUp() - self.login_user() - +class RoleViewsTestCase(RoleTestMixin, GenericViewTestCase): def _request_create_role_view(self): return self.post( viewname='permissions:role_create', data={ @@ -27,21 +23,21 @@ class PermissionsViewsTestCase(GroupTestMixin, RoleTestMixin, GenericViewTestCas ) def test_role_creation_view_no_permission(self): + role_count = Role.objects.count() + response = self._request_create_role_view() self.assertEqual(response.status_code, 403) - self.assertEqual(Role.objects.count(), 1) - self.assertFalse( - TEST_ROLE_LABEL in Role.objects.values_list('label', flat=True) - ) + + self.assertTrue(role_count == Role.objects.count()) def test_role_creation_view_with_permission(self): + role_count = Role.objects.count() + self.grant_permission(permission=permission_role_create) response = self._request_create_role_view() self.assertEqual(response.status_code, 302) - self.assertEqual(Role.objects.count(), 2) - self.assertTrue( - TEST_ROLE_LABEL in Role.objects.values_list('label', flat=True) - ) + + self.assertTrue(role_count + 1 == Role.objects.count()) def _request_role_delete_view(self): return self.post( @@ -51,22 +47,22 @@ class PermissionsViewsTestCase(GroupTestMixin, RoleTestMixin, GenericViewTestCas def test_role_delete_view_no_permission(self): self._create_test_role() + role_count = Role.objects.count() + response = self._request_role_delete_view() self.assertEqual(response.status_code, 404) - self.assertEqual(Role.objects.count(), 2) - self.assertTrue( - TEST_ROLE_LABEL in Role.objects.values_list('label', flat=True) - ) + + self.assertTrue(role_count == Role.objects.count()) def test_role_delete_view_with_access(self): self._create_test_role() - self.grant_access(permission=permission_role_delete, obj=self.test_role) + role_count = Role.objects.count() + + self.grant_access(obj=self.test_role, permission=permission_role_delete) response = self._request_role_delete_view() self.assertEqual(response.status_code, 302) - self.assertEqual(Role.objects.count(), 1) - self.assertFalse( - TEST_ROLE_LABEL in Role.objects.values_list('label', flat=True) - ) + + self.assertTrue(role_count - 1 == Role.objects.count()) def _request_role_edit_view(self): return self.post( @@ -78,44 +74,48 @@ class PermissionsViewsTestCase(GroupTestMixin, RoleTestMixin, GenericViewTestCas def test_role_edit_view_no_permission(self): self._create_test_role() - response = self._request_role_edit_view() + role_label = self.test_role.label + response = self._request_role_edit_view() self.assertEqual(response.status_code, 404) self.test_role.refresh_from_db() - self.assertEqual(Role.objects.count(), 2) - self.assertEqual(self.test_role.label, TEST_ROLE_LABEL) + self.assertTrue(role_label == self.test_role.label) def test_role_edit_view_with_access(self): self._create_test_role() - self.grant_access(permission=permission_role_edit, obj=self.test_role) + role_label = self.test_role.label + + self.grant_access(obj=self.test_role, permission=permission_role_edit) response = self._request_role_edit_view() - self.assertEqual(response.status_code, 302) - self.test_role.refresh_from_db() - self.assertEqual(Role.objects.count(), 2) - self.assertEqual(self.test_role.label, TEST_ROLE_LABEL_EDITED) + self.test_role.refresh_from_db() + self.assertTrue(role_label != self.test_role.label) def _request_role_list_view(self): return self.get(viewname='permissions:role_list') def test_role_list_view_no_permission(self): self._create_test_role() + response = self._request_role_list_view() self.assertEqual(response.status_code, 200) self.assertNotContains( - response=response, text=TEST_ROLE_LABEL, status_code=200 + response=response, text=self.test_role.label, status_code=200 ) def test_role_list_view_with_access(self): self._create_test_role() + self.grant_access(permission=permission_role_view, obj=self.test_role) response = self._request_role_list_view() self.assertContains( - response=response, text=TEST_ROLE_LABEL, status_code=200 + response=response, text=self.test_role.label, status_code=200 ) + +class RolePermissionViewsTestCase(PermissionTestMixin, RoleTestMixin, GenericViewTestCase): def _request_role_permissions_view(self): return self.get( viewname='permissions:role_permissions', @@ -124,17 +124,93 @@ class PermissionsViewsTestCase(GroupTestMixin, RoleTestMixin, GenericViewTestCas def test_role_permissions_view_no_permission(self): self._create_test_role() + response = self._request_role_permissions_view() - self.assertEqual(response.status_code, 403) + self.assertEqual(response.status_code, 404) def test_role_permissions_view_with_access(self): self._create_test_role() + self.grant_access( - permission=permission_permission_view, obj=self.test_role + obj=self.test_role, permission=permission_role_edit ) response = self._request_role_permissions_view() self.assertEqual(response.status_code, 200) + def _request_role_permissions_add_view(self): + return self.post( + viewname='permissions:role_permissions', + kwargs={'role_id': self.test_role.pk}, + data={'available-selection': self.test_permission.stored_permission.pk} + ) + + def test_role_permission_add_view_no_permission(self): + self._create_test_role() + self._create_test_permission() + + response = self._request_role_permissions_add_view() + self.assertEqual(response.status_code, 404) + + self.test_role.refresh_from_db() + self.assertTrue( + self.test_permission.stored_permission not in self.test_role.permissions.all() + ) + + def test_role_permission_add_view_with_access(self): + self._create_test_role() + self._create_test_permission() + + self.grant_access( + obj=self.test_role, permission=permission_role_edit + ) + + response = self._request_role_permissions_add_view() + self.assertEqual(response.status_code, 302) + + self.test_role.refresh_from_db() + self.assertTrue( + self.test_permission.stored_permission in self.test_role.permissions.all() + ) + + def _request_role_permissions_remove_view(self): + return self.post( + viewname='permissions:role_permissions', + kwargs={'role_id': self.test_role.pk}, + data={'added-selection': self.test_permission.stored_permission.pk} + ) + + def test_role_permission_remove_view_no_permission(self): + self._create_test_role() + self._create_test_permission() + self.test_role.grant(permission=self.test_permission) + + response = self._request_role_permissions_remove_view() + self.assertEqual(response.status_code, 404) + + self.test_role.refresh_from_db() + self.assertTrue( + self.test_permission.stored_permission in self.test_role.permissions.all() + ) + + def test_role_permission_remove_view_with_access(self): + self._create_test_role() + self._create_test_permission() + self.test_role.grant(permission=self.test_permission) + + self.grant_access( + obj=self.test_role, permission=permission_role_edit + ) + + response = self._request_role_permissions_remove_view() + self.assertEqual(response.status_code, 302) + + self.test_role.refresh_from_db() + self.assertTrue( + self.test_permission.stored_permission not in self.test_role.permissions.all() + ) + + +class RoleGroupViewsTestCase(GroupTestMixin, RoleTestMixin, GenericViewTestCase): def _request_role_groups_view(self): return self.get( viewname='permissions:role_groups', @@ -143,15 +219,196 @@ class PermissionsViewsTestCase(GroupTestMixin, RoleTestMixin, GenericViewTestCas def test_role_groups_view_no_permission(self): self._create_test_role() - response = self._request_role_groups_view() - self.assertEqual(response.status_code, 403) + self._create_test_group() - def test_role_groups_view_with_access(self): + response = self._request_role_groups_view() + self.assertNotContains( + response=response, text=self.test_role.label, status_code=404 + ) + self.assertNotContains( + response=response, text=self.test_group.name, status_code=404 + ) + + def test_role_groups_view_with_role_access(self): self._create_test_role() - self.grant_access(permission=permission_role_edit, obj=self.test_role) + self._create_test_group() + + self.grant_access(obj=self.test_role, permission=permission_role_edit) response = self._request_role_groups_view() self.assertEqual(response.status_code, 200) + self.assertContains( + response=response, text=self.test_role.label, status_code=200 + ) + self.assertNotContains( + response=response, text=self.test_group.name, status_code=200 + ) + def _request_role_groups_add_view(self): + return self.post( + viewname='permissions:role_groups', + kwargs={'role_id': self.test_role.pk}, + data={'available-selection': self.test_group.pk} + ) + + def test_role_group_add_view_no_permission(self): + self._create_test_role() + self._create_test_group() + + response = self._request_role_groups_add_view() + self.assertEqual(response.status_code, 404) + + self.test_role.refresh_from_db() + self.assertTrue( + self.test_group not in self.test_role.groups.all() + ) + + def test_role_group_add_view_with_role_access(self): + self._create_test_role() + self._create_test_group() + + self.grant_access( + obj=self.test_role, permission=permission_role_edit + ) + + response = self._request_role_groups_add_view() + self.assertContains( + response=response, text=self.test_role, status_code=200 + ) + self.assertNotContains( + response=response, text=self.test_group, status_code=200 + ) + + self.test_role.refresh_from_db() + self.assertTrue( + self.test_group not in self.test_role.groups.all() + ) + + def test_role_group_add_view_with_group_access(self): + self._create_test_role() + self._create_test_group() + + self.grant_access( + obj=self.test_group, permission=permission_group_edit + ) + + response = self._request_role_groups_add_view() + self.assertNotContains( + response=response, text=self.test_role, status_code=404 + ) + self.assertNotContains( + response=response, text=self.test_group, status_code=404 + ) + + self.test_role.refresh_from_db() + self.assertTrue( + self.test_group not in self.test_role.groups.all() + ) + + def test_role_group_add_view_with_full_access(self): + self._create_test_role() + self._create_test_group() + + self.grant_access( + obj=self.test_group, permission=permission_group_edit + ) + self.grant_access( + obj=self.test_role, permission=permission_role_edit + ) + + response = self._request_role_groups_add_view() + self.assertEqual(response.status_code, 302) + + self.test_role.refresh_from_db() + self.assertTrue( + self.test_group in self.test_role.groups.all() + ) + + def _request_role_groups_remove_view(self): + return self.post( + viewname='permissions:role_groups', + kwargs={'role_id': self.test_role.pk}, + data={'added-selection': self.test_group.pk} + ) + + def test_role_group_remove_view_no_permission(self): + self._create_test_role() + self._create_test_group() + self.test_role.groups.add(self.test_group) + + response = self._request_role_groups_remove_view() + self.assertEqual(response.status_code, 404) + + self.test_role.refresh_from_db() + self.assertTrue( + self.test_group in self.test_role.groups.all() + ) + + def test_role_group_remove_view_with_role_access(self): + self._create_test_role() + self._create_test_group() + self.test_role.groups.add(self.test_group) + + self.grant_access( + obj=self.test_role, permission=permission_role_edit + ) + + response = self._request_role_groups_remove_view() + self.assertContains( + response=response, text=self.test_role, status_code=200 + ) + self.assertNotContains( + response=response, text=self.test_group, status_code=200 + ) + + self.test_role.refresh_from_db() + self.assertTrue( + self.test_group in self.test_role.groups.all() + ) + + def test_role_group_remove_view_with_group_access(self): + self._create_test_role() + self._create_test_group() + self.test_role.groups.add(self.test_group) + + self.grant_access( + obj=self.test_group, permission=permission_group_edit + ) + + response = self._request_role_groups_remove_view() + self.assertNotContains( + response=response, text=self.test_role, status_code=404 + ) + self.assertNotContains( + response=response, text=self.test_group, status_code=404 + ) + + self.test_role.refresh_from_db() + self.assertTrue( + self.test_group in self.test_role.groups.all() + ) + + def test_role_group_remove_view_with_full_access(self): + self._create_test_role() + self._create_test_group() + self.test_role.groups.add(self.test_group) + + self.grant_access( + obj=self.test_group, permission=permission_group_edit + ) + self.grant_access( + obj=self.test_role, permission=permission_role_edit + ) + + response = self._request_role_groups_remove_view() + self.assertEqual(response.status_code, 302) + + self.test_role.refresh_from_db() + self.assertTrue( + self.test_group not in self.test_role.groups.all() + ) + + +class GroupRoleViewsTestCase(GroupTestMixin, RoleTestMixin, GenericViewTestCase): def _request_group_roles_view(self): return self.get( viewname='permissions:group_roles', @@ -160,11 +417,177 @@ class PermissionsViewsTestCase(GroupTestMixin, RoleTestMixin, GenericViewTestCas def test_group_roles_view_no_permission(self): self._create_test_group() + response = self._request_group_roles_view() - self.assertEqual(response.status_code, 403) + self.assertEqual(response.status_code, 404) def test_group_roles_view_with_access(self): self._create_test_group() - self.grant_access(permission=permission_group_edit, obj=self.test_group) + + self.grant_access(obj=self.test_group, permission=permission_group_edit) response = self._request_group_roles_view() self.assertEqual(response.status_code, 200) + + def _request_group_roles_add_view(self): + return self.post( + viewname='permissions:group_roles', + kwargs={'group_id': self.test_group.pk}, + data={'available-selection': self.test_role.pk} + ) + + def test_group_role_add_view_no_permission(self): + self._create_test_group() + self._create_test_role() + + response = self._request_group_roles_add_view() + self.assertEqual(response.status_code, 404) + + self.test_group.refresh_from_db() + self.assertTrue( + self.test_role not in self.test_group.roles.all() + ) + + def test_group_role_add_view_with_group_access(self): + self._create_test_group() + self._create_test_role() + + self.grant_access( + obj=self.test_group, permission=permission_group_edit + ) + + response = self._request_group_roles_add_view() + self.assertContains( + response=response, text=self.test_group, status_code=200 + ) + self.assertNotContains( + response=response, text=self.test_role, status_code=200 + ) + + self.test_group.refresh_from_db() + self.assertTrue( + self.test_role not in self.test_group.roles.all() + ) + + def test_group_role_add_view_with_role_access(self): + self._create_test_group() + self._create_test_role() + + self.grant_access( + obj=self.test_role, permission=permission_role_edit + ) + + response = self._request_group_roles_add_view() + self.assertNotContains( + response=response, text=self.test_group, status_code=404 + ) + self.assertNotContains( + response=response, text=self.test_role, status_code=404 + ) + + self.test_group.refresh_from_db() + self.assertTrue( + self.test_role not in self.test_group.roles.all() + ) + + def test_group_role_add_view_with_full_access(self): + self._create_test_group() + self._create_test_role() + + self.grant_access( + obj=self.test_role, permission=permission_role_edit + ) + self.grant_access( + obj=self.test_group, permission=permission_group_edit + ) + + response = self._request_group_roles_add_view() + self.assertEqual(response.status_code, 302) + + self.test_group.refresh_from_db() + self.assertTrue( + self.test_role in self.test_group.roles.all() + ) + + def _request_group_roles_remove_view(self): + return self.post( + viewname='permissions:group_roles', + kwargs={'group_id': self.test_group.pk}, + data={'added-selection': self.test_role.pk} + ) + + def test_group_role_remove_view_no_permission(self): + self._create_test_group() + self._create_test_role() + self.test_group.roles.add(self.test_role) + + response = self._request_group_roles_remove_view() + self.assertEqual(response.status_code, 404) + + self.test_group.refresh_from_db() + self.assertTrue( + self.test_role in self.test_group.roles.all() + ) + + def test_group_role_remove_view_with_group_access(self): + self._create_test_group() + self._create_test_role() + self.test_group.roles.add(self.test_role) + + self.grant_access( + obj=self.test_group, permission=permission_group_edit + ) + + response = self._request_group_roles_remove_view() + self.assertContains( + response=response, text=self.test_group, status_code=200 + ) + self.assertNotContains( + response=response, text=self.test_role, status_code=200 + ) + + self.test_group.refresh_from_db() + self.assertTrue( + self.test_role in self.test_group.roles.all() + ) + + def test_group_role_remove_view_with_role_access(self): + self._create_test_group() + self._create_test_role() + self.test_group.roles.add(self.test_role) + + self.grant_access( + obj=self.test_role, permission=permission_role_edit + ) + + response = self._request_group_roles_remove_view() + self.assertNotContains( + response=response, text=self.test_group, status_code=404 + ) + self.assertNotContains( + response=response, text=self.test_role, status_code=404 + ) + + self.test_group.refresh_from_db() + self.assertTrue( + self.test_role in self.test_group.roles.all() + ) + + def test_group_role_remove_view_with_full_access(self): + self._create_test_group() + self._create_test_role() + self.test_group.roles.add(self.test_role) + + self.grant_access( + obj=self.test_role, permission=permission_role_edit + ) + self.grant_access( + obj=self.test_group, permission=permission_group_edit + ) + + response = self._request_group_roles_remove_view() + self.assertEqual(response.status_code, 302) + + self.test_group.refresh_from_db() + self.assertTrue( + self.test_role not in self.test_group.roles.all() + ) diff --git a/mayan/apps/permissions/urls.py b/mayan/apps/permissions/urls.py index 89e1d4b7f6..4e1dd44d86 100644 --- a/mayan/apps/permissions/urls.py +++ b/mayan/apps/permissions/urls.py @@ -2,7 +2,9 @@ from __future__ import unicode_literals from django.conf.urls import url -from .api_views import APIPermissionList, APIRoleListView, APIRoleView +from .api_views import ( + PermissionNamespaceViewSet, PermissionViewSet, RoleAPIViewSet +) from .views import ( GroupRolesView, RoleCreateView, RoleDeleteView, RoleEditView, RoleGroupsView, RoleListView, RolePermissionsView @@ -36,14 +38,14 @@ urlpatterns = [ url(regex=r'^roles/list/$', name='role_list', view=RoleListView.as_view()), ] -api_urls = [ - url( - regex=r'^permissions/$', name='permission-list', - view=APIPermissionList.as_view(), - ), - url(regex=r'^roles/$', name='role-list', view=APIRoleListView.as_view()), - url( - regex=r'^roles/(?P[0-9]+)/$', name='role-detail', - view=APIRoleView.as_view() - ), -] +api_router_entries = ( + { + 'prefix': r'permission_namespaces', 'viewset': PermissionNamespaceViewSet, + 'basename': 'permission_namespace' + }, + { + 'prefix': r'permission_namespaces/(?P[^/.]+)/permissions', + 'viewset': PermissionViewSet, 'basename': 'permission' + }, + {'prefix': r'roles', 'viewset': RoleAPIViewSet, 'basename': 'role'}, +) diff --git a/mayan/apps/permissions/views.py b/mayan/apps/permissions/views.py index 06c63c04dc..69e302a77f 100644 --- a/mayan/apps/permissions/views.py +++ b/mayan/apps/permissions/views.py @@ -1,22 +1,17 @@ from __future__ import unicode_literals -import itertools - from django.contrib.auth.models import Group -from django.shortcuts import get_object_or_404 from django.template import RequestContext from django.urls import reverse_lazy from django.utils.encoding import force_text from django.utils.translation import ugettext_lazy as _ -from mayan.apps.acls.models import AccessControlList from mayan.apps.common.generics import ( - AssignRemoveView, SingleObjectCreateView, SingleObjectDeleteView, + AddRemoveView, SingleObjectCreateView, SingleObjectDeleteView, SingleObjectEditView, SingleObjectListView ) from mayan.apps.user_management.permissions import permission_group_edit -from .classes import Permission, PermissionNamespace from .icons import icon_role_list from .links import link_role_create from .models import Role, StoredPermission @@ -26,45 +21,33 @@ from .permissions import ( ) -class GroupRolesView(AssignRemoveView): - grouped = False - left_list_title = _('Available roles') - right_list_title = _('Group roles') - object_permission = permission_group_edit +class GroupRolesView(AddRemoveView): + action_add_method = 'roles_add' + action_remove_method = 'roles_remove' + main_object_model = Group + main_object_permission = permission_group_edit + main_object_pk_url_kwarg = 'group_id' + secondary_object_model = Role + secondary_object_permission = permission_role_edit + list_available_title = _('Available roles') + list_added_title = _('Group roles') + related_field = 'roles' - def add(self, item): - role = get_object_or_404(klass=Role, pk=item) - self.get_object().roles.add(role) + def get_actions_extra_kwargs(self): + return {'_user': self.request.user} def get_extra_context(self): return { - 'object': self.get_object(), - 'title': _('Roles of group: %s') % self.get_object() + 'object': self.main_object, + 'title': _('Roles of group: %s') % self.main_object, } - def get_object(self): - return get_object_or_404(klass=Group, pk=self.kwargs['group_id']) - - def left_list(self): - return [ - (force_text(role.pk), role.label) for role in set(Role.objects.all()) - set(self.get_object().roles.all()) - ] - - def right_list(self): - return [ - (force_text(role.pk), role.label) for role in self.get_object().roles.all() - ] - - def remove(self, item): - role = get_object_or_404(klass=Role, pk=item) - self.get_object().roles.remove(role) - class RoleCreateView(SingleObjectCreateView): fields = ('label',) model = Role - view_permission = permission_role_create post_action_redirect = reverse_lazy(viewname='permissions:role_list') + view_permission = permission_role_create class RoleDeleteView(SingleObjectDeleteView): @@ -81,43 +64,31 @@ class RoleEditView(SingleObjectEditView): pk_url_kwarg = 'role_id' -class RoleGroupsView(AssignRemoveView): - grouped = False - left_list_title = _('Available groups') - right_list_title = _('Role groups') - object_permission = permission_role_edit +class RoleGroupsView(AddRemoveView): + action_add_method = 'groups_add' + action_remove_method = 'groups_remove' + main_object_model = Role + main_object_permission = permission_role_edit + main_object_pk_url_kwarg = 'role_id' + secondary_object_model = Group + secondary_object_permission = permission_group_edit + list_available_title = _('Available groups') + list_added_title = _('Role groups') + related_field = 'groups' - def add(self, item): - group = get_object_or_404(klass=Group, pk=item) - self.get_object().groups.add(group) + def get_actions_extra_kwargs(self): + return {'_user': self.request.user} def get_extra_context(self): return { - 'object': self.get_object(), - 'title': _('Groups of role: %s') % self.get_object(), + 'object': self.main_object, + 'title': _('Groups of role: %s') % self.main_object, 'subtitle': _( 'Add groups to be part of a role. They will ' 'inherit the role\'s permissions and access controls.' ), } - def get_object(self): - return get_object_or_404(klass=Role, pk=self.kwargs['role_id']) - - def left_list(self): - return [ - (force_text(group.pk), group.name) for group in set(Group.objects.all()) - set(self.get_object().groups.all()) - ] - - def remove(self, item): - group = get_object_or_404(klass=Group, pk=item) - self.get_object().groups.remove(group) - - def right_list(self): - return [ - (force_text(group.pk), group.name) for group in self.get_object().groups.all() - ] - class RoleListView(SingleObjectListView): model = Role @@ -143,64 +114,48 @@ class RoleListView(SingleObjectListView): } -class RolePermissionsView(AssignRemoveView): +class RolePermissionsView(AddRemoveView): + action_add_method = 'permissions_add' + action_remove_method = 'permissions_remove' grouped = True - left_list_title = _('Available permissions') - object_permission = permission_role_edit - right_list_title = _('Granted permissions') + main_object_model = Role + main_object_permission = permission_role_edit + main_object_pk_url_kwarg = 'role_id' + list_available_title = _('Available permissions') + list_added_title = _('Granted permissions') + related_field = 'permissions' + secondary_object_model = StoredPermission - @staticmethod - def generate_choices(entries): - results = [] + def generate_choices(self, queryset): + namespaces_dictionary = {} - entries = sorted( - entries, key=lambda x: ( - x.volatile_permission.namespace.label, - x.volatile_permission.label - ) + # Sort permissions by their translatable label + object_list = sorted( + queryset, key=lambda permission: permission.volatile_permission.label ) - for namespace, permissions in itertools.groupby(entries, lambda entry: entry.namespace): - permission_options = [ - (force_text(permission.pk), permission) for permission in permissions - ] - results.append( - (PermissionNamespace.get(name=namespace), permission_options) + # Group permissions by namespace + for permission in object_list: + namespaces_dictionary.setdefault( + permission.volatile_permission.namespace.label, + [] + ) + namespaces_dictionary[permission.volatile_permission.namespace.label].append( + (permission.pk, force_text(permission)) ) - return results + # Sort permissions by their translatable namespace label + return sorted(namespaces_dictionary.items()) - def add(self, item): - permission = get_object_or_404(klass=StoredPermission, pk=item) - self.get_object().permissions.add(permission) + def get_actions_extra_kwargs(self): + return {'_user': self.request.user} def get_extra_context(self): return { - 'object': self.get_object(), + 'object': self.main_object, 'subtitle': _( 'Permissions granted here will apply to the entire system ' 'and all objects.' ), - 'title': _('Permissions for role: %s') % self.get_object(), + 'title': _('Permissions for role: %s') % self.main_object, } - - def get_object(self): - return get_object_or_404(klass=Role, pk=self.kwargs['role_id']) - - def left_list(self): - Permission.refresh() - - return RolePermissionsView.generate_choices( - entries=StoredPermission.objects.exclude( - id__in=self.get_object().permissions.values_list('pk', flat=True) - ) - ) - - def remove(self, item): - permission = get_object_or_404(klass=StoredPermission, pk=item) - self.get_object().permissions.remove(permission) - - def right_list(self): - return RolePermissionsView.generate_choices( - entries=self.get_object().permissions.all() - ) From d28bb60abd60ef089b6fad52a03d3a5830050d19 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Tue, 12 Feb 2019 03:40:12 -0400 Subject: [PATCH 127/209] Fix tag attach wizard step Signed-off-by: Roberto Rosario --- mayan/apps/tags/wizard_steps.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/mayan/apps/tags/wizard_steps.py b/mayan/apps/tags/wizard_steps.py index 5abac4bb90..ab392e5fea 100644 --- a/mayan/apps/tags/wizard_steps.py +++ b/mayan/apps/tags/wizard_steps.py @@ -9,6 +9,8 @@ from django.utils.translation import ugettext_lazy as _ from mayan.apps.sources.wizards import WizardStep from .forms import TagMultipleSelectionForm +from .models import Tag +from .permissions import permission_tag_attach class WizardStepTags(WizardStep): @@ -37,6 +39,8 @@ class WizardStepTags(WizardStep): def get_form_kwargs(self, wizard): return { 'help_text': _('Tags to be attached.'), + 'model': Tag, + 'permission': permission_tag_attach, 'user': wizard.request.user } From fb608bba98ab94ded77180d9612309054deffe66 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Tue, 12 Feb 2019 03:40:55 -0400 Subject: [PATCH 128/209] Fix issue in ExternalObjectListSerializerMixin Fix error when only an ID list field is specified. Signed-off-by: Roberto Rosario --- mayan/apps/rest_api/mixins.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/mayan/apps/rest_api/mixins.py b/mayan/apps/rest_api/mixins.py index 2324cb570b..714b25a9dd 100644 --- a/mayan/apps/rest_api/mixins.py +++ b/mayan/apps/rest_api/mixins.py @@ -42,12 +42,15 @@ class ExternalObjectListSerializerMixin(object): 'external_object_list_pk_list_field.' ) - if pk_field: pk_field_value = self.validated_data.get(pk_field) + else: + pk_field_value = None if pk_list_field: pk_list_field_value = self.validated_data.get(pk_list_field) + else: + pk_list_field = None if pk_field_value: id_list = (pk_field_value,) From 23b1375289194ebaf08a10729193fbb811167247 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Tue, 12 Feb 2019 03:42:37 -0400 Subject: [PATCH 129/209] Enclose add/remove tag methods in transactions Signed-off-by: Roberto Rosario --- mayan/apps/tags/models.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/mayan/apps/tags/models.py b/mayan/apps/tags/models.py index 142f2208af..6a363bd5aa 100644 --- a/mayan/apps/tags/models.py +++ b/mayan/apps/tags/models.py @@ -1,6 +1,6 @@ from __future__ import absolute_import, unicode_literals -from django.db import models +from django.db import models, transaction from django.urls import reverse from django.utils.encoding import python_2_unicode_compatible from django.utils.translation import ugettext_lazy as _ @@ -48,10 +48,12 @@ class Tag(models.Model): """ Attach a tag to a document and commit the corresponding event. """ - self.documents.add(document) - event_tag_attach.commit( - action_object=self, actor=user, target=document - ) + with transaction.atomic(): + for document in documents: + self.documents.add(document) + event_tag_attach.commit( + action_object=self, actor=user, target=document + ) def get_absolute_url(self): return reverse( @@ -72,14 +74,16 @@ class Tag(models.Model): def get_document_count(self, user): return self.get_documents(user=user).count() - def remove_from(self, document, user=None): + def remove_from(self, documents, _user=None): """ Remove a tag from a document and commit the corresponding event. """ - self.documents.remove(document) - event_tag_remove.commit( - action_object=self, actor=user, target=document - ) + with transaction.atomic(): + for document in documents: + self.documents.remove(document) + event_tag_remove.commit( + action_object=self, actor=user, target=document + ) def save(self, *args, **kwargs): user = kwargs.pop('_user', None) From 85890041732dad5ba633af2908e574a70ee2f824 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Tue, 12 Feb 2019 18:01:14 -0400 Subject: [PATCH 130/209] Add support for single or multiple objects modes View that use the MultipleObjectMixin can now fully operate as single object or multiple object views. Add the self.view_mode_single and self.view_mode_multiple flags. Add support for single, singular and plural titles and success messages via: success_message_single, success_message_singular, sucess_message_plural, title_single, title_singular and title_plural class attributes. Insert object_list and object as attributes of the view class to avoid calling the queryset again. Signed-off-by: Roberto Rosario --- mayan/apps/common/generics.py | 56 +++++++++++++++---------------- mayan/apps/common/mixins.py | 62 ++++++++++++++++++++++++++++------- 2 files changed, 79 insertions(+), 39 deletions(-) diff --git a/mayan/apps/common/generics.py b/mayan/apps/common/generics.py index 014dbe7bda..0506517621 100644 --- a/mayan/apps/common/generics.py +++ b/mayan/apps/common/generics.py @@ -251,32 +251,6 @@ class MultipleObjectDownloadView(RestrictedQuerysetMixin, MultipleObjectMixin, D return super(MultipleObjectDownloadView, self).get_queryset() -class SingleObjectDownloadView(RestrictedQuerysetMixin, SingleObjectMixin, DownloadViewBase): - """ - View that provides a .get_object() method to download content from a - single object. - """ - def __init__(self, *args, **kwargs): - result = super(SingleObjectDownloadView, self).__init__(*args, **kwargs) - - if self.__class__.mro()[0].get_queryset != SingleObjectDownloadView.get_queryset: - raise ImproperlyConfigured( - '%(cls)s is overloading the get_queryset method. Subclasses ' - 'should implement the get_source_queryset method instead. ' % { - 'cls': self.__class__.__name__ - } - ) - - return result - - def get_queryset(self): - try: - return super(SingleObjectDownloadView, self).get_queryset() - except ImproperlyConfigured: - self.queryset = self.get_source_queryset() - return super(SingleObjectDownloadView, self).get_queryset() - - class MultiFormView(DjangoFormView): prefix = None prefixes = {} @@ -607,7 +581,7 @@ class AddRemoveView(ExternalObjectMixin, ExtraContextMixin, ViewPermissionCheckM ) -class MultipleObjectFormActionView(ObjectActionMixin, ViewPermissionCheckMixin, RestrictedQuerysetMixin, MultipleObjectMixin, FormExtraKwargsMixin, ExtraContextMixin, RedirectionMixin, DjangoFormView): +class MultipleObjectFormActionView(ExtraContextMixin, ObjectActionMixin, ViewPermissionCheckMixin, RestrictedQuerysetMixin, MultipleObjectMixin, FormExtraKwargsMixin, RedirectionMixin, DjangoFormView): """ This view will present a form and upon receiving a POST request will perform an action on an object or queryset @@ -639,7 +613,7 @@ class MultipleObjectFormActionView(ObjectActionMixin, ViewPermissionCheckMixin, return super(MultipleObjectFormActionView, self).get_queryset() -class MultipleObjectConfirmActionView(ObjectActionMixin, ViewPermissionCheckMixin, RestrictedQuerysetMixin, MultipleObjectMixin, ExtraContextMixin, RedirectionMixin, TemplateView): +class MultipleObjectConfirmActionView(ExtraContextMixin, ObjectActionMixin, ViewPermissionCheckMixin, RestrictedQuerysetMixin, MultipleObjectMixin, RedirectionMixin, TemplateView): template_name = 'appearance/generic_confirm.html' def __init__(self, *args, **kwargs): @@ -738,6 +712,32 @@ class SingleObjectCreateView(ObjectNameMixin, ViewPermissionCheckMixin, ExtraCon return self.error_message_duplicate +class SingleObjectDownloadView(RestrictedQuerysetMixin, SingleObjectMixin, DownloadViewBase): + """ + View that provides a .get_object() method to download content from a + single object. + """ + def __init__(self, *args, **kwargs): + result = super(SingleObjectDownloadView, self).__init__(*args, **kwargs) + + if self.__class__.mro()[0].get_queryset != SingleObjectDownloadView.get_queryset: + raise ImproperlyConfigured( + '%(cls)s is overloading the get_queryset method. Subclasses ' + 'should implement the get_source_queryset method instead. ' % { + 'cls': self.__class__.__name__ + } + ) + + return result + + def get_queryset(self): + try: + return super(SingleObjectDownloadView, self).get_queryset() + except ImproperlyConfigured: + self.queryset = self.get_source_queryset() + return super(SingleObjectDownloadView, self).get_queryset() + + class SingleObjectDynamicFormCreateView(DynamicFormViewMixin, SingleObjectCreateView): pass diff --git a/mayan/apps/common/mixins.py b/mayan/apps/common/mixins.py index 4e761bcd8e..6e278a68f4 100644 --- a/mayan/apps/common/mixins.py +++ b/mayan/apps/common/mixins.py @@ -169,7 +169,7 @@ class MultipleInstanceActionMixin(object): # MultipleObjectFormActionView or MultipleObjectConfirmActionView model = None - success_message = _('Operation performed on %(count)d object') + success_message_singular = _('Operation performed on %(count)d object') success_message_plural = _('Operation performed on %(count)d objects') def get_pk_list(self): @@ -217,6 +217,13 @@ class MultipleObjectMixin(SingleObjectMixin): pk_list_key = 'id_list' pk_list_separator = PK_LIST_SEPARATOR + def dispatch(self, request, *args, **kwargs): + self.object_list = self.get_object_list() + if self.view_mode_single: + self.object = self.object_list.first() + + return super(MultipleObjectMixin, self).dispatch(request=request, *args, **kwargs) + def get(self, request, *args, **kwargs): """ Override BaseDetailView.get() @@ -243,6 +250,9 @@ class MultipleObjectMixin(SingleObjectMixin): `pk_list' argument in the URLconf, but subclasses can override this to return any object. """ + self.view_mode_single = False + self.view_mode_multiple = False + # Use a custom queryset if provided; this is required for subclasses # like DateDetailView if queryset is None: @@ -255,14 +265,17 @@ class MultipleObjectMixin(SingleObjectMixin): if pk is not None: queryset = queryset.filter(pk=pk) + self.view_mode_single = True # 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}) + self.view_mode_single = True if pk_list is not None: queryset = queryset.filter(pk__in=self.get_pk_list()) + self.view_mode_multiple = True # If none of those are defined, it's an error. if pk is None and slug is None and pk_list is None: @@ -305,22 +318,49 @@ class ObjectActionMixin(object): """ Mixin that performs an user action to a queryset """ - error_message = 'Unable to perform operation on object %(instance)s.' + error_message = 'Unable to perform operation on object %(instance)s; %(exception)s.' post_object_action_url = None - success_message = 'Operation performed on %(count)d object.' + success_message_single = 'Operation performed on %(object)s.' + success_message_singular = 'Operation performed on %(count)d object.' success_message_plural = 'Operation performed on %(count)d objects.' + title_single = 'Perform operation on %(object)s.' + title_singular = 'Perform operation on %(count)d object.' + title_plural = 'Perform operation on %(count)d objects.' + + def get_context_data(self, **kwargs): + context = super(ObjectActionMixin, self).get_context_data(**kwargs) + title = None + + if self.view_mode_single: + title = self.title_single % {'object': self.object} + elif self.view_mode_multiple: + title = ungettext( + singular=self.title_singular, + plural=self.title_plural, + number=self.object_list.count() + ) % { + 'count': self.object_list.count(), + } + + context['title'] = title + + return context def get_post_object_action_url(self): return self.post_object_action_url def get_success_message(self, count): - return ungettext( - singular=self.success_message, - plural=self.success_message_plural, - number=count - ) % { - 'count': count, - } + if self.view_mode_single: + return self.success_message_single % {'object': self.object} + + if self.view_mode_multiple: + return ungettext( + singular=self.success_message_singular, + plural=self.success_message_plural, + number=count + ) % { + 'count': count, + } def object_action(self, instance, form=None): # User supplied method @@ -330,7 +370,7 @@ class ObjectActionMixin(object): self.action_count = 0 self.action_id_list = [] - for instance in self.get_object_list(): + for instance in self.object_list: try: self.object_action(form=form, instance=instance) except Exception as exception: From 6a57a5a7de8369a64bd875b2a6910b7aa8008c18 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Thu, 14 Feb 2019 02:27:12 -0400 Subject: [PATCH 131/209] Improve filtering in AddRemove View Make sure to always used the base filtered source queryset. Remove the grouped attribute which is subclass specific. Signed-off-by: Roberto Rosario --- mayan/apps/common/generics.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/mayan/apps/common/generics.py b/mayan/apps/common/generics.py index 0506517621..a90321fa78 100644 --- a/mayan/apps/common/generics.py +++ b/mayan/apps/common/generics.py @@ -346,7 +346,6 @@ class MultiFormView(DjangoFormView): class AddRemoveView(ExternalObjectMixin, ExtraContextMixin, ViewPermissionCheckMixin, RestrictedQuerysetMixin, MultiFormView): form_classes = {'form_available': ChoiceForm, 'form_added': ChoiceForm} - grouped = False list_added_help_text = _( 'Select entries to be removed. Hold Control to select multiple ' 'entries. Once the selection is complete, click the button below ' @@ -429,18 +428,18 @@ class AddRemoveView(ExternalObjectMixin, ExtraContextMixin, ViewPermissionCheckM def forms_valid(self, forms): if 'available-add_all' in self.request.POST: - selection_add = self.get_secondary_object_list() + selection_add = self.get_list_available_queryset() else: - selection_add = self.get_secondary_object_list().filter( + selection_add = self.get_list_available_queryset().filter( pk__in=forms['form_available'].cleaned_data['selection'] ) self.action_add(queryset=selection_add) if 'added-remove_all' in self.request.POST: - selection_remove = self.get_secondary_object_list() + selection_remove = self.get_list_added_queryset() else: - selection_remove = self.get_secondary_object_list().filter( + selection_remove = self.get_list_added_queryset().filter( pk__in=forms['form_added'].cleaned_data['selection'] ) From 18e5ee1e4fb19b747d74e4b5f80119c815290a5d Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Thu, 14 Feb 2019 02:30:51 -0400 Subject: [PATCH 132/209] ACL app updates Update the ACL permission view to use the new AddRemoveView. Add ACL created and ACL edit events. Add permission adding and removal accesors to the ACL model. Signed-off-by: Roberto Rosario --- mayan/apps/acls/apps.py | 28 +++++-- mayan/apps/acls/events.py | 16 ++++ mayan/apps/acls/managers.py | 8 +- mayan/apps/acls/models.py | 37 ++++++++- mayan/apps/acls/views.py | 145 +++++++++++++++--------------------- 5 files changed, 132 insertions(+), 102 deletions(-) create mode 100644 mayan/apps/acls/events.py diff --git a/mayan/apps/acls/apps.py b/mayan/apps/acls/apps.py index 5d763cd628..b15022bce8 100644 --- a/mayan/apps/acls/apps.py +++ b/mayan/apps/acls/apps.py @@ -2,12 +2,16 @@ from __future__ import unicode_literals from django.utils.translation import ugettext_lazy as _ -from mayan.apps.common import ( - MayanAppConfig, menu_object, menu_secondary, menu_sidebar +from mayan.apps.common import MayanAppConfig, menu_object, menu_secondary +from mayan.apps.events import ModelEventType +from mayan.apps.events.links import ( + link_events_for_object, link_object_event_types_user_subcriptions_list ) +from mayan.apps.events.permissions import permission_events_view from mayan.apps.navigation import SourceColumn from .classes import ModelPermission +from .events import event_acl_created, event_acl_edited from .links import link_acl_create, link_acl_delete, link_acl_permissions @@ -21,25 +25,33 @@ class ACLsApp(MayanAppConfig): def ready(self): super(ACLsApp, self).ready() + from actstream import registry AccessControlList = self.get_model(model_name='AccessControlList') + ModelEventType.register( + event_types=(event_acl_created, event_acl_edited), + model=AccessControlList + ) ModelPermission.register_inheritance( model=AccessControlList, related='content_object', ) + SourceColumn( attribute='role', is_identifier=True, is_sortable=True, source=AccessControlList ) - SourceColumn( - attribute='get_permission_titles', include_label=True, - source=AccessControlList - ) menu_object.bind_links( - links=(link_acl_permissions, link_acl_delete,), + links=( + link_acl_permissions, link_acl_delete, + link_events_for_object, + link_object_event_types_user_subcriptions_list + ), sources=(AccessControlList,) ) - menu_sidebar.bind_links( + menu_secondary.bind_links( links=(link_acl_create,), sources=('acls:acl_list',) ) + + registry.register(AccessControlList) diff --git a/mayan/apps/acls/events.py b/mayan/apps/acls/events.py new file mode 100644 index 0000000000..8f75bc26e1 --- /dev/null +++ b/mayan/apps/acls/events.py @@ -0,0 +1,16 @@ +from __future__ import absolute_import, unicode_literals + +from django.utils.translation import ugettext_lazy as _ + +from mayan.apps.events import EventTypeNamespace + +namespace = EventTypeNamespace( + label=_('Access control lists'), name='acls' +) + +event_acl_created = namespace.add_event_type( + label=_('ACL created'), name='acl_created' +) +event_acl_edited = namespace.add_event_type( + label=_('ACL edited'), name='acl_edited' +) diff --git a/mayan/apps/acls/managers.py b/mayan/apps/acls/managers.py index 8b55a5af1c..d113be9254 100644 --- a/mayan/apps/acls/managers.py +++ b/mayan/apps/acls/managers.py @@ -151,7 +151,7 @@ class AccessControlListManager(models.Manager): else: raise PermissionDenied - def get_inherited_permissions(self, role, obj): + def get_inherited_permissions(self, obj, role): try: instance = obj.first() except AttributeError: @@ -177,11 +177,11 @@ class AccessControlListManager(models.Manager): parent_object = return_related( instance=instance, related_field=parent_accessor ) - content_type = ContentType.objects.get_for_model(parent_object) + content_type = ContentType.objects.get_for_model(model=parent_object) try: return self.get( - role=role, content_type=content_type, - object_id=parent_object.pk + content_type=content_type, object_id=parent_object.pk, + role=role ).permissions.all() except self.model.DoesNotExist: return StoredPermission.objects.none() diff --git a/mayan/apps/acls/models.py b/mayan/apps/acls/models.py index 4a7fb10726..997919364e 100644 --- a/mayan/apps/acls/models.py +++ b/mayan/apps/acls/models.py @@ -4,13 +4,14 @@ import logging from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType -from django.db import models +from django.db import models, transaction from django.urls import reverse from django.utils.encoding import force_text, python_2_unicode_compatible from django.utils.translation import ugettext_lazy as _ from mayan.apps.permissions.models import Role, StoredPermission +from .events import event_acl_created, event_acl_edited from .managers import AccessControlListManager logger = logging.getLogger(__name__) @@ -58,11 +59,10 @@ class AccessControlList(models.Model): def __str__(self): return _( - 'Permissions "%(permissions)s" to role "%(role)s" for "%(object)s"' + 'Role "%(role)s" permission\'s for "%(object)s"' ) % { - 'permissions': self.get_permission_titles(), 'object': self.content_object, - 'role': self.role + 'role': self.role, } def get_absolute_url(self): @@ -85,3 +85,32 @@ class AccessControlList(models.Model): return result or _('None') get_permission_titles.short_description = _('Permissions') + + def permissions_add(self, queryset, _user=None): + with transaction.atomic(): + event_acl_edited.commit( + actor=_user, target=self + ) + self.permissions.add(*queryset) + + def permissions_remove(self, queryset, _user=None): + with transaction.atomic(): + event_acl_edited.commit( + actor=_user, target=self + ) + self.permissions.remove(*queryset) + + def save(self, *args, **kwargs): + _user = kwargs.pop('_user', None) + + with transaction.atomic(): + is_new = not self.pk + super(AccessControlList, self).save(*args, **kwargs) + if is_new: + event_acl_created.commit( + actor=_user, target=self + ) + else: + event_acl_edited.commit( + actor=_user, target=self + ) diff --git a/mayan/apps/acls/views.py b/mayan/apps/acls/views.py index f4d43ffdd1..9d51e81140 100644 --- a/mayan/apps/acls/views.py +++ b/mayan/apps/acls/views.py @@ -1,9 +1,7 @@ from __future__ import absolute_import, unicode_literals -import itertools import logging -from django.shortcuts import get_object_or_404 from django.template import RequestContext from django.urls import reverse from django.utils.encoding import force_text @@ -13,11 +11,10 @@ from mayan.apps.common.mixins import ( ContentTypeViewMixin, ExternalObjectMixin ) from mayan.apps.common.generics import ( - AssignRemoveView, SingleObjectCreateView, SingleObjectDeleteView, + AddRemoveView, SingleObjectCreateView, SingleObjectDeleteView, SingleObjectListView ) -from mayan.apps.permissions import Permission, PermissionNamespace -from mayan.apps.permissions.models import Role, StoredPermission +from mayan.apps.permissions.models import Role from .classes import ModelPermission from .forms import ACLCreateForm @@ -60,7 +57,8 @@ class ACLCreateView(ContentTypeViewMixin, ExternalObjectMixin, SingleObjectCreat 'queryset': Role.objects.exclude( pk__in=self.get_external_object().acls.values('role') ), - 'widget_attributes': {'class': 'select2'} + 'widget_attributes': {'class': 'select2'}, + '_user': self.request.user } def get_instance_extra_data(self): @@ -140,109 +138,84 @@ class ACLListView(ContentTypeViewMixin, ExternalObjectMixin, SingleObjectListVie return self.get_external_object().acls.all() -class ACLPermissionsView(AssignRemoveView): - grouped = True - left_list_title = _('Available permissions') - right_list_title = _('Granted permissions') +class ACLPermissionsView(AddRemoveView): + action_add_method = 'permissions_add' + action_remove_method = 'permissions_remove' + main_object_model = AccessControlList + main_object_permission = permission_acl_edit + main_object_pk_url_kwarg = 'acl_id' + list_added_title = _('Granted permissions') + list_available_title = _('Available permissions') + related_field = 'permissions' - @staticmethod - def generate_choices(entries): - results = [] + def generate_choices(self, queryset): + namespaces_dictionary = {} - entries = sorted( - entries, key=lambda x: ( - x.volatile_permission.namespace.label, - x.volatile_permission.label - ) + # Sort permissions by their translatable label + object_list = sorted( + queryset, key=lambda permission: permission.volatile_permission.label ) - for namespace, permissions in itertools.groupby(entries, lambda entry: entry.namespace): - permission_options = [ - (force_text(permission.pk), permission) for permission in permissions - ] - results.append( - (PermissionNamespace.get(name=namespace), permission_options) + # Group permissions by namespace + for permission in object_list: + namespaces_dictionary.setdefault( + permission.volatile_permission.namespace.label, + [] + ) + namespaces_dictionary[permission.volatile_permission.namespace.label].append( + (permission.pk, force_text(permission)) ) - return results + # Sort permissions by their translatable namespace label + return sorted(namespaces_dictionary.items()) - def add(self, item): - permission = get_object_or_404(klass=StoredPermission, pk=item) - self.get_object().permissions.add(permission) - - def get_available_list(self): - return ModelPermission.get_for_instance( - instance=self.get_object().content_object - ).exclude(id__in=self.get_granted_list().values_list('pk', flat=True)) + def get_actions_extra_kwargs(self): + return {'_user': self.request.user} def get_disabled_choices(self): """ - Get permissions from a parent's acls but remove the permissions we - already hold for this object + Get permissions from a parent's ACLs. We return a list since that is + what the form widget's can process. """ - return map( - str, set( - self.get_object().get_inherited_permissions().values_list( - 'pk', flat=True - ) - ).difference( - self.get_object().permissions.values_list('pk', flat=True) - ) + return self.main_object.get_inherited_permissions().values_list( + 'pk', flat=True ) def get_extra_context(self): - acl = self.get_object() - return { - 'acl': acl, - 'object': acl.content_object, + 'acl': self.main_object, + 'object': self.main_object.content_object, 'navigation_object_list': ('object', 'acl'), - 'title': _('Role "%(role)s" permission\'s for "%(object)s"') % { - 'role': acl.role, - 'object': acl.content_object, + 'title': _('Role "%(role)s" permission\'s for "%(object)s".') % { + 'role': self.main_object.role, + 'object': self.main_object.content_object, } } - def get_granted_list(self): - """ - Merge of permissions we hold for this object and the permissions we - hold for this object's parent via another ACL. - """ - merged_pks = self.get_object().permissions.values_list( - 'pk', flat=True - ) | self.get_object().get_inherited_permissions().values_list( - 'pk', flat=True - ) - return StoredPermission.objects.filter(pk__in=merged_pks) - - def get_object(self): - return get_object_or_404( - klass=self.get_queryset(), pk=self.kwargs['acl_id'] - ) - - def get_queryset(self): - return AccessControlList.objects.restrict_queryset( - permission=permission_acl_edit, - queryset=AccessControlList.objects.all(), user=self.request.user - ) - - def get_right_list_help_text(self): - if self.get_object().get_inherited_permissions(): + def get_list_added_help_text(self): + if self.main_object.get_inherited_permissions(): return _( 'Disabled permissions are inherited from a parent object and ' 'can\'t be removed from this view, they need to be removed ' 'from the parent object\'s ACL view.' ) - return self.right_list_help_text + def get_list_added_queryset(self): + """ + Merge of permissions we hold for this object and the permissions we + hold for this object's parents via another ACL. .distinct() is added + in case the permission was added to the ACL and then added to a + parent ACL's and thus inherited and would appear twice. If + order to remove the double permission from the ACL it would need to be + remove from the parent first to enable the choice in the form, + remove it from the ACL and then re-add it to the parent ACL. + """ + queryset = super(ACLPermissionsView, self).get_list_added_queryset() + return ( + queryset | self.main_object.get_inherited_permissions() + ).distinct() - def left_list(self): - Permission.refresh() - return ACLPermissionsView.generate_choices(self.get_available_list()) - - def remove(self, item): - permission = get_object_or_404(klass=StoredPermission, pk=item) - self.get_object().permissions.remove(permission) - - def right_list(self): - return ACLPermissionsView.generate_choices(self.get_granted_list()) + def get_secondary_object_source_queryset(self): + return ModelPermission.get_for_instance( + instance=self.main_object.content_object + ) From b25c3be969e144bd9f5a24a42ec31b9580d7f6de Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Thu, 14 Feb 2019 02:34:23 -0400 Subject: [PATCH 133/209] Navigation improvements Rename the get_menu_links and get_menus_links to navigation_resolve_menu. Change the return value of the menu resolving to include the resolved object. Update the links display templates to show which object the links belong to when there is more than one object. Update the links display templates to show which menu the links belong to when there is more than one menu. Remove the sidebar menu and unify its links with the secondary menu. Signed-off-by: Roberto Rosario --- HISTORY.rst | 7 ++ .../appearance/templates/appearance/base.html | 68 ++++++++---- .../appearance/generic_list_horizontal.html | 6 +- .../generic_list_items_subtemplate.html | 33 +++--- .../appearance/generic_list_subtemplate.html | 42 +++++--- .../templates/appearance/list_toolbar.html | 26 +++-- .../templates/appearance/menu_main.html | 102 +++++++++--------- .../templates/appearance/menu_topbar.html | 30 +++--- mayan/apps/common/menus.py | 11 +- mayan/apps/common/templatetags/common_tags.py | 8 ++ mayan/apps/navigation/classes.py | 64 ++++++++--- .../navigation/generic_subnavigation.html | 16 +-- .../templatetags/navigation_tags.py | 44 +++++--- 13 files changed, 286 insertions(+), 171 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 4adcf98872..4feddded82 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -237,6 +237,13 @@ document type selection screen. The document type view permission is now required in addition to the index template edit permission. +- Update the links display templates to show which object the + links belong to when there is more than one object. +- Update the links display templates to show which menu + the links belong to when there is more than one menu. +- Remove the sidebar menu and unify its links with the + secondary menu. + 3.1.9 (2018-11-01) ================== diff --git a/mayan/apps/appearance/templates/appearance/base.html b/mayan/apps/appearance/templates/appearance/base.html index c1db3d4374..48cfcc0143 100644 --- a/mayan/apps/appearance/templates/appearance/base.html +++ b/mayan/apps/appearance/templates/appearance/base.html @@ -37,7 +37,7 @@ - {% get_menus_links names='facet,list facet' sort_results=True as links_facet %} + {% navigation_resolve_menus names='facet,list facet' sort_results=True as facet_menus_link_results %}