mirror of
https://github.com/jazzband/django-constance.git
synced 2026-03-16 22:40:24 +00:00
Added history view
This commit is contained in:
parent
eae9f10412
commit
e0d406d0a4
4 changed files with 179 additions and 0 deletions
|
|
@ -41,6 +41,14 @@ class ConstanceAdmin(admin.ModelAdmin):
|
|||
return [
|
||||
path("", self.admin_site.admin_view(self.changelist_view), name=f"{info}_changelist"),
|
||||
path("", self.admin_site.admin_view(self.changelist_view), name=f"{info}_add"),
|
||||
# Redirect <object_id>/change/ to the changelist so that "Recent actions" links in the admin index point
|
||||
# somewhere useful.
|
||||
path(
|
||||
"<path:object_id>/change/",
|
||||
lambda request, object_id: HttpResponseRedirect("../../"),
|
||||
name=f"{info}_change",
|
||||
),
|
||||
path("history/", self.admin_site.admin_view(self.history_view), name=f"{info}_history"),
|
||||
]
|
||||
|
||||
def get_config_value(self, name, options, form, initial):
|
||||
|
|
@ -160,6 +168,47 @@ class ConstanceAdmin(admin.ModelAdmin):
|
|||
request.current_app = self.admin_site.name
|
||||
return TemplateResponse(request, self.change_list_template, context)
|
||||
|
||||
def history_view(self, request, object_id=None, extra_context=None):
|
||||
"""Display the change history for constance config values."""
|
||||
from django.contrib.admin.views.main import PAGE_VAR
|
||||
|
||||
if not self.has_view_or_change_permission(request):
|
||||
raise PermissionDenied
|
||||
|
||||
ct = ContentType.objects.get_for_model(self.model)
|
||||
action_list = (
|
||||
LogEntry.objects.filter(
|
||||
content_type=ct,
|
||||
object_id="Config",
|
||||
)
|
||||
.select_related()
|
||||
.order_by("-action_time")
|
||||
)
|
||||
|
||||
paginator = self.get_paginator(request, action_list, 100)
|
||||
page_number = request.GET.get(PAGE_VAR, 1)
|
||||
page_obj = paginator.get_page(page_number)
|
||||
page_range = paginator.get_elided_page_range(page_obj.number)
|
||||
|
||||
context = {
|
||||
**self.admin_site.each_context(request),
|
||||
"title": _("Change history: %s") % self.model._meta.verbose_name_plural.capitalize(),
|
||||
"action_list": page_obj,
|
||||
"page_range": page_range,
|
||||
"page_var": PAGE_VAR,
|
||||
"pagination_required": paginator.count > 100,
|
||||
"opts": self.model._meta,
|
||||
"app_label": "constance",
|
||||
**(extra_context or {}),
|
||||
}
|
||||
|
||||
request.current_app = self.admin_site.name
|
||||
return TemplateResponse(
|
||||
request,
|
||||
"admin/constance/config_history.html",
|
||||
context,
|
||||
)
|
||||
|
||||
def _log_config_change(self, request, changed_fields):
|
||||
"""
|
||||
Create a Django admin LogEntry recording which config fields were changed.
|
||||
|
|
|
|||
|
|
@ -23,6 +23,9 @@
|
|||
{% block bodyclass %}{{ block.super }} change-list{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<ul class="object-tools">
|
||||
<li><a href="history/" class="historylink">{% trans 'History' %}</a></li>
|
||||
</ul>
|
||||
<div id="content-main" class="constance">
|
||||
<div class="module" id="changelist">
|
||||
<form id="changelist-form" action="" method="post" enctype="multipart/form-data">{% csrf_token %}
|
||||
|
|
|
|||
55
constance/templates/admin/constance/config_history.html
Normal file
55
constance/templates/admin/constance/config_history.html
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
{% extends "admin/base_site.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
<div class="breadcrumbs">
|
||||
<a href="{% url 'admin:index' %}">{% translate 'Home' %}</a>
|
||||
› <a href="{% url 'admin:app_list' app_label=opts.app_label %}">{{ opts.app_config.verbose_name }}</a>
|
||||
› <a href="{% url 'admin:constance_config_changelist' %}">{{ opts.verbose_name_plural|capfirst }}</a>
|
||||
› {% translate 'History' %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div id="content-main">
|
||||
<div id="change-history" class="module">
|
||||
|
||||
{% if action_list %}
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{% translate 'Date/time' %}</th>
|
||||
<th scope="col">{% translate 'User' %}</th>
|
||||
<th scope="col">{% translate 'Action' %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for action in action_list %}
|
||||
<tr>
|
||||
<th scope="row">{{ action.action_time|date:"DATETIME_FORMAT" }}</th>
|
||||
<td>{{ action.user.get_username }}{% if action.user.get_full_name %} ({{ action.user.get_full_name }}){% endif %}</td>
|
||||
<td>{{ action.get_change_message }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<p class="paginator">
|
||||
{% if pagination_required %}
|
||||
{% for i in page_range %}
|
||||
{% if i == action_list.paginator.ELLIPSIS %}
|
||||
{{ action_list.paginator.ELLIPSIS }}
|
||||
{% elif i == action_list.number %}
|
||||
<span class="this-page">{{ i }}</span>
|
||||
{% else %}
|
||||
<a href="?{{ page_var }}={{ i }}" {% if i == action_list.paginator.num_pages %} class="end" {% endif %}>{{ i }}</a>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{{ action_list.paginator.count }} {% blocktranslate count counter=action_list.paginator.count %}entry{% plural %}entries{% endblocktranslate %}
|
||||
</p>
|
||||
{% else %}
|
||||
<p>{% translate "This object doesn't have a change history." %}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
@ -401,6 +401,78 @@ class TestAdmin(TestCase):
|
|||
self.assertIsInstance(response, HttpResponseRedirect)
|
||||
self.assertEqual(LogEntry.objects.count(), initial_count)
|
||||
|
||||
def test_history_view(self):
|
||||
"""Test that the history view renders and shows LogEntry records."""
|
||||
from django.contrib.admin.models import CHANGE
|
||||
from django.contrib.admin.models import LogEntry
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
ct = ContentType.objects.get_for_model(self.model)
|
||||
LogEntry.objects.create(
|
||||
user_id=self.superuser.pk,
|
||||
content_type_id=ct.pk,
|
||||
object_id="Config",
|
||||
object_repr="Config",
|
||||
action_flag=CHANGE,
|
||||
change_message=json.dumps([{"changed": {"fields": ["INT_VALUE"]}}]),
|
||||
)
|
||||
|
||||
request = self.rf.get("/admin/constance/config/history/")
|
||||
request.user = self.superuser
|
||||
response = self.options.history_view(request)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
response.render()
|
||||
content = response.content.decode()
|
||||
self.assertIn("INT_VALUE", content)
|
||||
self.assertIn("History", content)
|
||||
|
||||
def test_history_view_empty(self):
|
||||
"""Test that the history view renders correctly with no entries."""
|
||||
request = self.rf.get("/admin/constance/config/history/")
|
||||
request.user = self.superuser
|
||||
response = self.options.history_view(request)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
response.render()
|
||||
content = response.content.decode()
|
||||
self.assertIn("0", content)
|
||||
|
||||
def test_history_view_permission_denied(self):
|
||||
"""Test that the history view denies access to users without permission."""
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
unprivileged = User.objects.create_user("noperm", "noperm", "c@c.cz")
|
||||
request = self.rf.get("/admin/constance/config/history/")
|
||||
request.user = unprivileged
|
||||
with self.assertRaises(PermissionDenied):
|
||||
self.options.history_view(request)
|
||||
|
||||
def test_changelist_has_history_link(self):
|
||||
"""Test that the changelist page contains a link to the history view."""
|
||||
request = self.rf.get("/admin/constance/config/")
|
||||
request.user = self.superuser
|
||||
response = self.options.changelist_view(request)
|
||||
response.render()
|
||||
content = response.content.decode()
|
||||
self.assertIn('href="history/"', content)
|
||||
self.assertIn("History", content)
|
||||
|
||||
def test_change_url_redirects_to_changelist(self):
|
||||
"""Test that the change URL (used by 'Recent actions') redirects to the changelist."""
|
||||
from django.urls import reverse
|
||||
|
||||
url = reverse("admin:constance_config_change", args=["Config"])
|
||||
self.assertIn("Config/change/", url)
|
||||
request = self.rf.get(url)
|
||||
request.user = self.superuser
|
||||
|
||||
# The change URL is a simple lambda redirect, so invoke it via URL resolution.
|
||||
from django.urls import resolve
|
||||
|
||||
match = resolve(url)
|
||||
response = match.func(request, object_id="Config")
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.url, "../../")
|
||||
|
||||
def test_labels(self):
|
||||
self.assertEqual(type(self.model._meta.label), str)
|
||||
self.assertEqual(type(self.model._meta.label_lower), str)
|
||||
|
|
|
|||
Loading…
Reference in a new issue