Last active
January 11, 2023 18:53
-
-
Save quevon24/83ea450da9355fa4b42fce52c85cd53f to your computer and use it in GitHub Desktop.
django-pghistory revert object from admin integration
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{% load i18n %} | |
{% load admin_urls %} | |
{% load getattribute from pghistory %} | |
<table id="change-history" class="table table-bordered table-striped"> | |
<thead> | |
<tr> | |
<th scope="col">{% trans 'PGH ID' %}</th> | |
{% for column in history_list_display %} | |
<th scope="col">{% trans column %}</th> | |
{% endfor %} | |
<th scope="col">{% trans 'PGH label' %}</th> | |
<th scope="col">{% trans 'PGH created at' %}</th> | |
</tr> | |
</thead> | |
<tbody> | |
{% for action in action_list %} | |
<tr> | |
<td><a | |
href="{% url opts|admin_urlname:'event_history' object.pk action.pk %}">{{ action.pgh_id }} | |
(Click to select)</a></td> | |
{% for column in history_list_display %} | |
<td scope="col">{{ action|getattribute:column }}</th> | |
{% endfor %} | |
<td>{{ action.pgh_label }}</td> | |
<td>{{ action.pgh_created_at }}</td> | |
</tr> | |
{% endfor %} | |
</tbody> | |
</table> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{% extends "admin/object_history.html" %} | |
{% load i18n %} | |
{% load admin_urls %} | |
{% load display_list from pghistory %} | |
{% block content %} | |
<div id="content-main"> | |
<p> | |
{% blocktrans %}Choose an object revision id from the list below to revert to a | |
previous version of this object.{% endblocktrans %}</p> | |
<div class="module"> | |
{% if action_list %} | |
{% display_list %} | |
{% else %} | |
<p>{% trans "This object doesn't have a change history." %}</p> | |
{% endif %} | |
</div> | |
</div> | |
{% endblock %} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{% extends "admin/change_form.html" %} | |
{% load i18n %} | |
{% block breadcrumbs %} | |
<div class="breadcrumbs"> | |
<a href="{% url "admin:index" %}">{% trans "Home" %}</a> › | |
<a href="{% url "admin:app_list" app_label %}">{{ app_label|capfirst|escape }}</a> | |
› | |
<a href="{% url changelist_url %}">{{ opts.verbose_name_plural|capfirst }}</a> | |
› | |
<a href="{% url change_url original.pk %}">{{ original|truncatewords:"18"}}</a> | |
› | |
<a href="..">{% trans "History" %}</a> › {% blocktrans with original_opts.verbose_name as verbose_name %}Revert "{{ verbose_name }}"{% endblocktrans %} | |
</div> | |
{% endblock %} | |
{% block submit_buttons_top %} | |
{% include "pghistory/submit_line.html" %} | |
{% endblock %} | |
{% block submit_buttons_bottom %} | |
{% include "pghistory/submit_line.html" %} | |
{% endblock %} | |
{% block form_top %} | |
<p>{% trans "Press the 'Revert' button below to revert to this version of the object." %}</p> | |
<div class="diff form-row"> | |
<h4>{% trans "Diff with previous event object" %}</h4> | |
<p> | |
{% if last_diff %} | |
{{ last_diff }} | |
{% else %} | |
{% trans "There are no previous changes." %} | |
{% endif %} | |
</p> | |
</div> | |
{% endblock %} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import pghistory.models | |
from django import http | |
from django.apps import apps as django_apps | |
from django.contrib import admin, messages | |
from django.contrib.admin import helpers | |
from django.contrib.admin.utils import unquote | |
from django.core.exceptions import PermissionDenied | |
from django.http import HttpResponseRedirect | |
from django.shortcuts import get_object_or_404, render | |
from django.urls import re_path, resolve, reverse | |
from django.utils.encoding import force_str | |
from django.utils.text import capfirst | |
from django.utils.translation import gettext as _ | |
class EventHistoryAdmin(admin.ModelAdmin): | |
object_history_template = "pghistory/object_history.html" | |
object_history_form_template = "pghistory/object_history_form.html" | |
related_name_event = "event" | |
order_by_history = "-pgh_id" | |
def _related_name_not_exist_redirect(self, request, opts, object_id): | |
"""Create a message informing the user that the object doesn't exist | |
and return a redirect to the admin index page. | |
""" | |
msg = _( | |
"%(name)s with related name doesn’t exist. Set correct " | |
"related_name_event in your model admin definition." | |
) % { | |
"name": opts.verbose_name, | |
} | |
self.message_user(request, msg, messages.WARNING) | |
url = reverse("admin:index", current_app=self.admin_site.name) | |
return HttpResponseRedirect(url) | |
def history_view(self, request, object_id, extra_context=None): | |
"""The event history admin view for this model.""" | |
model = self.model # Original model | |
obj = self.get_object(request, unquote(object_id)) # Original object | |
if obj is None: | |
# Object doesn't exist | |
return self._get_obj_does_not_exist_redirect( | |
request, model._meta, object_id | |
) | |
if not self.has_view_or_change_permission(request, obj): | |
# User don't have permission to change object | |
raise PermissionDenied | |
if not hasattr(obj, self.related_name_event): | |
# Model doesn't have the related name for events or is not | |
# specified | |
return self._related_name_not_exist_redirect(request, model._meta) | |
# Order all the events from object | |
action_list = getattr(obj, self.related_name_event).order_by( | |
self.order_by_history | |
) | |
# Store meta from model | |
opts = model._meta | |
context = { | |
**self.admin_site.each_context(request), | |
"title": _("Change history: %s") % obj, | |
"subtitle": None, | |
"action_list": action_list, | |
"module_name": str(capfirst(opts.verbose_name_plural)), | |
"object": obj, | |
"opts": opts, | |
"preserved_filters": self.get_preserved_filters(request), | |
**(extra_context or {}), | |
} | |
request.current_app = self.admin_site.name | |
return self.render_history_view( | |
request, self.object_history_template, context | |
) | |
def render_history_view(self, request, template, context, **kwargs): | |
"""Catch call to render, to allow overriding.""" | |
return render(request, template, context, **kwargs) | |
def get_urls(self): | |
"""Add additional urls to revert objects""" | |
urls = super().get_urls() | |
admin_site = self.admin_site | |
opts = self.model._meta | |
info = opts.app_label, opts.model_name | |
history_urls = [ | |
re_path( | |
"^([^/]+)/history/([^/]+)/$", | |
admin_site.admin_view(self.history_form_view), | |
name="%s_%s_event_history" % info, | |
) | |
] | |
return history_urls + urls | |
def response_change(self, request, obj): | |
"""Add message to indicate user if revert was successful""" | |
verbose_name = obj._meta.verbose_name | |
resolve_url = resolve(path=request.path) | |
if "_event_history" in resolve_url.url_name: | |
# Submit revert object message | |
msg = _('The %(name)s "%(obj)s" was reverted successfully.') % { | |
"name": force_str(verbose_name), | |
"obj": force_str(obj), | |
} | |
self.message_user(request, f"{msg}") | |
opts = self.model._meta | |
info = opts.app_label, opts.model_name | |
redirect_url = "admin:%s_%s_history" % info | |
return http.HttpResponseRedirect( | |
reverse(redirect_url, kwargs={"object_id": obj.pk}) | |
) | |
else: | |
# Submit save object message | |
return super(EventHistoryAdmin, self).response_change(request, obj) | |
def response_change_failed(self, request, obj): | |
"""Add message to indicate user if revert wasn't successful""" | |
verbose_name = obj._meta.verbose_name | |
msg = _( | |
'RuntimeError: The %(name)s "%(obj)s" can\'t be reverted. ' | |
"Maybe some fields were excluded for tracking." | |
) % { | |
"name": force_str(verbose_name), | |
"obj": force_str(obj), | |
} | |
self.message_user(request, f"{msg}", level=messages.ERROR) | |
opts = self.model._meta | |
info = opts.app_label, opts.model_name | |
redirect_url = "admin:%s_%s_history" % info | |
return http.HttpResponseRedirect( | |
reverse(redirect_url, kwargs={"object_id": obj.pk}) | |
) | |
def get_readonly_fields(self, request, obj=None): | |
"""Make all fields readonly""" | |
resolve_url = resolve(path=request.path) | |
if "_event_history" in resolve_url.url_name: | |
# This is revert object details view, make fields readonly | |
readonly_fields = list( | |
set( | |
[field.name for field in self.opts.local_fields] | |
+ [field.name for field in self.opts.local_many_to_many] | |
) | |
) | |
if "is_submitted" in readonly_fields: | |
readonly_fields.remove("is_submitted") | |
return readonly_fields | |
else: | |
# This is edit object view, call super for usual behavior | |
return super(EventHistoryAdmin, self).get_readonly_fields( | |
request, obj | |
) | |
def history_form_view( | |
self, request, object_id, version_id, extra_context=None | |
): | |
"""View to display form with object event data, you can revert it here""" | |
request.current_app = self.admin_site.name | |
original_opts = self.model._meta | |
original_model = self.model | |
model = self.model._meta.get_field( | |
self.related_name_event | |
).related_model | |
# Get original object | |
original_obj = get_object_or_404( | |
original_model, **{original_opts.pk.attname: object_id} | |
) | |
# Get event object using version_id | |
revert_obj = get_object_or_404( | |
model, | |
**{original_opts.pk.attname: object_id, "pgh_id": version_id}, | |
) | |
revert_obj._state.adding = False | |
if not self.has_change_permission(request, revert_obj): | |
# You can't revert objects | |
raise PermissionDenied | |
formsets = [] | |
form_class = self.get_form(request, revert_obj) | |
if request.method == "POST": | |
# Call revert method | |
try: | |
revert_obj.revert() | |
except RuntimeError: | |
# Object can't be reverted, maybe some fields were excluded | |
return self.response_change_failed(request, original_obj) | |
return self.response_change(request, original_obj) | |
else: | |
# Display form with object data, this can't be edited | |
form = form_class(instance=revert_obj) | |
# Generate full admin form | |
admin_form = helpers.AdminForm( | |
form, | |
self.get_fieldsets(request, revert_obj), | |
self.prepopulated_fields, | |
self.get_readonly_fields(request, revert_obj), | |
model_admin=self, | |
) | |
pghistory_events_last_change = ( | |
pghistory.models.Events.objects.filter( | |
pgh_obj_model=f"{original_opts.app_label}.{original_opts.object_name}", | |
pgh_obj_id=original_obj.pk, | |
pgh_id=revert_obj.pk, | |
) | |
.order_by("-pgh_created_at") | |
.first() | |
) | |
last_diff = None | |
if pghistory_events_last_change: | |
last_diff = pghistory_events_last_change.pgh_diff | |
model_name = original_opts.model_name # Original model name | |
url_triplet = self.admin_site.name, original_opts.app_label, model_name | |
changelist_url_name = "%s:%s_%s_changelist" % url_triplet | |
change_url_name = "%s:%s_%s_change" % url_triplet | |
history_url_name = "%s:%s_%s_history" % url_triplet | |
context = { | |
"title": _("Revert %s") % force_str(revert_obj), | |
"adminform": admin_form, | |
"object_id": object_id, | |
"original": original_obj, | |
"is_popup": False, | |
"media": self.media + admin_form.media, | |
"errors": helpers.AdminErrorList(form, formsets), | |
"app_label": original_opts.app_label, | |
"original_opts": original_opts, | |
"changelist_url": changelist_url_name, | |
"change_url": change_url_name, | |
"history_url": history_url_name, | |
# Context variables copied from render_change_form | |
"add": False, | |
"change": True, | |
"has_add_permission": self.has_add_permission(request), | |
# Permission on original object, to avoid add extra permissions for | |
# generated event table | |
"has_change_permission": self.has_change_permission( | |
request, original_obj | |
), | |
"has_delete_permission": self.has_delete_permission( | |
request, original_obj | |
), | |
"has_file_field": True, | |
"has_absolute_url": False, | |
"form_url": "", | |
"opts": model._meta, | |
"content_type_id": self.content_type_model_cls.objects.get_for_model( | |
self.model | |
).id, | |
"save_as": self.save_as, | |
"save_on_top": self.save_on_top, | |
"root_path": getattr(self.admin_site, "root_path", None), | |
"last_diff": last_diff, | |
"revert_obj": revert_obj, | |
} | |
context.update(self.admin_site.each_context(request)) | |
context.update(extra_context or {}) | |
extra_kwargs = {} | |
return self.render_history_view( | |
request, self.object_history_form_template, context, **extra_kwargs | |
) | |
@property | |
def content_type_model_cls(self): | |
"""Returns the ContentType model class.""" | |
return django_apps.get_model("contenttypes.contenttype") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{% load i18n %} | |
<div class="submit-row"> | |
<input type="submit" value="{% trans 'Revert' %}" class="default" name="_save" {{ onclick_attrib }}/> | |
</div> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment