Source code for superdjango.db.history.models

# Imports

from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ObjectDoesNotExist
from django.db import models
from django.utils.encoding import force_str
from django.utils.translation import gettext_lazy as _
from superdjango.conf import SUPERDJANGO
from superdjango.shortcuts import get_user_name
from .constants import CREATE_VERBS, DELETE_VERBS, DETAIL_VERBS, UPDATE_VERBS

# Exports

__all__ = (
    "HistoryModel",
)

# Constants

AUTH_USER_MODEL = settings.AUTH_USER_MODEL

# Models


[docs]class HistoryModel(models.Model): """An abstract model for implementing a running record history. .. code-block:: python from superdjango.db.history.models import HistoryModel class LogEntry(HistoryModel): class Meta: get_latest_by = "added_dt" ordering = ["-added_dt"] verbose_name = _("Log Entry") verbose_name_plural = _("Log Entries") """ absolute_url = models.CharField( _("URL"), blank=True, help_text=_("The URL of the model instance."), max_length=1024, null=True ) added_dt = models.DateTimeField( _("added date/time"), auto_now_add=True, help_text=_("Date and time the action was taken.") ) content_type = models.ForeignKey( ContentType, help_text=_("The internal content type of the object."), on_delete=models.CASCADE, related_name="%(app_label)s_%(class)s_record_history", verbose_name=_('content type') ) object_id = models.CharField( _('object id'), help_text=_("The object (record) ID."), max_length=256 ) object_label = models.CharField( _("object label"), blank=True, help_text=_("The string representation of the object, i.e. it's title, label, etc."), max_length=128 ) user = models.ForeignKey( AUTH_USER_MODEL, blank=True, help_text=_("The user that performed the action."), null=True, on_delete=models.SET_NULL, related_name="%(app_label)s_%(class)s_record_history", verbose_name=_("user") ) user_name = models.CharField( _("user name"), blank=True, help_text=_("The name of the user that performed the action."), max_length=128 ) # create, delete, detail (view), update, or other custom verbs. No choices are defined to allow custom verbs. CREATE = "create" DELETE = "delete" DETAIL = "detail" UPDATE = "update" verb = models.CharField( _("verb"), help_text=_("The verb (action) taken on a record."), max_length=128 ) verb_display = models.CharField( _("verb display"), blank=True, help_text=_("The display value of the verb."), max_length=128, null=True ) verbose_name = models.CharField( _("verbose name"), blank=True, help_text=_("The verbose name of the model."), max_length=256 ) class Meta: abstract = True def __str__(self): return self.get_message() @property def action(self): """An alias for ``get_verb_display()``.""" return self.get_verb_display()
[docs] @classmethod def get_for(cls, record, limit=10): """Get history entries for a given record. :param record: The record (model instance) for which history is to be acquired. :param limit: The max number of history records to return. :type limit: int :rtype: django.db.models.QuerySet """ content_type = ContentType.objects.get_for_model(record) qs = cls.objects.filter(content_type=content_type, object_id=record.pk) if limit: return qs[:limit] return qs
[docs] def get_message(self): """Get the message that describes the action. :rtype: str .. note:: The added date/time of the entry is *not* localized for the current user's timezone. To support this, you'll need to implement the message in a template. """ template = '%(user_name)s %(verb)s "%(object_label)s" %(verbose_name)s at %(added_dt)s.' # noinspection PyUnresolvedReferences added_dt = self.added_dt.strftime(SUPERDJANGO.DATETIME_MASK) # noinspection PyUnresolvedReferences verbose_name = self.verbose_name.lower() context = { 'added_dt': added_dt, 'object_label': self.object_label, 'user_name': self.performed_by, 'verb': self.get_verb_display(), 'verbose_name': verbose_name, } return _(template % context)
[docs] def get_object(self): """Get the model instance that was the subject of the entry. :returns: The model instance. If the object was deleted, ``None`` is returned. """ try: # noinspection PyUnresolvedReferences return self.content_type.get_object_for_this_type(pk=self.object_id) except ObjectDoesNotExist: return None
[docs] def get_verb_display(self): """Get the human-friendly verb. :rtype: str """ if self.verb_display: return self.verb_display elif self.is_create: return "added" elif self.is_delete: return "removed" elif self.is_detail: return "viewed" elif self.is_update: return "updated" else: return self.verb
[docs] def get_url(self): """Get the URL of the model instance. The URL may not be available. Additionally, if the action represents a delete, no URL is returned. :rtype: str | None """ if self.is_delete: return None return self.absolute_url
@property def is_create(self): """Indicates the action is an addition. :rtype: bool """ return self.verb in CREATE_VERBS @property def is_delete(self): """Indicates the action is a delete. :rtype: bool """ return self.verb in DELETE_VERBS @property def is_detail(self): """Indicates the action is a detail. :rtype: bool """ return self.verb in DETAIL_VERBS @property def is_update(self): """Indicates the action was an update. :rtype: bool """ return self.verb in UPDATE_VERBS
[docs] @classmethod def log(cls, record, user, verb, fields=None, url=None, verb_display=None): """Create a new history entry. :param record: The model instance. :param user: The user (instance) performing the action. :param verb: The action taken. :type verb: str :param fields: A list of changed fields. :type fields: list[superdjango.db.history.utils.FieldChange] :param url: The URL of the model instance. Typically that of the detail view. If omitted, an attempt will be made to acquire the URL from ``get_absolute_url()``. :type url: str :param verb_display: The human0friendly name of the action taken. :type verb_display: str :returns: The log entry instance. .. note:: By default, nothing is done with ``fields``. When you extend the class, you may save the fields to the extending model, or iterate over them to save each change to a separate model that refers back to the new history instance. See ``log_field_changes()``. """ if hasattr(record, "get_display_name") and callable(record.get_display_name): object_label = record.get_display_name() else: object_label = force_str(str(record)) # noinspection PyProtectedMember verbose_name = record._meta.verbose_name if url is None and verb not in DELETE_VERBS: try: url = record.get_absolute_url() except AttributeError: pass kwargs = { 'absolute_url': url, 'content_type': ContentType.objects.get_for_model(record), 'object_id': record.pk, 'object_label': object_label, 'user': user, 'user_name': get_user_name(user), 'verb': verb, 'verb_display': verb_display, 'verbose_name': verbose_name, } # noinspection PyUnresolvedReferences history = cls(**kwargs) history.save() cls.log_field_changes(history, verb, fields=fields) return history
[docs] @classmethod def log_field_changes(cls, instance, verb, fields=None): """Log changes to fields. :param instance: The history record, NOT the original model instance. :param verb: The action taken. This allows verbs such as create or delete to be ignored. :type verb: str :param fields: A list of changed fields. :type fields: list[superdjango.db.history.utils.FieldChange] """ pass
@property def message(self): """An alias for ``get_message()``.""" return self.get_message() @property def performed_by(self): """Get the name of the user that performed the action. :rtype: str """ # noinspection PyUnresolvedReferences if self.user_id: return get_user_name(self.user) return self.user_name