Source code for pretalx.common.models.log

# SPDX-FileCopyrightText: 2017-present Tobias Kunze
# SPDX-License-Identifier: AGPL-3.0-only WITH LicenseRef-Pretalx-AGPL-3.0-Terms

import logging

from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import FieldDoesNotExist
from django.db import models
from django.db.models.fields.related import ManyToManyRel, ManyToOneRel
from django.utils.functional import cached_property
from django_scopes import ScopedManager


[docs] class ActivityLog(models.Model): """This model logs actions within an event. It is **not** designed to provide a complete or reliable audit trail. :param is_orga_action: True, if the logged action was performed by a privileged user. """ event = models.ForeignKey( to="event.Event", on_delete=models.PROTECT, related_name="log_entries", null=True, blank=True, ) person = models.ForeignKey( to="person.User", on_delete=models.PROTECT, related_name="log_entries", null=True, blank=True, ) content_type = models.ForeignKey(to=ContentType, on_delete=models.CASCADE) object_id = models.PositiveIntegerField(db_index=True) content_object = GenericForeignKey("content_type", "object_id") timestamp = models.DateTimeField(auto_now_add=True, db_index=True) action_type = models.CharField(max_length=200) data = models.JSONField(null=True, blank=True, default=dict) is_orga_action = models.BooleanField(default=False) objects = ScopedManager(event="event") class Meta: ordering = ("-timestamp",) def __str__(self): """Custom __str__ to help with debugging.""" event = getattr(self.event, "slug", "None") person = getattr(self.person, "name", "None") return f"ActivityLog(event={event}, person={person}, content_object={self.content_object}, action_type={self.action_type})" @cached_property def json_data(self): # Kept for backwards compatibility, as well as to avoid None-checks return self.data or {} @cached_property def display(self) -> str: from pretalx.common.signals import activitylog_display # noqa: PLC0415 for _receiver, response in activitylog_display.send( self.event, activitylog=self ): if response: return response logger = logging.getLogger(__name__) logger.warning('Unknown log action "%s".', self.action_type) return self.action_type @cached_property def display_object(self) -> str: """Returns a link (formatted HTML) to the object in question.""" from pretalx.common.signals import activitylog_object_link # noqa: PLC0415 try: if not self.content_object: return "" except AttributeError: # pragma: no cover # Content types are terrible, terrible magic return "" responses = activitylog_object_link.send(sender=self.event, activitylog=self) if responses: for _receiver, response in responses: if response: return response return "" @cached_property def changes(self): if not self.data or not self.event or not self.data.get("changes"): return obj = self.content_object if not obj: return result = {} for key, value in self.data["changes"].items(): display = value.copy() if not value.get("old") and not value.get("new"): continue if key.startswith("question-"): question_pk = key.split("-", 1)[-1] question = self.event.questions.filter(pk=question_pk).first() if question: display["question"] = question display["label"] = question.question else: try: if field := obj.__class__._meta.get_field(key): display["field"] = field if isinstance(field, (ManyToOneRel, ManyToManyRel)): display["label"] = ( field.related_model._meta.verbose_name_plural ) else: display["label"] = field.verbose_name except FieldDoesNotExist: display["label"] = key.capitalize() result[key] = display return result