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 <Roberto.Rosario@mayan-edms.com>
This commit is contained in:
Roberto Rosario
2019-01-31 05:53:09 -04:00
parent 3976766abe
commit e007af6b3f
14 changed files with 333 additions and 351 deletions

View File

@@ -225,6 +225,8 @@
- Remove the permissions to grant or revoke a permission to a role. - Remove the permissions to grant or revoke a permission to a role.
The instead the role edit permission is used. The instead the role edit permission is used.
- Add a test mixin to generate random model primary keys. - 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) 3.1.9 (2018-11-01)
================== ==================

View File

@@ -7,7 +7,7 @@ from mayan.apps.documents.permissions import permission_document_view
from .models import DocumentCheckout from .models import DocumentCheckout
from .permissions import ( from .permissions import (
permission_document_checkin, permission_document_checkin_override, permission_document_check_in, permission_document_check_in_override,
permission_document_checkout_detail_view permission_document_checkout_detail_view
) )
from .serializers import ( from .serializers import (
@@ -78,12 +78,12 @@ class APICheckedoutDocumentView(generics.RetrieveDestroyAPIView):
if document.checkout_info().user == request.user: if document.checkout_info().user == request.user:
AccessControlList.objects.check_access( AccessControlList.objects.check_access(
permissions=permission_document_checkin, user=request.user, permissions=permission_document_check_in, user=request.user,
obj=document obj=document
) )
else: else:
AccessControlList.objects.check_access( AccessControlList.objects.check_access(
permissions=permission_document_checkin_override, permissions=permission_document_check_in_override,
user=request.user, obj=document user=request.user, obj=document
) )

View File

@@ -10,7 +10,7 @@ from django.utils.translation import ugettext_lazy as _
from mayan.apps.acls import ModelPermission from mayan.apps.acls import ModelPermission
from mayan.apps.common import ( 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.dashboards.dashboards import dashboard_main
from mayan.apps.events import ModelEventType from mayan.apps.events import ModelEventType
@@ -23,8 +23,9 @@ from .events import (
) )
from .handlers import handler_check_new_version_creation from .handlers import handler_check_new_version_creation
from .links import ( from .links import (
link_checkin_document, link_checkout_document, link_checkout_info, link_document_check_in, link_document_checkout, link_document_checkout_info,
link_checkout_list link_document_checkout_list, link_document_multiple_check_in,
link_document_multiple_checkout
) )
from .literals import CHECK_EXPIRED_CHECK_OUTS_INTERVAL from .literals import CHECK_EXPIRED_CHECK_OUTS_INTERVAL
from .methods import ( from .methods import (
@@ -32,7 +33,7 @@ from .methods import (
method_is_checked_out method_is_checked_out
) )
from .permissions import ( 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 permission_document_checkout, permission_document_checkout_detail_view
) )
from .queues import * # NOQA from .queues import * # NOQA
@@ -79,8 +80,8 @@ class CheckoutsApp(MayanAppConfig):
ModelPermission.register( ModelPermission.register(
model=Document, permissions=( model=Document, permissions=(
permission_document_checkout, permission_document_checkout,
permission_document_checkin, permission_document_check_in,
permission_document_checkin_override, permission_document_check_in_override,
permission_document_checkout_detail_view permission_document_checkout_detail_view
) )
) )
@@ -115,13 +116,18 @@ class CheckoutsApp(MayanAppConfig):
widget=DashboardWidgetTotalCheckouts, order=-1 widget=DashboardWidgetTotalCheckouts, order=-1
) )
menu_facet.bind_links(links=(link_checkout_info,), sources=(Document,)) menu_facet.bind_links(links=(link_document_checkout_info,), sources=(Document,))
menu_main.bind_links(links=(link_checkout_list,), position=98) 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( menu_sidebar.bind_links(
links=(link_checkout_document, link_checkin_document), links=(link_document_checkout, link_document_check_in),
sources=( sources=(
'checkouts:checkout_info', 'checkouts:checkout_document', 'checkouts:document_checkout_info', 'checkouts:document_checkout',
'checkouts:checkin_document' 'checkouts:document_check_in'
) )
) )

View File

@@ -14,7 +14,7 @@ from .permissions import permission_document_checkout_detail_view
class DashboardWidgetTotalCheckouts(DashboardWidgetNumeric): class DashboardWidgetTotalCheckouts(DashboardWidgetNumeric):
icon_class = icon_dashboard_checkouts icon_class = icon_dashboard_checkouts
label = _('Checkedout documents') label = _('Checkedout documents')
link = reverse_lazy(viewname='checkouts:checkout_list') link = reverse_lazy(viewname='checkouts:document_checkout_list')
def render(self, request): def render(self, request):
AccessControlList = apps.get_model( AccessControlList = apps.get_model(

View File

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

View File

@@ -8,8 +8,8 @@ from .icons import (
icon_checkin_document, icon_checkout_document, icon_checkout_info icon_checkin_document, icon_checkout_document, icon_checkout_info
) )
from .permissions import ( from .permissions import (
permission_document_checkin, permission_document_checkin_override, permission_document_check_in, permission_document_checkout,
permission_document_checkout, permission_document_checkout_detail_view permission_document_checkout_detail_view
) )
@@ -29,23 +29,32 @@ def is_not_checked_out(context):
return True return True
link_checkout_list = Link( link_document_checkout_list = Link(
icon_class=icon_checkout_info, text=_('Checkouts'), icon_class=icon_checkout_info, text=_('Checkouts'),
view='checkouts:checkout_list' view='checkouts:document_checkout_list'
) )
link_checkout_document = Link( link_document_checkout = Link(
args='object.pk', condition=is_not_checked_out, condition=is_not_checked_out, icon_class=icon_checkout_document,
icon_class=icon_checkout_document, kwargs={'document_id': 'object.pk'},
permission=permission_document_checkout, text=_('Check out document'), permission=permission_document_checkout, text=_('Check out document'),
view='checkouts:checkout_document', view='checkouts:document_checkout',
) )
link_checkin_document = Link( link_document_multiple_checkout = Link(
args='object.pk', condition=is_checked_out, icon_class=icon_checkout_document,
icon_class=icon_checkin_document, permission=permission_document_checkin, permission=permission_document_checkout, text=_('Check out'),
text=_('Check in document'), view='checkouts:checkin_document', view='checkouts:document_multiple_checkout',
) )
link_checkout_info = Link( link_document_check_in = Link(
args='resolved_object.pk', icon_class=icon_checkout_info, 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, permission=permission_document_checkout_detail_view,
text=_('Check in/out'), view='checkouts:checkout_info', text=_('Check in/out'), view='checkouts:document_checkout_info',
) )

View File

@@ -2,39 +2,45 @@ from __future__ import absolute_import, unicode_literals
import logging import logging
from django.apps import apps from django.core.exceptions import PermissionDenied
from django.db import models from django.db import models, transaction
from django.utils.timezone import now 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 mayan.apps.documents.models import Document
from .events import ( from .events import (
event_document_auto_check_in, event_document_check_in, event_document_auto_check_in, event_document_check_in,
event_document_forceful_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 .literals import STATE_CHECKED_IN, STATE_CHECKED_OUT
from .permissions import permission_document_check_in_override
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class DocumentCheckoutManager(models.Manager): 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): def check_in_document(self, document, user=None):
try: try:
document_checkout = self.model.objects.get(document=document) document_checkout = self.model.objects.get(document=document)
except self.model.DoesNotExist: except self.model.DoesNotExist:
raise DocumentNotCheckedOut raise DocumentNotCheckedOut(
_('Document not checked out.')
)
else: else:
with transaction.atomic():
if user: if user:
if self.get_document_checkout_info(document=document).user != 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( event_document_forceful_check_in.commit(
actor=user, target=document actor=user, target=document
) )
@@ -57,15 +63,10 @@ class DocumentCheckoutManager(models.Manager):
def checked_out_documents(self): def checked_out_documents(self):
return Document.objects.filter( return Document.objects.filter(
pk__in=self.model.objects.all().values_list( pk__in=self.model.objects.values('document__id')
'document__pk', flat=True
)
) )
def get_by_natural_key(self, document_natural_key): def get_by_natural_key(self, document_natural_key):
Document = apps.get_model(
app_label='documents', model_name='Document'
)
try: try:
document = Document.objects.get_by_natural_key(document_natural_key) document = Document.objects.get_by_natural_key(document_natural_key)
except Document.DoesNotExist: except Document.DoesNotExist:
@@ -87,15 +88,15 @@ class DocumentCheckoutManager(models.Manager):
def get_expired_check_outs(self): def get_expired_check_outs(self):
expired_list = Document.objects.filter( expired_list = Document.objects.filter(
pk__in=self.model.objects.filter( pk__in=self.filter(
expiration_datetime__lte=now() expiration_datetime__lte=now()
).values_list('document__pk', flat=True) ).values('document__id')
) )
logger.debug('expired_list: %s', expired_list) logger.debug('expired_list: %s', expired_list)
return expired_list return expired_list
def is_document_checked_out(self, document): def is_document_checked_out(self, document):
if self.model.objects.filter(document=document): if self.filter(document=document).exists():
return True return True
else: else:
return False return False
@@ -105,13 +106,7 @@ class NewVersionBlockManager(models.Manager):
def block(self, document): def block(self, document):
self.get_or_create(document=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): def get_by_natural_key(self, document_natural_key):
Document = apps.get_model(
app_label='documents', model_name='Document'
)
try: try:
document = Document.objects.get_by_natural_key(document_natural_key) document = Document.objects.get_by_natural_key(document_natural_key)
except Document.DoesNotExist: except Document.DoesNotExist:
@@ -119,5 +114,12 @@ class NewVersionBlockManager(models.Manager):
return self.get(document__pk=document.pk) 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): def unblock(self, document):
self.filter(document=document).delete() self.filter(document=document).delete()

View File

@@ -4,7 +4,7 @@ import logging
from django.conf import settings from django.conf import settings
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import models from django.db import models, transaction
from django.urls import reverse from django.urls import reverse
from django.utils.encoding import python_2_unicode_compatible from django.utils.encoding import python_2_unicode_compatible
from django.utils.timezone import now from django.utils.timezone import now
@@ -68,13 +68,14 @@ class DocumentCheckout(models.Model):
) )
def delete(self, *args, **kwargs): def delete(self, *args, **kwargs):
# TODO: enclose in transaction with transaction.atomic():
NewVersionBlock.objects.unblock(self.document) NewVersionBlock.objects.unblock(document=self.document)
super(DocumentCheckout, self).delete(*args, **kwargs) super(DocumentCheckout, self).delete(*args, **kwargs)
def get_absolute_url(self): def get_absolute_url(self):
return reverse( 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): def natural_key(self):
@@ -82,11 +83,13 @@ class DocumentCheckout(models.Model):
natural_key.dependencies = ['documents.Document'] natural_key.dependencies = ['documents.Document']
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
# TODO: enclose in transaction
new_checkout = not self.pk new_checkout = not self.pk
if not new_checkout or self.document.is_checked_out(): if not new_checkout or self.document.is_checked_out():
raise DocumentAlreadyCheckedOut raise DocumentAlreadyCheckedOut(
_('Document already checked out.')
)
with transaction.atomic():
result = super(DocumentCheckout, self).save(*args, **kwargs) result = super(DocumentCheckout, self).save(*args, **kwargs)
if new_checkout: if new_checkout:
event_document_check_out.commit( event_document_check_out.commit(

View File

@@ -6,10 +6,10 @@ from mayan.apps.permissions import PermissionNamespace
namespace = PermissionNamespace(label=_('Document checkout'), name='checkouts') 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' 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' label=_('Forcefully check in documents'), name='checkin_document_override'
) )
permission_document_checkout = namespace.add_permission( permission_document_checkout = namespace.add_permission(

View File

@@ -42,8 +42,8 @@ class NewDocumentCheckoutSerializer(serializers.ModelSerializer):
document = Document.objects.get(pk=validated_data.pop('document_pk')) document = Document.objects.get(pk=validated_data.pop('document_pk'))
AccessControlList.objects.check_access( AccessControlList.objects.check_access(
permissions=permission_document_checkout, obj=document, permissions=permission_document_checkout,
obj=document, user=self.context['request'].user user=self.context['request'].user
) )
validated_data['document'] = document validated_data['document'] = document

View File

@@ -23,7 +23,7 @@ class DocumentCheckoutTestCase(DocumentTestMixin, BaseTestCase):
DocumentCheckout.objects.checkout_document( DocumentCheckout.objects.checkout_document(
document=self.document, expiration_datetime=expiration_datetime, 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()) 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): def test_checkin_in(self):
expiration_datetime = now() + datetime.timedelta(days=1) expiration_datetime = now() + datetime.timedelta(days=1)
DocumentCheckout.objects.checkout_document( DocumentCheckout.objects.checkout_document(
document=self.document, expiration_datetime=expiration_datetime, 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() self.document.check_in()
@@ -72,13 +55,13 @@ class DocumentCheckoutTestCase(DocumentTestMixin, BaseTestCase):
DocumentCheckout.objects.checkout_document( DocumentCheckout.objects.checkout_document(
document=self.document, expiration_datetime=expiration_datetime, 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): with self.assertRaises(DocumentAlreadyCheckedOut):
DocumentCheckout.objects.checkout_document( DocumentCheckout.objects.checkout_document(
document=self.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 block_new_version=True
) )
@@ -91,7 +74,7 @@ class DocumentCheckoutTestCase(DocumentTestMixin, BaseTestCase):
DocumentCheckout.objects.checkout_document( DocumentCheckout.objects.checkout_document(
document=self.document, expiration_datetime=expiration_datetime, 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) time.sleep(.11)
@@ -100,18 +83,6 @@ class DocumentCheckoutTestCase(DocumentTestMixin, BaseTestCase):
self.assertFalse(self.document.is_checked_out()) 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): class NewVersionBlockTestCase(DocumentTestMixin, BaseTestCase):
def test_blocking(self): def test_blocking(self):
@@ -141,3 +112,32 @@ class NewVersionBlockTestCase(DocumentTestMixin, BaseTestCase):
self.assertFalse( self.assertFalse(
NewVersionBlock.objects.is_blocked(document=self.document) 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)

View File

@@ -8,62 +8,53 @@ from django.utils.timezone import now
from mayan.apps.common.literals import TIME_DELTA_UNIT_DAYS from mayan.apps.common.literals import TIME_DELTA_UNIT_DAYS
from mayan.apps.documents.tests import GenericDocumentViewTestCase from mayan.apps.documents.tests import GenericDocumentViewTestCase
from mayan.apps.sources.links import link_upload_version 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 ..models import DocumentCheckout
from ..permissions import ( 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 permission_document_checkout, permission_document_checkout_detail_view
) )
class DocumentCheckoutViewTestCase(GenericDocumentViewTestCase): 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): def _request_document_check_in_view(self):
return self.post( return self.post(
viewname='checkouts:checkin_document', viewname='checkouts:document_check_in',
kwargs={'document_pk': self.document.pk} kwargs={'document_id': self.document.pk}
) )
def test_checkin_document_view_no_permission(self): def test_document_check_in_view_no_permission(self):
self.login_user() self._checkout_document()
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())
response = self._request_document_check_in_view() 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()) self.assertTrue(self.document.is_checked_out())
def test_checkin_document_view_with_access(self): def test_document_check_in_view_with_access(self):
self.login_user() self._checkout_document()
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())
self.grant_access( self.grant_access(
obj=self.document, permission=permission_document_checkin obj=self.document, permission=permission_document_check_in
) )
self.grant_access( self.grant_access(
obj=self.document, obj=self.document,
permission=permission_document_checkout_detail_view permission=permission_document_checkout_detail_view
) )
response = self._request_document_check_in_view() response = self._request_document_check_in_view()
self.assertEquals(response.status_code, 302) self.assertEquals(response.status_code, 302)
self.assertFalse(self.document.is_checked_out()) self.assertFalse(self.document.is_checked_out())
self.assertFalse( self.assertFalse(
DocumentCheckout.objects.is_document_checked_out( DocumentCheckout.objects.is_document_checked_out(
@@ -73,8 +64,8 @@ class DocumentCheckoutViewTestCase(GenericDocumentViewTestCase):
def _request_document_checkout_view(self): def _request_document_checkout_view(self):
return self.post( return self.post(
viewname='checkouts:checkout_document', viewname='checkouts:document_checkout',
kwargs={'document_pk': self.document.pk}, kwargs={'document_id': self.document.pk},
data={ data={
'expiration_datetime_0': 2, 'expiration_datetime_0': 2,
'expiration_datetime_1': TIME_DELTA_UNIT_DAYS, 'expiration_datetime_1': TIME_DELTA_UNIT_DAYS,
@@ -83,14 +74,11 @@ class DocumentCheckoutViewTestCase(GenericDocumentViewTestCase):
) )
def test_checkout_document_view_no_permission(self): def test_checkout_document_view_no_permission(self):
self.login_user()
response = self._request_document_checkout_view() 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()) self.assertFalse(self.document.is_checked_out())
def test_checkout_document_view_with_access(self): def test_checkout_document_view_with_access(self):
self.login_user()
self.grant_access( self.grant_access(
obj=self.document, permission=permission_document_checkout obj=self.document, permission=permission_document_checkout
) )
@@ -98,9 +86,9 @@ class DocumentCheckoutViewTestCase(GenericDocumentViewTestCase):
obj=self.document, obj=self.document,
permission=permission_document_checkout_detail_view permission=permission_document_checkout_detail_view
) )
response = self._request_document_checkout_view() response = self._request_document_checkout_view()
self.assertEquals(response.status_code, 302) self.assertEquals(response.status_code, 302)
self.assertTrue(self.document.is_checked_out()) self.assertTrue(self.document.is_checked_out())
def test_document_new_version_after_checkout(self): def test_document_new_version_after_checkout(self):
@@ -113,25 +101,15 @@ class DocumentCheckoutViewTestCase(GenericDocumentViewTestCase):
- Link to upload version view should not resolve - Link to upload version view should not resolve
- Upload version view should reject request - Upload version view should reject request
""" """
self.login( self.login_superuser()
username=TEST_ADMIN_USERNAME, password=TEST_ADMIN_PASSWORD
)
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.assertTrue(self.document.is_checked_out())
response = self.post( response = self.post(
viewname='sources:upload_version', viewname='sources:upload_version',
kwargs={'document_pk': self.document.pk}, kwargs={'document_id': self.document.pk},
follow=True follow=True
) )
self.assertContains( self.assertContains(
response, text='blocked from uploading', response, text='blocked from uploading',
status_code=200 status_code=200
@@ -139,7 +117,7 @@ class DocumentCheckoutViewTestCase(GenericDocumentViewTestCase):
response = self.get( response = self.get(
viewname='documents:document_version_list', viewname='documents:document_version_list',
kwargs={'document_pk': self.document.pk}, kwargs={'document_id': self.document.pk},
follow=True follow=True
) )
@@ -163,28 +141,22 @@ class DocumentCheckoutViewTestCase(GenericDocumentViewTestCase):
DocumentCheckout.objects.checkout_document( DocumentCheckout.objects.checkout_document(
document=self.document, expiration_datetime=expiration_datetime, 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.assertTrue(self.document.is_checked_out())
self.login( self.grant_access(
username=TEST_USER_USERNAME, password=TEST_USER_PASSWORD obj=self.document, permission=permission_document_check_in
) )
self.grant_access(
self.role.permissions.add( obj=self.document, permission=permission_document_checkout
permission_document_checkin.stored_permission
) )
self.role.permissions.add(
permission_document_checkout.stored_permission
)
response = self.post( response = self.post(
viewname='checkouts:checkin_document', viewname='checkouts:document_check_in',
kwargs={'document_pk': self.document.pk}, kwargs={'document_id': self.document.pk},
follow=True follow=True
) )
self.assertContains( self.assertContains(
response, text='Insufficient permissions', status_code=403 response, text='Insufficient permissions', status_code=403
) )
@@ -192,34 +164,20 @@ class DocumentCheckoutViewTestCase(GenericDocumentViewTestCase):
self.assertTrue(self.document.is_checked_out()) self.assertTrue(self.document.is_checked_out())
def test_forcefull_check_in_document_view_with_permission(self): def test_forcefull_check_in_document_view_with_permission(self):
expiration_datetime = now() + datetime.timedelta(days=1) self._checkout_document()
DocumentCheckout.objects.checkout_document( self.grant_access(
document=self.document, expiration_datetime=expiration_datetime, obj=self.document, permission=permission_document_check_in
user=self.admin_user, block_new_version=True
) )
self.grant_access(
self.assertTrue(self.document.is_checked_out()) obj=self.document, permission=permission_document_check_in_override
self.login(
username=TEST_USER_USERNAME, password=TEST_USER_PASSWORD
) )
self.grant_access(
self.role.permissions.add( obj=self.document, permission=permission_document_checkout_detail_view
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
) )
response = self.post( response = self.post(
viewname='checkouts:checkin_document', viewname='checkouts:document_check_in',
kwargs={'document_pk': self.document.pk}, kwargs={'document_id': self.document.pk},
follow=True follow=True
) )

View File

@@ -4,26 +4,34 @@ from django.conf.urls import url
from .api_views import APICheckedoutDocumentListView, APICheckedoutDocumentView from .api_views import APICheckedoutDocumentListView, APICheckedoutDocumentView
from .views import ( from .views import (
CheckoutDetailView, CheckoutDocumentView, CheckoutListView, DocumentCheckinView, DocumentCheckoutView, DocumentCheckoutDetailView,
DocumentCheckinView DocumentCheckoutListView
) )
urlpatterns = [ urlpatterns = [
url( url(
regex=r'^documents/$', name='checkout_list', regex=r'^documents/$', name='document_checkout_list',
view=CheckoutListView.as_view() view=DocumentCheckoutListView.as_view()
), ),
url( url(
regex=r'^documents/(?P<document_pk>\d+)/check/out/$', regex=r'^documents/(?P<document_id>\d+)/check_in/$',
name='checkout_document', view=CheckoutDocumentView.as_view() name='document_check_in', view=DocumentCheckinView.as_view()
), ),
url( url(
regex=r'^documents/(?P<document_pk>\d+)/check/in/$', regex=r'^documents/multiple/check_in/$',
name='checkin_document', view=DocumentCheckinView.as_view() name='document_multiple_check_in', view=DocumentCheckinView.as_view()
), ),
url( url(
regex=r'^documents/(?P<document_pk>\d+)/check/info/$', regex=r'^documents/(?P<document_id>\d+)/checkout/$',
name='checkout_info', view=CheckoutDetailView.as_view() 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<document_id>\d+)/checkout/info/$',
name='document_checkout_info', view=DocumentCheckoutDetailView.as_view()
), ),
] ]
@@ -33,7 +41,7 @@ api_urls = [
view=APICheckedoutDocumentListView.as_view() view=APICheckedoutDocumentListView.as_view()
), ),
url( url(
regex=r'^checkouts/(?P<document_pk>[0-9]+)/checkout_info/$', regex=r'^checkouts/(?P<document_id>\d+)/checkout_info/$',
name='checkedout-document-view', name='checkedout-document-view',
view=APICheckedoutDocumentView.as_view() view=APICheckedoutDocumentView.as_view()
), ),

View File

@@ -1,86 +1,145 @@
from __future__ import absolute_import, unicode_literals 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.shortcuts import get_object_or_404
from django.urls import reverse 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.acls.models import AccessControlList
from mayan.apps.common.generics import ( from mayan.apps.common.generics import (
ConfirmView, SingleObjectCreateView, SingleObjectDetailView MultipleObjectConfirmActionView, MultipleObjectFormActionView,
SingleObjectDetailView
) )
from mayan.apps.common.utils import encapsulate from mayan.apps.common.utils import encapsulate
from mayan.apps.documents.models import Document from mayan.apps.documents.models import Document
from mayan.apps.documents.views import DocumentListView from mayan.apps.documents.views import DocumentListView
from .exceptions import DocumentAlreadyCheckedOut, DocumentNotCheckedOut
from .forms import DocumentCheckoutDefailForm, DocumentCheckoutForm from .forms import DocumentCheckoutDefailForm, DocumentCheckoutForm
from .icons import icon_checkout_info from .icons import icon_checkout_info
from .models import DocumentCheckout from .models import DocumentCheckout
from .permissions import ( from .permissions import (
permission_document_checkin, permission_document_checkin_override, permission_document_check_in, permission_document_checkout,
permission_document_checkout, permission_document_checkout_detail_view permission_document_checkout_detail_view
) )
class CheckoutDocumentView(SingleObjectCreateView): class DocumentCheckinView(MultipleObjectConfirmActionView):
form_class = DocumentCheckoutForm 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): def get_extra_context(self):
self.document = get_object_or_404( queryset = self.get_object_list()
klass=Document, pk=self.kwargs['document_pk']
result = {
'title': ungettext(
singular='Check in %(count)d document',
plural='Check in %(count)d documents',
number=queryset.count()
) % {
'count': queryset.count(),
}
}
if queryset.count() == 1:
result.update(
{
'object': queryset.first(),
'title': _(
'Check in document: %s'
) % queryset.first()
}
) )
AccessControlList.objects.check_access( return result
obj=self.document, permissions=permission_document_checkout,
user=request.user
)
return super( def get_post_object_action_url(self):
CheckoutDocumentView, self if self.action_count == 1:
).dispatch(request, *args, **kwargs) return reverse(
viewname='checkouts:document_checkout_info',
def form_valid(self, form): kwargs={'document_id': self.action_id_list[0]}
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.')
)
except Exception as exception:
messages.error(
request=self.request,
message=_('Error trying to check out document; %s') % exception
) )
else: else:
messages.success( super(DocumentCheckinView, self).get_post_action_redirect()
request=self.request,
message=_( def object_action(self, form, instance):
'Document "%s" checked out successfully.' DocumentCheckout.objects.check_in_document(
) % self.document document=instance, user=self.request.user
) )
return HttpResponseRedirect(redirect_to=self.get_success_url())
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 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): def get_extra_context(self):
return { return {
'object': self.document, 'object': self.get_object(),
'title': _('Check out document: %s') % self.document 'title': _(
'Check out details for document: %s'
) % self.get_object()
} }
def get_post_action_redirect(self): def get_object(self):
return reverse( return get_object_or_404(klass=Document, pk=self.kwargs['document_id'])
viewname='checkouts:checkout_info',
kwargs={'document_pk': self.document.pk}
)
class CheckoutListView(DocumentListView): class DocumentCheckoutListView(DocumentListView):
def get_document_queryset(self): def get_document_queryset(self):
return AccessControlList.objects.restrict_queryset( return AccessControlList.objects.restrict_queryset(
permission=permission_document_checkout_detail_view, permission=permission_document_checkout_detail_view,
@@ -89,7 +148,7 @@ class CheckoutListView(DocumentListView):
) )
def get_extra_context(self): def get_extra_context(self):
context = super(CheckoutListView, self).get_extra_context() context = super(DocumentCheckoutListView, self).get_extra_context()
context.update( context.update(
{ {
'extra_columns': ( 'extra_columns': (
@@ -123,81 +182,3 @@ class CheckoutListView(DocumentListView):
} }
) )
return context 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
)