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, ValidationError from django.http import HttpResponseRedirect from django.utils.encoding import force_text from django.utils.translation import ugettext_lazy as _ from django.views.generic import DetailView from django.views.generic import FormView as DjangoFormView from django.views.generic import 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 .forms import ChoiceForm from .icons import ( icon_assign_remove_add, icon_assign_remove_remove, icon_sort_down, icon_sort_up ) from .literals import ( TEXT_CHOICE_ITEMS, TEXT_CHOICE_LIST, TEXT_LIST_AS_ITEMS_VARIABLE_NAME, TEXT_LIST_AS_ITEMS_PARAMETER, 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, ExtraContextMixin, FormExtraKwargsMixin, ListModeMixin, MultipleObjectMixin, ObjectActionMixin, ObjectListPermissionFilterMixin, ObjectNameMixin, ObjectPermissionCheckMixin, RedirectionMixin, ViewPermissionCheckMixin ) from .settings import setting_paginate_by __all__ = ( 'AssignRemoveView', 'ConfirmView', 'FormView', 'MultiFormView', 'MultipleObjectConfirmActionView', 'MultipleObjectFormActionView', 'SingleObjectCreateView', 'SingleObjectDeleteView', 'SingleObjectDetailView', 'SingleObjectEditView', 'SingleObjectListView', 'SimpleView' ) class AssignRemoveView(ExtraContextMixin, ViewPermissionCheckMixin, ObjectPermissionCheckMixin, TemplateView): decode_content_type = False left_list_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.' ) right_list_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.' ) 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(), help_text=self.get_left_list_help_text() ) 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, 'form_css_classes': 'form-hotkey-double-click', '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, 'form_css_classes': 'form-hotkey-double-click', '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 MultiFormView(DjangoFormView): prefix = None prefixes = {} 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 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(self.get_success_url()) def forms_invalid(self, forms): return self.render_to_response(self.get_context_data(forms=forms)) def get(self, request, *args, **kwargs): form_classes = self.get_form_classes() forms = self.get_forms(form_classes) 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, }) return kwargs 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): form_classes = self.get_form_classes() forms = self.get_forms(form_classes) if all([form.is_valid() for form in forms.values()]): return self.forms_valid(forms) else: return self.forms_invalid(forms) 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' error_message_duplicate = None 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.validate_unique() except ValidationError as exception: context = self.get_context_data() error_message = self.get_error_message_duplicate() or _( 'Duplicate data error: %(error)s' ) % { 'error': '\n'.join(exception.messages) } messages.error( request=self.request, message=error_message ) return super( SingleObjectCreateView, self ).form_invalid(form=form) 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 } ) return super( SingleObjectCreateView, self ).form_invalid(form=form) 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()) def get_error_message_duplicate(self): return self.error_message_duplicate 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 } ) return super( SingleObjectEditView, self ).form_invalid(form=form) 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(ListModeMixin, 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_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) 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