Files
mayan-edms/mayan/apps/common/generics.py
2019-04-28 00:57:54 -04:00

813 lines
29 KiB
Python

from __future__ import absolute_import, unicode_literals
from django.conf import settings
from django.contrib import messages
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ImproperlyConfigured
from django.db import transaction
from django.http import HttpResponseRedirect
from django.urls import reverse
from django.utils.encoding import force_text
from django.utils.translation import ugettext_lazy as _
from django.views.generic import (
FormView as DjangoFormView, DetailView, TemplateView
)
from django.views.generic.detail import SingleObjectMixin
from django.views.generic.edit import (
CreateView, DeleteView, FormMixin, ModelFormMixin, UpdateView
)
from django.views.generic.list import ListView
from django_downloadview import (
TextIteratorIO, VirtualDownloadView, VirtualFile
)
from pure_pagination.mixins import PaginationMixin
from mayan.apps.acls.models import AccessControlList
from .forms import ChoiceForm
from .icons import (
icon_add_all, icon_remove_all, icon_assign_remove_add,
icon_assign_remove_remove, icon_sort_down, icon_sort_up
)
from .literals import (
TEXT_SORT_FIELD_PARAMETER, TEXT_SORT_FIELD_VARIABLE_NAME,
TEXT_SORT_ORDER_CHOICE_ASCENDING, TEXT_SORT_ORDER_PARAMETER,
TEXT_SORT_ORDER_VARIABLE_NAME
)
from .mixins import (
DeleteExtraDataMixin, DynamicFormViewMixin, ExternalObjectMixin,
ExtraContextMixin, FormExtraKwargsMixin, MultipleObjectMixin,
ObjectActionMixin, ObjectListPermissionFilterMixin, ObjectNameMixin,
ObjectPermissionCheckMixin, RedirectionMixin, RestrictedQuerysetMixin,
ViewPermissionCheckMixin
)
from .settings import setting_paginate_by
__all__ = (
'AssignRemoveView', 'ConfirmView', 'FormView', 'MultiFormView',
'MultipleObjectConfirmActionView', 'MultipleObjectFormActionView',
'SingleObjectCreateView', 'SingleObjectDeleteView',
'SingleObjectDetailView', 'SingleObjectEditView', 'SingleObjectListView',
'SimpleView'
)
# Required by other views, moved to the top
class MultiFormView(DjangoFormView):
prefix = None
prefixes = {}
template_name = 'appearance/generic_form.html'
def _create_form(self, form_name, klass):
form_kwargs = self.get_form_kwargs(form_name)
form_create_method = 'create_%s_form' % form_name
if hasattr(self, form_create_method):
form = getattr(self, form_create_method)(**form_kwargs)
else:
form = klass(**form_kwargs)
return form
def all_forms_valid(self, forms):
return None
def dispatch(self, request, *args, **kwargs):
form_classes = self.get_form_classes()
self.forms = self.get_forms(form_classes)
return super(MultiFormView, self).dispatch(request, *args, **kwargs)
def forms_valid(self, forms):
for form_name, form in forms.items():
form_valid_method = '%s_form_valid' % form_name
if hasattr(self, form_valid_method):
return getattr(self, form_valid_method)(form)
self.all_forms_valid(forms)
return HttpResponseRedirect(redirect_to=self.get_success_url())
def forms_invalid(self, forms):
return self.render_to_response(self.get_context_data(forms=forms))
def get_context_data(self, **kwargs):
"""
Insert the form into the context dict.
"""
if 'forms' not in kwargs:
kwargs['forms'] = self.get_forms(
form_classes=self.get_form_classes()
)
return super(FormMixin, self).get_context_data(**kwargs)
def get_form_classes(self):
return self.form_classes
def get_form_kwargs(self, form_name):
kwargs = {}
kwargs.update({'initial': self.get_initial(form_name)})
kwargs.update({'prefix': self.get_prefix(form_name)})
if self.request.method in ('POST', 'PUT'):
kwargs.update({
'data': self.request.POST,
'files': self.request.FILES,
})
kwargs.update(self.get_form_extra_kwargs(form_name=form_name) or {})
return kwargs
def get_form_extra_kwargs(self, form_name):
return None
def get_forms(self, form_classes):
return dict(
[
(
key, self._create_form(key, klass)
) for key, klass in form_classes.items()
]
)
def get_initial(self, form_name):
initial_method = 'get_%s_initial' % form_name
if hasattr(self, initial_method):
return getattr(self, initial_method)()
else:
return self.initial.copy()
def get_prefix(self, form_name):
return self.prefixes.get(form_name, self.prefix)
def post(self, request, *args, **kwargs):
if all([form.is_valid() for form in self.forms.values()]):
return self.forms_valid(forms=self.forms)
else:
return self.forms_invalid(forms=self.forms)
class AddRemoveView(ExternalObjectMixin, ExtraContextMixin, ViewPermissionCheckMixin, RestrictedQuerysetMixin, MultiFormView):
form_classes = {'form_available': ChoiceForm, 'form_added': ChoiceForm}
list_added_help_text = _(
'Select entries to be removed. Hold Control to select multiple '
'entries. Once the selection is complete, click the button below '
'or double click the list to activate the action.'
)
list_available_help_text = _(
'Select entries to be added. Hold Control to select multiple '
'entries. Once the selection is complete, click the button below '
'or double click the list to activate the action.'
)
# Form titles
list_added_title = None
list_available_title = None
# Attributes to filter the object to which selections will be added or
# remove
main_object_model = None
main_object_permission = None
main_object_pk_url_kwarg = None
main_object_pk_url_kwargs = None
main_object_source_queryset = None
# Attributes to filter the queryset of the selection
secondary_object_model = None
secondary_object_permission = None
secondary_object_source_queryset = None
# Main object methods to use to add and remove selections
main_object_method_add = None
main_object_method_remove = None
# If a method is not specified, use this related field to add and remove
# selections
related_field = None
prefixes = {'form_available': 'available', 'form_added': 'added'}
def __init__(self, *args, **kwargs):
self.external_object_class = self.main_object_model
self.external_object_permission = self.main_object_permission
self.external_object_pk_url_kwarg = self.main_object_pk_url_kwarg
self.external_object_pk_url_kwargs = self.main_object_pk_url_kwargs
self.external_object_queryset = self.main_object_source_queryset
super(AddRemoveView, self).__init__(*args, **kwargs)
def _action_add(self, queryset):
kwargs = {'queryset': queryset}
kwargs.update(self.get_action_add_extra_kwargs())
kwargs.update(self.get_actions_extra_kwargs())
if hasattr(self, 'action_add'):
with transaction.atomic():
self.action_add(**kwargs)
elif self.main_object_method_add:
getattr(self.main_object, self.main_object_method_add)(**kwargs)
elif self.related_field:
getattr(self.main_object, self.related_field).add(*queryset)
else:
raise ImproperlyConfigured(
'View %s must be called with a main_object_method_add, a '
'related_field, or an action_add '
'method.' % self.__class__.__name__
)
def _action_remove(self, queryset):
kwargs = {'queryset': queryset}
kwargs.update(self.get_action_remove_extra_kwargs())
kwargs.update(self.get_actions_extra_kwargs())
if hasattr(self, 'action_remove'):
with transaction.atomic():
self.action_remove(**kwargs)
elif self.main_object_method_remove:
getattr(self.main_object, self.main_object_method_remove)(**kwargs)
elif self.related_field:
getattr(self.main_object, self.related_field).remove(*queryset)
else:
raise ImproperlyConfigured(
'View %s must be called with a main_object_method_remove, a '
'related_field, or an action_remove '
'method.' % self.__class__.__name__
)
def dispatch(self, request, *args, **kwargs):
self.main_object = self.get_external_object()
result = super(AddRemoveView, self).dispatch(request=request, *args, **kwargs)
return result
def forms_valid(self, forms):
if 'available-add_all' in self.request.POST:
selection_add = self.get_list_available_queryset()
else:
selection_add = self.get_list_available_queryset().filter(
pk__in=forms['form_available'].cleaned_data['selection']
)
self._action_add(queryset=selection_add)
if 'added-remove_all' in self.request.POST:
selection_remove = self.get_list_added_queryset()
else:
selection_remove = self.get_list_added_queryset().filter(
pk__in=forms['form_added'].cleaned_data['selection']
)
self._action_remove(queryset=selection_remove)
return super(AddRemoveView, self).forms_valid(forms=forms)
def generate_choices(self, queryset):
for obj in queryset:
yield (obj.pk, force_text(obj))
def get_action_add_extra_kwargs(self):
# Keyword arguments to apply to the add method
return {}
def get_action_remove_extra_kwargs(self):
# Keyword arguments to apply to the remove method
return {}
def get_actions_extra_kwargs(self):
# Keyword arguments to apply to both the add and remove methods
return {}
def get_context_data(self, **kwargs):
# Use get_context_data to leave the get_extra_context for subclasses
context = super(AddRemoveView, self).get_context_data(**kwargs)
context.update(
{
'subtemplates_list': [
{
'name': 'appearance/generic_form_subtemplate.html',
'column_class': 'col-xs-12 col-sm-6 col-md-6 col-lg-6',
'context': {
'extra_buttons': [
{
'label': _('Add all'),
'icon_class': icon_add_all,
'name': 'add_all',
}
],
'form': self.forms['form_available'],
'form_css_classes': 'form-hotkey-double-click',
'hide_labels': True,
'submit_icon_class': icon_assign_remove_add,
'submit_label': _('Add'),
'title': self.list_available_title or ' ',
}
},
{
'name': 'appearance/generic_form_subtemplate.html',
'column_class': 'col-xs-12 col-sm-6 col-md-6 col-lg-6',
'context': {
'extra_buttons': [
{
'label': _('Remove all'),
'icon_class': icon_remove_all,
'name': 'remove_all',
}
],
'form': self.forms['form_added'],
'form_css_classes': 'form-hotkey-double-click',
'hide_labels': True,
'submit_icon_class': icon_assign_remove_remove,
'submit_label': _('Remove'),
'title': self.list_added_title or ' ',
}
}
]
}
)
return context
def get_disabled_choices(self):
return ()
def get_form_extra_kwargs(self, form_name):
if form_name == 'form_available':
return {
'choices': self.generate_choices(
queryset=self.get_list_available_queryset()
),
'help_text': self.get_list_available_help_text()
}
else:
return {
'choices': self.generate_choices(
queryset=self.get_list_added_queryset()
),
'disabled_choices': self.get_disabled_choices(),
'help_text': self.get_list_added_help_text()
}
def get_list_added_help_text(self):
return self.list_added_help_text
def get_list_added_queryset(self):
if not self.related_field:
raise ImproperlyConfigured(
'View %s must be called with either a related_field or '
'override .get_list_added_queryset().' % self.__class__.__name__
)
return self.get_secondary_object_list().filter(
pk__in=getattr(self.main_object, self.related_field).values('pk')
)
def get_list_available_help_text(self):
return self.list_available_help_text
def get_list_available_queryset(self):
return self.get_secondary_object_list().exclude(
pk__in=self.get_list_added_queryset().values('pk')
)
def get_secondary_object_list(self):
queryset = self.get_secondary_object_source_queryset()
if queryset is None:
queryset = self.secondary_object_model._meta.default_manager.all()
if self.secondary_object_permission:
return AccessControlList.objects.filter_by_access(
permission=self.secondary_object_permission, queryset=queryset,
user=self.request.user
)
else:
return queryset
def get_secondary_object_source_queryset(self):
return self.secondary_object_source_queryset
def get_success_url(self):
# Redirect to the same view
return reverse(
viewname=self.request.resolver_match.view_name,
kwargs=self.request.resolver_match.kwargs
)
class AssignRemoveView(ExtraContextMixin, ViewPermissionCheckMixin, ObjectPermissionCheckMixin, TemplateView):
decode_content_type = False
right_list_help_text = None
left_list_help_text = None
grouped = False
left_list_title = None
right_list_title = None
template_name = 'appearance/generic_form.html'
LEFT_LIST_NAME = 'left_list'
RIGHT_LIST_NAME = 'right_list'
@staticmethod
def generate_choices(choices):
results = []
for choice in choices:
ct = ContentType.objects.get_for_model(choice)
label = force_text(choice)
results.append(('%s,%s' % (ct.model, choice.pk), '%s' % (label)))
# Sort results by the label not the key value
return sorted(results, key=lambda x: x[1])
def left_list(self):
# Subclass must override
raise NotImplementedError
def right_list(self):
# Subclass must override
raise NotImplementedError
def add(self, item):
# Subclass must override
raise NotImplementedError
def remove(self, item):
# Subclass must override
raise NotImplementedError
def get_disabled_choices(self):
return ()
def get_left_list_help_text(self):
return self.left_list_help_text
def get_right_list_help_text(self):
return self.right_list_help_text
def get(self, request, *args, **kwargs):
self.unselected_list = ChoiceForm(
prefix=self.LEFT_LIST_NAME, choices=self.left_list()
)
self.selected_list = ChoiceForm(
prefix=self.RIGHT_LIST_NAME, choices=self.right_list(),
disabled_choices=self.get_disabled_choices(),
help_text=self.get_right_list_help_text()
)
return self.render_to_response(self.get_context_data())
def process_form(self, prefix, items_function, action_function):
if '%s-submit' % prefix in self.request.POST.keys():
form = ChoiceForm(
self.request.POST, prefix=prefix,
choices=items_function()
)
if form.is_valid():
for selection in form.cleaned_data['selection']:
if self.grouped:
flat_list = []
for group in items_function():
flat_list.extend(group[1])
else:
flat_list = items_function()
label = dict(flat_list)[selection]
if self.decode_content_type:
model, pk = selection.split(',')
selection_obj = ContentType.objects.get(
model=model
).get_object_for_this_type(pk=pk)
else:
selection_obj = selection
try:
action_function(selection_obj)
except Exception:
if settings.DEBUG:
raise
else:
messages.error(
self.request,
_('Unable to transfer selection: %s.') % label
)
def post(self, request, *args, **kwargs):
self.process_form(
prefix=self.LEFT_LIST_NAME, items_function=self.left_list,
action_function=self.add
)
self.process_form(
prefix=self.RIGHT_LIST_NAME, items_function=self.right_list,
action_function=self.remove
)
return self.get(request, *args, **kwargs)
def get_context_data(self, **kwargs):
data = super(AssignRemoveView, self).get_context_data(**kwargs)
data.update({
'subtemplates_list': [
{
'name': 'appearance/generic_form_subtemplate.html',
'column_class': 'col-xs-12 col-sm-6 col-md-6 col-lg-6',
'context': {
'form': self.unselected_list,
'title': self.left_list_title or ' ',
'submit_label': _('Add'),
'submit_icon_class': icon_assign_remove_add,
'hide_labels': True,
}
},
{
'name': 'appearance/generic_form_subtemplate.html',
'column_class': 'col-xs-12 col-sm-6 col-md-6 col-lg-6',
'context': {
'form': self.selected_list,
'title': self.right_list_title or ' ',
'submit_label': _('Remove'),
'submit_icon_class': icon_assign_remove_remove,
'hide_labels': True,
}
},
],
})
return data
class ConfirmView(ObjectListPermissionFilterMixin, ObjectPermissionCheckMixin, ViewPermissionCheckMixin, ExtraContextMixin, RedirectionMixin, TemplateView):
template_name = 'appearance/generic_confirm.html'
def post(self, request, *args, **kwargs):
self.view_action()
return HttpResponseRedirect(self.get_success_url())
class FormView(ViewPermissionCheckMixin, ExtraContextMixin, RedirectionMixin, FormExtraKwargsMixin, DjangoFormView):
template_name = 'appearance/generic_form.html'
class DynamicFormView(DynamicFormViewMixin, FormView):
pass
class MultipleObjectFormActionView(ObjectActionMixin, MultipleObjectMixin, FormExtraKwargsMixin, ViewPermissionCheckMixin, ExtraContextMixin, RedirectionMixin, DjangoFormView):
"""
This view will present a form and upon receiving a POST request will
perform an action on an object or queryset
"""
template_name = 'appearance/generic_form.html'
def __init__(self, *args, **kwargs):
result = super(MultipleObjectFormActionView, self).__init__(*args, **kwargs)
if self.__class__.mro()[0].get_queryset != MultipleObjectFormActionView.get_queryset:
raise ImproperlyConfigured(
'%(cls)s is overloading the get_queryset method. Subclasses '
'should implement the get_object_list method instead. ' % {
'cls': self.__class__.__name__
}
)
return result
def form_valid(self, form):
self.view_action(form=form)
return super(MultipleObjectFormActionView, self).form_valid(form=form)
def get_queryset(self):
try:
return super(MultipleObjectFormActionView, self).get_queryset()
except ImproperlyConfigured:
self.queryset = self.get_object_list()
return super(MultipleObjectFormActionView, self).get_queryset()
class MultipleObjectConfirmActionView(ObjectActionMixin, MultipleObjectMixin, ObjectListPermissionFilterMixin, ViewPermissionCheckMixin, ExtraContextMixin, RedirectionMixin, TemplateView):
template_name = 'appearance/generic_confirm.html'
def post(self, request, *args, **kwargs):
self.view_action()
return HttpResponseRedirect(self.get_success_url())
class SimpleView(ViewPermissionCheckMixin, ExtraContextMixin, TemplateView):
pass
class SingleObjectCreateView(ObjectNameMixin, ViewPermissionCheckMixin, ExtraContextMixin, RedirectionMixin, FormExtraKwargsMixin, CreateView):
template_name = 'appearance/generic_form.html'
def form_valid(self, form):
# This overrides the original Django form_valid method
self.object = form.save(commit=False)
if hasattr(self, 'get_instance_extra_data'):
for key, value in self.get_instance_extra_data().items():
setattr(self.object, key, value)
if hasattr(self, 'get_save_extra_data'):
save_extra_data = self.get_save_extra_data()
else:
save_extra_data = {}
try:
self.object.save(**save_extra_data)
except Exception as exception:
context = self.get_context_data()
messages.error(
self.request,
_('%(object)s not created, error: %(error)s') % {
'object': self.get_object_name(context=context),
'error': exception
}
)
else:
context = self.get_context_data()
messages.success(
self.request,
_(
'%(object)s created successfully.'
) % {'object': self.get_object_name(context=context)}
)
return HttpResponseRedirect(self.get_success_url())
class SingleObjectDynamicFormCreateView(DynamicFormViewMixin, SingleObjectCreateView):
pass
class SingleObjectDeleteView(ObjectNameMixin, DeleteExtraDataMixin, ViewPermissionCheckMixin, ObjectPermissionCheckMixin, ExtraContextMixin, RedirectionMixin, DeleteView):
template_name = 'appearance/generic_confirm.html'
def get_context_data(self, **kwargs):
context = super(SingleObjectDeleteView, self).get_context_data(**kwargs)
context.update({'delete_view': True})
return context
def delete(self, request, *args, **kwargs):
self.object = self.get_object()
context = self.get_context_data()
object_name = self.get_object_name(context=context)
try:
result = super(SingleObjectDeleteView, self).delete(request, *args, **kwargs)
except Exception as exception:
messages.error(
self.request,
_('%(object)s not deleted, error: %(error)s.') % {
'object': object_name,
'error': exception
}
)
raise exception
else:
messages.success(
self.request,
_(
'%(object)s deleted successfully.'
) % {'object': object_name}
)
return result
class SingleObjectDetailView(ViewPermissionCheckMixin, ObjectPermissionCheckMixin, FormExtraKwargsMixin, ExtraContextMixin, ModelFormMixin, DetailView):
template_name = 'appearance/generic_form.html'
def get_context_data(self, **kwargs):
context = super(SingleObjectDetailView, self).get_context_data(**kwargs)
context.update({'read_only': True, 'form': self.get_form()})
return context
class SingleObjectDownloadView(ViewPermissionCheckMixin, ObjectPermissionCheckMixin, VirtualDownloadView, SingleObjectMixin):
TextIteratorIO = TextIteratorIO
VirtualFile = VirtualFile
class SingleObjectEditView(ObjectNameMixin, ViewPermissionCheckMixin, ObjectPermissionCheckMixin, ExtraContextMixin, FormExtraKwargsMixin, RedirectionMixin, UpdateView):
template_name = 'appearance/generic_form.html'
def form_valid(self, form):
# This overrides the original Django form_valid method
self.object = form.save(commit=False)
if hasattr(self, 'get_instance_extra_data'):
for key, value in self.get_instance_extra_data().items():
setattr(self.object, key, value)
if hasattr(self, 'get_save_extra_data'):
save_extra_data = self.get_save_extra_data()
else:
save_extra_data = {}
context = self.get_context_data()
object_name = self.get_object_name(context=context)
try:
self.object.save(**save_extra_data)
except Exception as exception:
messages.error(
self.request,
_('%(object)s not updated, error: %(error)s.') % {
'object': object_name,
'error': exception
}
)
raise exception
else:
messages.success(
self.request,
_(
'%(object)s updated successfully.'
) % {'object': object_name}
)
return HttpResponseRedirect(self.get_success_url())
def get_object(self, queryset=None):
obj = super(SingleObjectEditView, self).get_object(queryset=queryset)
if hasattr(self, 'get_instance_extra_data'):
for key, value in self.get_instance_extra_data().items():
setattr(obj, key, value)
return obj
class SingleObjectDynamicFormEditView(DynamicFormViewMixin, SingleObjectEditView):
pass
class SingleObjectListView(PaginationMixin, ViewPermissionCheckMixin, ObjectListPermissionFilterMixin, ExtraContextMixin, RedirectionMixin, ListView):
template_name = 'appearance/generic_list.html'
def __init__(self, *args, **kwargs):
result = super(SingleObjectListView, self).__init__(*args, **kwargs)
if self.__class__.mro()[0].get_queryset != SingleObjectListView.get_queryset:
raise ImproperlyConfigured(
'%(cls)s is overloading the get_queryset method. Subclasses '
'should implement the get_object_list method instead. ' % {
'cls': self.__class__.__name__
}
)
return result
def get_context_data(self, **kwargs):
context = super(SingleObjectListView, self).get_context_data(**kwargs)
context.update(
{
TEXT_SORT_FIELD_VARIABLE_NAME: self.get_sort_field(),
TEXT_SORT_ORDER_VARIABLE_NAME: self.get_sort_order(),
'icon_sort': self.get_sort_icon(),
}
)
return context
def get_paginate_by(self, queryset):
return setting_paginate_by.value
def get_queryset(self):
try:
queryset = super(SingleObjectListView, self).get_queryset()
except ImproperlyConfigured:
self.queryset = self.get_object_list()
queryset = super(SingleObjectListView, self).get_queryset()
self.field_name = self.get_sort_field()
if self.get_sort_order() == TEXT_SORT_ORDER_CHOICE_ASCENDING:
sort_order = ''
else:
sort_order = '-'
if self.field_name:
queryset = queryset.order_by(
'{}{}'.format(sort_order, self.field_name)
)
return queryset
def get_sort_field(self):
return self.request.GET.get(TEXT_SORT_FIELD_PARAMETER)
def get_sort_icon(self):
sort_order = self.get_sort_order()
if not sort_order:
return
elif sort_order == TEXT_SORT_ORDER_CHOICE_ASCENDING:
return icon_sort_down
else:
return icon_sort_up
def get_sort_order(self):
return self.request.GET.get(TEXT_SORT_ORDER_PARAMETER)