from django import forms
from django.http import HttpResponseRedirect
from django.utils.translation import gettext_lazy as _
from superdjango.assets import StyleSheet, JavaScript
from superdjango.html.library import Table as HTMLTable
from ..constants import VERB
from .crud import UIModelView
from .forms import UIFormMixin, UIFormSetMixin
__all__ = (
"UIBatchChangeView",
"UIBulkCompareView",
"UIBulkDeleteView",
"UIBulkEditView",
)
# TODO: Implement UIBulkTrashView to support TrashModel. Can re-use bulk delete with some modifications.
# TODO: Implement UIBulkEditView using compare view as starting point.
# https://docs.djangoproject.com/en/2.1/topics/forms/formsets/
# http://kevindias.com/writing/django-class-based-views-multiple-inline-formsets/
# https://whoisnicoleharris.com/2015/01/06/implementing-django-formsets.html
# http://blog.mattwoodward.com/2015/02/dynamically-adding-forms-to-and.html
# https://medium.com/@adandan01/django-inline-formsets-example-mybook-420cc4b6225d
# TODO: Consider UIBulkCreateView that allows a batch of records to be created all at once.
# UIImportView (becomes additional option for list view actions)
# UIExportView (becomes additional option for list view actions)
class UIBatchChangeForm(forms.Form):
"""Supports the UIBatchChangeView."""
# TODO: Since the original intent didn't work out, is UIBatchChangeForm really needed?
pass
# Views
class UIBatchChangeView(UIFormMixin, UIModelView):
"""Change the value of one or more fields cross selected objects."""
# TODO: UIBatchChangeView needs to somehow support a "no change" option when multiple fields have been configured.
# For example, a dropdown with an empty value of "no change". Maybe still use ui.get_form_field() but wrapped in a
# local get_form_field() which intelligently adds an empty value to dropdowns, converts booleans to null booleans,
# and so on. We might also disallow char fields.
after_items_message = None
before_items_message = None
changed_count = 0
confirmed_keyword = "confirmed"
"""The POST key used to indicate whether the change is confirmed. This must be present in order to continue with
the update operation.
"""
form = None
selected_objects = list()
# noinspection PyUnusedLocal
def get(self, request, *args, **kwargs):
"""Handle GET requests. Overridden to prevent form handling."""
context = self.get_context_data()
return self.render_to_response(context)
def get_after_items_message(self):
"""Get the message to be displayed after the list of items to be changed.
:rtype: str
"""
context = self._get_message_context()
return self.after_items_message % context
def get_before_items_message(self):
"""Get the message to be displayed before the list of items to be changed.
:rtype: str
"""
context = self._get_message_context()
return self.before_items_message % context
# def get_breadcrumbs(self):
# """Get breadcrumbs for batch change."""
# crumbs = super().get_breadcrumbs()
# crumbs.add(self.get_subtitle(), "")
#
# return crumbs
def get_context_data(self, **kwargs):
"""Add ``selected_objects`` to the context."""
context = super().get_context_data(**kwargs)
context['after_items_message'] = self.get_after_items_message()
context['before_items_message'] = self.get_before_items_message()
context['confirmed_keyword'] = self.confirmed_keyword
context['selected_objects'] = self.selected_objects
return context
def get_css(self):
"""Batch change supplies only empty CSS configuration."""
return StyleSheet()
def get_form(self, request):
fields = dict()
for name in self.fields:
model_field = self.ui.meta.get_field(name)
field = self.ui.get_form_field(model_field, request, required=False)
fields[name] = field
form_class = type("BatchChangeForm", (UIBatchChangeForm,), fields)
return form_class(data=request.POST)
def get_js(self):
"""Batch change supplies only empty JavScript configuration."""
return JavaScript()
def get_selected_objects(self):
"""Get the objects that have been selected for comparison.
:rtype: list
"""
lookup_field = self.ui.get_lookup_field()
selected_objects = list()
for i in self.request.POST.getlist("selected_objects"):
if lookup_field == "pk":
i = int(i)
criteria = {lookup_field: i}
try:
record = self.ui.model.objects.get(**criteria)
selected_objects.append(record)
except self.ui.model.DoesNotExist:
pass
return selected_objects
def get_verb(self):
return VERB.BATCH_CHANGE
# noinspection PyUnusedLocal
def post(self, request, *args, **kwargs):
"""Handle POST by calling the ``submit()`` method.
When ``confirmed_keyword`` is defined, this is checked prior to calling ``submit()``. This allows data to be
posted to the view *without* the confirmation in order to re-use the view for confirming additional action.
"""
form = self.get_form(request)
self.selected_objects = self.get_selected_objects()
confirmed = request.POST.get(self.confirmed_keyword, None)
if confirmed and form.is_valid():
self.submit(form, request)
return HttpResponseRedirect(self.get_success_url())
context = self.get_context_data(form=form)
return self.render_to_response(context)
# noinspection PyUnusedLocal
def submit(self, form, request):
"""Update the model instances."""
objects = self.get_selected_objects()
for o in objects:
self.ui.save_history(o, request, self.get_verb())
for field in self.fields:
new_value = form.cleaned_data[field]
current_value = getattr(o, field)
if new_value and new_value != current_value:
setattr(o, field, new_value)
if self.ui.is_audit_model():
o.audit(request.user)
else:
o.save()
self.changed_count += 1
self.messages.success(self.get_success_message())
def _get_message_context(self):
context = super()._get_message_context()
context['count'] = self.changed_count
return context
class UIBulkCompareView(UIModelView):
"""Compare two or more objects with one another.
The ``fields`` property is used to determine which fields are compared.
"""
row_actions = None
row_actions_label = None
# def get_breadcrumbs(self):
# """Get breadcrumbs for multiple object comparison."""
# crumbs = super().get_breadcrumbs()
# crumbs.add(self.get_subtitle(), "")
#
# return crumbs
def get_context_data(self, **kwargs):
"""Add ``selected_objects`` to the context."""
context = super().get_context_data(**kwargs)
# Get the selected records from POST.
selected_objects = self.get_selected_objects()
context['selected_objects'] = selected_objects
context['total_selected_objects'] = len(selected_objects)
# Build the table columns.
table = HTMLTable()
table.column("compare_field", label=_("Field"))
column_ids = list()
for row in selected_objects:
column_ids.append(row.pk)
table.column("compare_%s" % row.pk, label=self.get_display_name(row))
# Add the fields to each data row.
controls = self.get_controls()
for control in controls:
data = list()
data.append(control)
for row in selected_objects:
value = control.get_datum(row, self.request)
data.append(value)
table.row(data)
# Row actions, if present are added directly to the table.
if self.row_actions is not None:
table.actions = list()
table.row_actions_label = self.row_actions_label
for row in selected_objects:
actions = list()
for action in self.row_actions:
_action = self.ui.get_action(self.request, action, record=row)
if _action is not None:
actions.append(_action)
table.actions.append(actions)
# Add the finished table to the context.
context['table'] = table
# Support for history.
for row in selected_objects:
self.ui.save_history(None, row, self.request, self.get_verb())
return context
def get_selected_objects(self):
"""Get the objects that have been selected for comparison.
:rtype: list
"""
lookup_field = self.ui.get_lookup_field()
selected_objects = list()
for i in self.request.POST.getlist("selected_objects"):
if lookup_field == "pk":
i = int(i)
criteria = {lookup_field: i}
try:
record = self.ui.model.objects.get(**criteria)
selected_objects.append(record)
except self.ui.model.DoesNotExist:
pass
return selected_objects
def get_title(self):
"""Get only the model's verbose name."""
return self.ui.get_verbose_name()
def get_verb(self):
return VERB.BULK_COMPARE
# noinspection PyUnusedLocal
def post(self, request, *args, **kwargs):
"""Handle POSTed objects for comparison."""
context = self.get_context_data()
return self.render_to_response(context)
class UIBulkDeleteView(UIFormMixin, UIModelView):
after_items_message = None
before_items_message = None
confirmed_keyword = "confirmed"
"""The POST key used to indicate whether the deletion is confirmed. This must be present in order to continue with
the delete operation.
"""
deleted_count = 0
selected_objects = list()
# noinspection PyUnusedLocal
def get(self, request, *args, **kwargs):
"""Handle GET requests. Overridden to prevent form handling."""
context = self.get_context_data()
return self.render_to_response(context)
def get_after_items_message(self):
"""Get the message to be displayed after the list of items to be deleted.
:rtype: str
"""
context = self._get_message_context()
return self.after_items_message % context
def get_before_items_message(self):
"""Get the message to be displayed before the list of items to be deleted.
:rtype: str
"""
context = self._get_message_context()
return self.before_items_message % context
# def get_breadcrumbs(self):
# """Get breadcrumbs for multiple object comparison."""
# crumbs = super().get_breadcrumbs()
# crumbs.add(self.get_subtitle(), "")
#
# return crumbs
def get_context_data(self, **kwargs):
"""Add ``selected_objects`` to the context."""
context = super().get_context_data(**kwargs)
context['after_items_message'] = self.get_after_items_message()
context['before_items_message'] = self.get_before_items_message()
context['confirmed_keyword'] = self.confirmed_keyword
context['selected_objects'] = self.selected_objects
return context
def get_css(self):
"""Bulk delete provides no CSS configuration."""
return StyleSheet()
def get_js(self):
"""Bulk delete provides no JavaScript configuration."""
return JavaScript()
def get_selected_objects(self):
"""Get the objects that have been selected for delete.
:rtype: list
"""
selected_objects = list()
lookup_field = self.ui.get_lookup_field()
for i in self.request.POST.getlist("selected_objects"):
if lookup_field == "pk":
i = int(i)
criteria = {lookup_field: i}
try:
record = self.ui.model.objects.get(**criteria)
selected_objects.append(record)
except self.ui.model.DoesNotExist:
pass
return selected_objects
def get_verb(self):
return VERB.BULK_DELETE
# noinspection PyUnusedLocal
def post(self, request, *args, **kwargs):
"""Handle POST by calling the ``submit()`` method.
``confirmed_keyword`` is checked prior to calling ``submit()``. This allows data to be posted to the view
*without* the confirmation in order to re-use the view for confirming additional action.
"""
confirmed = request.POST.get(self.confirmed_keyword, None)
if confirmed:
self.submit(request)
return HttpResponseRedirect(self.get_success_url())
self.selected_objects = self.get_selected_objects()
context = self.get_context_data()
return self.render_to_response(context)
# noinspection PyUnusedLocal
def submit(self, request):
"""Delete the model instances."""
objects = self.get_selected_objects()
for o in objects:
self.ui.save_history(o, self.request, self.get_verb())
o.delete()
self.deleted_count += 1
self.messages.success(self.get_success_message())
def _get_message_context(self):
context = super()._get_message_context()
context['count'] = self.deleted_count
return context
class UIBulkEditView(UIFormSetMixin, UIModelView):
pass