1from django.core.exceptions import ImproperlyConfigured
2from django.db import models
3from django.http import Http404
4from django.utils.translation import gettext as _
5from django.views.generic.base import ContextMixin, TemplateResponseMixin, View
6
7
8class SingleObjectMixin(ContextMixin):
9 """
10 Provide the ability to retrieve a single object for further manipulation.
11 """
12
13 model = None
14 queryset = None
15 slug_field = "slug"
16 context_object_name = None
17 slug_url_kwarg = "slug"
18 pk_url_kwarg = "pk"
19 query_pk_and_slug = False
20
21 def get_object(self, queryset=None):
22 """
23 Return the object the view is displaying.
24
25 Require `self.queryset` and a `pk` or `slug` argument in the URLconf.
26 Subclasses can override this to return any object.
27 """
28 # Use a custom queryset if provided; this is required for subclasses
29 # like DateDetailView
30 if queryset is None:
31 queryset = self.get_queryset()
32
33 # Next, try looking up by primary key.
34 pk = self.kwargs.get(self.pk_url_kwarg)
35 slug = self.kwargs.get(self.slug_url_kwarg)
36 if pk is not None:
37 queryset = queryset.filter(pk=pk)
38
39 # Next, try looking up by slug.
40 if slug is not None and (pk is None or self.query_pk_and_slug):
41 slug_field = self.get_slug_field()
42 queryset = queryset.filter(**{slug_field: slug})
43
44 # If none of those are defined, it's an error.
45 if pk is None and slug is None:
46 raise AttributeError(
47 "Generic detail view %s must be called with either an object "
48 "pk or a slug in the URLconf." % self.__class__.__name__
49 )
50
51 try:
52 # Get the single item from the filtered queryset
53 obj = queryset.get()
54 except queryset.model.DoesNotExist:
55 raise Http404(
56 _("No %(verbose_name)s found matching the query")
57 % {"verbose_name": queryset.model._meta.verbose_name}
58 )
59 return obj
60
61 def get_queryset(self):
62 """
63 Return the `QuerySet` that will be used to look up the object.
64
65 This method is called by the default implementation of get_object() and
66 may not be called if get_object() is overridden.
67 """
68 if self.queryset is None:
69 if self.model:
70 return self.model._default_manager.all()
71 else:
72 raise ImproperlyConfigured(
73 "%(cls)s is missing a QuerySet. Define "
74 "%(cls)s.model, %(cls)s.queryset, or override "
75 "%(cls)s.get_queryset()." % {"cls": self.__class__.__name__}
76 )
77 return self.queryset.all()
78
79 def get_slug_field(self):
80 """Get the name of a slug field to be used to look up by slug."""
81 return self.slug_field
82
83 def get_context_object_name(self, obj):
84 """Get the name to use for the object."""
85 if self.context_object_name:
86 return self.context_object_name
87 elif isinstance(obj, models.Model):
88 return obj._meta.model_name
89 else:
90 return None
91
92 def get_context_data(self, **kwargs):
93 """Insert the single object into the context dict."""
94 context = {}
95 if self.object:
96 context["object"] = self.object
97 context_object_name = self.get_context_object_name(self.object)
98 if context_object_name:
99 context[context_object_name] = self.object
100 context.update(kwargs)
101 return super().get_context_data(**context)
102
103
104class BaseDetailView(SingleObjectMixin, View):
105 """
106 Base view for displaying a single object.
107
108 This requires subclassing to provide a response mixin.
109 """
110
111 def get(self, request, *args, **kwargs):
112 self.object = self.get_object()
113 context = self.get_context_data(object=self.object)
114 return self.render_to_response(context)
115
116
117class SingleObjectTemplateResponseMixin(TemplateResponseMixin):
118 template_name_field = None
119 template_name_suffix = "_detail"
120
121 def get_template_names(self):
122 """
123 Return a list of template names to be used for the request. May not be
124 called if render_to_response() is overridden. Return a list containing
125 ``template_name``, if set on the value. Otherwise, return a list
126 containing:
127
128 * the contents of the ``template_name_field`` field on the
129 object instance that the view is operating upon (if available)
130 * ``<app_label>/<model_name><template_name_suffix>.html``
131 """
132 try:
133 names = super().get_template_names()
134 except ImproperlyConfigured:
135 # If template_name isn't specified, it's not a problem --
136 # we just start with an empty list.
137 names = []
138
139 # If self.template_name_field is set, grab the value of the field
140 # of that name from the object; this is the most specific template
141 # name, if given.
142 if self.object and self.template_name_field:
143 name = getattr(self.object, self.template_name_field, None)
144 if name:
145 names.insert(0, name)
146
147 # The least-specific option is the default <app>/<model>_detail.html;
148 # only use this if the object in question is a model.
149 if isinstance(self.object, models.Model):
150 object_meta = self.object._meta
151 names.append(
152 "%s/%s%s.html"
153 % (
154 object_meta.app_label,
155 object_meta.model_name,
156 self.template_name_suffix,
157 )
158 )
159 elif getattr(self, "model", None) is not None and issubclass(
160 self.model, models.Model
161 ):
162 names.append(
163 "%s/%s%s.html"
164 % (
165 self.model._meta.app_label,
166 self.model._meta.model_name,
167 self.template_name_suffix,
168 )
169 )
170
171 # If we still haven't managed to find any template names, we should
172 # re-raise the ImproperlyConfigured to alert the user.
173 if not names:
174 raise ImproperlyConfigured(
175 "SingleObjectTemplateResponseMixin requires a definition "
176 "of 'template_name', 'template_name_field', or 'model'; "
177 "or an implementation of 'get_template_names()'."
178 )
179
180 return names
181
182
183class DetailView(SingleObjectTemplateResponseMixin, BaseDetailView):
184 """
185 Render a "detail" view of an object.
186
187 By default this is a model instance looked up from `self.queryset`, but the
188 view will support display of *any* object by overriding `self.get_object()`.
189 """