Coverage for apis_core/generic/views.py: 43%
159 statements
« prev ^ index » next coverage.py v7.6.1, created at 2024-09-16 07:42 +0000
« prev ^ index » next coverage.py v7.6.1, created at 2024-09-16 07:42 +0000
1from dal import autocomplete
2from django import forms, http
3from django.conf import settings
4from django.contrib.auth.mixins import PermissionRequiredMixin
5from django.forms import modelform_factory
6from django.template.exceptions import TemplateDoesNotExist
7from django.template.loader import select_template
8from django.urls import reverse, reverse_lazy
9from django.views.generic import DetailView
10from django.views.generic.base import TemplateView
11from django.views.generic.edit import CreateView, DeleteView, FormView, UpdateView
12from django_filters.views import FilterView
13from django_tables2 import SingleTableMixin
14from django_tables2.columns import library
15from django_tables2.tables import table_factory
17from apis_core.core.mixins import ListViewObjectFilterMixin
18from apis_core.utils.helpers import create_object_from_uri
20from .filtersets import GenericFilterSet, filterset_factory
21from .forms import GenericImportForm, GenericModelForm
22from .helpers import (
23 first_member_match,
24 generate_search_filter,
25 module_paths,
26 permission_fullname,
27 template_names_via_mro,
28)
29from .tables import GenericTable
32class Overview(TemplateView):
33 template_name = "generic/overview.html"
36class GenericModelMixin:
37 """
38 A mixin providing the common functionality for all the views working
39 with `generic` models - that is models that are accessed via the
40 contenttype framework (using `app_label.model`).
41 It sets the `.model` of the view and generates a list of possible template
42 names (based on the MRO of the model).
43 If the view has a `permission_action_required` attribute, this is used
44 to set the permission required to access the view for this specific model.
45 """
47 def setup(self, *args, **kwargs):
48 super().setup(*args, **kwargs)
49 if contenttype := kwargs.get("contenttype"):
50 self.model = contenttype.model_class()
51 self.queryset = self.model.objects.all()
53 def get_template_names(self):
54 template_names = []
55 if hasattr(super(), "get_template_names"):
56 template_names = super().get_template_names()
57 suffix = ".html"
58 if hasattr(self, "template_name_suffix"):
59 suffix = self.template_name_suffix + ".html"
60 additional_templates = template_names_via_mro(self.model, suffix) + [
61 f"generic/generic{suffix}"
62 ]
63 template_names += filter(
64 lambda template: template not in template_names, additional_templates
65 )
66 return template_names
68 def get_permission_required(self):
69 if hasattr(settings, "APIS_VIEW_PASSES_TEST"):
70 if settings.APIS_VIEW_PASSES_TEST(self):
71 return []
72 if hasattr(self, "permission_action_required"):
73 return [permission_fullname(self.permission_action_required, self.model)]
74 return []
77class List(
78 ListViewObjectFilterMixin,
79 GenericModelMixin,
80 PermissionRequiredMixin,
81 SingleTableMixin,
82 FilterView,
83):
84 """
85 List view for a generic model.
86 Access requires the `<model>_view` permission.
87 It is based on django-filters FilterView and django-tables SingleTableMixin.
88 The table class is overridden by the first match from
89 the `first_member_match` helper.
90 The filterset class is overridden by the first match from
91 the `first_member_match` helper.
92 The queryset is overridden by the first match from
93 the `first_member_match` helper.
94 """
96 template_name_suffix = "_list"
97 permission_action_required = "view"
99 def get_table_class(self):
100 table_modules = module_paths(self.model, path="tables", suffix="Table")
101 table_class = first_member_match(table_modules, GenericTable)
102 return table_factory(self.model, table_class)
104 def get_table_kwargs(self):
105 kwargs = super().get_table_kwargs()
107 # we look at the selected columns and exclude
108 # all modelfields that are not part of that list
109 selected_columns = self.request.GET.getlist(
110 "columns",
111 self.get_filterset(self.get_filterset_class()).form["columns"].initial,
112 )
113 modelfields = self.model._meta.get_fields()
114 kwargs["exclude"] = [
115 field.name for field in modelfields if field.name not in selected_columns
116 ]
118 # now we look at the selected columns and
119 # add all modelfields and annotated fields that
120 # are part of the selected columns to the extra_columns
121 annotationfields = list()
122 for key, value in self.object_list.query.annotations.items():
123 fake_field = getattr(value, "field", value.output_field)
124 setattr(fake_field, "name", key)
125 annotationfields.append(fake_field)
126 extra_fields = list(
127 filter(
128 lambda x: x.name in selected_columns,
129 modelfields + tuple(annotationfields),
130 )
131 )
132 kwargs["extra_columns"] = [
133 (field.name, library.column_for_field(field, accessor=field.name))
134 for field in extra_fields
135 if field.name not in self.get_table_class().base_columns
136 ]
138 return kwargs
140 def get_filterset_class(self):
141 filterset_modules = module_paths(
142 self.model, path="filtersets", suffix="FilterSet"
143 )
144 filterset_class = first_member_match(filterset_modules, GenericFilterSet)
145 return filterset_factory(self.model, filterset_class)
147 def _get_columns_choices(self, columns_exclude):
148 # we start with the model fields
149 choices = [
150 (field.name, getattr(field, "verbose_name", field.name))
151 for field in self.model._meta.get_fields()
152 ]
153 # we add any annotated fields to that
154 choices += [(key, key) for key in self.get_queryset().query.annotations.keys()]
155 # now we drop all the choices that are listed in columns_exclude
156 choices = list(filter(lambda x: x[0] not in columns_exclude, choices))
157 return choices
159 def _get_columns_initial(self, columns_exclude):
160 return [
161 field
162 for field in self.get_table().columns.names()
163 if field not in columns_exclude
164 ]
166 def get_filterset(self, filterset_class):
167 """
168 We override the `get_filterset` method, so we can inject a
169 `columns` selector into the form
170 """
171 filterset = super().get_filterset(filterset_class)
172 columns_exclude = filterset.form.columns_exclude
174 # we inject a `columns` selector in the beginning of the form
175 columns = forms.MultipleChoiceField(
176 required=False,
177 choices=self._get_columns_choices(columns_exclude),
178 initial=self._get_columns_initial(columns_exclude),
179 )
180 filterset.form.fields = {**{"columns": columns}, **filterset.form.fields}
182 return filterset
184 def get_queryset(self):
185 queryset_methods = module_paths(
186 self.model, path="querysets", suffix="ListViewQueryset"
187 )
188 queryset = first_member_match(queryset_methods) or (lambda x: x)
189 return self.filter_queryset(queryset(self.model.objects.all()))
192class Detail(GenericModelMixin, PermissionRequiredMixin, DetailView):
193 """
194 Detail view for a generic model.
195 Access requires the `<model>_view` permission.
196 """
198 permission_action_required = "view"
201class Create(GenericModelMixin, PermissionRequiredMixin, CreateView):
202 """
203 Create view for a generic model.
204 Access requires the `<model>_add` permission.
205 The form class is overridden by the first match from
206 the `first_member_match` helper.
207 """
209 template_name = "generic/generic_form.html"
210 permission_action_required = "add"
212 def get_form_class(self):
213 form_modules = module_paths(self.model, path="forms", suffix="Form")
214 form_class = first_member_match(form_modules, GenericModelForm)
215 return modelform_factory(self.model, form_class)
217 def get_success_url(self):
218 return self.object.get_create_success_url()
221class Delete(GenericModelMixin, PermissionRequiredMixin, DeleteView):
222 """
223 Delete view for a generic model.
224 Access requires the `<model>_delete` permission.
225 """
227 permission_action_required = "delete"
229 def get_success_url(self):
230 return reverse(
231 "apis_core:generic:list",
232 args=[self.request.resolver_match.kwargs["contenttype"]],
233 )
235 def delete(self, *args, **kwargs):
236 if "HX-Request" in self.request.headers:
237 return (
238 reverse_lazy(
239 "apis_core:generic:list",
240 args=[self.request.resolver_match.kwargs["contenttype"]],
241 ),
242 )
243 return super().delete(*args, **kwargs)
246class Update(GenericModelMixin, PermissionRequiredMixin, UpdateView):
247 """
248 Update view for a generic model.
249 Access requires the `<model>_change` permission.
250 The form class is overridden by the first match from
251 the `first_member_match` helper.
252 """
254 permission_action_required = "change"
256 def get_form_class(self):
257 form_modules = module_paths(self.model, path="forms", suffix="Form")
258 form_class = first_member_match(form_modules, GenericModelForm)
259 return modelform_factory(self.model, form_class)
261 def get_success_url(self):
262 return self.object.get_update_success_url()
265class Autocomplete(
266 GenericModelMixin, PermissionRequiredMixin, autocomplete.Select2QuerySetView
267):
268 """
269 Autocomplete view for a generic model.
270 Access requires the `<model>_view` permission.
271 The queryset is overridden by the first match from
272 the `first_member_match` helper.
273 """
275 permission_action_required = "view"
276 template_name_suffix = "_autocomplete_result"
277 create_field = "thisisnotimportant" # because we are using create_object_from_uri
279 def setup(self, *args, **kwargs):
280 super().setup(*args, **kwargs)
281 try:
282 template = select_template(self.get_template_names())
283 self.template = template.template.name
284 except TemplateDoesNotExist:
285 self.template = None
287 def get_queryset(self):
288 queryset_methods = module_paths(
289 self.model, path="querysets", suffix="AutocompleteQueryset"
290 )
291 queryset = first_member_match(queryset_methods)
292 if queryset:
293 return queryset(self.model, self.q)
294 return self.model.objects.filter(generate_search_filter(self.model, self.q))
296 def get_results(self, context):
297 external_only = self.kwargs.get("external_only", False)
298 results = [] if external_only else super().get_results(context)
299 queryset_methods = module_paths(
300 self.model, path="querysets", suffix="ExternalAutocomplete"
301 )
302 ExternalAutocomplete = first_member_match(queryset_methods)
303 if ExternalAutocomplete:
304 results.extend(ExternalAutocomplete().get_results(self.q))
305 return results
307 def create_object(self, value):
308 return create_object_from_uri(value, self.queryset.model)
310 def post(self, request, *args, **kwargs):
311 try:
312 return super().post(request, *args, **kwargs)
313 except Exception as e:
314 return http.JsonResponse({"error": str(e)})
317class Import(GenericModelMixin, PermissionRequiredMixin, FormView):
318 template_name = "generic/generic_import_form.html"
319 template_name_suffix = "_import"
320 permission_action_required = "add"
322 def get_form_class(self):
323 form_modules = module_paths(self.model, path="forms", suffix="ImportForm")
324 form_class = first_member_match(form_modules, GenericImportForm)
325 return modelform_factory(self.model, form_class)
327 def form_valid(self, form):
328 self.object = form.cleaned_data["url"]
329 return super().form_valid(form)
331 def get_success_url(self):
332 return self.object.get_absolute_url()