Files
mayan-edms/mayan/apps/documents/views/document_views.py
Roberto Rosario 74dfa53787 Update documents app
Rename the DeletedDocument proxy model to a TrashedDocument.

Rename the deleted_document views to trashed_document.

Rename the document and deleted_document URL parameters to
trashed_document.

Update URL parameters to the '_id' form.

Add keyword arguments.

Update use of .filter_by_access().

Enclose trashed document restore method in a transaction.

Sort arguments.

Update app for compliance with MERCs 5 and 6.

Add document page view tests.

Add favorite document view tests.

Movernize tests.

Replace use of urlencode with furl.

Update views to use ExternalObjectMixin.

Refactor the document and version download views.

Rename the DocumentDocumentTypeEditView to DocumentChangeTypeView.

Move the trashed document views to their own module.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-28 05:25:48 -04:00

767 lines
25 KiB
Python

from __future__ import absolute_import, unicode_literals
import logging
from furl import furl
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.encoding import force_text
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ungettext
from mayan.apps.acls.models import AccessControlList
from mayan.apps.common.compressed_files import ZipArchive
from mayan.apps.common.exceptions import ActionError
from mayan.apps.common.generics import (
FormView, MultipleObjectConfirmActionView, MultipleObjectDownloadView,
MultipleObjectFormActionView, SingleObjectDetailView, SingleObjectEditView,
SingleObjectListView
)
from mayan.apps.common.mixins import ExternalObjectMixin
from mayan.apps.converter.models import Transformation
from mayan.apps.converter.permissions import (
permission_transformation_delete, permission_transformation_edit
)
from ..events import event_document_download, event_document_view
from ..forms import (
DocumentDownloadForm, DocumentForm, DocumentPageNumberForm,
DocumentPreviewForm, DocumentPrintForm, DocumentPropertiesForm,
DocumentTypeFilteredSelectForm
)
from ..icons import (
icon_document_list, icon_document_list_favorites,
icon_document_list_recent_access, icon_document_list_recent_added,
icon_duplicated_document_list
)
from ..literals import DEFAULT_ZIP_FILENAME, PAGE_RANGE_RANGE
from ..models import (
Document, DuplicatedDocument, FavoriteDocument, RecentDocument
)
from ..permissions import (
permission_document_download, permission_document_print,
permission_document_properties_edit, permission_document_tools,
permission_document_view
)
from ..settings import (
setting_favorite_count, setting_print_height, setting_print_width,
setting_recent_added_count
)
from ..tasks import task_update_page_count
from ..utils import parse_range
__all__ = (
'DocumentChangeTypeView', 'DocumentDownloadFormView',
'DocumentDownloadView', 'DocumentDuplicatesListView', 'DocumentEditView',
'DocumentListView', 'DocumentPreviewView', 'DocumentPrintView',
'DocumentTransformationsClearView', 'DocumentTransformationsCloneView',
'DocumentUpdatePageCountView', 'DocumentView', 'DuplicatedDocumentListView',
'FavoriteAddView', 'FavoriteDocumentListView', 'FavoriteRemoveView',
'RecentAccessDocumentListView', 'RecentAddedDocumentListView'
)
logger = logging.getLogger(__name__)
class DocumentChangeTypeView(MultipleObjectFormActionView):
form_class = DocumentTypeFilteredSelectForm
model = Document
object_permission = permission_document_properties_edit
pk_url_kwarg = 'document_id'
success_message = _(
'Document type change request performed on %(count)d document'
)
success_message_plural = _(
'Document type change request performed on %(count)d documents'
)
def get_extra_context(self):
queryset = self.get_object_list()
result = {
'submit_label': _('Change'),
'title': ungettext(
singular='Change the type of the selected document',
plural='Change the type of the selected documents',
number=queryset.count()
)
}
if queryset.count() == 1:
result.update(
{
'object': queryset.first(),
'title': _(
'Change the type of the document: %s'
) % queryset.first()
}
)
return result
def get_form_extra_kwargs(self):
result = {
'user': self.request.user
}
return result
def object_action(self, form, instance):
instance.set_document_type(
document_type=form.cleaned_data['document_type'],
_user=self.request.user
)
messages.success(
message=_(
'Document type for "%s" changed successfully.'
) % instance, request=self.request
)
class DocumentDownloadFormView(MultipleObjectFormActionView):
form_class = DocumentDownloadForm
model = Document
object_permission = permission_document_download
pk_url_kwarg = 'document_id'
querystring_form_fields = ('compressed', 'zip_filename')
viewname = 'documents:document_multiple_download'
def form_valid(self, form):
# Turn a queryset into a comma separated list of primary keys
id_list = ','.join(
[
force_text(pk) for pk in self.get_object_list().values_list('pk', flat=True)
]
)
# Construct URL with querystring to pass on to the next view
url = furl(
args={
'id_list': id_list
}, path=reverse(viewname=self.viewname)
)
# Pass the form field data as URL querystring to the next view
for field in self.querystring_form_fields:
data = form.cleaned_data[field]
if data:
url.args['field'] = data
return HttpResponseRedirect(redirect_to=url.tostr())
def get_extra_context(self):
context = {
'submit_label': _('Download'),
'title': _('Download documents'),
}
if self.queryset.count() == 1:
context['object'] = self.queryset.first()
return context
def get_form_kwargs(self):
kwargs = super(DocumentDownloadFormView, self).get_form_kwargs()
self.queryset = self.get_object_list()
kwargs.update({'queryset': self.queryset})
return kwargs
class DocumentDownloadView(MultipleObjectDownloadView):
model = Document
object_permission = permission_document_download
pk_url_kwarg = 'document_id'
@staticmethod
def commit_event(item, request):
event_document_download.commit(
actor=request.user,
target=item
)
@staticmethod
def get_item_file(item):
return item.open()
def get_file(self):
queryset = self.get_object_list()
zip_filename = self.request.GET.get(
'zip_filename', DEFAULT_ZIP_FILENAME
)
if self.request.GET.get('compressed') == 'True' or queryset.count() > 1:
compressed_file = ZipArchive()
compressed_file.create()
for item in queryset:
with DocumentDownloadView.get_item_file(item=item) as file_object:
compressed_file.add_file(
file_object=file_object,
filename=self.get_item_label(item=item)
)
DocumentDownloadView.commit_event(
item=item, request=self.request
)
compressed_file.close()
return DocumentDownloadView.VirtualFile(
compressed_file.as_file(zip_filename), name=zip_filename
)
else:
item = queryset.first()
DocumentDownloadView.commit_event(
item=item, request=self.request
)
return DocumentDownloadView.VirtualFile(
DocumentDownloadView.get_item_file(item=item),
name=self.get_item_label(item=item)
)
def get_item_label(self, item):
return item.label
class DocumentListView(SingleObjectListView):
object_permission = permission_document_view
def get_context_data(self, **kwargs):
try:
return super(DocumentListView, self).get_context_data(**kwargs)
except Exception as exception:
messages.error(
message=_(
'Error retrieving document list: %(exception)s.'
) % {
'exception': exception
}, request=self.request
)
self.object_list = Document.objects.none()
return super(DocumentListView, self).get_context_data(**kwargs)
def get_document_queryset(self):
return Document.objects.defer(
'description', 'uuid', 'date_added', 'language', 'in_trash',
'deleted_date_time'
).all()
def get_extra_context(self):
return {
'hide_links': True,
'hide_object': True,
'list_as_items': True,
'no_results_icon': icon_document_list,
'no_results_text': _(
'This could mean that no documents have been uploaded or '
'that your user account has not been granted the view '
'permission for any document or document type.'
),
'no_results_title': _('No documents available'),
'table_cell_container_classes': 'td-container-thumbnail',
'title': _('All documents'),
}
def get_source_queryset(self):
return self.get_document_queryset()
class DocumentDuplicatesListView(ExternalObjectMixin, DocumentListView):
external_object_class = Document
external_object_permission = permission_document_view
external_object_pk_url_kwarg = 'document_id'
def get_document(self):
document = self.get_external_object()
document.add_as_recent_document_for_user(user=self.request.user)
return document
def get_extra_context(self):
context = super(DocumentDuplicatesListView, self).get_extra_context()
context.update(
{
'no_results_icon': icon_duplicated_document_list,
'no_results_text': _(
'Only exact copies of this document will be shown in the '
'this list.'
),
'no_results_title': _(
'There are no duplicates for this document'
),
'object': self.get_document(),
'title': _('Duplicates for document: %s') % self.get_document(),
}
)
return context
def get_source_queryset(self):
return self.get_document().get_duplicates()
class DocumentEditView(SingleObjectEditView):
form_class = DocumentForm
model = Document
object_permission = permission_document_properties_edit
pk_url_kwarg = 'document_id'
def dispatch(self, request, *args, **kwargs):
result = super(
DocumentEditView, self
).dispatch(request, *args, **kwargs)
self.get_object().add_as_recent_document_for_user(request.user)
return result
def get_extra_context(self):
return {
'object': self.get_object(),
'title': _('Edit properties of document: %s') % self.get_object(),
}
def get_save_extra_data(self):
return {
'_user': self.request.user
}
def get_post_action_redirect(self):
return reverse(
viewname='documents:document_properties',
kwargs={'document_id': self.get_object().pk}
)
class DocumentPreviewView(SingleObjectDetailView):
form_class = DocumentPreviewForm
model = Document
object_permission = permission_document_view
pk_url_kwarg = 'document_id'
def dispatch(self, request, *args, **kwargs):
result = super(
DocumentPreviewView, self
).dispatch(request, *args, **kwargs)
self.get_object().add_as_recent_document_for_user(request.user)
event_document_view.commit(
actor=request.user, target=self.get_object()
)
return result
def get_extra_context(self):
return {
'hide_labels': True,
'object': self.get_object(),
'title': _('Preview of document: %s') % self.get_object(),
}
class DocumentView(SingleObjectDetailView):
form_class = DocumentPropertiesForm
model = Document
object_permission = permission_document_view
pk_url_kwarg = 'document_id'
def dispatch(self, request, *args, **kwargs):
result = super(DocumentView, self).dispatch(request, *args, **kwargs)
self.get_object().add_as_recent_document_for_user(request.user)
return result
def get_extra_context(self):
return {
'document': self.get_object(),
'object': self.get_object(),
'title': _('Properties for document: %s') % self.get_object(),
}
class DocumentUpdatePageCountView(MultipleObjectConfirmActionView):
model = Document
object_permission = permission_document_tools
pk_url_kwarg = 'document_id'
success_message = _(
'%(count)d document queued for page count recalculation'
)
success_message_plural = _(
'%(count)d documents queued for page count recalculation'
)
def get_extra_context(self):
queryset = self.get_queryset()
result = {
'title': ungettext(
'Recalculate the page count of the selected document?',
'Recalculate the page count of the selected documents?',
queryset.count()
)
}
if queryset.count() == 1:
result.update(
{
'object': queryset.first(),
'title': _(
'Recalculate the page count of the document: %s?'
) % queryset.first()
}
)
return result
def object_action(self, form, instance):
latest_version = instance.latest_version
if latest_version:
task_update_page_count.apply_async(
kwargs={'version_id': latest_version.pk}
)
else:
messages.error(
message=_(
'Document "%(document)s" is empty. Upload at least one '
'document version before attempting to detect the '
'page count.'
) % {
'document': instance,
}, request=self.request
)
class DocumentTransformationsClearView(MultipleObjectConfirmActionView):
model = Document
object_permission = permission_transformation_delete
pk_url_kwarg = 'document_id'
success_message = _(
'Transformation clear request processed for %(count)d document'
)
success_message_plural = _(
'Transformation clear request processed for %(count)d documents'
)
def get_extra_context(self):
queryset = self.get_queryset()
result = {
'title': ungettext(
'Clear all the page transformations for the selected document?',
'Clear all the page transformations for the selected document?',
queryset.count()
)
}
if queryset.count() == 1:
result.update(
{
'object': queryset.first(),
'title': _(
'Clear all the page transformations for the '
'document: %s?'
) % queryset.first()
}
)
return result
def object_action(self, form, instance):
try:
for page in instance.pages.all():
Transformation.objects.get_for_model(page).delete()
except Exception as exception:
messages.error(
message=_(
'Error deleting the page transformations for '
'document: %(document)s; %(error)s.'
) % {
'document': instance, 'error': exception
}, request=self.request
)
class DocumentTransformationsCloneView(ExternalObjectMixin, FormView):
external_object_class = Document
external_object_permission = permission_transformation_edit
external_object_pk_url_kwarg = 'document_id'
form_class = DocumentPageNumberForm
def form_valid(self, form):
instance = self.get_object()
try:
target_pages = instance.pages.exclude(
pk=form.cleaned_data['page'].pk
)
for page in target_pages:
Transformation.objects.get_for_model(obj=page).delete()
Transformation.objects.copy(
source=form.cleaned_data['page'], targets=target_pages
)
except Exception as exception:
messages.error(
message=_(
'Error deleting the page transformations for '
'document: %(document)s; %(error)s.'
) % {
'document': instance, 'error': exception
}, request=self.request
)
else:
messages.success(
message=_(
'Transformations cloned successfully.'
), request=self.request
)
return super(DocumentTransformationsCloneView, self).form_valid(form=form)
def get_extra_context(self):
instance = self.get_object()
context = {
'object': instance,
'submit_label': _('Submit'),
'title': _(
'Clone page transformations for document: %s'
) % instance,
}
return context
def get_form_extra_kwargs(self):
return {
'document': self.get_object()
}
def get_object(self):
document = self.get_external_object()
document.add_as_recent_document_for_user(user=self.request.user)
return document
class DocumentPrintView(FormView):
form_class = DocumentPrintForm
def dispatch(self, request, *args, **kwargs):
self.page_group = self.request.GET.get('page_group')
self.page_range = self.request.GET.get('page_range')
return super(DocumentPrintView, self).dispatch(request, *args, **kwargs)
def get(self, request, *args, **kwargs):
if not self.page_group and not self.page_range:
return super(DocumentPrintView, self).get(request, *args, **kwargs)
else:
instance = self.get_object()
if self.page_group == PAGE_RANGE_RANGE:
if self.page_range:
page_range = parse_range(self.page_range)
pages = instance.pages.filter(page_number__in=page_range)
else:
pages = instance.pages.all()
else:
pages = instance.pages.all()
context = self.get_context_data()
context.update(
{
'appearance_type': 'plain',
'pages': pages,
'height': setting_print_height.value,
'width': setting_print_width.value
}
)
return self.render_to_response(context=context)
def get_extra_context(self):
instance = self.get_object()
context = {
'form_action': reverse(
viewname='documents:document_print',
kwargs={'document_id': instance.pk}
),
'object': instance,
'submit_label': _('Submit'),
'submit_method': 'GET',
'submit_target': '_blank',
'title': _('Print: %s') % instance,
}
return context
def get_object(self):
obj = get_object_or_404(
klass=self.get_object_list(), pk=self.kwargs['document_id']
)
obj.add_as_recent_document_for_user(user=self.request.user)
return obj
def get_object_list(self):
return AccessControlList.objects.restrict_queryset(
permission=permission_document_print, queryset=Document.objects.all(),
user=self.request.user
)
def get_template_names(self):
if self.page_group or self.page_range:
return ('documents/document_print.html',)
else:
return (self.template_name,)
class DuplicatedDocumentListView(DocumentListView):
def get_document_queryset(self):
return DuplicatedDocument.objects.get_duplicated_documents()
def get_extra_context(self):
context = super(DuplicatedDocumentListView, self).get_extra_context()
context.update(
{
'no_results_icon': icon_duplicated_document_list,
'no_results_text': _(
'Duplicates are documents that are composed of the exact '
'same file, down to the last byte. Files that have the '
'same text or OCR but are not identical or were saved '
'using a different file format will not appear as '
'duplicates.'
),
'no_results_title': _(
'There are no duplicated documents'
),
'title': _('Duplicated documents')
}
)
return context
class FavoriteDocumentListView(DocumentListView):
def get_document_queryset(self):
return FavoriteDocument.objects.get_for_user(user=self.request.user)
def get_extra_context(self):
context = super(FavoriteDocumentListView, self).get_extra_context()
context.update(
{
'no_results_icon': icon_document_list_favorites,
'no_results_text': _(
'Favorited documents will be listed in this view. '
'Up to %(count)d documents can be favorited per user. '
) % {'count': setting_favorite_count.value},
'no_results_title': _('There are no favorited documents.'),
'title': _('Favorites'),
}
)
return context
class FavoriteAddView(MultipleObjectConfirmActionView):
model = Document
object_permission = permission_document_view
success_message = _(
'%(count)d document added to favorites.'
)
success_message_plural = _(
'%(count)d documents added to favorites.'
)
def get_extra_context(self):
queryset = self.get_queryset()
return {
'submit_label': _('Add'),
'submit_icon_class': icon_document_list_favorites,
'title': ungettext(
singular='Add the selected document to favorites',
plural='Add the selected documents to favorites',
number=queryset.count()
)
}
def object_action(self, form, instance):
FavoriteDocument.objects.add_for_user(
document=instance, user=self.request.user
)
class FavoriteRemoveView(MultipleObjectConfirmActionView):
error_message = _('Document "%(instance)s" is not in favorites.')
model = Document
object_permission = permission_document_view
success_message = _(
'%(count)d document removed from favorites.'
)
success_message_plural = _(
'%(count)d documents removed from favorites.'
)
def get_extra_context(self):
queryset = self.get_object_list()
return {
'submit_label': _('Remove'),
'submit_icon_class': icon_document_list_favorites,
'title': ungettext(
singular='Remove the selected document from favorites',
plural='Remove the selected documents from favorites',
number=queryset.count()
)
}
def object_action(self, form, instance):
try:
FavoriteDocument.objects.remove_for_user(
document=instance, user=self.request.user
)
except FavoriteDocument.DoesNotExist:
raise ActionError
class RecentAccessDocumentListView(DocumentListView):
def get_document_queryset(self):
return RecentDocument.objects.get_for_user(user=self.request.user)
def get_extra_context(self):
context = super(RecentAccessDocumentListView, self).get_extra_context()
context.update(
{
'no_results_icon': icon_document_list_recent_access,
'no_results_text': _(
'This view will list the latest documents viewed or '
'manipulated in any way by this user account.'
),
'no_results_title': _(
'There are no recently accessed document'
),
'title': _('Recently accessed'),
}
)
return context
class RecentAddedDocumentListView(DocumentListView):
def get_document_queryset(self):
ids = Document.objects.order_by('-date_added')[
:setting_recent_added_count.value
].values_list('pk', flat=True)
return Document.objects.filter(pk__in=ids).order_by('-date_added')
def get_extra_context(self):
context = super(RecentAddedDocumentListView, self).get_extra_context()
context.update(
{
'no_results_icon': icon_document_list_recent_added,
'no_results_text': _(
'This view will list the latest documents uploaded '
'in the system.'
),
'no_results_title': _(
'There are no recently added document'
),
'title': _('Recently added'),
}
)
return context