import functools
from crispy_forms.helper import FormHelper
from django import forms
from django.utils.translation import gettext as _, ngettext
from django.views import generic
from django.http import JsonResponse
from ..model import ModelMixin
from .filter import FilterMixin
from .json import JsonMixin
from .swagger import swagger_json_operation, swagger_list_response
from .object import ObjectMixin
from .pagination import PaginationMixin
from .search import SearchMixin
from .template import TemplateViewMixin
from .tables2 import Tables2Mixin
[docs]
class ListMixin:
"""List view behaviour: navigation tag, empty-state text, list actions.
Attributes:
default_template_name (str): Template for the list page.
tags (list[str]): Menu discovery tags. Default ``['navigation']``.
urlpath (str): URL segment for this view. Default ``''`` (list root).
permission_shortcode (str): Django permission prefix for list access.
Default ``'view'``.
icon (str): Bootstrap Icons name; defaults to the model controller ``icon``.
color (str): Bulma color for the navigation icon; defaults to the model
controller ``color``.
pagination_target (str): Unpoly target for paginated list updates.
filter_target (str): Unpoly target for filter form submissions.
"""
default_template_name = 'djmvc/list.html'
tags = ['navigation']
urlpath = ''
permission_shortcode = 'view'
@property
def title(self):
"""Page heading from the model's ``verbose_name_plural``."""
return self.model._meta.verbose_name_plural.capitalize()
@property
def icon(self):
"""Bootstrap Icons name; falls back to the model controller ``icon``."""
return getattr(self.controller.model_controller, 'icon', None)
@property
def color(self):
"""Bulma color for the navigation icon; falls back to the model controller."""
return getattr(self.controller.model_controller, 'color', None)
[docs]
def breadcrumbs(self):
"""List views have no parent breadcrumbs."""
return []
@functools.cached_property
def list_actions(self):
"""Permitted bulk-action views for the list action bar."""
return self.controller.get_tagged_views('list_action', request=self.request)
@property
def empty_list_message(self):
return _('No %(verbose_name)s found matching the query') % {
'verbose_name': self.model_meta.verbose_name_plural,
}
@property
def list_action_count_label_one(self):
"""Selection count label when exactly one row is selected."""
return ngettext(
'%(total_count)s selected',
'All %(total_count)s selected',
1,
) % {'total_count': 1}
@property
def list_action_count_label_other(self):
"""Selection count label template when multiple rows are selected."""
return ngettext(
'%(total_count)s selected',
'All %(total_count)s selected',
2,
) % {'total_count': '__COUNT__'}
[docs]
class ListView(
ListMixin,
JsonMixin,
SearchMixin,
FilterMixin,
PaginationMixin,
Tables2Mixin,
TemplateViewMixin,
ModelMixin,
generic.ListView,
):
pagination_target = '[up-list]'
filter_target = '[up-list]'
[docs]
def get_queryset(self):
qs = self.get_scoped_queryset()
if self.filter_fields and self.filterset is not None:
qs = self.filterset.qs
return self.search_filter(qs)
[docs]
def get_filter_field_names(self):
"""Filter bar field names: search input plus :attr:`filter_fields`."""
names = list(self.filter_fields or [])
if self.search_fields and self.search_param not in names:
names.insert(0, self.search_param)
return names
def _uses_default_filter_form(self):
return 'filter_form_class' not in type(self).__dict__
def _build_composed_filter_form_class(self):
field_names = self.get_filter_field_names()
if not field_names:
return None
view = self
form_fields = {}
for name in field_names:
if name == self.search_param:
form_fields[name] = self.search_form_field()
elif self.filterset is not None:
form_fields[name] = self.filterset.form.fields[name]
def __init__(form_self, *args, view=None, **kwargs):
forms.Form.__init__(form_self, *args, **kwargs)
form_self.helper = FormHelper()
form_self.helper.form_method = 'get'
form_self.helper.form_tag = False
form_self.helper.disable_csrf = True
form_self.helper.form_show_labels = True
if view is not None:
page_kwarg = getattr(view, 'page_kwarg', 'page')
for key, value in view.request.GET.items():
if key in form_self.fields or key == page_kwarg:
continue
form_self.fields[key] = forms.CharField(
widget=forms.HiddenInput(),
initial=value,
)
form_fields['__init__'] = __init__
return type(
f'{self.model.__name__}FilterForm',
(forms.Form,),
form_fields,
)
@functools.cached_property
def filter_form(self):
if (
'filter_form_class' in type(self).__dict__
and type(self).filter_form_class is not None
):
return type(self).filter_form_class(self.request.GET)
form_class = self._build_composed_filter_form_class()
if form_class is None:
return None
return form_class(self.request.GET, view=self)
@property
def has_active_filters(self):
form = self.filter_form
if form is None:
return False
return any(
form.data.get(name)
for name, field in form.fields.items()
if not isinstance(field.widget, forms.HiddenInput)
)
def clear_filter_url(self):
qs = self.request.GET.copy()
form = self.filter_form
if form is not None:
for name, field in form.fields.items():
if not isinstance(field.widget, forms.HiddenInput):
qs.pop(name, None)
page_kwarg = getattr(self, 'page_kwarg', 'page')
qs.pop(page_kwarg, None)
path = self.request.path
return f'{path}?{qs.urlencode()}' if qs else path
def json_get(self, request, *args, **kwargs):
queryset = self.get_queryset()
paginate_by = self.get_paginate_by(queryset)
if paginate_by:
paginator, page, object_list, _is_paginated = self.paginate_queryset(
queryset,
paginate_by,
)
self.object_list = object_list
paginator_data = {
'page_number': page.number,
'per_page': paginator.per_page,
'total_pages': paginator.num_pages,
'total_objects': paginator.count,
'has_next': page.has_next(),
'has_previous': page.has_previous(),
}
else:
self.object_list = list(queryset)
paginator_data = None
return JsonResponse({
'results': [self.serialize(obj) for obj in self.object_list],
'paginator': paginator_data,
})
def get_swagger_get(self):
return swagger_json_operation(
self,
str(self.title),
parameters=[],
responses=swagger_list_response(self.model),
)
[docs]
class DetailListView(ObjectMixin, ListView):
"""List of related rows shown on an object detail page."""
default_template_name = 'djmvc/detaillist.html'
tags = ['object']
@property
def title(self):
return super(ListMixin, self).title