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.urls import path, re_path
4from django.core.exceptions import ImproperlyConfigured
5from superdjango.exceptions import IMustBeMissingSomething, NoViewForYou
6from superdjango.patterns import PATTERNS
7from .crud import CreateView, DeleteView, DetailView, ListView, UpdateView
9# Exports
11__all__ = (
12 "ModelViewSet",
13 "ViewSet",
14)
16# BaseView Sets
19class ViewSet(object):
20 """A generic viewset, the main purpose of which is to collect views together to consistently generate their URLs."""
22 views = None
24 def __init__(self, app_name=None, namespace=None):
25 """Initialize a generic view set.
27 :param app_name: The app name.
28 :type app_name: str
30 :param namespace: The namespace in which the app will operate.
31 :type namespace: str
33 """
34 self.app_name = app_name
35 self.namespace = namespace
37 @classmethod
38 def get_pattern_name(cls, view_class):
39 """Get the name of a view.
41 :param view_class: The class-based view.
42 :type view_class: class
44 :rtype: str
46 :raise: ImproperlyConfigured
48 .. note::
49 The view may override the default behavior by implementing a ``pattern_name`` property or the
50 ``get_pattern_name()`` method. This method must return a ``str``.
52 """
53 try:
54 return view_class.get_pattern_name()
55 except AttributeError:
56 pass
58 try:
59 return view_class.pattern_name
60 except AttributeError:
61 pass
63 e = "'%s' must define 'pattern_name' attribute or implement the 'get_pattern_name()' method."
64 raise ImproperlyConfigured(e % view_class.__class__.__name__)
66 @classmethod
67 def get_pattern_value(cls, view_class):
68 """Get the value (path) of a view.
70 :param view_class: The class-based view.
71 :type view_class: class
73 :rtype: str
75 :raise: ImproperlyConfigured
77 """
78 try:
79 return view_class.get_pattern_value()
80 except AttributeError:
81 pass
83 try:
84 return view_class.pattern_value
85 except AttributeError:
86 pass
88 e = "'%s' must define 'pattern_value' attribute or implement the 'get_pattern_value()' method."
89 raise ImproperlyConfigured(e % view_class.__class__.__name__)
91 @classmethod
92 def get_pattern_regex(cls, view_class):
93 """Get the regular expression of a view.
95 :param view_class: The class-based view.
96 :type view_class: class
98 :rtype: str | None
100 .. note::
101 This method does *not* raise an improperly configured, allowing the other methods to fall back to
102 ``get_pattern_value()``.
104 """
105 try:
106 return view_class.get_pattern_regex()
107 except AttributeError:
108 pass
110 try:
111 return view_class.pattern_regex
112 except AttributeError:
113 pass
115 return None
117 def get_url(self, view_class, prefix=None):
118 """Get the url of a view.
120 :param view_class: The class-based view.
121 :type view_class: class
123 :param prefix: A prefix added to the regex. Note that the URL will fail to resolve if the ``^`` appears in the
124 regex for the view.
126 :rtype: URLPattern
128 .. note::
129 The view may override the default behavior by implementing a ``get_url()`` method. This method must return
130 an instance of ``URLPattern`` using ``re_path()`` or ``path()``.
132 """
133 # This allows the view to completely override the URL generation process.
134 try:
135 return view_class.get_url()
136 except AttributeError:
137 pass
139 name = self.get_pattern_name(view_class)
141 # Handle regular expressions.
142 regex = self.get_pattern_regex(view_class)
143 if regex is not None:
144 if prefix is not None:
145 regex = prefix + regex
147 return re_path(regex, view_class.as_view(), name=name)
149 # Handle paths.
150 value = self.get_pattern_value(view_class)
152 if prefix is not None:
153 value = prefix + value
155 return path(value, view_class.as_view(), name=name)
157 def get_urls(self, prefix=None):
158 """Generate the url objects.
160 :param prefix: A prefix added to the URL. Note that the URL will fail to resolve if ``^`` appears in the
161 regex for the view.
162 :type prefix: str
164 :rtype: list
166 """
167 views = self.get_views()
169 urls = list()
170 for view_class in views:
171 urls.append(self.get_url(view_class, prefix=prefix))
173 return urls
175 def get_views(self):
176 """Get the views included in the view set.
178 :rtype: list
179 :raise: ImproperlyConfigured
181 """
182 if self.views is not None:
183 return self.views
185 e = "'%s' must either define 'views' or override 'get_views()'"
186 raise ImproperlyConfigured(e % self.__class__.__name__)
189class ModelViewSet(ViewSet):
190 """Extends :py:class:`ViewSet` to add support for models.
192 **BaseView Name**
194 Unless the ``get_pattern_name()`` method is defined on the view, the name will be automatically determined based on
195 the name of the model plus a suffix for the purpose of the view class:
197 - ``_create``
198 - ``_delete``
199 - ``_detail``
200 - ``_list``
201 - ``_update``
203 The name of the view (used to reverse the URL) is established in the following manner:
205 1. Use the ``pattern_name`` property if it has been set on the view class.
206 2. Use the result of ``get_pattern_name()`` if the method is defined on the view class.
207 3. If the view is an extension of any of the model views, the name will be automatically set based on the name of
208 the model plus the purpose of the view class: ``_create``, ``_delete``, ``_detail``, ``_list``, and
209 ``_update``.
210 4. If none of the other methods have produced a result, the name of the view class will be used. This is rarely
211 ideal, but does provide a default and prevents throwing an error.
213 **Pattern Value**
215 Unless the ``get_pattern_value()`` method is defined, the base pattern will be automatically determined based on the
216 purpose of the view class:
218 - ``create``
219 - ``delete``
220 - ``detail``
221 - ``update``
223 List views are assumed to be the index of the app's URLs. Views that require an identifier will use the
224 ``lookup_field`` on the view class.
226 **Example**
228 .. code-block:: py
230 # views.py
231 from superdjango.views.models import CreateView, DeleteView, DetailView, ListView, UpdateView
232 from superdjango.views.viewsets import ModelViewSet
233 from .models import Task
235 class CreateTask(CreateView):
236 model = Task
237 # ...
239 class DeleteTask(DeleteView):
240 model = Task
241 # ...
243 class ListTasks(ListView)
244 model = Task
245 # ...
247 class TaskDetail(DetailView):
248 model = Task
249 # ...
251 class UpdateTask(UpdateView):
252 model = Task
253 # ...
255 class TaskViewSet(ViewSet):
256 views = [
257 CreateTask,
258 DeleteTask,
259 ListTasks,
260 TaskDetail,
261 UpdateTask,
262 ]
264 .. code-block:: py
266 # urls.py
267 from views import TaskViewSet
269 urlpatterns = TaskViewSet().get_urls()
271 """
273 @classmethod
274 def get_pattern_name(cls, view_class):
275 """Get the name of a view based on the implemented model view.
277 :param view_class: The class-based view.
278 :type view_class: class
280 :rtype: str
282 :raise: NoViewForYou
284 """
286 # This allows the view class to override the automatic behavior below. This is helpful when multiple model views
287 # are defined in the same view set.
288 try:
289 return ViewSet.get_pattern_name(view_class)
290 except ImproperlyConfigured:
291 pass
293 # The model attribute must be defined.
294 try:
295 model = view_class.get_model()
296 except (AttributeError, IMustBeMissingSomething) as e:
297 raise NoViewForYou(str(e))
299 # App name and model name are both required for a unique view name.
300 # noinspection PyProtectedMember
301 app_label = model._meta.app_label
302 # noinspection PyProtectedMember
303 model_name = model._meta.model_name
305 # Get the action/verb used in the name.
306 verb = cls.get_pattern_verb(view_class)
307 if verb is None:
308 message = "A verb for the %s view could not be determined; add a class method get_pattern_verb() or " \
309 "class attribute or pattern_verb to help identify this view."
310 raise NoViewForYou(message % view_class.__name__)
312 # We have a winner.
313 return "%s_%s_%s" % (app_label, model_name, verb)
315 @classmethod
316 def get_pattern_value(cls, view_class):
317 """Get the path of the model view based on the CBV it extends.
319 :param view_class: The class-based view.
320 :type view_class: class
322 :rtype: str
324 :raise: NoViewForYou
326 """
327 # This allows the view class to override the automatic behavior below.
328 try:
329 return ViewSet.get_pattern_value(view_class)
330 except ImproperlyConfigured:
331 pass
333 # The model attribute must be defined.
334 try:
335 model = view_class.get_model()
336 except (AttributeError, IMustBeMissingSomething) as e:
337 raise NoViewForYou(str(e))
339 # noinspection PyProtectedMember
340 model_name = model._meta.model_name
342 # Get the action/verb that the view represents.
343 verb = cls.get_pattern_verb(view_class)
345 if verb is None:
346 message = "A verb for the %s view could not be determined; add a class method get_pattern_verb() or " \
347 "class attribute or pattern_verb to help identify this view."
348 raise NoViewForYou(message % view_class.__name__)
350 # Add and list verbs are not included in the URL and do not utilize a pattern.
351 if verb == "create":
352 return "%s/create/" % model_name
353 elif verb == "list":
354 return "%s/" % model_name
355 else:
356 try:
357 pattern = PATTERNS[view_class.lookup_field]
358 return "%s/%s/%s/" % (model_name, verb, pattern)
359 except KeyError:
360 message = "A pattern for the %s verb could not be determined because the lookup_field of %s view is " \
361 "either undefined or is not a recognized pattern identifier."
362 raise NoViewForYou(message % (verb, view_class.__name__))
364 @classmethod
365 def get_pattern_verb(cls, view_class):
366 """Get the action (verb) represented by the view class.
368 :rtype: str | None
370 """
371 # This allows the view class to override the automatic behavior below. This is helpful for custom model views
372 # that define their own verb.
373 try:
374 return view_class.get_pattern_verb()
375 except AttributeError:
376 pass
378 try:
379 return view_class.pattern_verb
380 except AttributeError:
381 pass
383 if issubclass(view_class, CreateView):
384 return "create"
385 elif issubclass(view_class, DeleteView):
386 return "delete"
387 elif issubclass(view_class, DetailView):
388 return "detail"
389 elif issubclass(view_class, ListView):
390 return "list"
391 elif issubclass(view_class, UpdateView):
392 return "update"
393 else:
394 return None