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 FieldDoesNotExist
4from django.db import models
5from django.urls import path, re_path, reverse, reverse_lazy, NoReverseMatch
7# Exports
9__all__ = (
10 "Pattern",
11 "ModelPattern",
12)
14# Classes
17class Pattern(object):
18 """A generic pattern for representing a view."""
20 def __init__(self, name, namespace=None):
21 self.name = name
22 self.namespace = namespace
24 def __repr__(self):
25 return "<%s %s:%s>" % (self.__class__.__name__, self.namespace, self.name)
27 def get_name(self, namespace=None):
28 """Get the name of the view with the ``namespace`` if one has been provided.
30 :param namespace: The namespace in which the view operates.
31 :type namespace: str
33 :rtype: str
35 """
36 _namespace = None
37 if namespace or self.namespace:
38 _namespace = namespace or self.namespace
40 if _namespace is not None:
41 return "%s_%s" % (_namespace, self.name)
43 return self.name
45 def reverse(self, *args, **kwargs):
46 return reverse(self.get_name(), args=args, kwargs=kwargs)
48 def reverse_lazy(self, *args, **kwargs):
49 return reverse_lazy(self.get_name(), args=args, kwargs=kwargs)
52class ModelPattern(object):
53 """A helper class for encapsulating data for model views to derive the necessary components for automatic URL
54 definition.
56 Primarily used by the UI package to automatically generate URLs for the user interface. However, programmatic use is
57 also possible:
59 .. code-block:: python
61 update = ModelPattern(
62 Project,
63 ProjectUpdate,
64 lookup_field="uuid"
65 )
67 """
69 def __init__(self, model, view, list_path="", lookup_field=None, lookup_key=None, name=None, namespace=None,
70 prefix=None, regex=None, verb=None):
71 """Initialize a view pattern.
73 :param model: The model class. This is used only during instantiation.
74 :type model: class
76 :param view: The view class.
77 :type view: class
79 :param list_path: The part of the path that represents the model's list view. This defaults to an empty string,
80 but may be given if something else will live at the "index" of the model's views. For example,
81 ``list_path="index"`` or ``list_path="list"``.
82 :type list_path: str
84 :param lookup_field: The name of the field used to uniquely identify a record. This is required for any view
85 that resolves to a specific record. Default: ``uuid``.
86 :type lookup_field: str
88 :param name: If given, this is used as the pattern name of the view instead of composing the name using pattern
89 attributes.
90 :type name: str
92 :param namespace: The namespace in which the view operates.
93 :type namespace: str
95 :param prefix: The prefix of the URL. For example, the model's ``app_label``.
96 :type prefix: str
98 :param regex: A regular expression used to identify the view in URLs. This is optional.
99 :type regex: str
101 :param verb: The name (verb) of the view. For example: "create", "list", "detail", etc.
102 :type verb: str
104 .. important::
105 ``lookup_field`` should only be provided for views that actually require identifying a specific record.
107 """
108 self.list_path = list_path
109 self.lookup_field = lookup_field
110 self.lookup_key = lookup_key or lookup_field
111 self.namespace = namespace
112 self.prefix = prefix
113 self.view = view
114 self.verb = verb
115 # noinspection PyProtectedMember
116 self._app_label = model._meta.app_label
117 self._name = name
118 self._regex = regex
119 # noinspection PyProtectedMember
120 self._model_name = model._meta.model_name
122 def __repr__(self):
123 return "<%s %s>" % (self.__class__.__name__, self.get_name())
125 def get_name(self, namespace=None):
126 """Get the name of the view.
128 :param namespace: The namespace in which the view operates.
129 :type namespace: str
131 :rtype: str
133 """
134 # if self._name is not None:
135 # name = self._name
136 # else:
137 # name = self.get_verb()
139 # The only certain way of providing unique pattern name is to include the app label and model name before the
140 # name of the action.
141 if self._name:
142 pattern_name = "%s_%s" % (self._app_label, self._name)
143 else:
144 pattern_name = "%s_%s_%s" % (self._app_label, self._model_name, self.get_verb())
146 # Handle namespace.
147 _namespace = None
148 if namespace or self.namespace:
149 _namespace = namespace or self.namespace
151 if _namespace is not None:
152 pattern_name = "%s_%s" % (_namespace, pattern_name)
154 return pattern_name
156 def get_path(self):
157 """Get the URL path for the view.
159 :rtype: str
161 """
162 name = self.get_name()
164 if "_create" in name:
165 value = "create/"
166 elif "_list" in name:
167 value = self.list_path
168 elif self.lookup_field is not None:
169 if self.lookup_field == "pk":
170 converter = "<int:%s>" % self.lookup_key
171 else:
172 converter = "<%s:%s>" % (self.lookup_field, self.lookup_key)
174 value = "%s/%s/" % (self.get_verb().replace("_", "/"), converter)
175 else:
176 value = self.get_verb().replace("_", "/") + "/"
178 value = "%s/%s" % (self._model_name, value)
180 if self.prefix is not None:
181 value = "%s/%s" % (self.prefix, value)
183 return value
185 def get_regex(self):
186 """Get the regular expression for the view.
188 :rtype: str | None
190 """
191 if self._regex is None:
192 return None
194 regex = self._regex
195 if self.prefix:
196 regex = "%s/%s" % (self.prefix, regex)
198 return regex
200 def get_url(self):
201 """Get the URL object for the view.
203 :rtype: RegexPattern | RoutePattern
205 """
206 if self._regex is not None:
207 return re_path(self.get_regex(), self.view, name=self.get_name())
209 return path(self.get_path(), self.view, name=self.get_name())
211 def get_verb(self):
212 """Get the action (verb) that the view represents.
214 :rtype: str
216 """
217 if self.verb is not None:
218 return self.verb
220 return self.view.__name__.replace("_view", "")
222 @property
223 def name(self):
224 """Alias for ``get_name()``."""
225 return self.get_name()
227 @property
228 def path(self):
229 """Alias for ``get_path()``."""
230 return self.get_path()
232 @property
233 def regex(self):
234 """Alias for ``get_regex()``."""
235 return self.get_regex()
237 def reverse(self, namespace=None, record=None):
238 """Use Django's ``reverse()`` to obtain the URL.
240 :param namespace: The namespace in which the view operates.
241 :type namespace: str
243 :param record: The model instance.
245 :rtype: str | None
247 """
248 args = None
249 if record is not None:
250 args = [self._get_identifier(record)]
252 try:
253 return reverse(self.get_name(namespace=namespace), args=args)
254 except NoReverseMatch:
255 return None
257 # TODO: Determine whether reverse_lazy() is actually useful.
258 def reverse_lazy(self, namespace=None, record=None):
259 """Use Django's ``reverse_lazy()`` to obtain the URL.
261 :param namespace: The namespace in which the view operates.
262 :type namespace: str
264 :param record: The model instance.
266 :rtype: str
268 """
269 args = None
270 if record is not None:
271 args = [self._get_identifier(record)]
273 return reverse_lazy(self.get_name(namespace=namespace), args=args)
275 # noinspection PyMethodMayBeStatic
276 def _field_exists(self, field_name, record, unique=False):
277 """Determine if the given field name is defined on the model.
279 :param record: The model instance may be used to determine which field to use.
281 :param field_name: The name of the field to check.
282 :type field_name: str
284 :param unique: Also test whether the field is unique.
285 :type unique: bool
287 :rtype: bool
289 """
290 try:
291 # noinspection PyProtectedMember
292 field = record._meta.get_field(field_name)
293 if unique:
295 if isinstance(field, models.UUIDField):
296 return True
298 return field.unique
300 return True
301 except FieldDoesNotExist:
302 return False
304 def _get_identifier(self, record):
305 """Get the value of the given record's lookup field.
307 :param record: The model instance.
309 :rtype: int | str
311 """
312 lookup_field = self._get_lookup_field(record)
313 return getattr(record, lookup_field)
315 def _get_lookup_field(self, record):
316 """Get (guess) the name of the field that uniquely identifies the record.
318 :param record: The model instance may be used to determine which field to use.
320 :rtype: str
322 """
323 if self.lookup_field is not None:
324 return self.lookup_field
326 if self._field_exists("unique_id", record, unique=True):
327 return "unique_id"
329 if self._field_exists("uuid", record, unique=True):
330 return "uuid"
332 if self._field_exists("slug", record, unique=True):
333 return "slug"
335 return "pk"