# 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