diff --git a/CHANGES.rst b/CHANGES.rst index 0279c0dc..22031643 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -15,6 +15,7 @@ Unreleased - Added temporary requirement on ``asgiref>=3.6`` while the minimum required Django version is lower than 4.2 (gh-1261) - Small performance optimization of the ``clean-duplicate_history`` command (gh-1015) +- Allow setting change reason through ``SimpleHistoryAdmin``'s change form (gh-1232) 3.4.0 (2023-08-18) ------------------ diff --git a/simple_history/admin.py b/simple_history/admin.py index 34e3033f..f99909b0 100644 --- a/simple_history/admin.py +++ b/simple_history/admin.py @@ -1,4 +1,4 @@ -from django import http +from django import forms, http from django.apps import apps as django_apps from django.conf import settings from django.contrib import admin @@ -17,6 +17,8 @@ SIMPLE_HISTORY_EDIT = getattr(settings, "SIMPLE_HISTORY_EDIT", False) +class HistoryChangeReasonForm(forms.ModelForm): + history_change_reason = forms.CharField(required=False) class SimpleHistoryAdmin(admin.ModelAdmin): object_history_template = "simple_history/object_history.html" @@ -224,9 +226,30 @@ def render_history_view(self, request, template, context, **kwargs): def save_model(self, request, obj, form, change): """Set special model attribute to user for reference after save""" + obj._change_reason = form.cleaned_data.get("history_change_reason") obj._history_user = request.user super().save_model(request, obj, form, change) + form = HistoryChangeReasonForm + + def get_fields(self, request, obj=None): + return [ + field + for field in super().get_fields(request, obj) + if field != "history_change_reason" + ] + + def get_fieldsets(self, request, obj=None): + return super().get_fieldsets(request, obj) + ([ + ( + "History", + { + "classes": ["collapse"], + "fields": ["history_change_reason"], + }, + ) + ] if 'history_change_reason' in self.form.declared_fields else []) + @property def content_type_model_cls(self): """Returns the ContentType model class.""" diff --git a/simple_history/tests/tests/test_admin.py b/simple_history/tests/tests/test_admin.py index 2b441030..317c4f1e 100644 --- a/simple_history/tests/tests/test_admin.py +++ b/simple_history/tests/tests/test_admin.py @@ -218,6 +218,50 @@ def test_history_user_on_save_in_admin(self): [p.history_user for p in Poll.history.all()], [self.user, self.user] ) + def test_history_change_reason_on_save_in_admin(self): + self.login() + + # Ensure polls created via admin interface save correct change reason + change_reason = "New change reason" + initial_data = poll_data = { + "question": "new poll?", + "pub_date_0": "2012-01-01", + "pub_date_1": "10:00:00", + "history_change_reason": change_reason, + } + self.client.post(reverse("admin:tests_poll_add"), data=poll_data) + poll = Poll.objects.get() + self.assertEqual(poll.history.get().history_change_reason, change_reason) + + + # Ensure polls modified via admin interface save correct change reason + change_reason = "Edited change reason" + poll_data = { + "question": "new poll?", + "pub_date_0": "2011-01-01", + "pub_date_1": "10:00:00", + "history_change_reason": change_reason, + } + self.client.post(reverse("admin:tests_poll_change", args=[poll.id]), data=poll_data) + poll.refresh_from_db() + self.assertEqual(poll.pub_date.year, 2011) + self.assertEqual(poll.history.count(), 2) + self.assertEqual(poll.history.latest().history_change_reason, change_reason) + + # Let's emulate a revert + change_reason = "Revert to history record 0" + response = self.client.get(get_history_url(poll, 0)) + form = response.context.get("adminform").form + self.assertEqual(form["history_change_reason"].value(), None) # Always starts empty + self.assertEqual(form["pub_date"].value().year, 2012) + + self.client.post(get_history_url(poll, 0), data=initial_data | {"history_change_reason": change_reason}) + + poll.refresh_from_db() + self.assertEqual(poll.pub_date.year, 2012) + self.assertEqual(poll.history.count(), 3) + self.assertEqual(poll.history.latest().history_change_reason, change_reason) + def test_underscore_in_pk(self): self.login() book = Book(isbn="9780147_513731")