diff --git a/mayan/apps/checkouts/apps.py b/mayan/apps/checkouts/apps.py index 56041bdb25..7c067edb0b 100644 --- a/mayan/apps/checkouts/apps.py +++ b/mayan/apps/checkouts/apps.py @@ -11,6 +11,7 @@ from mayan.apps.common.menus import ( ) from mayan.apps.dashboards.dashboards import dashboard_main from mayan.apps.events.classes import ModelEventType +from mayan.apps.navigation.classes import SourceColumn from .dashboard_widgets import DashboardWidgetTotalCheckouts from .events import ( @@ -46,6 +47,8 @@ class CheckoutsApp(MayanAppConfig): def ready(self): super(CheckoutsApp, self).ready() + CheckedOutDocument = self.get_model(model_name='CheckedOutDocument') + DocumentCheckout = self.get_model(model_name='DocumentCheckout') Document = apps.get_model( app_label='documents', model_name='Document' ) @@ -79,6 +82,22 @@ class CheckoutsApp(MayanAppConfig): permission_document_check_out_detail_view ) ) + ModelPermission.register_inheritance( + model=DocumentCheckout, related='document' + ) + + SourceColumn( + attribute='get_user_display', include_label=True, order=99, + source=CheckedOutDocument + ) + SourceColumn( + attribute='get_checkout_datetime', include_label=True, order=99, + source=CheckedOutDocument + ) + SourceColumn( + attribute='get_checkout_expiration', include_label=True, order=99, + source=CheckedOutDocument + ) dashboard_main.add_widget( widget=DashboardWidgetTotalCheckouts, order=-1 @@ -91,9 +110,19 @@ class CheckoutsApp(MayanAppConfig): menu_multi_item.bind_links( links=( link_check_in_document_multiple, - link_check_out_document_multiple + ), sources=(CheckedOutDocument,) + ) + menu_multi_item.bind_links( + links=( + link_check_in_document_multiple, + link_check_out_document_multiple, ), sources=(Document,) ) + menu_multi_item.unbind_links( + links=( + link_check_out_document_multiple, + ), sources=(CheckedOutDocument,) + ) menu_secondary.bind_links( links=(link_check_out_document, link_check_in_document), sources=( diff --git a/mayan/apps/checkouts/managers.py b/mayan/apps/checkouts/managers.py index 674a77257e..7b6b77635d 100644 --- a/mayan/apps/checkouts/managers.py +++ b/mayan/apps/checkouts/managers.py @@ -6,6 +6,7 @@ from django.apps import apps from django.db import models, transaction from django.utils.timezone import now +from mayan.apps.acls.models import AccessControlList from mayan.apps.documents.models import Document from .events import ( @@ -14,10 +15,53 @@ from .events import ( ) from .exceptions import DocumentNotCheckedOut from .literals import STATE_CHECKED_OUT, STATE_CHECKED_IN +from .permissions import ( + permission_document_check_in, permission_document_check_in_override +) logger = logging.getLogger(__name__) +class DocumentCheckoutBusinessLogicManager(models.Manager): + def check_in_document(self, document, user=None): + queryset = document._meta.default_manager.filter(pk=document.pk) + return self.check_in_documents(queryset=queryset, user=user) + + def check_in_documents(self, queryset, user=None): + if user: + user_document_checkouts = AccessControlList.objects.restrict_queryset( + permission=permission_document_check_in, + queryset=self.filter(user_id=user.pk, document__in=queryset), + user=user + ) + + others_document_checkouts = AccessControlList.objects.restrict_queryset( + permission=permission_document_check_in_override, + queryset=self.exclude(user_id=user.pk, document__in=queryset), + user=user + ) + + with transaction.atomic(): + if user: + for checkout in user_document_checkouts: + event_document_check_in.commit( + actor=user, target=checkout.document + ) + checkout.delete() + + for checkout in others_document_checkouts: + event_document_forceful_check_in.commit( + actor=user, target=checkout.document + ) + checkout.delete() + else: + for checkout in self.filter(document__in=queryset): + event_document_auto_check_in.commit( + target=checkout.document + ) + checkout.delete() + + class DocumentCheckoutManager(models.Manager): def are_document_new_versions_allowed(self, document, user=None): try: @@ -27,25 +71,6 @@ class DocumentCheckoutManager(models.Manager): else: return not check_out_info.block_new_version - def check_in_document(self, document, user=None): - try: - document_check_out = self.model.objects.get(document=document) - except self.model.DoesNotExist: - raise DocumentNotCheckedOut - else: - with transaction.atomic(): - if user: - if self.get_check_out_info(document=document).user != user: - event_document_forceful_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) - - document_check_out.delete() - def check_in_expired_check_outs(self): for document in self.expired_check_outs(): document.check_in() @@ -57,7 +82,11 @@ class DocumentCheckoutManager(models.Manager): ) def checked_out_documents(self): - return Document.objects.filter( + CheckedOutDocument = apps.get_model( + app_label='checkouts', model_name='CheckedOutDocument' + ) + + return CheckedOutDocument.objects.filter( pk__in=self.model.objects.values('document__id') ) @@ -74,7 +103,11 @@ class DocumentCheckoutManager(models.Manager): return STATE_CHECKED_IN def expired_check_outs(self): - expired_list = Document.objects.filter( + CheckedOutDocument = apps.get_model( + app_label='checkouts', model_name='CheckedOutDocument' + ) + + expired_list = CheckedOutDocument.objects.filter( pk__in=self.model.objects.filter( expiration_datetime__lte=now() ).values_list('document__pk', flat=True) @@ -83,9 +116,6 @@ class DocumentCheckoutManager(models.Manager): return expired_list 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: diff --git a/mayan/apps/checkouts/methods.py b/mayan/apps/checkouts/methods.py index 680f6cf4c7..3972807a8f 100644 --- a/mayan/apps/checkouts/methods.py +++ b/mayan/apps/checkouts/methods.py @@ -8,7 +8,7 @@ def method_check_in(self, user=None): app_label='checkouts', model_name='DocumentCheckout' ) - return DocumentCheckout.objects.check_in_document( + return DocumentCheckout.business_logic.check_in_document( document=self, user=user ) diff --git a/mayan/apps/checkouts/models.py b/mayan/apps/checkouts/models.py index c724e21db8..5efc322dff 100644 --- a/mayan/apps/checkouts/models.py +++ b/mayan/apps/checkouts/models.py @@ -14,7 +14,10 @@ from mayan.apps.documents.models import Document from .events import event_document_check_out from .exceptions import DocumentAlreadyCheckedOut -from .managers import DocumentCheckoutManager, NewVersionBlockManager +from .managers import ( + DocumentCheckoutBusinessLogicManager, DocumentCheckoutManager, + NewVersionBlockManager +) logger = logging.getLogger(__name__) @@ -49,6 +52,7 @@ class DocumentCheckout(models.Model): ) objects = DocumentCheckoutManager() + business_logic = DocumentCheckoutBusinessLogicManager() class Meta: ordering = ('pk',) @@ -81,13 +85,13 @@ class DocumentCheckout(models.Model): natural_key.dependencies = ['documents.Document'] def save(self, *args, **kwargs): - new_checkout = not self.pk - if not new_checkout or self.document.is_checked_out(): + is_new = not self.pk + if not is_new or self.document.is_checked_out(): raise DocumentAlreadyCheckedOut with transaction.atomic(): result = super(DocumentCheckout, self).save(*args, **kwargs) - if new_checkout: + if is_new: event_document_check_out.commit( actor=self.user, target=self.document ) @@ -119,3 +123,24 @@ class NewVersionBlock(models.Model): def natural_key(self): return self.document.natural_key() natural_key.dependencies = ['documents.Document'] + + +class CheckedOutDocument(Document): + class Meta: + proxy = True + + def get_user_display(self): + check_out_info = self.get_check_out_info() + return check_out_info.user.get_full_name() or check_out_info.user + + get_user_display.short_description = _('User') + + def get_checkout_datetime(self): + return self.get_check_out_info().checkout_datetime + + get_checkout_datetime.short_description = _('Checkout time and date') + + def get_checkout_expiration(self): + return self.get_check_out_info().expiration_datetime + + get_checkout_expiration.short_description = _('Checkout expiration') diff --git a/mayan/apps/checkouts/tests/mixins.py b/mayan/apps/checkouts/tests/mixins.py index 2bf2041ab8..840fee34bf 100644 --- a/mayan/apps/checkouts/tests/mixins.py +++ b/mayan/apps/checkouts/tests/mixins.py @@ -5,6 +5,7 @@ import datetime from django.utils.timezone import now from mayan.apps.common.literals import TIME_DELTA_UNIT_DAYS +from mayan.apps.common.tests.utils import as_id_list from ..models import DocumentCheckout @@ -12,7 +13,10 @@ from ..models import DocumentCheckout class DocumentCheckoutTestMixin(object): _test_document_check_out_seconds = 0.1 - def _check_out_test_document(self, user=None): + def _check_out_test_document(self, document=None, user=None): + if not document: + document = self.test_document + if not user: user = self._test_case_user @@ -21,7 +25,7 @@ class DocumentCheckoutTestMixin(object): ) self.test_check_out = DocumentCheckout.objects.check_out_document( - block_new_version=True, document=self.test_document, + block_new_version=True, document=document, expiration_datetime=self._check_out_expiration_datetime, user=user ) @@ -42,6 +46,13 @@ class DocumentCheckoutViewTestMixin(object): } ) + def _request_test_document_multiple_check_in_post_view(self): + return self.post( + viewname='checkouts:check_in_document_multiple', data={ + 'id_list': as_id_list(items=self.test_documents) + } + ) + def _request_test_document_check_out_view(self): return self.post( viewname='checkouts:check_out_document', kwargs={ @@ -53,6 +64,16 @@ class DocumentCheckoutViewTestMixin(object): } ) + def _request_test_document_multiple_check_out_post_view(self): + return self.post( + viewname='checkouts:check_out_document_multiple', data={ + 'block_new_version': True, + 'expiration_datetime_0': TIME_DELTA_UNIT_DAYS, + 'expiration_datetime_1': 2, + 'id_list': as_id_list(items=self.test_documents) + } + ) + def _request_test_document_check_out_detail_view(self): return self.get( viewname='checkouts:check_out_info', kwargs={ diff --git a/mayan/apps/checkouts/tests/test_models.py b/mayan/apps/checkouts/tests/test_models.py index cd2ea0202f..dbfdd64d50 100644 --- a/mayan/apps/checkouts/tests/test_models.py +++ b/mayan/apps/checkouts/tests/test_models.py @@ -7,8 +7,7 @@ from mayan.apps.documents.tests import GenericDocumentTestCase, DocumentTestMixi from mayan.apps.documents.tests.literals import TEST_SMALL_DOCUMENT_PATH from ..exceptions import ( - DocumentAlreadyCheckedOut, DocumentNotCheckedOut, - NewDocumentVersionNotAllowed + DocumentAlreadyCheckedOut, NewDocumentVersionNotAllowed ) from ..models import DocumentCheckout, NewVersionBlock @@ -49,10 +48,6 @@ class DocumentCheckoutTestCase(DocumentCheckoutTestMixin, GenericDocumentTestCas block_new_version=True ) - def test_checkin_without_checkout(self): - with self.assertRaises(DocumentNotCheckedOut): - self.test_document.check_in() - def test_auto_check_in(self): self._check_out_test_document() diff --git a/mayan/apps/checkouts/tests/test_views.py b/mayan/apps/checkouts/tests/test_views.py index ccfbef95af..3910dedd98 100644 --- a/mayan/apps/checkouts/tests/test_views.py +++ b/mayan/apps/checkouts/tests/test_views.py @@ -1,6 +1,5 @@ from __future__ import unicode_literals -from mayan.apps.common.literals import TIME_DELTA_UNIT_DAYS from mayan.apps.documents.permissions import permission_document_view from mayan.apps.documents.tests import GenericDocumentViewTestCase from mayan.apps.sources.links import link_document_version_upload @@ -23,8 +22,8 @@ class DocumentCheckoutViewTestCase( self._check_out_test_document() response = self._request_test_document_check_in_get_view() - self.assertContains( - response=response, text=self.test_document.label, status_code=200 + self.assertNotContains( + response=response, text=self.test_document.label, status_code=404 ) self.assertTrue(self.test_document.is_checked_out()) @@ -68,6 +67,86 @@ class DocumentCheckoutViewTestCase( ) ) + def test_document_multiple_check_in_post_view_no_permission(self): + # Upload second document + self.upload_document() + + self._check_out_test_document(document=self.test_documents[0]) + self._check_out_test_document(document=self.test_documents[1]) + + response = self._request_test_document_multiple_check_in_post_view() + self.assertEqual(response.status_code, 404) + + self.assertTrue(self.test_documents[0].is_checked_out()) + self.assertTrue(self.test_documents[1].is_checked_out()) + self.assertTrue( + DocumentCheckout.objects.is_checked_out( + document=self.test_documents[0] + ) + ) + self.assertTrue( + DocumentCheckout.objects.is_checked_out( + document=self.test_documents[1] + ) + ) + + def test_document_multiple_check_in_post_view_with_document_0_access(self): + # Upload second document + self.upload_document() + + self._check_out_test_document(document=self.test_documents[0]) + self._check_out_test_document(document=self.test_documents[1]) + + self.grant_access( + obj=self.test_documents[0], permission=permission_document_check_in + ) + + response = self._request_test_document_multiple_check_in_post_view() + self.assertEqual(response.status_code, 302) + + self.assertFalse(self.test_documents[0].is_checked_out()) + self.assertTrue(self.test_documents[1].is_checked_out()) + self.assertFalse( + DocumentCheckout.objects.is_checked_out( + document=self.test_documents[0] + ) + ) + self.assertTrue( + DocumentCheckout.objects.is_checked_out( + document=self.test_documents[1] + ) + ) + + def test_document_multiple_check_in_post_view_with_access(self): + # Upload second document + self.upload_document() + + self._check_out_test_document(document=self.test_documents[0]) + self._check_out_test_document(document=self.test_documents[1]) + + self.grant_access( + obj=self.test_documents[0], permission=permission_document_check_in + ) + self.grant_access( + obj=self.test_documents[1], permission=permission_document_check_in + ) + + response = self._request_test_document_multiple_check_in_post_view() + self.assertEqual(response.status_code, 302) + + self.assertFalse(self.test_documents[0].is_checked_out()) + self.assertFalse(self.test_documents[1].is_checked_out()) + self.assertFalse( + DocumentCheckout.objects.is_checked_out( + document=self.test_documents[0] + ) + ) + self.assertFalse( + DocumentCheckout.objects.is_checked_out( + document=self.test_documents[1] + ) + ) + def test_document_check_out_view_no_permission(self): response = self._request_test_document_check_out_view() self.assertEqual(response.status_code, 404) @@ -88,6 +167,102 @@ class DocumentCheckoutViewTestCase( self.assertTrue(self.test_document.is_checked_out()) + def test_document_multiple_check_out_post_view_no_permission(self): + # Upload second document + self.upload_document() + + self.grant_access( + obj=self.test_documents[0], + permission=permission_document_check_out_detail_view + ) + self.grant_access( + obj=self.test_documents[1], + permission=permission_document_check_out_detail_view + ) + + response = self._request_test_document_multiple_check_out_post_view() + self.assertEqual(response.status_code, 404) + + self.assertFalse(self.test_documents[0].is_checked_out()) + self.assertFalse(self.test_documents[1].is_checked_out()) + self.assertFalse( + DocumentCheckout.objects.is_checked_out( + document=self.test_documents[0] + ) + ) + self.assertFalse( + DocumentCheckout.objects.is_checked_out( + document=self.test_documents[1] + ) + ) + + def test_document_multiple_check_out_post_view_with_document_0_access(self): + # Upload second document + self.upload_document() + + self.grant_access( + obj=self.test_documents[0], permission=permission_document_check_out + ) + self.grant_access( + obj=self.test_documents[0], + permission=permission_document_check_out_detail_view + ) + self.grant_access( + obj=self.test_documents[1], + permission=permission_document_check_out_detail_view + ) + + response = self._request_test_document_multiple_check_out_post_view() + self.assertEqual(response.status_code, 302) + + self.assertTrue(self.test_documents[0].is_checked_out()) + self.assertFalse(self.test_documents[1].is_checked_out()) + self.assertTrue( + DocumentCheckout.objects.is_checked_out( + document=self.test_documents[0] + ) + ) + self.assertFalse( + DocumentCheckout.objects.is_checked_out( + document=self.test_documents[1] + ) + ) + + def test_document_multiple_check_out_post_view_with_access(self): + # Upload second document + self.upload_document() + + self.grant_access( + obj=self.test_documents[0], permission=permission_document_check_out + ) + self.grant_access( + obj=self.test_documents[1], permission=permission_document_check_out + ) + self.grant_access( + obj=self.test_documents[0], + permission=permission_document_check_out_detail_view + ) + self.grant_access( + obj=self.test_documents[1], + permission=permission_document_check_out_detail_view + ) + + response = self._request_test_document_multiple_check_out_post_view() + self.assertEqual(response.status_code, 302) + + self.assertTrue(self.test_documents[0].is_checked_out()) + self.assertTrue(self.test_documents[1].is_checked_out()) + self.assertTrue( + DocumentCheckout.objects.is_checked_out( + document=self.test_documents[0] + ) + ) + self.assertTrue( + DocumentCheckout.objects.is_checked_out( + document=self.test_documents[1] + ) + ) + def test_document_check_out_detail_view_no_permission(self): self._check_out_test_document() @@ -177,45 +352,39 @@ class DocumentCheckoutViewTestCase( self.assertEqual(resolved_link, None) - def test_document_forcefull_check_in_view_no_permission(self): + def test_document_check_in_forcefull_view_no_permission(self): # Gitlab issue #237 # Forcefully checking in a document by a user without adequate # permissions throws out an error - self._create_test_case_superuser() - self._check_out_test_document(user=self._test_case_superuser) + self._create_test_user() + self._check_out_test_document(user=self.test_user) self.grant_access( obj=self.test_document, permission=permission_document_check_in ) - response = self.post( - viewname='checkouts:check_in_document', kwargs={ - 'pk': self.test_document.pk - } - ) - self.assertContains( - response=response, text='Insufficient permissions', status_code=403 - ) - - self.assertTrue(self.test_document.is_checked_out()) - - 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) - - self.grant_access( - obj=self.test_document, permission=permission_document_check_in - ) - self.grant_access( - obj=self.test_document, permission=permission_document_check_in_override - ) - response = self.post( viewname='checkouts:check_in_document', kwargs={ 'pk': self.test_document.pk } ) self.assertEqual(response.status_code, 302) + self.assertTrue(self.test_document.is_checked_out()) + def test_document_check_in_forcefull_view_with_access(self): + self._create_test_user() + self._check_out_test_document(user=self.test_user) + + self.grant_access( + obj=self.test_document, + permission=permission_document_check_in_override + ) + + response = self.post( + viewname='checkouts:check_in_document', kwargs={ + 'pk': self.test_document.pk + } + ) + self.assertEqual(response.status_code, 302) self.assertFalse(self.test_document.is_checked_out()) diff --git a/mayan/apps/checkouts/views.py b/mayan/apps/checkouts/views.py index 93f54a8190..440c73a5f2 100644 --- a/mayan/apps/checkouts/views.py +++ b/mayan/apps/checkouts/views.py @@ -1,21 +1,16 @@ 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 _, ungettext from mayan.apps.acls.models import AccessControlList from mayan.apps.common.generics import ( - ConfirmView, MultipleObjectConfirmActionView, MultipleObjectFormActionView, - 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 DocumentCheckoutForm, DocumentCheckoutDefailForm from .icons import icon_check_out_info from .models import DocumentCheckout @@ -25,69 +20,9 @@ from .permissions import ( ) -""" -class DocumentCheckinView(ConfirmView): - def get_extra_context(self): - document = self.get_object() - - context = { - 'object': document, - } - - if document.get_check_out_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['pk']) - - def get_post_action_redirect(self): - return reverse( - viewname='checkouts:check_out_info', kwargs={ - 'pk': self.get_object().pk - } - ) - - def view_action(self): - document = self.get_object() - - if document.get_check_out_info().user == self.request.user: - AccessControlList.objects.check_access( - obj=document, permissions=(permission_document_check_in,), - user=self.request.user - ) - else: - AccessControlList.objects.check_access( - obj=document, - permissions=(permission_document_check_in_override,), - user=self.request.user - ) - - try: - document.check_in(user=self.request.user) - except DocumentNotCheckedOut: - messages.error( - message=_('Document has not been checked out.'), - request=self.request - ) - else: - messages.success( - message=_( - '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.' @@ -126,63 +61,30 @@ class DocumentCheckinView(MultipleObjectConfirmActionView): else: super(DocumentCheckinView, self).get_post_action_redirect() + def get_source_queryset(self): + # object_permission is None to disable restricting queryset mixin + # and restrict the queryset ourselves from two permissions + + source_queryset = super(DocumentCheckinView, self).get_source_queryset() + + check_in_queryset = AccessControlList.objects.restrict_queryset( + permission=permission_document_check_in, queryset=source_queryset, + user=self.request.user + ) + + check_in_override_queryset = AccessControlList.objects.restrict_queryset( + permission=permission_document_check_in_override, + queryset=source_queryset, user=self.request.user + ) + + return check_in_queryset | check_in_override_queryset + def object_action(self, form, instance): - DocumentCheckout.objects.check_in_document( + DocumentCheckout.business_logic.check_in_document( document=instance, user=self.request.user ) - - -""" -class CheckoutDocumentView(SingleObjectCreateView): - form_class = DocumentCheckoutForm - - def dispatch(self, request, *args, **kwargs): - self.document = get_object_or_404(klass=Document, pk=self.kwargs['pk']) - - AccessControlList.objects.check_access( - obj=self.document, permissions=(permission_document_check_out,), - user=request.user - ) - - 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( - message=_('Document already checked out.'), - request=self.request - ) - else: - messages.success( - message=_( - 'Document "%s" checked out successfully.' - ) % self.document, request=self.request - ) - - return HttpResponseRedirect(redirect_to=self.get_success_url()) - - def get_extra_context(self): - return { - 'object': self.document, - 'title': _('Check out document: %s') % self.document - } - - def get_post_action_redirect(self): - return reverse( - viewname='checkouts:check_out_info', kwargs={ - 'pk': self.document.pk - } - ) -""" class DocumentCheckoutView(MultipleObjectFormActionView): error_message = 'Unable to checkout document "%(instance)s". %(exception)s' form_class = DocumentCheckoutForm @@ -261,26 +163,6 @@ class DocumentCheckoutListView(DocumentListView): context = super(DocumentCheckoutListView, self).get_extra_context() context.update( { - 'extra_columns': ( - { - 'name': _('User'), - 'attribute': encapsulate( - lambda document: document.get_check_out_info().user.get_full_name() or document.get_check_out_info().user - ) - }, - { - 'name': _('Checkout time and date'), - 'attribute': encapsulate( - lambda document: document.get_check_out_info().checkout_datetime - ) - }, - { - 'name': _('Checkout expiration'), - 'attribute': encapsulate( - lambda document: document.get_check_out_info().expiration_datetime - ) - }, - ), 'no_results_icon': icon_check_out_info, 'no_results_text': _( 'Checking out a document, blocks certain operations '