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