# Imports
from django.urls import path, re_path
from django.core.exceptions import ImproperlyConfigured
from superdjango.exceptions import IMustBeMissingSomething, NoViewForYou
from superdjango.patterns import PATTERNS
from .crud import CreateView, DeleteView, DetailView, ListView, UpdateView
# Exports
__all__ = (
"ModelViewSet",
"ViewSet",
)
# BaseView Sets
[docs]class ViewSet(object):
"""A generic viewset, the main purpose of which is to collect views together to consistently generate their URLs."""
views = None
[docs] def __init__(self, app_name=None, namespace=None):
"""Initialize a generic view set.
:param app_name: The app name.
:type app_name: str
:param namespace: The namespace in which the app will operate.
:type namespace: str
"""
self.app_name = app_name
self.namespace = namespace
[docs] @classmethod
def get_pattern_name(cls, view_class):
"""Get the name of a view.
:param view_class: The class-based view.
:type view_class: class
:rtype: str
:raise: ImproperlyConfigured
.. note::
The view may override the default behavior by implementing a ``pattern_name`` property or the
``get_pattern_name()`` method. This method must return a ``str``.
"""
try:
return view_class.get_pattern_name()
except AttributeError:
pass
try:
return view_class.pattern_name
except AttributeError:
pass
e = "'%s' must define 'pattern_name' attribute or implement the 'get_pattern_name()' method."
raise ImproperlyConfigured(e % view_class.__class__.__name__)
[docs] @classmethod
def get_pattern_value(cls, view_class):
"""Get the value (path) of a view.
:param view_class: The class-based view.
:type view_class: class
:rtype: str
:raise: ImproperlyConfigured
"""
try:
return view_class.get_pattern_value()
except AttributeError:
pass
try:
return view_class.pattern_value
except AttributeError:
pass
e = "'%s' must define 'pattern_value' attribute or implement the 'get_pattern_value()' method."
raise ImproperlyConfigured(e % view_class.__class__.__name__)
[docs] @classmethod
def get_pattern_regex(cls, view_class):
"""Get the regular expression of a view.
:param view_class: The class-based view.
:type view_class: class
:rtype: str | None
.. note::
This method does *not* raise an improperly configured, allowing the other methods to fall back to
``get_pattern_value()``.
"""
try:
return view_class.get_pattern_regex()
except AttributeError:
pass
try:
return view_class.pattern_regex
except AttributeError:
pass
return None
[docs] def get_url(self, view_class, prefix=None):
"""Get the url of a view.
:param view_class: The class-based view.
:type view_class: class
:param prefix: A prefix added to the regex. Note that the URL will fail to resolve if the ``^`` appears in the
regex for the view.
:rtype: URLPattern
.. note::
The view may override the default behavior by implementing a ``get_url()`` method. This method must return
an instance of ``URLPattern`` using ``re_path()`` or ``path()``.
"""
# This allows the view to completely override the URL generation process.
try:
return view_class.get_url()
except AttributeError:
pass
name = self.get_pattern_name(view_class)
# Handle regular expressions.
regex = self.get_pattern_regex(view_class)
if regex is not None:
if prefix is not None:
regex = prefix + regex
return re_path(regex, view_class.as_view(), name=name)
# Handle paths.
value = self.get_pattern_value(view_class)
if prefix is not None:
value = prefix + value
return path(value, view_class.as_view(), name=name)
[docs] def get_urls(self, prefix=None):
"""Generate the url objects.
:param prefix: A prefix added to the URL. Note that the URL will fail to resolve if ``^`` appears in the
regex for the view.
:type prefix: str
:rtype: list
"""
views = self.get_views()
urls = list()
for view_class in views:
urls.append(self.get_url(view_class, prefix=prefix))
return urls
[docs] def get_views(self):
"""Get the views included in the view set.
:rtype: list
:raise: ImproperlyConfigured
"""
if self.views is not None:
return self.views
e = "'%s' must either define 'views' or override 'get_views()'"
raise ImproperlyConfigured(e % self.__class__.__name__)
[docs]class ModelViewSet(ViewSet):
"""Extends :py:class:`ViewSet` to add support for models.
**BaseView Name**
Unless the ``get_pattern_name()`` method is defined on the view, the name will be automatically determined based on
the name of the model plus a suffix for the purpose of the view class:
- ``_create``
- ``_delete``
- ``_detail``
- ``_list``
- ``_update``
The name of the view (used to reverse the URL) is established in the following manner:
1. Use the ``pattern_name`` property if it has been set on the view class.
2. Use the result of ``get_pattern_name()`` if the method is defined on the view class.
3. If the view is an extension of any of the model views, the name will be automatically set based on the name of
the model plus the purpose of the view class: ``_create``, ``_delete``, ``_detail``, ``_list``, and
``_update``.
4. If none of the other methods have produced a result, the name of the view class will be used. This is rarely
ideal, but does provide a default and prevents throwing an error.
**Pattern Value**
Unless the ``get_pattern_value()`` method is defined, the base pattern will be automatically determined based on the
purpose of the view class:
- ``create``
- ``delete``
- ``detail``
- ``update``
List views are assumed to be the index of the app's URLs. Views that require an identifier will use the
``lookup_field`` on the view class.
**Example**
.. code-block:: py
# views.py
from superdjango.views.models import CreateView, DeleteView, DetailView, ListView, UpdateView
from superdjango.views.viewsets import ModelViewSet
from .models import Task
class CreateTask(CreateView):
model = Task
# ...
class DeleteTask(DeleteView):
model = Task
# ...
class ListTasks(ListView)
model = Task
# ...
class TaskDetail(DetailView):
model = Task
# ...
class UpdateTask(UpdateView):
model = Task
# ...
class TaskViewSet(ViewSet):
views = [
CreateTask,
DeleteTask,
ListTasks,
TaskDetail,
UpdateTask,
]
.. code-block:: py
# urls.py
from views import TaskViewSet
urlpatterns = TaskViewSet().get_urls()
"""
[docs] @classmethod
def get_pattern_name(cls, view_class):
"""Get the name of a view based on the implemented model view.
:param view_class: The class-based view.
:type view_class: class
:rtype: str
:raise: NoViewForYou
"""
# This allows the view class to override the automatic behavior below. This is helpful when multiple model views
# are defined in the same view set.
try:
return ViewSet.get_pattern_name(view_class)
except ImproperlyConfigured:
pass
# The model attribute must be defined.
try:
model = view_class.get_model()
except (AttributeError, IMustBeMissingSomething) as e:
raise NoViewForYou(str(e))
# App name and model name are both required for a unique view name.
# noinspection PyProtectedMember
app_label = model._meta.app_label
# noinspection PyProtectedMember
model_name = model._meta.model_name
# Get the action/verb used in the name.
verb = cls.get_pattern_verb(view_class)
if verb is None:
message = "A verb for the %s view could not be determined; add a class method get_pattern_verb() or " \
"class attribute or pattern_verb to help identify this view."
raise NoViewForYou(message % view_class.__name__)
# We have a winner.
return "%s_%s_%s" % (app_label, model_name, verb)
[docs] @classmethod
def get_pattern_value(cls, view_class):
"""Get the path of the model view based on the CBV it extends.
:param view_class: The class-based view.
:type view_class: class
:rtype: str
:raise: NoViewForYou
"""
# This allows the view class to override the automatic behavior below.
try:
return ViewSet.get_pattern_value(view_class)
except ImproperlyConfigured:
pass
# The model attribute must be defined.
try:
model = view_class.get_model()
except (AttributeError, IMustBeMissingSomething) as e:
raise NoViewForYou(str(e))
# noinspection PyProtectedMember
model_name = model._meta.model_name
# Get the action/verb that the view represents.
verb = cls.get_pattern_verb(view_class)
if verb is None:
message = "A verb for the %s view could not be determined; add a class method get_pattern_verb() or " \
"class attribute or pattern_verb to help identify this view."
raise NoViewForYou(message % view_class.__name__)
# Add and list verbs are not included in the URL and do not utilize a pattern.
if verb == "create":
return "%s/create/" % model_name
elif verb == "list":
return "%s/" % model_name
else:
try:
pattern = PATTERNS[view_class.lookup_field]
return "%s/%s/%s/" % (model_name, verb, pattern)
except KeyError:
message = "A pattern for the %s verb could not be determined because the lookup_field of %s view is " \
"either undefined or is not a recognized pattern identifier."
raise NoViewForYou(message % (verb, view_class.__name__))
[docs] @classmethod
def get_pattern_verb(cls, view_class):
"""Get the action (verb) represented by the view class.
:rtype: str | None
"""
# This allows the view class to override the automatic behavior below. This is helpful for custom model views
# that define their own verb.
try:
return view_class.get_pattern_verb()
except AttributeError:
pass
try:
return view_class.pattern_verb
except AttributeError:
pass
if issubclass(view_class, CreateView):
return "create"
elif issubclass(view_class, DeleteView):
return "delete"
elif issubclass(view_class, DetailView):
return "detail"
elif issubclass(view_class, ListView):
return "list"
elif issubclass(view_class, UpdateView):
return "update"
else:
return None