diff --git a/boofilsic/settings.py b/boofilsic/settings.py index 96b83929..37b87cc3 100644 --- a/boofilsic/settings.py +++ b/boofilsic/settings.py @@ -55,7 +55,7 @@ INSTALLED_APPS = [ "oauth2_provider", "tz_detect", "sass_processor", - "simple_history", + "auditlog", "markdownx", "polymorphic", "easy_thumbnails", @@ -85,7 +85,7 @@ MIDDLEWARE = [ "django.middleware.clickjacking.XFrameOptionsMiddleware", "hijack.middleware.HijackUserMiddleware", "tz_detect.middleware.TimezoneMiddleware", - "simple_history.middleware.HistoryRequestMiddleware", + "auditlog.middleware.AuditlogMiddleware", "maintenance_mode.middleware.MaintenanceModeMiddleware", # this should be last ] diff --git a/catalog/apps.py b/catalog/apps.py index 0b71ad8b..74fb6c63 100644 --- a/catalog/apps.py +++ b/catalog/apps.py @@ -10,7 +10,8 @@ class CatalogConfig(AppConfig): from catalog import models from catalog import sites from journal import models as journal_models - from catalog.models import init_catalog_search_models + from catalog.models import init_catalog_search_models, init_catalog_audit_log from catalog import api init_catalog_search_models() + init_catalog_audit_log() diff --git a/catalog/common/models.py b/catalog/common/models.py index c7642df1..f7d44734 100644 --- a/catalog/common/models.py +++ b/catalog/common/models.py @@ -1,3 +1,4 @@ +from functools import cached_property from polymorphic.models import PolymorphicModel from django.db import models import logging @@ -8,7 +9,6 @@ from django.utils import timezone from django.core.files.uploadedfile import SimpleUploadedFile from django.contrib.contenttypes.models import ContentType from django.utils.baseconv import base62 -from simple_history.models import HistoricalRecords import uuid from typing import cast from .utils import DEFAULT_ITEM_COVER, item_cover_path, resource_cover_path @@ -17,6 +17,8 @@ from django.conf import settings from users.models import User from django.db import connection from ninja import Schema +from auditlog.context import disable_auditlog +from auditlog.models import LogEntry, AuditlogHistoryField _logger = logging.getLogger(__name__) @@ -259,7 +261,6 @@ class Item(SoftDeleteMixin, PolymorphicModel): created_time = models.DateTimeField(auto_now_add=True) edited_time = models.DateTimeField(auto_now=True) is_deleted = models.BooleanField(default=False, db_index=True) - history = HistoricalRecords() merged_to_item = models.ForeignKey( "Item", null=True, @@ -279,6 +280,15 @@ class Item(SoftDeleteMixin, PolymorphicModel): ] ] + _content_type_ids = [] + + @cached_property + def history(self): + # can't use AuditlogHistoryField bc it will only return history with current content type + return LogEntry.objects.filter( + object_id=self.id, content_type_id__in=self._content_type_ids + ) + def clear(self): self.set_parent_item(None) self.primary_lookup_id_value = None @@ -348,13 +358,25 @@ class Item(SoftDeleteMixin, PolymorphicModel): if model not in Item.__subclasses__(): raise ValueError("invalid model to recast to") ct = ContentType.objects.get_for_model(model) + old_ct = self.polymorphic_ctype tbl = self.__class__._meta.db_table - obj = model(item_ptr_id=self.pk, polymorphic_ctype=ct) - obj.save_base(raw=True) - obj.save(update_fields=["polymorphic_ctype"]) - with connection.cursor() as cursor: - cursor.execute(f"DELETE FROM {tbl} WHERE item_ptr_id = %s", [self.pk]) - return model.objects.get(pk=obj.pk) + with disable_auditlog(): + # disable audit as serialization won't work here + obj = model(item_ptr_id=self.pk, polymorphic_ctype=ct) + obj.save_base(raw=True) + obj.save(update_fields=["polymorphic_ctype"]) + with connection.cursor() as cursor: + cursor.execute(f"DELETE FROM {tbl} WHERE item_ptr_id = %s", [self.pk]) + obj = model.objects.get(pk=obj.pk) + LogEntry.objects.log_create( + obj, + action=LogEntry.Action.UPDATE, + changes={ + "polymorphic_ctype_id": [old_ct.id, ct.id], + "__model__": [old_ct.model, ct.model], + }, + ) + return obj @property def uuid(self): diff --git a/catalog/models.py b/catalog/models.py index ff535449..47b95e21 100644 --- a/catalog/models.py +++ b/catalog/models.py @@ -24,6 +24,7 @@ from .search.models import Indexer from django.contrib.contenttypes.models import ContentType from django.conf import settings import logging +from auditlog.registry import auditlog _logger = logging.getLogger(__name__) @@ -86,3 +87,16 @@ def init_catalog_search_models(): Indexer.update_model_indexable(Performance) # Indexer.update_model_indexable(PerformanceProduction) # Indexer.update_model_indexable(CatalogCollection) + + +def init_catalog_audit_log(): + for cls in Item.__subclasses__(): + auditlog.register( + cls, exclude_fields=["metadata", "created_time", "edited_time"] + ) + + auditlog.register( + ExternalResource, include_fields=["item", "id_type", "id_value", "url"] + ) + + Item._content_type_ids = list(all_content_types().values()) diff --git a/catalog/templates/catalog_edit.html b/catalog/templates/catalog_edit.html index 3f816959..8bf4a510 100644 --- a/catalog/templates/catalog_edit.html +++ b/catalog/templates/catalog_edit.html @@ -53,6 +53,39 @@ onclick="{% if item %}window.location='{{ item.url }}'{% else %}history.go(-1){% endif %}"> + {% if request.user.is_superuser %} +
+ + + + + + + + + + {% for log in item.history.all %} + + + + {% for key, value in log.changes_dict.items %} + + + + + + {% empty %} +

No data.

+ {% endfor %} + {% empty %} +

No history for this item has been logged yet.

+ {% endfor %} + +
FieldFromTo
+ ({{ log.id }}) {{ log.actor }} {{ log.get_action_display }} on {{ log.timestamp }} +
{{ key }}{{ value.0|default:"None" }}{{ value.1|default:"None" }}
+
+ {% endif %}