Source code for fmft.views
"""
Filtered Model Formset Table views.md
- re-useable integrations of FilterView, ModelFormsetView, and SingleTableMixin
Purpose:
- sef of abstract class-based views.md that provide a declarative syntax to hide
the integration details
Core Problem:
- Filters, ModelFormsets, and Tables all need a queryset
- in a F-MF-T view, they need to share the same queryset - getting the MRO right
is essential!
- Tables need special logic to render a formset, and need the formset available at
construct any "extra" rows.
- Formset data need to be the [paged] table_data. Chicken meet Egg.
"""
import django_filters.views as filters
import django_tables2 as tables
from django.views.generic.list import (
MultipleObjectMixin,
MultipleObjectTemplateResponseMixin,
)
from django_tables2 import RequestConfig
from extra_views.formsets import (
BaseFormSetFactory,
ModelFormSetMixin,
ModelFormSetView,
ProcessFormSetView,
)
from . import formset_tables
[docs]class FilteredTableView(tables.SingleTableMixin, filters.FilterView):
"""
Too easy - this one is batteries included. Thanks django-tables2 and
django-filters, you are awesome.
How does it work:
- concrete view provides the base queryset
- FilterView.get() sets self.object_list = a filtered version of the base queryset
- SingleTableMixin looks for self.object_list, thus loading the filtered queryset
- Template should render the filterset and the table (see template
filtered_table.html)
"""
# Minimal configuration:
filterset_class = None
table_class = None
# queryset, or get_queryset() as defined by MultipleObjectMixin to customize base
# queryset for filtered data
[docs]class FilterViewMixin(filters.FilterMixin, MultipleObjectMixin):
"""
Logic factored out from BaseFilterView so it can be mixed in, e.g., with a
modelformset view
"""
[docs] def configure_filterset(self):
"""configure the filterset, and return its object_list"""
# Code duplicated directly from django_filters.BaseFilterView.get
filterset_class = self.get_filterset_class()
self.filterset = self.get_filterset(filterset_class)
if (
not self.filterset.is_bound
or self.filterset.is_valid()
or not self.get_strict()
):
object_list = self.filterset.qs
else:
object_list = self.filterset.queryset.none()
return object_list
[docs] def get_context_data(self, **kwargs):
return super().get_context_data(
filter=self.filterset, object_list=self.object_list, **kwargs
)
[docs]class BaseModelFormSetView(
MultipleObjectTemplateResponseMixin, ModelFormSetMixin, ProcessFormSetView
):
"""A Base class that emulates formsets.ModelFormSetView, but without its request
handlers"""
formset_class = formset_tables.BaseModelFormSet
pass
[docs]class FilteredModelFormsetView(FilterViewMixin, BaseModelFormSetView):
"""
A Filtered Formset View.
Not sure how useful this is without a Table, but hey, maybe you just love writing
table template logic :-P
Core Problem:
- need filtered object_list to construct formset, but that logic is buried in
FilterView.get()
Solution: re-write .get() / .post() so the two views.md play nicely together, with
formset.queryset=filterset.qs
How does it work:
- FilterViewMixin.configure_filterset duplicates logic from BaseFilterView to
build_old the object_list
- custom get(), post() mix that logic in with the get() / post() logic from
ModelFormSetView to handle constructing, validating, and saving the modelformset
"""
# Minimal configuration:
model = None
filterset_class = None
form_class = None
factory_kwargs = dict(extra=0)
# queryset, or get_queryset() as defined by MultipleObjectMixin to customize base
# queryset for filtered data
[docs] def get_formset_kwargs(self):
kwargs = super().get_formset_kwargs()
kwargs["queryset"] = (
self.object_list
) # use filterset.qs as the formset queryset
return kwargs
[docs] def get(self, request, *args, **kwargs):
self.object_list = self.configure_filterset()
return super().get(request, *args, **kwargs)
[docs] def post(self, request, *args, **kwargs):
self.object_list = self.configure_filterset()
return super().post(request, *args, **kwargs)
[docs]class BaseModelFormSetSingleTableMixin(BaseFormSetFactory, tables.SingleTableMixin):
"""
A version of SingleTableMixin that injects the formset into the view's Table class,
mixed with a BaseFormSetFactory that fetches formset from table rather than
constructing one itself.
"""
model = None
request = None
formset_class = formset_tables.BaseModelFormSet
_table = None
_formset = None
[docs] def get_formset_and_table(self):
"""Table and formset need to be constructed together - formset needs table's qs,
table needs forms"""
if not (self._table and self._formset):
formset_class = self.get_formset()
formset_kwargs = self.get_formset_kwargs()
table = formset_tables.get_table(
self.get_table_data(),
self.get_table_class(),
self.get_table_kwargs(),
formset_class,
formset_kwargs,
)
table = RequestConfig(
self.request, paginate=self.get_table_pagination(table)
).configure(
table
) # duplicates code from SingleTableMixin.get_table
self._formset = formset_tables.get_formset(
table, formset_class, formset_kwargs
)
self._table = table
return self._formset, self._table
@property
def the_table(self):
"""There should be only one table, one table to rule them all!"""
_, table = self.get_formset_and_table()
return table
[docs] def get_table(self, **kwargs):
"""Fake! kwargs are ignored, cached value returned instead"""
return self.the_table
@property
def the_formset(self):
"""Critically, there may only be one formset built with data from the one
table."""
formset, _ = self.get_formset_and_table()
return formset
[docs] def construct_formset(self):
"""Fake! don't construct another formset, just use the one integrated with
the table."""
return self.the_formset
[docs] def get_table_data(self):
"""Override to use the view's object_list, which we assume is available,
from somewhere"""
# Note: don't use this qs to populate formset - the table further
# sorts/paginates this queryset, so essential that the formset uses the
# table's modified version.
# a formset qs must be ordered, and BaseFormSet will add order_by clause if not,
# which will crash if the table has already added a pagination slice.
# Head that off here by preempting that logic on the table's base queryset...
qs = super().get_table_data()
if hasattr(qs, "_query") and not qs.ordered:
qs = qs.order_by(self.model._meta.pk.name)
return qs
[docs]class ModelFormsetTableView(BaseModelFormSetSingleTableMixin, ModelFormSetView):
"""
A ModelFormset View loaded into a table.
Mix-and-match form fields with non-form fields and customize layout in code instead
of in template logic.
How does it work:
- form defines which table fields are rendered as form fields, other table columns
rendered as usual;
ProcessFormsetView does the heavy lifting in post()
"""
# Minimal configuration:
model = None
table_class = None
form_class = None
factory_kwargs = dict(extra=0)
# queryset, or get_queryset() as defined by MultipleObjectMixin to customize base
# queryset for filtered data
# Note: A ModelFormset queryset MUST be ordered;
# a default ordering will be applied if it is not. Recommend: add order_by
# clause to view's base queryset.
# if you want to export data, mixin an ExportMixin as usual.
[docs]class FilteredModelFormsetTableView(
BaseModelFormSetSingleTableMixin, FilteredModelFormsetView
):
"""
The whole enchilada - A Filtered Model Formset View loaded into a table.
Mix and match from above to get it all.
How does it work:
form defines which table fields are rendered as form fields, other table columns
rendered as usual;
ProcessFormsetView does the heavy lifting in post()
"""
# Minimal configuration:
model = None
filterset_class = None
table_class = None
form_class = None
factory_kwargs = dict(extra=0)
# queryset, or get_queryset() as defined by MultipleObjectMixin to customize base
# queryset for filtered data
# Note: A ModelFormset queryset MUST be ordered;
# a default ordering will be applied if it is not. Recommend: add order_by
# clause to view's base queryset.
# if you want to export data, mixin an ExportMixin as usual.