Initial commit to support multidocument checkouts

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
This commit is contained in:
Roberto Rosario
2019-07-11 20:00:17 -04:00
parent 8a7da6a103
commit c44090aca6
8 changed files with 243 additions and 105 deletions

View File

@@ -6,7 +6,9 @@ from django.utils.translation import ugettext_lazy as _
from mayan.apps.acls.classes import ModelPermission
from mayan.apps.common.apps import MayanAppConfig
from mayan.apps.common.menus import menu_facet, menu_main, menu_secondary
from mayan.apps.common.menus import (
menu_facet, menu_main, menu_multi_item, menu_secondary
)
from mayan.apps.dashboards.dashboards import dashboard_main
from mayan.apps.events.classes import ModelEventType
@@ -17,8 +19,9 @@ from .events import (
)
from .handlers import handler_check_new_version_creation
from .links import (
link_check_in_document, link_check_out_document, link_check_out_info,
link_check_out_list
link_check_in_document, link_check_in_document_multiple,
link_check_out_document, link_check_out_document_multiple,
link_check_out_info, link_check_out_list
)
from .methods import (
method_check_in, method_get_check_out_info, method_get_check_out_state,
@@ -85,6 +88,12 @@ class CheckoutsApp(MayanAppConfig):
links=(link_check_out_info,), sources=(Document,)
)
menu_main.bind_links(links=(link_check_out_list,), position=98)
menu_multi_item.bind_links(
links=(
link_check_in_document_multiple,
link_check_out_document_multiple
), sources=(Document,)
)
menu_secondary.bind_links(
links=(link_check_out_document, link_check_in_document),
sources=(

View File

@@ -38,16 +38,26 @@ link_check_out_document = Link(
args='object.pk', condition=is_not_checked_out,
icon_class=icon_check_out_document,
permissions=(permission_document_check_out,),
text=_('Check out document'), view='checkouts:check_out_document',
text=_('Check out document'), view='checkouts:check_out_document'
)
link_check_out_document_multiple = Link(
icon_class=icon_check_out_document,
permissions=(permission_document_check_out,), text=_('Check out'),
view='checkouts:check_out_document_multiple'
)
link_check_in_document = Link(
args='object.pk', icon_class=icon_check_in_document,
condition=is_checked_out, permissions=(
permission_document_check_in, permission_document_check_in_override
), text=_('Check in document'), view='checkouts:check_in_document',
), text=_('Check in document'), view='checkouts:check_in_document'
)
link_check_in_document_multiple = Link(
icon_class=icon_check_in_document,
permissions=(permission_document_check_in,), text=_('Check in'),
view='checkouts:check_in_document_multiple'
)
link_check_out_info = Link(
args='resolved_object.pk', icon_class=icon_check_out_info, permissions=(
permission_document_check_out_detail_view,
), text=_('Check in/out'), view='checkouts:check_out_info',
), text=_('Check in/out'), view='checkouts:check_out_info'
)

View File

@@ -4,6 +4,8 @@ import datetime
from django.utils.timezone import now
from mayan.apps.common.literals import TIME_DELTA_UNIT_DAYS
from ..models import DocumentCheckout
@@ -23,3 +25,40 @@ class DocumentCheckoutTestMixin(object):
expiration_datetime=self._check_out_expiration_datetime,
user=user
)
class DocumentCheckoutViewTestMixin(object):
def _request_test_document_check_in_get_view(self):
return self.get(
viewname='checkouts:check_in_document', kwargs={
'pk': self.test_document.pk
}
)
def _request_test_document_check_in_post_view(self):
return self.post(
viewname='checkouts:check_in_document', kwargs={
'pk': self.test_document.pk
}
)
def _request_test_document_check_out_view(self):
return self.post(
viewname='checkouts:check_out_document', kwargs={
'pk': self.test_document.pk
}, data={
'block_new_version': True,
'expiration_datetime_0': TIME_DELTA_UNIT_DAYS,
'expiration_datetime_1': 2
}
)
def _request_test_document_check_out_detail_view(self):
return self.get(
viewname='checkouts:check_out_info', kwargs={
'pk': self.test_document.pk
}
)
def _request_test_document_check_out_list_view(self):
return self.get(viewname='checkouts:check_out_list')

View File

@@ -65,7 +65,7 @@ class CheckoutsAPITestCase(DocumentCheckoutTestMixin, DocumentTestMixin, BaseAPI
force_text(self.test_document.uuid)
)
def _request_document_checkout_view(self):
def _request_test_document_check_out_view(self):
return self.post(
viewname='rest_api:checkout-document-list', data={
'document_pk': self.test_document.pk,
@@ -74,7 +74,7 @@ class CheckoutsAPITestCase(DocumentCheckoutTestMixin, DocumentTestMixin, BaseAPI
)
def test_document_checkout_no_access(self):
response = self._request_document_checkout_view()
response = self._request_test_document_check_out_view()
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(DocumentCheckout.objects.count(), 0)
@@ -82,7 +82,7 @@ class CheckoutsAPITestCase(DocumentCheckoutTestMixin, DocumentTestMixin, BaseAPI
def test_document_checkout_with_access(self):
self.grant_access(permission=permission_document_check_out, obj=self.test_document)
response = self._request_document_checkout_view()
response = self._request_test_document_check_out_view()
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(

View File

@@ -12,64 +12,53 @@ from ..permissions import (
permission_document_check_out, permission_document_check_out_detail_view
)
from .mixins import DocumentCheckoutTestMixin
from .mixins import DocumentCheckoutTestMixin, DocumentCheckoutViewTestMixin
class DocumentCheckoutViewTestCase(DocumentCheckoutTestMixin, GenericDocumentViewTestCase):
def _request_document_check_in_get_view(self):
return self.get(
viewname='checkouts:check_in_document', kwargs={
'pk': self.test_document.pk
}
)
def test_check_in_document_get_view_no_permission(self):
class DocumentCheckoutViewTestCase(
DocumentCheckoutTestMixin, DocumentCheckoutViewTestMixin,
GenericDocumentViewTestCase
):
def test_document_check_in_get_view_no_permission(self):
self._check_out_test_document()
response = self._request_document_check_in_get_view()
response = self._request_test_document_check_in_get_view()
self.assertContains(
response=response, text=self.test_document.label, status_code=200
)
self.assertTrue(self.test_document.is_checked_out())
def test_check_in_document_get_view_with_access(self):
def test_document_check_in_get_view_with_access(self):
self._check_out_test_document()
self.grant_access(
obj=self.test_document, permission=permission_document_check_in
)
response = self._request_document_check_in_get_view()
response = self._request_test_document_check_in_get_view()
self.assertContains(
response=response, text=self.test_document.label, status_code=200
)
self.assertTrue(self.test_document.is_checked_out())
def _request_document_check_in_post_view(self):
return self.post(
viewname='checkouts:check_in_document', kwargs={
'pk': self.test_document.pk
}
)
def test_check_in_document_post_view_no_permission(self):
def test_document_check_in_post_view_no_permission(self):
self._check_out_test_document()
response = self._request_document_check_in_post_view()
self.assertEqual(response.status_code, 403)
response = self._request_test_document_check_in_post_view()
self.assertEqual(response.status_code, 404)
self.assertTrue(self.test_document.is_checked_out())
def test_check_in_document_post_view_with_access(self):
def test_document_check_in_post_view_with_access(self):
self._check_out_test_document()
self.grant_access(
obj=self.test_document, permission=permission_document_check_in
)
response = self._request_document_check_in_post_view()
response = self._request_test_document_check_in_post_view()
self.assertEqual(response.status_code, 302)
self.assertFalse(self.test_document.is_checked_out())
@@ -79,24 +68,13 @@ class DocumentCheckoutViewTestCase(DocumentCheckoutTestMixin, GenericDocumentVie
)
)
def _request_document_checkout_view(self):
return self.post(
viewname='checkouts:check_out_document', kwargs={
'pk': self.test_document.pk
}, data={
'expiration_datetime_0': 2,
'expiration_datetime_1': TIME_DELTA_UNIT_DAYS,
'block_new_version': True
}
)
def test_check_out_document_view_no_permission(self):
response = self._request_document_checkout_view()
self.assertEqual(response.status_code, 403)
def test_document_check_out_view_no_permission(self):
response = self._request_test_document_check_out_view()
self.assertEqual(response.status_code, 404)
self.assertFalse(self.test_document.is_checked_out())
def test_check_out_document_view_with_access(self):
def test_document_check_out_view_with_access(self):
self.grant_access(
obj=self.test_document, permission=permission_document_check_out
)
@@ -105,28 +83,21 @@ class DocumentCheckoutViewTestCase(DocumentCheckoutTestMixin, GenericDocumentVie
permission=permission_document_check_out_detail_view
)
response = self._request_document_checkout_view()
response = self._request_test_document_check_out_view()
self.assertEqual(response.status_code, 302)
self.assertTrue(self.test_document.is_checked_out())
def _request_check_out_detail_view(self):
return self.get(
viewname='checkouts:check_out_info', kwargs={
'pk': self.test_document.pk
}
)
def test_checkout_detail_view_no_permission(self):
def test_document_check_out_detail_view_no_permission(self):
self._check_out_test_document()
response = self._request_check_out_detail_view()
response = self._request_test_document_check_out_detail_view()
self.assertNotContains(
response, text=STATE_LABELS[STATE_CHECKED_OUT], status_code=404
)
def test_checkout_detail_view_with_access(self):
def test_document_check_out_detail_view_with_access(self):
self._check_out_test_document()
self.grant_access(
@@ -134,15 +105,12 @@ class DocumentCheckoutViewTestCase(DocumentCheckoutTestMixin, GenericDocumentVie
permission=permission_document_check_out_detail_view
)
response = self._request_check_out_detail_view()
response = self._request_test_document_check_out_detail_view()
self.assertContains(
response, text=STATE_LABELS[STATE_CHECKED_OUT], status_code=200
)
def _request_check_out_list_view(self):
return self.get(viewname='checkouts:check_out_list')
def test_checkout_list_view_no_permission(self):
def test_document_checkout_list_view_no_permission(self):
self._check_out_test_document()
self.grant_access(
@@ -150,12 +118,12 @@ class DocumentCheckoutViewTestCase(DocumentCheckoutTestMixin, GenericDocumentVie
permission=permission_document_view
)
response = self._request_check_out_list_view()
response = self._request_test_document_check_out_list_view()
self.assertNotContains(
response=response, text=self.test_document.label, status_code=200
)
def test_checkout_list_view_with_access(self):
def test_document_checkout_list_view_with_access(self):
self._check_out_test_document()
self.grant_access(
@@ -167,12 +135,12 @@ class DocumentCheckoutViewTestCase(DocumentCheckoutTestMixin, GenericDocumentVie
permission=permission_document_view
)
response = self._request_check_out_list_view()
response = self._request_test_document_check_out_list_view()
self.assertContains(
response=response, text=self.test_document.label, status_code=200
)
def test_document_new_version_after_check_out(self):
def test_document_check_out_new_version(self):
"""
Gitlab issue #231
User shown option to upload new version of a document even though it
@@ -209,7 +177,7 @@ class DocumentCheckoutViewTestCase(DocumentCheckoutTestMixin, GenericDocumentVie
self.assertEqual(resolved_link, None)
def test_forcefull_check_in_document_view_no_permission(self):
def test_document_forcefull_check_in_view_no_permission(self):
# Gitlab issue #237
# Forcefully checking in a document by a user without adequate
# permissions throws out an error
@@ -232,7 +200,7 @@ class DocumentCheckoutViewTestCase(DocumentCheckoutTestMixin, GenericDocumentVie
self.assertTrue(self.test_document.is_checked_out())
def test_forcefull_check_in_document_view_with_permission(self):
def test_document_forcefull_check_in_view_with_permission(self):
self._create_test_case_superuser()
self._check_out_test_document(user=self._test_case_superuser)

View File

@@ -4,25 +4,34 @@ from django.conf.urls import url
from .api_views import APICheckedoutDocumentListView, APICheckedoutDocumentView
from .views import (
CheckoutDocumentView, CheckoutDetailView, CheckoutListView,
DocumentCheckinView
DocumentCheckinView, DocumentCheckoutDetailView, DocumentCheckoutView,
DocumentCheckoutListView
)
urlpatterns = [
url(
regex=r'^list/$', view=CheckoutListView.as_view(), name='check_out_list'
regex=r'^documents/$', view=DocumentCheckoutListView.as_view(),
name='check_out_list'
),
url(
regex=r'^(?P<pk>\d+)/check/out/$', view=CheckoutDocumentView.as_view(),
name='check_out_document'
),
url(
regex=r'^(?P<pk>\d+)/check/in/$', view=DocumentCheckinView.as_view(),
regex=r'^documents/(?P<pk>\d+)/check_in/$', view=DocumentCheckinView.as_view(),
name='check_in_document'
),
url(
regex=r'^(?P<pk>\d+)/check/info/$', view=CheckoutDetailView.as_view(),
name='check_out_info'
regex=r'^documents/multiple/check_in/$',
name='check_in_document_multiple', view=DocumentCheckinView.as_view()
),
url(
regex=r'^documents/(?P<pk>\d+)/check_out/$', view=DocumentCheckoutView.as_view(),
name='check_out_document'
),
url(
regex=r'^documents/multiple/check_out/$',
name='check_out_document_multiple', view=DocumentCheckoutView.as_view()
),
url(
regex=r'^documents/(?P<pk>\d+)/checkout/info/$',
view=DocumentCheckoutDetailView.as_view(), name='check_out_info'
),
]

View File

@@ -4,11 +4,12 @@ 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
ConfirmView, MultipleObjectConfirmActionView, MultipleObjectFormActionView,
SingleObjectCreateView, SingleObjectDetailView
)
from mayan.apps.common.utils import encapsulate
from mayan.apps.documents.models import Document
@@ -24,6 +25,7 @@ from .permissions import (
)
"""
class DocumentCheckinView(ConfirmView):
def get_extra_context(self):
document = self.get_object()
@@ -80,8 +82,59 @@ class DocumentCheckinView(ConfirmView):
'Document "%s" checked in successfully.'
) % document, request=self.request
)
"""
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 = 'pk'
success_message_singular = '%(count)d document checked in.'
success_message_plural = '%(count)d documents checked in.'
def get_extra_context(self):
queryset = self.get_object_list()
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()
}
)
return result
def get_post_object_action_url(self):
if self.action_count == 1:
return reverse(
viewname='checkouts:document_checkout_info',
kwargs={'pk': self.action_id_list[0]}
)
else:
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 CheckoutDocumentView(SingleObjectCreateView):
form_class = DocumentCheckoutForm
@@ -129,9 +182,74 @@ class CheckoutDocumentView(SingleObjectCreateView):
'pk': self.document.pk
}
)
"""
class DocumentCheckoutView(MultipleObjectFormActionView):
error_message = 'Unable to checkout document "%(instance)s". %(exception)s'
form_class = DocumentCheckoutForm
model = Document
object_permission = permission_document_check_out
pk_url_kwarg = 'pk'
success_message_singular = '%(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={'pk': self.action_id_list[0]}
)
else:
super(DocumentCheckoutView, self).get_post_action_redirect()
def object_action(self, form, instance):
DocumentCheckout.objects.check_out_document(
block_new_version=form.cleaned_data['block_new_version'],
document=instance,
expiration_datetime=form.cleaned_data['expiration_datetime'],
user=self.request.user,
)
class CheckoutListView(DocumentListView):
class DocumentCheckoutDetailView(SingleObjectDetailView):
form_class = DocumentCheckoutDefailForm
model = Document
object_permission = permission_document_check_out_detail_view
def get_extra_context(self):
return {
'object': self.object,
'title': _(
'Check out details for document: %s'
) % self.object
}
class DocumentCheckoutListView(DocumentListView):
def get_document_queryset(self):
return AccessControlList.objects.restrict_queryset(
permission=permission_document_check_out_detail_view,
@@ -140,7 +258,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': (
@@ -165,26 +283,11 @@ class CheckoutListView(DocumentListView):
),
'no_results_icon': icon_check_out_info,
'no_results_text': _(
'Checking out a document blocks certain document '
'operations for a predetermined amount of '
'time.'
'Checking out a document, blocks certain operations '
'for a predetermined amount of time.'
),
'no_results_title': _('No documents have been checked out'),
'title': _('Documents checked out'),
'title': _('Checked out documents'),
}
)
return context
class CheckoutDetailView(SingleObjectDetailView):
form_class = DocumentCheckoutDefailForm
model = Document
object_permission = permission_document_check_out_detail_view
def get_extra_context(self):
return {
'object': self.object,
'title': _(
'Check out details for document: %s'
) % self.object
}

View File

@@ -32,8 +32,8 @@ class SplitTimeDeltaWidget(forms.widgets.MultiWidget):
return (None, None)
def value_from_datadict(self, querydict, files, name):
unit = querydict.get('{}_1'.format(name))
period = querydict.get('{}_0'.format(name))
unit = querydict.get('{}_0'.format(name))
period = querydict.get('{}_1'.format(name))
if not unit or not period:
return now()