Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ class MyModelAdmin(ModelAdmin):

- [django-guardian](https://github.com/django-guardian/django-guardian) - [Integration guide](https://unfoldadmin.com/docs/integrations/django-guardian/)
- [django-import-export](https://github.com/django-import-export/django-import-export) - [Integration guide](https://unfoldadmin.com/docs/integrations/django-import-export/)
- [django-reversion](https://github.com/etianen/django-reversion) / [django-reversion-compare](https://github.com/jedie/django-reversion-compare) - [Integration guide](https://unfoldadmin.com/docs/integrations/django-reversion/)
- [django-simple-history](https://github.com/jazzband/django-simple-history) - [Integration guide](https://unfoldadmin.com/docs/integrations/django-simple-history/)
- [django-constance](https://github.com/jazzband/django-constance) - [Integration guide](https://unfoldadmin.com/docs/integrations/django-constance/)
- [django-celery-beat](https://github.com/celery/django-celery-beat) - [Integration guide](https://unfoldadmin.com/docs/integrations/django-celery-beat/)
Expand Down
1 change: 1 addition & 0 deletions docs/installation/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ INSTALLED_APPS = [
"unfold.contrib.inlines", # optional, if special inlines are needed
"unfold.contrib.import_export", # optional, if django-import-export package is used
"unfold.contrib.guardian", # optional, if django-guardian package is used
"unfold.contrib.reversion", # optional, if django-reversion package is used
"unfold.contrib.simple_history", # optional, if django-simple-history package is used
"unfold.contrib.location_field", # optional, if django-location-field package is used
"unfold.contrib.constance", # optional, if django-constance package is used
Expand Down
58 changes: 58 additions & 0 deletions docs/integrations/django-reversion.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
---
title: django-reversion
order: 0
description: Integrate django-reversion and django-reversion-compare with Django Unfold to style object history, recover flows, and version comparison screens in the admin interface.
---

# django-reversion

To integrate django-reversion with Unfold, add `unfold.contrib.reversion` to your `INSTALLED_APPS` setting after `unfold` and before `reversion`. If you also use django-reversion-compare, keep `unfold.contrib.reversion` before `reversion_compare` as well so Unfold's template overrides take precedence.

```python
# settings.py

INSTALLED_APPS = [
"unfold",
# ...
"unfold.contrib.reversion",
# ...
"reversion",
"reversion_compare", # optional
]
```

For plain django-reversion history and recover flows, inherit from both `VersionAdmin` and `unfold.admin.ModelAdmin` in your admin configuration:

```python
# admin.py

from django.contrib import admin
from reversion.admin import VersionAdmin

from unfold.admin import ModelAdmin

from .models import ExampleModel


@admin.register(ExampleModel)
class ExampleAdmin(VersionAdmin, ModelAdmin):
pass
```

If you also want side-by-side version comparison, inherit from `CompareVersionAdmin` instead:

```python
# admin.py

from django.contrib import admin
from reversion_compare.admin import CompareVersionAdmin

from unfold.admin import ModelAdmin

from .models import ExampleModel


@admin.register(ExampleModel)
class ExampleCompareAdmin(CompareVersionAdmin, ModelAdmin):
pass
```
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ dev = [
"django-import-export>=4.3",
"django-location-field>=2.7",
"django-money>=3.5",
"django-reversion>=6.1",
"django-reversion-compare>=0.18.1",
"django-simple-history>=3.11",
"django-stubs>=5.2",
"pillow>=12.1",
Expand Down
Empty file.
6 changes: 6 additions & 0 deletions src/unfold/contrib/reversion/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from django.apps import AppConfig


class ReversionConfig(AppConfig):
name = "unfold.contrib.reversion"
label = "unfold_reversion"
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
.reversion-compare {
--del: rgb(244 63 94 / 0.16);
--ins: rgb(16 185 129 / 0.18);
}

.reversion-compare pre.highlight {
max-width: none;
overflow-x: auto;
border: 1px solid var(--color-base-200);
border-radius: 6px;
padding: 0.875rem 1rem;
background-color: var(--color-base-50);
color: var(--color-font-default-light);
}

html.dark .reversion-compare pre.highlight,
html[data-theme="dark"] .reversion-compare pre.highlight {
border-color: var(--color-base-800);
background-color: rgb(255 255 255 / 0.02);
color: var(--color-font-default-dark);
}

.reversion-compare del,
.reversion-compare ins {
border-radius: 4px;
padding: 0 0.125rem;
text-decoration: none;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
{% extends "admin/base_site.html" %}
{% load i18n static admin_urls unfold %}

{% block extrastyle %}
{{ block.super }}
<link rel="stylesheet" type="text/css" href="{% static 'reversion_compare.css' %}">
<link rel="stylesheet" type="text/css" href="{% static 'unfold/reversion/css/reversion-compare.css' %}">
{% endblock %}

{% block content %}
{% url opts|admin_urlname:'history' original.pk as history_url %}
{% url opts|admin_urlname:'revision' version1.object_id version1.id as save_url_local %}

<div class="reversion-compare flex flex-col gap-4">
<div class="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
<h1 class="text-2xl font-semibold tracking-tight text-font-important-light dark:text-font-important-dark">
{% blocktrans %}Compare{% endblocktrans %} {{ original|default:obj }}
</h1>

<div class="flex flex-wrap gap-2">
{% if prev_url %}
{% component "unfold/components/button.html" with href=prev_url variant="default" icon="arrow_back" %}
{% trans "Previous" %}
{% endcomponent %}
{% endif %}

{% if next_url %}
{% component "unfold/components/button.html" with href=next_url variant="default" %}
{% trans "Next" %}
<span class="material-symbols-outlined">arrow_forward</span>
{% endcomponent %}
{% endif %}

{% component "unfold/components/button.html" with href=history_url variant="default" %}
{% trans "Back to history" %}
{% endcomponent %}

{% component "unfold/components/button.html" with href=save_url|default:save_url_local variant="secondary" icon="restore_page" %}
{% trans "Revert to this version" %}
{% endcomponent %}
</div>
</div>

<p class="mb-0 text-sm text-base-500 dark:text-base-400">
{% blocktrans with date1=version1.revision.date_created|date:_("DATETIME_FORMAT") date2=version2.revision.date_created|date:_("DATETIME_FORMAT") %}
Compare <strong>{{ date1 }}</strong> with <strong>{{ date2 }}</strong>:
{% endblocktrans %}
</p>

{% include "reversion-compare/compare_partial.html" %}
</div>
{% endblock %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
{% load i18n %}

<div class="flex flex-col gap-4">
{% for field_diff in compare_data %}
<section class="overflow-hidden rounded-default border border-base-200 shadow-xs dark:border-base-800">
<header class="border-b border-base-200 px-4 py-3 dark:border-base-800">
<h2 class="text-sm font-medium text-font-important-light dark:text-font-important-dark">
{% firstof field_diff.field.verbose_name field_diff.field.related_name %}
{% if field_diff.is_related and not field_diff.follow %}
<sup class="text-amber-600 dark:text-amber-400">*</sup>
{% endif %}
</h2>

{% if field_diff.field.help_text %}
<p class="mt-1 text-xs text-base-500 dark:text-base-400">{{ field_diff.field.help_text|safe }}</p>
{% endif %}
</header>

<div class="px-4 py-4">
{{ field_diff.diff }}
</div>
</section>
{% empty %}
{% trans "There are no differences." as message %}
{% include "unfold/helpers/messages/info.html" with message=message class="mb-0" %}
{% endfor %}

<section class="rounded-default border border-base-200 p-4 shadow-xs dark:border-base-800">
<h2 class="text-sm font-medium text-font-important-light dark:text-font-important-dark">
{% trans "Revision comment" %}
</h2>

<blockquote class="mt-3 rounded-default border border-base-200 bg-base-50 px-4 py-3 text-sm text-font-default-light dark:border-base-800 dark:bg-white/[.02] dark:text-font-default-dark">
{{ version2.revision.comment|default:_("(no comment exists)") }}
</blockquote>
</section>

{% if has_unfollowed_fields %}
{% blocktrans asvar note_message %}
Fields or entries marked with <sup>*</sup> are not under reversion control.
Some marked information may be incomplete or inaccurate.
{% endblocktrans %}
{% include "unfold/helpers/messages/warning.html" with message=note_message class="mb-0" %}
{% endif %}
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
{% extends "admin/base_site.html" %}
{% load i18n l10n admin_urls unfold %}

{% block content %}
{% trans "Choose a date from the list below to revert to a previous version of this object." as message %}
{% include "unfold/helpers/messages/info.html" with message=message %}

{% if action_list %}
{% url opts|admin_urlname:'compare' object.pk as compare_base %}

<form method="get" action="{{ compare_base }}" class="flex flex-col gap-4">
<div class="flex justify-end">
{% if comparable %}
{% component "unfold/components/button.html" with submit=1 variant="default" %}
{% trans "Compare" %}
{% endcomponent %}
{% else %}
<button type="submit"
disabled
class="font-medium inline-flex group items-center gap-1 relative rounded-default justify-center whitespace-nowrap px-3 py-2 border border-base-200 bg-white shadow-xs text-important cursor-not-allowed opacity-50 dark:border-base-700 dark:bg-transparent">
{% trans "Compare" %}
</button>
{% endif %}
</div>

<table id="change-history" class="border-base-200 border-spacing-none border-separate mb-2 w-full lg:border lg:rounded-default lg:shadow-xs lg:dark:border-base-800">
<thead class="hidden lg:table-header-group text-base-900 dark:text-base-100">
<tr>
<th class="align-middle font-medium px-3 py-2 text-left lg:text-center">
{% translate "Older" %}
</th>
<th class="align-middle font-medium px-3 py-2 text-left lg:text-center">
{% translate "Newer" %}
</th>
<th class="align-middle font-medium px-3 py-2 text-left">
{% translate "Date/time" %}
</th>
<th class="align-middle font-medium px-3 py-2 text-left">
{% translate "User" %}
</th>
<th class="align-middle font-medium px-3 py-2 text-left">
{% translate "Comment" %}
</th>
</tr>
</thead>
<tbody>
{% for action in action_list %}
<tr class="block border mb-3 rounded-default shadow-xs lg:table-row lg:border-none lg:mb-0 lg:shadow-none dark:border-base-800">
<td class="align-middle flex border-t border-base-200 font-normal px-3 py-2 text-left before:flex before:capitalize before:content-[attr(data-label)] before:items-center before:mr-auto first:border-t-0 lg:before:hidden lg:first:border-t lg:py-3 lg:table-cell lg:text-center dark:border-base-800" data-label="{% translate 'Older' %}">
{% if comparable %}
<input type="radio"
name="version_id1"
value="{{ action.version.pk|unlocalize }}"
class="appearance-none bg-white block border border-base-300 h-4 min-w-4 relative rounded-full w-4 dark:bg-base-900 dark:border-base-700 after:absolute after:bg-transparent after:content-[''] after:flex after:h-2 after:items-center after:justify-center after:leading-none after:left-1/2 after:rounded-full after:text-white after:top-1/2 after:-translate-x-1/2 after:-translate-y-1/2 after:w-2 dark:after:text-base-700 dark:after:bg-transparent checked:bg-primary-600 checked:border-primary-600 checked:after:bg-white dark:checked:after:bg-base-900 mx-auto lg:inline-block{% if action.first %} invisible{% endif %}"
{% if version1.pk|unlocalize == action.version.pk|unlocalize or action.second %}checked="checked"{% endif %}>
{% else %}
&mdash;
{% endif %}
</td>
<td class="align-middle flex border-t border-base-200 font-normal px-3 py-2 text-left before:flex before:capitalize before:content-[attr(data-label)] before:items-center before:mr-auto first:border-t-0 lg:before:hidden lg:first:border-t lg:py-3 lg:table-cell lg:text-center dark:border-base-800" data-label="{% translate 'Newer' %}">
{% if comparable %}
<input type="radio"
name="version_id2"
value="{{ action.version.pk|unlocalize }}"
class="appearance-none bg-white block border border-base-300 h-4 min-w-4 relative rounded-full w-4 dark:bg-base-900 dark:border-base-700 after:absolute after:bg-transparent after:content-[''] after:flex after:h-2 after:items-center after:justify-center after:leading-none after:left-1/2 after:rounded-full after:text-white after:top-1/2 after:-translate-x-1/2 after:-translate-y-1/2 after:w-2 dark:after:text-base-700 dark:after:bg-transparent checked:bg-primary-600 checked:border-primary-600 checked:after:bg-white dark:checked:after:bg-base-900 mx-auto lg:inline-block"
{% if version2.pk|unlocalize == action.version.pk|unlocalize or action.first == 1 %}checked="checked"{% endif %}>
{% else %}
&mdash;
{% endif %}
</td>
<th class="align-middle flex border-t border-base-200 font-normal px-3 py-2 text-left before:flex before:capitalize before:content-[attr(data-label)] before:items-center before:mr-auto first:border-t-0 lg:before:hidden lg:first:border-t lg:py-3 lg:table-cell dark:border-base-800" data-label="{% translate 'Date/time' %}">
{% if action.url %}
<a href="{{ action.url }}" class="text-primary-600 hover:underline dark:text-primary-500">
{{ action.revision.date_created|date:"DATETIME_FORMAT" }}
</a>
{% else %}
{{ action.revision.date_created|date:"DATETIME_FORMAT" }}
{% endif %}
</th>
<td class="align-middle flex border-t border-base-200 font-normal px-3 py-2 text-left before:flex before:capitalize before:content-[attr(data-label)] before:items-center before:mr-auto first:border-t-0 lg:before:hidden lg:first:border-t lg:py-3 lg:table-cell dark:border-base-800" data-label="{% translate 'User' %}">
{% if action.revision.user %}
{{ action.revision.user.get_username }}
{% if action.revision.user.get_full_name %}
({{ action.revision.user.get_full_name }})
{% endif %}
{% else %}
&mdash;
{% endif %}
</td>
<td class="align-middle flex border-t border-base-200 font-normal px-3 py-2 text-left before:flex before:capitalize before:content-[attr(data-label)] before:items-center before:mr-auto first:border-t-0 lg:before:hidden lg:first:border-t lg:py-3 lg:table-cell dark:border-base-800" data-label="{% translate 'Comment' %}">
{{ action.revision.comment|default:"" }}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</form>
{% else %}
{% trans "This object doesn’t have a change history. It probably wasn’t added via this admin site." as message %}
{% include "unfold/helpers/messages/warning.html" with message=message %}
{% endif %}
{% endblock %}
12 changes: 12 additions & 0 deletions src/unfold/contrib/reversion/templates/reversion/change_list.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{% extends "admin/change_list.html" %}
{% load i18n admin_urls %}

{% block object-tools-items %}
{% if not is_popup and has_add_permission and has_change_permission %}
{% blocktrans with cl.opts.verbose_name_plural|escape as name asvar title %}Recover deleted {{ name }}{% endblocktrans %}
{% url opts|admin_urlname:'recoverlist' as recover_url %}
{% include "unfold/helpers/icon_action.html" with title=title link=recover_url icon="history" %}
{% endif %}

{{ block.super }}
{% endblock %}
Loading