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:
@@ -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)
|
||||
==================
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -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'
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
13
mayan/apps/checkouts/hooks.py
Normal file
13
mayan/apps/checkouts/hooks.py
Normal 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
|
||||
)
|
||||
@@ -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',
|
||||
)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -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<document_pk>\d+)/check/out/$',
|
||||
name='checkout_document', view=CheckoutDocumentView.as_view()
|
||||
regex=r'^documents/(?P<document_id>\d+)/check_in/$',
|
||||
name='document_check_in', view=DocumentCheckinView.as_view()
|
||||
),
|
||||
url(
|
||||
regex=r'^documents/(?P<document_pk>\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<document_pk>\d+)/check/info/$',
|
||||
name='checkout_info', view=CheckoutDetailView.as_view()
|
||||
regex=r'^documents/(?P<document_id>\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<document_id>\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<document_pk>[0-9]+)/checkout_info/$',
|
||||
regex=r'^checkouts/(?P<document_id>\d+)/checkout_info/$',
|
||||
name='checkedout-document-view',
|
||||
view=APICheckedoutDocumentView.as_view()
|
||||
),
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user