-
-
Notifications
You must be signed in to change notification settings - Fork 3.7k
/
model.py
679 lines (573 loc) · 24.3 KB
/
model.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
from warnings import warn
from django.core.exceptions import ImproperlyConfigured
from django.forms.models import modelform_factory
from django.shortcuts import redirect
from django.urls import path
from django.utils.functional import cached_property
from wagtail.admin.admin_url_finder import (
ModelAdminURLFinder,
register_admin_url_finder,
)
from wagtail.admin.panels.group import ObjectList
from wagtail.admin.views import generic
from wagtail.admin.views.generic import history, usage
from wagtail.models import ReferenceIndex
from wagtail.permissions import ModelPermissionPolicy
from wagtail.utils.deprecation import RemovedInWagtail70Warning
from .base import ViewSet, ViewSetGroup
class ModelViewSet(ViewSet):
"""
A viewset to allow listing, creating, editing and deleting model instances.
All attributes and methods from :class:`~wagtail.admin.viewsets.base.ViewSet`
are available.
For more information on how to use this class, see :ref:`generic_views`.
"""
#: Register the model to the reference index to track its usage.
#: For more details, see :ref:`managing_the_reference_index`.
add_to_reference_index = True
#: The view class to use for the index view; must be a subclass of ``wagtail.admin.views.generic.IndexView``.
index_view_class = generic.IndexView
#: The view class to use for the create view; must be a subclass of ``wagtail.admin.views.generic.CreateView``.
add_view_class = generic.CreateView
#: The view class to use for the edit view; must be a subclass of ``wagtail.admin.views.generic.EditView``.
edit_view_class = generic.EditView
#: The view class to use for the delete view; must be a subclass of ``wagtail.admin.views.generic.DeleteView``.
delete_view_class = generic.DeleteView
#: The view class to use for the history view; must be a subclass of ``wagtail.admin.views.generic.history.HistoryView``.
history_view_class = history.HistoryView
#: The view class to use for the usage view; must be a subclass of ``wagtail.admin.views.generic.usage.UsageView``.
usage_view_class = usage.UsageView
#: The view class to use for the copy view; must be a subclass of ``wagtail.admin.views.generic.CopyView``.
copy_view_class = generic.CopyView
#: The view class to use for the inspect view; must be a subclass of ``wagtail.admin.views.generic.InspectView``.
inspect_view_class = generic.InspectView
# Breadcrumbs can be turned off until we have a design that can be consistently applied
_show_breadcrumbs = True
#: The prefix of template names to look for when rendering the admin views.
template_prefix = ""
#: The number of items to display per page in the index view. Defaults to 20.
list_per_page = 20
#: The default ordering to use for the index view.
#: Can be a string or a list/tuple in the same format as Django's
#: :attr:`~django.db.models.Options.ordering`.
ordering = None
#: Whether to enable the inspect view. Defaults to ``False``.
inspect_view_enabled = False
#: The model fields or attributes to display in the inspect view.
#:
#: If the field has a corresponding :meth:`~django.db.models.Model.get_FOO_display`
#: method on the model, the method's return value will be used instead.
#:
#: If you have ``wagtail.images`` installed, and the field's value is an instance of
#: ``wagtail.images.models.AbstractImage``, a thumbnail of that image will be rendered.
#:
#: If you have ``wagtail.documents`` installed, and the field's value is an instance of
#: ``wagtail.docs.models.AbstractDocument``, a link to that document will be rendered,
#: along with the document title, file extension and size.
inspect_view_fields = []
#: The fields to exclude from the inspect view.
inspect_view_fields_exclude = []
#: Whether to enable the copy view. Defaults to ``True``.
copy_view_enabled = True
def __init__(self, name=None, **kwargs):
super().__init__(name=name, **kwargs)
if not self.model:
raise ImproperlyConfigured(
"ModelViewSet %r must define a `model` attribute or pass a `model` argument"
% self
)
self.model_opts = self.model._meta
self.app_label = self.model_opts.app_label
self.model_name = self.model_opts.model_name
@property
def permission_policy(self):
return ModelPermissionPolicy(self.model)
@cached_property
def name(self):
"""
Viewset name, to use as the URL prefix and namespace.
Defaults to the :attr:`~django.db.models.Options.model_name`.
"""
return self.model_name
def get_common_view_kwargs(self, **kwargs):
view_kwargs = super().get_common_view_kwargs(
**{
"model": self.model,
"permission_policy": self.permission_policy,
"index_url_name": self.get_url_name("index"),
"index_results_url_name": self.get_url_name("index_results"),
"history_url_name": self.get_url_name("history"),
"usage_url_name": self.get_url_name("usage"),
"add_url_name": self.get_url_name("add"),
"edit_url_name": self.get_url_name("edit"),
"delete_url_name": self.get_url_name("delete"),
"header_icon": self.icon,
"_show_breadcrumbs": self._show_breadcrumbs,
**kwargs,
}
)
if self.copy_view_enabled:
view_kwargs["copy_url_name"] = self.get_url_name("copy")
if self.inspect_view_enabled:
view_kwargs["inspect_url_name"] = self.get_url_name("inspect")
return view_kwargs
def get_index_view_kwargs(self, **kwargs):
view_kwargs = {
"template_name": self.index_template_name,
"results_template_name": self.index_results_template_name,
"list_display": self.list_display,
"list_filter": self.list_filter,
"list_export": self.list_export,
"export_headings": self.export_headings,
"export_filename": self.export_filename,
"filterset_class": self.filterset_class,
"search_fields": self.search_fields,
"search_backend_name": self.search_backend_name,
"paginate_by": self.list_per_page,
**kwargs,
}
if self.ordering:
view_kwargs["default_ordering"] = self.ordering
return view_kwargs
def get_add_view_kwargs(self, **kwargs):
return {
"panel": self._edit_handler,
"form_class": self.get_form_class(),
"template_name": self.create_template_name,
**kwargs,
}
def get_edit_view_kwargs(self, **kwargs):
return {
"panel": self._edit_handler,
"form_class": self.get_form_class(for_update=True),
"template_name": self.edit_template_name,
**kwargs,
}
def get_delete_view_kwargs(self, **kwargs):
return {
"template_name": self.delete_template_name,
**kwargs,
}
def get_history_view_kwargs(self, **kwargs):
return {
"template_name": self.history_template_name,
"history_results_url_name": self.get_url_name("history_results"),
"header_icon": "history",
**kwargs,
}
def get_usage_view_kwargs(self, **kwargs):
return {
"template_name": self.get_templates(
"usage", fallback=self.usage_view_class.template_name
),
**kwargs,
}
def get_inspect_view_kwargs(self, **kwargs):
return {
"template_name": self.inspect_template_name,
"fields": self.inspect_view_fields,
"fields_exclude": self.inspect_view_fields_exclude,
**kwargs,
}
def get_copy_view_kwargs(self, **kwargs):
return self.get_add_view_kwargs(**kwargs)
@property
def index_view(self):
return self.construct_view(
self.index_view_class, **self.get_index_view_kwargs()
)
@property
def index_results_view(self):
return self.construct_view(
self.index_view_class, **self.get_index_view_kwargs(), results_only=True
)
@property
def add_view(self):
return self.construct_view(self.add_view_class, **self.get_add_view_kwargs())
@property
def edit_view(self):
return self.construct_view(self.edit_view_class, **self.get_edit_view_kwargs())
@property
def delete_view(self):
return self.construct_view(
self.delete_view_class, **self.get_delete_view_kwargs()
)
@property
def redirect_to_edit_view(self):
def redirect_to_edit(request, pk):
warn(
(
"%s's `/<pk>/` edit view URL pattern has been "
"deprecated in favour of /edit/<pk>/."
)
% (self.__class__.__name__),
category=RemovedInWagtail70Warning,
)
return redirect(self.get_url_name("edit"), pk, permanent=True)
return redirect_to_edit
@property
def redirect_to_delete_view(self):
def redirect_to_delete(request, pk):
warn(
(
"%s's `/<pk>/delete/` delete view URL pattern has been "
"deprecated in favour of /delete/<pk>/."
)
% (self.__class__.__name__),
category=RemovedInWagtail70Warning,
)
return redirect(self.get_url_name("delete"), pk, permanent=True)
return redirect_to_delete
@property
def history_view(self):
return self.construct_view(
self.history_view_class, **self.get_history_view_kwargs()
)
@property
def history_results_view(self):
return self.construct_view(
self.history_view_class, **self.get_history_view_kwargs(), results_only=True
)
@property
def usage_view(self):
return self.construct_view(
self.usage_view_class, **self.get_usage_view_kwargs()
)
@property
def inspect_view(self):
return self.construct_view(
self.inspect_view_class, **self.get_inspect_view_kwargs()
)
@property
def copy_view(self):
return self.construct_view(self.copy_view_class, **self.get_copy_view_kwargs())
def get_templates(self, name="index", fallback=""):
"""
Utility function that provides a list of templates to try for a given
view, when the template isn't overridden by one of the template
attributes on the class.
"""
if not self.template_prefix:
return [fallback]
templates = [
f"{self.template_prefix}{self.app_label}/{self.model_name}/{name}.html",
f"{self.template_prefix}{self.app_label}/{name}.html",
f"{self.template_prefix}{name}.html",
]
if fallback:
templates.append(fallback)
return templates
@cached_property
def index_template_name(self):
"""
A template to be used when rendering ``index_view``.
Default: if :attr:`template_prefix` is specified, an ``index.html``
template in the prefix directory and its ``{app_label}/{model_name}/``
or ``{app_label}/`` subdirectories will be used. Otherwise, the
``index_view_class.template_name`` will be used.
"""
return self.get_templates(
"index",
fallback=self.index_view_class.template_name,
)
@cached_property
def index_results_template_name(self):
"""
A template to be used when rendering ``index_results_view``.
Default: if :attr:`template_prefix` is specified, a ``index_results.html``
template in the prefix directory and its ``{app_label}/{model_name}/``
or ``{app_label}/`` subdirectories will be used. Otherwise, the
``index_view_class.results_template_name`` will be used.
"""
return self.get_templates(
"index_results",
fallback=self.index_view_class.results_template_name,
)
@cached_property
def create_template_name(self):
"""
A template to be used when rendering ``add_view``.
Default: if :attr:`template_prefix` is specified, a ``create.html``
template in the prefix directory and its ``{app_label}/{model_name}/``
or ``{app_label}/`` subdirectories will be used. Otherwise, the
``add_view_class.template_name`` will be used.
"""
return self.get_templates(
"create",
fallback=self.add_view_class.template_name,
)
@cached_property
def edit_template_name(self):
"""
A template to be used when rendering ``edit_view``.
Default: if :attr:`template_prefix` is specified, an ``edit.html``
template in the prefix directory and its ``{app_label}/{model_name}/``
or ``{app_label}/`` subdirectories will be used. Otherwise, the
``edit_view_class.template_name`` will be used.
"""
return self.get_templates(
"edit",
fallback=self.edit_view_class.template_name,
)
@cached_property
def delete_template_name(self):
"""
A template to be used when rendering ``delete_view``.
Default: if :attr:`template_prefix` is specified, a ``confirm_delete.html``
template in the prefix directory and its ``{app_label}/{model_name}/``
or ``{app_label}/`` subdirectories will be used. Otherwise, the
``delete_view_class.template_name`` will be used.
"""
return self.get_templates(
"confirm_delete",
fallback=self.delete_view_class.template_name,
)
@cached_property
def history_template_name(self):
"""
A template to be used when rendering ``history_view``.
Default: if :attr:`template_prefix` is specified, a ``history.html``
template in the prefix directory and its ``{app_label}/{model_name}/``
or ``{app_label}/`` subdirectories will be used. Otherwise, the
``history_view_class.template_name`` will be used.
"""
return self.get_templates(
"history",
fallback=self.history_view_class.template_name,
)
@cached_property
def inspect_template_name(self):
"""
A template to be used when rendering ``inspect_view``.
Default: if :attr:`template_prefix` is specified, an ``inspect.html``
template in the prefix directory and its ``{app_label}/{model_name}/``
or ``{app_label}/`` subdirectories will be used. Otherwise, the
``inspect_view_class.template_name`` will be used.
"""
return self.get_templates(
"inspect",
fallback=self.inspect_view_class.template_name,
)
@cached_property
def list_display(self):
"""
A list or tuple, where each item is either:
- The name of a field on the model;
- The name of a callable or property on the model that accepts a single
parameter for the model instance; or
- An instance of the ``wagtail.admin.ui.tables.Column`` class.
If the name refers to a database field, the ability to sort the listing
by the database column will be offerred and the field's verbose name
will be used as the column header.
If the name refers to a callable or property, an ``admin_order_field``
attribute can be defined on it to point to the database column for
sorting. A ``short_description`` attribute can also be defined on the
callable or property to be used as the column header.
This list will be passed to the ``list_display`` attribute of the index
view. If left unset, the ``list_display`` attribute of the index view
will be used instead, which by default is defined as
``["__str__", wagtail.admin.ui.tables.UpdatedAtColumn()]``.
"""
return self.index_view_class.list_display
@cached_property
def list_filter(self):
"""
A list or tuple, where each item is the name of model fields of type
``BooleanField``, ``CharField``, ``DateField``, ``DateTimeField``,
``IntegerField`` or ``ForeignKey``.
Alternatively, it can also be a dictionary that maps a field name to a
list of lookup expressions.
This will be passed as django-filter's ``FilterSet.Meta.fields``
attribute. See
`its documentation <https://django-filter.readthedocs.io/en/stable/guide/usage.html#generating-filters-with-meta-fields>`_
for more details.
If ``filterset_class`` is set, this attribute will be ignored.
"""
return self.index_view_class.list_filter
@cached_property
def filterset_class(self):
"""
A subclass of ``wagtail.admin.filters.WagtailFilterSet``, which is a
subclass of `django_filters.FilterSet <https://django-filter.readthedocs.io/en/stable/ref/filterset.html>`_.
This will be passed to the ``filterset_class`` attribute of the index view.
"""
return self.index_view_class.filterset_class
@cached_property
def search_fields(self):
"""
The fields to use for the search in the index view.
If set to ``None`` and :attr:`search_backend_name` is set to use a Wagtail search backend,
the ``search_fields`` attribute of the model will be used instead.
"""
return self.index_view_class.search_fields
@cached_property
def search_backend_name(self):
"""
The name of the Wagtail search backend to use for the search in the index view.
If set to a falsy value, the search will fall back to use Django's QuerySet API.
"""
return self.index_view_class.search_backend_name
@cached_property
def list_export(self):
"""
A list or tuple, where each item is the name of a field, an attribute,
or a single-argument callable on the model to be exported.
"""
return self.index_view_class.list_export
@cached_property
def export_headings(self):
"""
A dictionary of export column heading overrides in the format
``{field_name: heading}``.
"""
return self.index_view_class.export_headings
@cached_property
def export_filename(self):
"""
The base file name for the exported listing, without extensions.
If unset, the model's :attr:`~django.db.models.Options.db_table` will be
used instead.
"""
return self.model._meta.db_table
@cached_property
def menu_label(self):
return self.model_opts.verbose_name_plural.title()
@cached_property
def menu_item_class(self):
from wagtail.admin.menu import MenuItem
def is_shown(_self, request):
return self.permission_policy.user_has_any_permission(
request.user, ("add", "change", "delete")
)
return type(
f"{self.model.__name__}MenuItem",
(MenuItem,),
{"is_shown": is_shown},
)
def formfield_for_dbfield(self, db_field, **kwargs):
return db_field.formfield(**kwargs)
def get_form_class(self, for_update=False):
"""
Returns the form class to use for the create / edit forms.
"""
# If an edit handler is defined, use it to construct the form class.
if self._edit_handler:
return self._edit_handler.get_form_class()
# Otherwise, use Django's modelform_factory.
fields = self.get_form_fields()
exclude = self.get_exclude_form_fields()
if fields is None and exclude is None:
raise ImproperlyConfigured(
"Subclasses of ModelViewSet must specify 'get_form_class', 'form_fields' "
"or 'exclude_form_fields'."
)
return modelform_factory(
self.model,
formfield_callback=self.formfield_for_dbfield,
fields=fields,
exclude=exclude,
)
def get_form_fields(self):
"""
Returns a list or tuple of field names to be included in the create / edit forms.
"""
return getattr(self, "form_fields", None)
def get_exclude_form_fields(self):
"""
Returns a list or tuple of field names to be excluded from the create / edit forms.
"""
return getattr(self, "exclude_form_fields", None)
def get_edit_handler(self):
"""
Returns the appropriate edit handler for this ``ModelViewSet`` class.
It can be defined either on the model itself or on the ``ModelViewSet``,
as the ``edit_handler`` or ``panels`` properties. If none of these are
defined, it will return ``None`` and the form will be constructed as
a Django form using :meth:`get_form_class` (without using
:ref:`forms_panels_overview`).
"""
if hasattr(self, "edit_handler"):
edit_handler = self.edit_handler
elif hasattr(self, "panels"):
panels = self.panels
edit_handler = ObjectList(panels)
elif hasattr(self.model, "edit_handler"):
edit_handler = self.model.edit_handler
elif hasattr(self.model, "panels"):
panels = self.model.panels
edit_handler = ObjectList(panels)
else:
return None
return edit_handler.bind_to_model(self.model)
@cached_property
def _edit_handler(self):
"""
An edit handler that has been bound to the model class,
to be used across views.
"""
return self.get_edit_handler()
@property
def url_finder_class(self):
return type(
"_ModelAdminURLFinder",
(ModelAdminURLFinder,),
{
"permission_policy": self.permission_policy,
"edit_url_name": self.get_url_name("edit"),
},
)
def register_admin_url_finder(self):
register_admin_url_finder(self.model, self.url_finder_class)
def register_reference_index(self):
if self.add_to_reference_index:
ReferenceIndex.register_model(self.model)
def get_urlpatterns(self):
urlpatterns = [
path("", self.index_view, name="index"),
path("results/", self.index_results_view, name="index_results"),
path("new/", self.add_view, name="add"),
path("edit/<str:pk>/", self.edit_view, name="edit"),
path("delete/<str:pk>/", self.delete_view, name="delete"),
path("history/<str:pk>/", self.history_view, name="history"),
path(
"history-results/<str:pk>/",
self.history_results_view,
name="history_results",
),
path("usage/<str:pk>/", self.usage_view, name="usage"),
]
if self.inspect_view_enabled:
urlpatterns.append(
path("inspect/<str:pk>/", self.inspect_view, name="inspect")
)
if self.copy_view_enabled:
urlpatterns.append(path("copy/<str:pk>/", self.copy_view, name="copy"))
# RemovedInWagtail70Warning: Remove legacy URL patterns
urlpatterns += self._legacy_urlpatterns
return urlpatterns
@cached_property
def _legacy_urlpatterns(self):
# RemovedInWagtail70Warning: Remove legacy URL patterns
return [
path("<str:pk>/", self.redirect_to_edit_view),
path("<str:pk>/delete/", self.redirect_to_delete_view),
]
def on_register(self):
super().on_register()
self.register_admin_url_finder()
self.register_reference_index()
class ModelViewSetGroup(ViewSetGroup):
"""
A container for grouping together multiple
:class:`~wagtail.admin.viewsets.model.ModelViewSet` instances.
All attributes and methods from
:class:`~wagtail.admin.viewsets.base.ViewSetGroup` are available.
"""
def get_app_label_from_subitems(self):
for instance in self.registerables:
if app_label := getattr(instance, "app_label", ""):
return app_label.title()
return ""
@cached_property
def menu_label(self):
return self.get_app_label_from_subitems()