Coverage for superdjango/db/history/models.py : 48%

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.conf import settings
4from django.contrib.contenttypes.models import ContentType
5from django.core.exceptions import ObjectDoesNotExist
6from django.db import models
7from django.utils.encoding import force_str
8from django.utils.translation import gettext_lazy as _
9from superdjango.conf import SUPERDJANGO
10from superdjango.shortcuts import get_user_name
11from .constants import CREATE_VERBS, DELETE_VERBS, DETAIL_VERBS, UPDATE_VERBS
13# Exports
15__all__ = (
16 "HistoryModel",
17)
19# Constants
21AUTH_USER_MODEL = settings.AUTH_USER_MODEL
23# Models
26class HistoryModel(models.Model):
27 """An abstract model for implementing a running record history.
29 .. code-block:: python
31 from superdjango.db.history.models import HistoryModel
33 class LogEntry(HistoryModel):
35 class Meta:
36 get_latest_by = "added_dt"
37 ordering = ["-added_dt"]
38 verbose_name = _("Log Entry")
39 verbose_name_plural = _("Log Entries")
41 """
43 absolute_url = models.CharField(
44 _("URL"),
45 blank=True,
46 help_text=_("The URL of the model instance."),
47 max_length=1024,
48 null=True
49 )
51 added_dt = models.DateTimeField(
52 _("added date/time"),
53 auto_now_add=True,
54 help_text=_("Date and time the action was taken.")
55 )
57 content_type = models.ForeignKey(
58 ContentType,
59 help_text=_("The internal content type of the object."),
60 on_delete=models.CASCADE,
61 related_name="%(app_label)s_%(class)s_record_history",
62 verbose_name=_('content type')
63 )
65 object_id = models.CharField(
66 _('object id'),
67 help_text=_("The object (record) ID."),
68 max_length=256
69 )
71 object_label = models.CharField(
72 _("object label"),
73 blank=True,
74 help_text=_("The string representation of the object, i.e. it's title, label, etc."),
75 max_length=128
76 )
78 user = models.ForeignKey(
79 AUTH_USER_MODEL,
80 blank=True,
81 help_text=_("The user that performed the action."),
82 null=True,
83 on_delete=models.SET_NULL,
84 related_name="%(app_label)s_%(class)s_record_history",
85 verbose_name=_("user")
86 )
88 user_name = models.CharField(
89 _("user name"),
90 blank=True,
91 help_text=_("The name of the user that performed the action."),
92 max_length=128
93 )
95 # create, delete, detail (view), update, or other custom verbs. No choices are defined to allow custom verbs.
96 CREATE = "create"
97 DELETE = "delete"
98 DETAIL = "detail"
99 UPDATE = "update"
100 verb = models.CharField(
101 _("verb"),
102 help_text=_("The verb (action) taken on a record."),
103 max_length=128
104 )
106 verb_display = models.CharField(
107 _("verb display"),
108 blank=True,
109 help_text=_("The display value of the verb."),
110 max_length=128,
111 null=True
112 )
114 verbose_name = models.CharField(
115 _("verbose name"),
116 blank=True,
117 help_text=_("The verbose name of the model."),
118 max_length=256
119 )
121 class Meta:
122 abstract = True
124 def __str__(self):
125 return self.get_message()
127 @property
128 def action(self):
129 """An alias for ``get_verb_display()``."""
130 return self.get_verb_display()
132 def get_message(self):
133 """Get the message that describes the action.
135 :rtype: str
137 .. note::
138 The added date/time of the entry is *not* localized for the current user's timezone. To support this, you'll
139 need to implement the message in a template.
141 """
142 template = '%(user_name)s %(verb)s "%(object_label)s" %(verbose_name)s at %(added_dt)s.'
144 # noinspection PyUnresolvedReferences
145 added_dt = self.added_dt.strftime(SUPERDJANGO.DATETIME_MASK)
147 # noinspection PyUnresolvedReferences
148 verbose_name = self.verbose_name.lower()
150 context = {
151 'added_dt': added_dt,
152 'object_label': self.object_label,
153 'user_name': self.performed_by,
154 'verb': self.get_verb_display(),
155 'verbose_name': verbose_name,
156 }
158 return _(template % context)
160 def get_object(self):
161 """Get the model instance that was the subject of the entry.
163 :returns: The model instance. If the object was deleted, ``None`` is returned.
165 """
166 try:
167 # noinspection PyUnresolvedReferences
168 return self.content_type.get_object_for_this_type(pk=self.object_id)
169 except ObjectDoesNotExist:
170 return None
172 def get_verb_display(self):
173 """Get the human-friendly verb.
175 :rtype: str
177 """
178 if self.verb_display:
179 return self.verb_display
180 elif self.is_create:
181 return "added"
182 elif self.is_delete:
183 return "removed"
184 elif self.is_detail:
185 return "viewed"
186 elif self.is_update:
187 return "updated"
188 else:
189 return self.verb
191 def get_url(self):
192 """Get the URL of the model instance.
194 The URL may not be available. Additionally, if the action represents a delete, no URL is returned.
196 :rtype: str | None
198 """
199 if self.is_delete:
200 return None
202 return self.absolute_url
204 @property
205 def is_create(self):
206 """Indicates the action is an addition.
208 :rtype: bool
210 """
211 return self.verb in CREATE_VERBS
213 @property
214 def is_delete(self):
215 """Indicates the action is a delete.
217 :rtype: bool
219 """
220 return self.verb in DELETE_VERBS
222 @property
223 def is_detail(self):
224 """Indicates the action is a detail.
226 :rtype: bool
228 """
229 return self.verb in DETAIL_VERBS
231 @property
232 def is_update(self):
233 """Indicates the action was an update.
235 :rtype: bool
237 """
238 return self.verb in UPDATE_VERBS
240 @classmethod
241 def log(cls, record, user, verb, fields=None, url=None, verb_display=None):
242 """Create a new history entry.
244 :param record: The model instance.
246 :param user: The user (instance) performing the action.
248 :param verb: The action taken.
249 :type verb: str
251 :param fields: A list of changed fields.
252 :type fields: list[superdjango.db.history.utils.FieldChange]
254 :param url: The URL of the model instance. Typically that of the detail view. If omitted, an attempt will be
255 made to acquire the URL from ``get_absolute_url()``.
256 :type url: str
258 :param verb_display: The human0friendly name of the action taken.
259 :type verb_display: str
261 :returns: The log entry instance.
263 .. note::
264 By default, nothing is done with ``fields``. When you extend the class, you may save the fields to the
265 extending model, or iterate over them to save each change to a separate model that refers back to the new
266 history instance. See ``log_field_changes()``.
268 """
269 if hasattr(record, "get_display_name") and callable(record.get_display_name):
270 object_label = record.get_display_name()
271 else:
272 object_label = force_str(str(record))
274 # noinspection PyProtectedMember
275 verbose_name = record._meta.verbose_name
277 if url is None and verb not in DELETE_VERBS:
278 try:
279 url = record.get_absolute_url()
280 except AttributeError:
281 pass
283 kwargs = {
284 'absolute_url': url,
285 'content_type': ContentType.objects.get_for_model(record),
286 'object_id': record.pk,
287 'object_label': object_label,
288 'user': user,
289 'user_name': get_user_name(user),
290 'verb': verb,
291 'verb_display': verb_display,
292 'verbose_name': verbose_name,
293 }
295 # noinspection PyUnresolvedReferences
296 history = cls(**kwargs)
297 history.save()
299 cls.log_field_changes(history, verb, fields=fields)
301 return history
303 @classmethod
304 def log_field_changes(cls, instance, verb, fields=None):
305 """Log changes to fields.
307 :param instance: The history record, NOT the original model instance.
309 :param verb: The action taken. This allows verbs such as create or delete to be ignored.
310 :type verb: str
312 :param fields: A list of changed fields.
313 :type fields: list[superdjango.db.history.utils.FieldChange]
315 """
316 pass
318 @property
319 def message(self):
320 """An alias for ``get_message()``."""
321 return self.get_message()
323 @property
324 def performed_by(self):
325 """Get the name of the user that performed the action.
327 :rtype: str
329 """
330 # noinspection PyUnresolvedReferences
331 if self.user_id:
332 return get_user_name(self.user)
334 return self.user_name