Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# Imports
3from django.core.exceptions import ImproperlyConfigured
4from django.core.paginator import Paginator, InvalidPage
5from django.forms import models as model_forms
6from django.http import HttpResponseRedirect
7from django.shortcuts import get_object_or_404
8from django.urls import reverse, NoReverseMatch
9from django.utils.translation import ugettext_lazy as _
10import os
11from superdjango.exceptions import IMustBeMissingSomething
12from .base import BaseView
13from .forms import FormMixin
15# Exports
17__all__ = (
18 "CreateView",
19 "DeleteView",
20 "DetailView",
21 "ListView",
22 "ModelView",
23 "UpdateView",
24)
26# Base
29class ModelView(FormMixin, BaseView):
30 """Base class for model interactions.
32 To avoid the excessive use of mixins, an engineering compromise was struck when developing the ``ModelView``:
34 1. The view extends the base view which includes a number of commonly useful features and context variables.
35 2. The view also extends :py:class:`FormMixin`, which doesn't make sense for detail and list views, but provides the
36 basis for sensible defaults with create and update views.
37 3. The delete view may be customized to include a form if one is desired. For example, to add a checkbox for an
38 additional confirmation step.
39 4. Other than ``get_object_list()`` and use of ``get_queryset()``, ``ModelView`` does *not* implement list logic.
40 :py:class:`ListView` provides this instead.
41 5. No authentication or authorization logic is provided by default. It is up to the developer to utilize
42 ``superdjango.views.access`` or their own custom logic if these features are desired.
44 """
46 context_object_name = None
47 """The name of the model object or objects when used in a template."""
49 field_names = None
50 """A list of field names to be included in the view. For form views, these are the names included in the form. For
51 detail and list views, these may be used to display record data.
52 """
54 lookup_field = "pk"
55 """The field used to uniquely identify the model."""
57 lookup_key = None
58 """The key in GET used to uniquely identify the model. Defaults to the ``lookup_field``."""
60 model = None
61 """The model (class) to which the view pertains. Required."""
63 object = None
64 """The record (instance) of a model for create, detail, delete, and update views. This is not used by list views."""
66 object_list = None
67 """The record instances of a model for list views."""
69 prefetch_related = None
70 """A list of foreign key or many to many fields that should be called using Django's
71 `prefetch_related <https://docs.djangoproject.com/en/stable/ref/models/querysets/#prefetch-related>`_.
72 """
74 queryset = None
75 """The queryset used to acquire the object or object list. See ``get_queryset()``."""
77 select_related = None
78 """A list of foreign key fields that should be called using Django's
79 `select_related <https://docs.djangoproject.com/en/stable/ref/models/querysets/#select-related>`_.
80 """
82 template_name_suffix = None
83 """The suffix added to the end of template names when automatically generated. This should be overridden by child
84 classes.
85 """
87 @classmethod
88 def get_app_label(cls):
89 """Get the app label (name) for the model represented by the view.
91 :rtype: str
93 """
94 # noinspection PyProtectedMember
95 return cls.get_model()._meta.app_label
97 def get_cancel_url(self):
98 """Return the value of the class attribute, if defined. Otherwise attempt to return the list view for the
99 current model.
101 :rtype: str | None
103 """
104 if self.cancel_url is not None:
105 return self.cancel_url
107 view_name = "%s_%s_list" % (self.get_app_label(), self.get_model_name())
109 try:
110 return reverse(view_name)
111 except NoReverseMatch:
112 return None
114 def get_context_data(self, **kwargs):
115 """Get context used to render the response.
117 The following variables are added to the context:
119 - ``verbose_name``: The verbose name of the model.
120 - ``verbose_name_plural``: The plural name of the model.
122 For single-object views, the ``object`` variable is also added along with a specific name if
123 ``context_object_name`` is defined.
125 For multi-object views, the ``object_list`` variable is added along with a specific name if
126 ``context_object_name`` is defined.
128 """
129 context = super().get_context_data(**kwargs)
131 context_object_name = self.get_context_object_name()
133 if self.object is not None:
134 context['object'] = self.object
136 if context_object_name is not None:
137 context[context_object_name] = self.object
138 elif self.object_list is not None:
139 context['object_list'] = self.object_list
141 if context_object_name is not None:
142 context[context_object_name] = self.object_list
143 else:
144 pass
146 context['verbose_name'] = self.get_verbose_name()
147 context['verbose_name_plural'] = self.get_verbose_name_plural()
149 return context
151 def get_context_object_name(self):
152 """Get the name of the model (or models) used in templates.
154 :rtype: str | None
156 """
157 if self.context_object_name is not None:
158 return self.context_object_name
160 return None
162 def get_field_names(self):
163 """Get the field names to be included in the model form.
165 :rtype: list[str]
167 .. tip::
168 If the ``field_names`` class attribute is not defined, *all* of the model's fields are included, which is
169 probably *not* what you want.
171 """
172 if self.field_names is not None:
173 return self.field_names
175 field_names = list()
177 model = self.get_model()
179 # noinspection PyProtectedMember
180 for field in model._meta.get_fields():
181 field_names.append(field.name)
183 return field_names
185 def get_form_class(self):
186 """Get the form class to use for instantiating a model form. If ``form_class`` is not defined, the
187 :py:func:`django.forms.models.modelform_factory()` is used in conjunction with ``get_field_names()``.
189 :raise: AttributeError
191 """
192 if self.form_class is not None:
193 return self.form_class
195 field_names = self.get_field_names()
197 model = self.get_model()
199 return model_forms.modelform_factory(model, fields=field_names)
201 @classmethod
202 def get_lookup_field(cls):
203 """Get the name of the field used to uniquely identify a model instance.
205 :rtype: str
206 :raises: IMustBeMissingSomething
208 """
209 if cls.lookup_field is not None:
210 return cls.lookup_field
212 raise IMustBeMissingSomething(cls.__name__, "lookup_field", "get_lookup_field")
214 @classmethod
215 def get_lookup_key(cls):
216 """Get the key used in GET to uniquely identify a model instance.
218 :rtype: str
220 """
221 if cls.lookup_key is not None:
222 return cls.lookup_key
224 return cls.get_lookup_field()
226 @classmethod
227 def get_model(cls):
228 """Get the model class used by the view.
230 :raise: IMustBeMissingSomething
232 .. note::
233 This saves checking for the model in the various class methods rather than accessing ``cls.model`` and
234 handling the exception every time the model is referenced.
236 """
237 if cls.model is not None:
238 return cls.model
240 raise IMustBeMissingSomething(cls.__name__, "model", "get_model")
242 @classmethod
243 def get_model_name(cls):
244 """Get the model name in lower case.
246 :rtype: str
247 :raise: IMustBeMissingSomething
248 :raises: See ``get_model()``.
250 """
251 model = cls.get_model()
252 return model.__name__.lower()
254 def get_object(self):
255 """Get the object (model instance) that may be used in :py:class:`CreateView`, :py:class:`DetailView`, and
256 :py:class:`DeleteView`.
258 """
259 lookup_field = self.get_lookup_field()
260 lookup_key = self.get_lookup_key()
262 queryset = self.get_queryset()
264 try:
265 # noinspection PyUnresolvedReferences
266 criteria = {lookup_field: self.kwargs[lookup_key]}
267 except KeyError:
268 e = 'The "%s" lookup key for the "%s" lookup field was not found in kwargs for the "%s" view.'
269 raise ImproperlyConfigured(e % (lookup_key, lookup_field, self.__class__.__name__))
271 return get_object_or_404(queryset, **criteria)
273 def get_object_list(self):
274 """Get the objects to be displayed by list views.
276 :rtype: django.db.models.QuerySet
278 """
279 return self.get_queryset()
281 def get_queryset(self):
282 """Get the queryset used by the view. This will either be a list or individual instance.
284 :rtype: django.db.models.QuerySet
286 """
287 if self.queryset is not None:
288 # noinspection PyProtectedMember
289 return self.queryset._clone()
291 model = self.get_model()
293 # noinspection PyProtectedMember
294 queryset = model._default_manager.all()
296 if isinstance(self.select_related, (list, tuple)):
297 queryset = queryset.select_related(*self.select_related)
299 if isinstance(self.prefetch_related, (list, tuple)):
300 queryset = queryset.prefetch_related(*self.prefetch_related)
302 return queryset
304 def get_success_url(self):
305 """Return the value of the class attribute, if defined. Otherwise attempt to return the list view for the
306 current model. Alternatively, the absolute URL will be returned if the model defines ``get_absolute_url()``.
308 :raise: IMustBeMissingSomething
310 """
311 if self.success_url is not None:
312 return self.success_url
314 view_name = "%s_%s_list" % (self.get_app_label(), self.get_model_name())
316 try:
317 return reverse(view_name)
318 except NoReverseMatch:
319 pass
321 try:
322 return self.object.get_absolute_url()
323 except AttributeError:
324 pass
326 raise IMustBeMissingSomething(self.__class__.__name__, "get_success_url")
328 def get_template_name_suffix(self):
329 """Get the suffix for the current view template.
331 :rtype: str
333 .. tip::
334 Extending classes should define the ``template_name_suffix``. The suffix should include an underscore for
335 separation. For example, the suffix for a view for creating a new record would be ``_add``.
337 The default behavior here is to return an empty string if ``template_name_suffix`` is not defined.
339 """
340 if self.template_name_suffix is not None:
341 return self.template_name_suffix
343 return ""
345 def get_template_names(self):
346 """Get the template names that may be used for rendering the response.
348 :rtype: list[str]
350 The possible names are generated like so:
352 1. If the child class defines a ``template_name``, this is always returned as the first element of the list.
353 2. This method first defines templates that may be defined by the local project in the form of
354 ``{app_label}/{model_name_lower}{template_name_suffix}.html``.
356 """
357 templates = list()
359 # This will be the first template that is checked, which means if takes priority over any other possible
360 # template names.
361 if self.template_name is not None:
362 templates.append(self.template_name)
364 # The default template name is based on the suffix, which may supplied by the extending class.
365 template_name_suffix = self.get_template_name_suffix()
366 if template_name_suffix is not None:
367 file_name = "%s%s.html" % (self.get_model_name(), template_name_suffix)
369 # noinspection PyProtectedMember
370 templates.append(os.path.join(self.model._meta.app_label, file_name))
372 return templates
374 def get_verbose_name(self):
375 """Get the verbose name for the model.
377 :rtype: str
379 """
380 # noinspection PyProtectedMember
381 return self.get_model()._meta.verbose_name
383 def get_verbose_name_plural(self):
384 """Get the plural verbose name for the model.
386 :rtype: str
388 """
389 # noinspection PyProtectedMember
390 return self.get_model()._meta.verbose_name_plural
392# Views
395class CreateView(ModelView):
396 """Present, validate, and submit a form for a new model record (instance).
398 .. note::
399 Because :py:class:`ModelView`` extends :py:class:`FormMixin`, this class does *not* need to implement ``get()``,
400 ``get_form()``, ``form_invalid()``, or ``post()``.
402 See also ``ModelView.get_success_url()``.
404 """
406 template_name_suffix = "_form"
408 def form_valid(self, form):
409 """Override to save the new record."""
410 self.object = form.save()
412 return super().form_valid(form)
415class DeleteView(ModelView):
416 """Delete and existing model record (instance)."""
417 field_names = []
418 template_name_suffix = "_confirm_delete"
420 def get(self, request, *args, **kwargs):
421 """Load the object and render the template. No form is used."""
422 self.object = self.get_object()
423 context = self.get_context_data()
424 return self.render_to_response(context)
426 def post(self, request, *args, **kwargs):
427 """Delete the object and redirect using the success URL."""
428 self.object = self.get_object()
429 self.object.delete()
431 success_message = self.get_success_message()
432 if success_message is not None:
433 self.messages.success(success_message)
435 return HttpResponseRedirect(self.get_success_url())
438class DetailView(ModelView):
439 """Display detail for a model record (instance)."""
441 template_name_suffix = "_detail"
443 # noinspection PyUnusedLocal
444 def get(self, request, *args, **kwargs):
445 """Get the model record (instance) to be displayed."""
446 self.object = self.get_object()
448 context = self.get_context_data()
449 return self.render_to_response(context)
452class ListView(ModelView):
453 """List model records."""
455 allow_empty = True
456 """Indicates whether an empty queryset is allowed. When ``False`` an empty queryset will raise an ``Http404``."""
458 empty_message = None
459 """The message to display when there are no results."""
461 limit = None
462 """The total records to be displayed on a page. Setting this value will invoke pagination."""
464 page_keyword = "page"
465 """The GET key used to indicate the pagination page number. Defaults to ``page``."""
467 pagination_style = "previous-next"
468 """The style for navigation through paginated results, ``numbers`` or ``previous-next``."""
470 template_name_suffix = "_list"
472 # noinspection PyUnusedLocal
473 def get(self, request, *args, **kwargs):
474 """Get the queryset and optionally apply pagination.
476 The context includes ``no_results_message`` when ``allow_empty`` is ``True`` and no results are found.
478 ``is_paginated`` is also included and is either ``True`` or ``False``.
480 If pagination is enabled, additional variables are also added to the context:
482 - ``current_page``: The same as ``page_object``.
483 - ``page_keyword``: The GET keyword used to indicate the current page number.
484 - ``page_obj``: The current page object from ``get_paginated_queryset()``.
485 - ``pagination_style``: The preferred output of pagination links.
486 - ``paginator``: The paginator instance from ``get_paginator()``.
488 """
489 queryset = self.get_object_list()
491 # Handle no results.
492 if not queryset.exists():
493 if not self.allow_empty:
494 self.dispatch_not_found(self.get_empty_message())
496 context = self.get_context_data(no_results_message=self.get_empty_message())
497 return self.render_to_response(context)
499 # Handle pagination.
500 limit = self.get_limit()
501 if limit is not None:
502 page = self.get_paginated_queryset(queryset, limit)
503 self.object_list = page
504 context = self.get_context_data(
505 current_page=page, # superdjango
506 page_keyword=self.get_page_keyword(),
507 page_obj=page, # django
508 pagination_style=self.pagination_style,
509 is_paginated=True,
510 paginator=page.paginator
511 )
512 else:
513 self.object_list = queryset
514 context = self.get_context_data(
515 current_page=None, # superdjango
516 page_keyword=self.get_page_keyword(),
517 page_obj=None, # django
518 pagination_style=self.pagination_style,
519 is_paginated=False,
520 paginator=None
521 )
523 return self.render_to_response(context)
525 def get_empty_message(self):
526 """Get the message to display when there are no results.
528 :rtype: str
530 """
531 if self.empty_message is not None:
532 return self.empty_message
534 return _("There are currently no %s." % self.get_verbose_name_plural())
536 def get_limit(self):
537 """Get the number of records to display per page.
539 :rtype: int | None
541 """
542 return self.limit
544 def get_page_keyword(self):
545 """Get the keyword used to identify the page number in GET.
547 :rtype: str
549 """
550 return self.page_keyword
552 def get_paginated_queryset(self, queryset, limit):
553 """Paginate the given queryset.
555 :param queryset: The query set to be paginated.
556 :type queryset: django.db.Queryset
558 :param limit: The number of objects per page.
559 :type limit: int
561 :rtype: Page
563 """
564 # Paginator is needed for num_pages below.
565 paginator = self.get_paginator(queryset, limit)
567 # The page keyword determines what how the page number is identified in the URL.
568 page_keyword = self.get_page_keyword()
570 # Get the current page number.
571 page_query_param = self.request.GET.get(page_keyword)
572 page_number = page_query_param or 1
574 try:
575 page_number = int(page_number)
576 except ValueError:
577 if page_number == 'last':
578 page_number = paginator.num_pages
579 else:
580 message = _("The page number could not be determined.")
581 self.dispatch_not_found(message)
583 # Return the page instance or die trying.
584 try:
585 return paginator.page(page_number)
586 except InvalidPage as e:
587 message = _("Invalid page (%s): %s" % (page_number, e))
588 self.dispatch_not_found(message)
590 # noinspection PyMethodMayBeStatic
591 def get_paginator(self, queryset, per_page):
592 """Get a paginator instance.
594 :rtype: Paginator
596 """
597 return Paginator(queryset, per_page)
600class UpdateView(ModelView):
601 """Update and existing model record (instance).
603 .. note::
604 Because :py:class:`ModelView`` extends :py:class:`FormMixin`, this class does *not* need to implement
605 ``get_form()`` or ``form_invalid()``. ``get()`` and ``post()`` must be implemented so that the current model
606 instance is bound to the form.
608 See also ``ModelView.get_success_url()``.
610 """
612 template_name_suffix = "_form"
614 def form_valid(self, form):
615 """Save the record."""
616 self.object = form.save()
618 return super().form_valid(form)
620 def get(self, request, *args, **kwargs):
621 """Get the object and add ``form`` to the context."""
622 self.object = self.get_object()
624 form = self.get_form(instance=self.object)
626 context = self.get_context_data(form=form)
628 return self.render_to_response(context)
630 def post(self, request, *args, **kwargs):
631 """Get the object and form and check whether the form is valid."""
632 self.object = self.get_object()
634 form = self.get_form(data=request.POST, files=request.FILES, instance=self.object)
635 if form.is_valid():
636 return self.form_valid(form)
638 return self.form_invalid(form)