diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml
new file mode 100644
index 00000000..6d36bd68
--- /dev/null
+++ b/.github/workflows/python-package.yml
@@ -0,0 +1,76 @@
+name: Python package
+
+on: [push, pull_request]
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ services:
+ postgres:
+ image: postgres
+ env:
+ POSTGRES_PASSWORD: postgres
+ options: >-
+ --health-cmd pg_isready
+ --health-interval 10s
+ --health-timeout 5s
+ --health-retries 5
+ ports:
+ - 5432:5432
+ mysql:
+ image: mysql
+ env:
+ MYSQL_ROOT_PASSWORD: root
+ MYSQL_DATABASE: root
+ options: >-
+ --health-cmd "mysqladmin -uroot -proot ping"
+ --health-interval 10s
+ --health-timeout 5s
+ --health-retries 5
+ ports:
+ - 3306:3306
+ env:
+ PYTHONDEVMODE: 1
+ DJANGO_DATABASE_HOST_POSTGRES: localhost
+ DJANGO_DATABASE_USER_POSTGRES: postgres
+ DJANGO_DATABASE_NAME_POSTGRES: postgres
+ DJANGO_DATABASE_PASSWORD_POSTGRES: postgres
+ DJANGO_DATABASE_HOST_MYSQL: 127.0.0.1
+ DJANGO_DATABASE_USER_MYSQL: root
+ DJANGO_DATABASE_NAME_MYSQL: root
+ DJANGO_DATABASE_PASSWORD_MYSQL: root
+ strategy:
+ matrix:
+ python-version: [3.8, 3.9, '3.10', '3.11', '3.12']
+ django-version:
+ - '>=5.0,<6.0'
+ - '>=4.2,<5.0'
+ exclude:
+ - python-version: 3.9
+ django-version: '>=5.0,<6.0'
+ - python-version: 3.8
+ django-version: '>=5.0,<6.0'
+ steps:
+ - uses: actions/checkout@v2
+ - name: Set up Python ${{ matrix.python-version }}
+ uses: actions/setup-python@v2
+ with:
+ python-version: ${{ matrix.python-version }}
+ - name: Install dependencies (Django ${{ matrix.django-version }})
+ run: |
+ python -m pip install --upgrade pip
+ python -m pip install --pre django'${{ matrix.django-version }}'
+ python -m pip install flake8 coverage sphinx sphinx_rtd_theme psycopg2 mysqlclient -e .
+ - name: Lint with flake8
+ run: |
+ flake8
+ - name: Check no missing migrations
+ run: |
+ tests/manage.py makemigrations --check
+ - name: Test with unittest
+ run: |
+ coverage run tests/manage.py test tests
+ coverage report
+ - name: Build docs
+ run: |
+ (cd docs && sphinx-build -n -W . _build)
diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml
new file mode 100644
index 00000000..87b85830
--- /dev/null
+++ b/.github/workflows/python-publish.yml
@@ -0,0 +1,26 @@
+name: Upload Python Package
+
+on:
+ release:
+ types: [created]
+
+jobs:
+ deploy:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2
+ - name: Set up Python
+ uses: actions/setup-python@v2
+ with:
+ python-version: '3.x'
+ - name: Install dependencies
+ run: |
+ python -m pip install --upgrade pip
+ pip install setuptools wheel twine
+ - name: Build and publish
+ env:
+ TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
+ TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
+ run: |
+ python setup.py sdist bdist_wheel
+ twine upload dist/*
diff --git a/.gitignore b/.gitignore
index 2cb006ac..1937b9e0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,6 +3,7 @@
.project
.pydevproject
.settings
+.venv
*.pyc
*.pyo
dist
@@ -13,3 +14,4 @@ docs/_build
.coverage
*.sqlite3
.idea
+.idea/
diff --git a/.readthedocs.yaml b/.readthedocs.yaml
new file mode 100644
index 00000000..c8dc7d98
--- /dev/null
+++ b/.readthedocs.yaml
@@ -0,0 +1,11 @@
+version: 2
+build:
+ os: ubuntu-20.04
+ tools:
+ python: "3.9"
+sphinx:
+ configuration: docs/conf.py
+python:
+ install:
+ - method: pip
+ path: .
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 64b44432..00000000
--- a/.travis.yml
+++ /dev/null
@@ -1,53 +0,0 @@
-sudo: false
-language: python
-dist: xenial
-python:
- - 3.8
- - 3.7
- - 3.6
-cache: pip
-env:
- global:
- - PYTHONWARNINGS=default,ignore::PendingDeprecationWarning,ignore::ResourceWarning
- - DJANGO_DATABASE_USER_POSTGRES=postgres
- - DJANGO_DATABASE_USER_MYSQL=travis
- matrix:
- - DJANGO='>=3.1,<3.2'
- - DJANGO='>=3.0,<3.1'
- - DJANGO='>=2.2,<3.0'
- - DJANGO='>=2.1,<2.2'
- - DJANGO='>=2.0,<2.1'
-matrix:
- fast_finish: true
-addons:
- apt:
- packages:
- - libmysqlclient-dev
-services:
- - postgresql
- - mysql
-install:
- - pip install --pre django$DJANGO
- - pip install flake8 coverage sphinx psycopg2 mysqlclient -e .
-before_script:
- - mysql -e 'create database test_project'
- - psql -c 'create database test_project;' -U postgres;
-script:
- - flake8
- - coverage run tests/manage.py test tests
- - (cd docs && sphinx-build -n -W . _build)
-after_success:
- - coverage report
-deploy:
- provider: pypi
- user: etianen
- password:
- secure: XW4/9HiChbPJSJe4d/MRcO+ViPGhW1iQ8kVi814KJh7mCxOAKijpW5hfdc9oSKB6d8iYB3OzZ7naIUU9GMce40bpeTgPDLVBLCSYKRNLuVoJdh+Q6ItGUiFf8kAJz5jgopG80QnCpLA9JvYxKVJ4amfYWWm204eQmIEnRRAd+Jk=
- on:
- tags: true
- condition: $DJANGO = '>=3.1,<3.2'
- python: 3.6
- repo: etianen/django-reversion
- distributions: sdist bdist_wheel
-notifications:
- email: false
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 536818e0..5336ec80 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -3,6 +3,132 @@
django-reversion changelog
==========================
+5.1.0 - 2024-08-09
+------------------
+
+- Django 5 support (@jeremy-engel).
+- Use bulk_create`` on supported databases (@stianjensen).
+
+
+5.0.12 - 2024-01-30
+-------------------
+
+- Fix missing migration introduced in v5.0.11.
+
+
+5.0.11 - 2024-01-29
+-------------------
+
+- Improved the Chinese translation (@zengqiu).
+
+
+5.0.10 - 2023-12-30
+-------------------
+
+- Fix N+1 queries while rendering the ``recover_list.html`` template (@armonge).
+
+
+5.0.9 - 2023-12-20
+------------------
+
+- Broken release.
+
+
+5.0.8 - 2023-11-08
+------------------
+
+- Fix ``get_deleted`` (@siddarta-weis, @etianen).
+
+
+5.0.7 - 2023-11-07
+------------------
+
+- Speed up ``get_deleted`` (@caullla).
+
+
+5.0.6 - 2023-09-29
+------------------
+
+- Fix handling case of missing object in admin revert (@julianklotz)
+
+
+5.0.5 - 2023-09-19
+------------------
+
+- Handling case of missing object in admin revert (@etianen, @PavelPancocha)
+- CI improvements (@etianen, @browniebroke)
+
+
+5.0.4 - 2022-11-12
+------------------
+
+- Fix warning log formatting for failed reverts (@tony).
+
+
+5.0.3 - 2022-10-02
+------------------
+
+- A revision will no longer be created if a transaction is marked as rollback, as this would otherwise cause an
+ additional database error (@proofit404).
+- A warning log is now emitted if a revert fails due to database integrity errors, making debugging the final
+ ``RevertError`` easier.
+
+
+5.0.2 - 2022-08-06
+------------------
+
+- Fixed doc builds on readthedocs (@etianen).
+
+
+5.0.1 - 2022-06-18
+------------------
+
+- Fix admin detail view in multi-database configuration (@atten).
+
+
+5.0.0 - 2022-02-20
+------------------
+
+- Added support for using django-reversion contexts in ``asyncio`` tasks (@bellini666).
+- **Breaking:** Dropped support for Python 3.6.
+
+
+4.0.2 - 2022-01-30
+------------------
+
+- Improved performance of `createinitialrevisions` management command (@philipstarkey).
+
+
+4.0.1 - 2021-11-04
+------------------
+
+- Django 4.0b support (@smithdc1, @kevinmarsh).
+- Optimized ``VersionQuerySet.get_deleted``.
+
+
+4.0.0 - 2021-07-09
+------------------
+
+- **Breaking:** The ``create_revision`` view decorator and ``RevisionMiddleware`` no longer roll back the revision and
+ database transaction on response status code >= 400. It's the responsibility of the view to use `transaction.atomic()`
+ to roll back any invalid data. This can be enabled globally by setting ``ATOMIC_REQUESTS=True``. (@etianen)
+
+ https://docs.djangoproject.com/en/3.1/ref/settings/#std:setting-DATABASE-ATOMIC_REQUESTS
+
+- Fixing gettext plural forms with Django (@martinsvoboda).
+- Deprecation removals (@lociii, @Peter-van-Tol).
+- CI testing improvements (@etianen, @michael-k).
+- Documentation fixes (@erikrw, @jedie, @michael-k).
+
+
+3.0.9 - 2021-01-22
+------------------
+
+- Significant speedup to ``Version.objects.get_deleted(...)`` database query for PostgreSQL (@GeyseR).
+- Testing against Django 3.1 (@michael-k).
+- Django 4.0 compatibility improvements (@GitRon).
+
+
3.0.8 - 2020-08-31
------------------
@@ -26,13 +152,14 @@ django-reversion changelog
- Documentation fixes (@chicheng).
-3.0.5 - 2019-02-12
+3.0.5 - 2019-12-02
------------------
- Improved performance of `get_deleted` for large datasets (@jeremy-engel).
- Django 3.0 compatibility (@claudep).
- Drops Django < 1.11 compatibility (@claudep).
-- Fixed errors in manageement commands when `django.contrib.admin` is not in `INSTALLED_APPS` (@irtimir).
+- Drops Python 2.7 compatibility (@claudep).
+- Fixed errors in management commands when `django.contrib.admin` is not in `INSTALLED_APPS` (@irtimir).
3.0.4 - 2019-05-22
@@ -77,7 +204,7 @@ django-reversion changelog
------------------
- **Breaking:** ``Revision.comment`` now contains the raw JSON change message generated by django admin, rather than
- a string. Accesing ``Revision.comment`` directly is no longer recommended. Instead, use ``Revision.get_comment()``.
+ a string. Accessing ``Revision.comment`` directly is no longer recommended. Instead, use ``Revision.get_comment()``.
(@RamezIssac).
- **BREAKING:** django-reversion now uses ``_base_manager`` to calculate deleted models, not ``_default_manager``. This
change will only affect models that perform default filtering in their ``_default_manager`` (@ivissani).
@@ -434,7 +561,7 @@ Models
.. code:: python
- # New-style import for accesssing admin class.
+ # New-style import for accessing admin class.
from reversion.admin import VersionAdmin
# Use the admin class directly.
@@ -461,7 +588,7 @@ Models
.. code:: python
- # New-style import for accesssing the low-level API.
+ # New-style import for accessing the low-level API.
from reversion import revisions as reversion
# Use low-level API methods from the revisions namespace.
@@ -486,7 +613,7 @@ Models
.. code:: python
- # New-style import for accesssing the reversion signals.
+ # New-style import for accessing the reversion signals.
from reversion.signals import pre_revision_commit, post_revision_commit
# Use reversion signals directly.
@@ -615,7 +742,7 @@ Models
----------------
* Django 1.5 compatibility.
-* Experimantal Python 3.3 compatibility!
+* Experimental Python 3.3 compatibility!
1.6.6 - 12/02/2013
@@ -655,7 +782,7 @@ Models
------------------
* Swedish translation.
-* Fixing formating for PyPi readme and license.
+* Fixing formatting for PyPi readme and license.
* Minor features and bugfixes.
@@ -719,8 +846,8 @@ Models
* Added Polish translation.
* Added French translation.
* Improved resilience of unit tests.
-* Improved scaleability of Version.object.get_deleted() method.
-* Improved scaleability of createinitialrevisions command.
+* Improved scalability of Version.object.get_deleted() method.
+* Improved scalability of createinitialrevisions command.
* Removed post_syncdb hook.
* Added new createinitialrevisions management command.
* Fixed DoesNotExistError with OneToOneFields and follow.
diff --git a/README.rst b/README.rst
index 7d37cc1f..ac9b6d42 100644
--- a/README.rst
+++ b/README.rst
@@ -1,6 +1,8 @@
==========================
encrypted-django-reversion
==========================
+|PyPI latest| |PyPI Version| |PyPI License| |TravisCI| |Docs|
+
This is a forked version of https://github.com/etianen/django-reversion
@@ -17,8 +19,24 @@ from a TextField into a EncryptedTextField (from django-searchable-encrypted-fie
Requirements
============
+
+- Python 3.8 or later
+- Django 4.2 or later
- django-searchable-encrypted-fields>=0.1
+Features
+========
+
+- Roll back to any point in a model instance's history.
+- Recover deleted model instances.
+- Simple admin integration.
+
+Documentation
+=============
+
+Check out the latest ``django-reversion`` documentation at `Getting Started `_
+
+
Installation
============
add this line to your requirement.txt
diff --git a/docs/admin.rst b/docs/admin.rst
index e0f72f1e..e9c1df15 100644
--- a/docs/admin.rst
+++ b/docs/admin.rst
@@ -5,6 +5,16 @@ Admin integration
django-reversion can be used to add rollback and recovery to your admin site.
+.. Important::
+ Using the admin integration's preview feature will restore your model inside a temporary transaction, then roll
+ back the transaction once the preview is rendered.
+
+ The ``Model.save()`` method, along with any ``pre_save`` and ``post_save`` signals, will be run as part of the preview transaction. Any non-transactional side-effects of these functions (e.g. filesystem, cache) will **not be rolled back** at the end of the preview.
+
+ The ``raw=True`` flag will be set in ``pre_save`` and ``post_save`` signals, allowing you to distinguish preview transactions from regular database transactions and avoid non-transactional side-effects.
+
+ Alternatively, use `transaction.on_commit() `_ to register side-effects to be carried out only on committed transactions.
+
.. Warning::
The admin integration requires that your database engine supports transactions. This is the case for PostgreSQL, SQLite and MySQL InnoDB. If you are using MySQL MyISAM, upgrade your database tables to InnoDB!
diff --git a/docs/common-problems.rst b/docs/common-problems.rst
index 0d3148ea..56c5a162 100644
--- a/docs/common-problems.rst
+++ b/docs/common-problems.rst
@@ -3,6 +3,15 @@
Common problems
===============
+Incompatible version data
+-------------------------
+
+Django-reversion stores the versions of a model as JSON. If a model changes, the migrations are not applied to the stored JSON data. Therefore it can happen that an old version can no longer be restored. In this case the following error occurs:
+
+.. code:: python
+
+ reversion.errors.RevertError: Could not load - incompatible version data.
+
RegistrationError: class 'myapp.MyModel' has already been registered with Reversion
-----------------------------------------------------------------------------------
diff --git a/docs/conf.py b/docs/conf.py
index e3fcb427..dd2adcd5 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -1,5 +1,4 @@
#!/usr/bin/env python3
-# -*- coding: utf-8 -*-
#
# django-reversion documentation build configuration file, created by
# sphinx-quickstart on Thu Jun 2 08:41:36 2016.
@@ -21,6 +20,7 @@
# import sys
# sys.path.insert(0, os.path.abspath('.'))
+import os
from reversion import __version__
# -- General configuration ------------------------------------------------
@@ -32,7 +32,11 @@
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
-extensions = []
+extensions = ["sphinx.ext.autodoc", "sphinx.ext.intersphinx"]
+
+intersphinx_mapping = {
+ "python": ("https://docs.python.org/3", None),
+}
# Add any paths that contain templates here, relative to this directory.
templates_path = []
@@ -69,7 +73,7 @@
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
-language = None
+language = 'en'
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
@@ -113,25 +117,19 @@
# If true, keep warnings as "system message" paragraphs in the built documents.
# keep_warnings = False
+suppress_warnings = ["image.nonlocal_uri"]
+
# If true, `todo` and `todoList` produce output, else they produce nothing.
todo_include_todos = False
# -- Options for HTML output ----------------------------------------------
-# The theme to use for HTML and HTML Help pages. See the documentation for
-# a list of builtin themes.
-#
-# html_theme = 'alabaster'
-
-# Theme options are theme-specific and customize the look and feel of a theme
-# further. For a list of options available for each theme, see the
-# documentation.
-#
-# html_theme_options = {}
-
-# Add any paths that contain custom themes here, relative to this directory.
-# html_theme_path = []
+# Use RTD theme locally.
+if not os.environ.get('READTHEDOCS', None) == 'True':
+ import sphinx_rtd_theme
+ html_theme = "sphinx_rtd_theme"
+ html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
# The name for this set of Sphinx documents.
# " v documentation" by default.
diff --git a/reversion/__init__.py b/reversion/__init__.py
index f6bc91b7..98983a6e 100644
--- a/reversion/__init__.py
+++ b/reversion/__init__.py
@@ -36,4 +36,4 @@
get_registered_models,
)
-__version__ = VERSION = (3, 0, 8)
+__version__ = VERSION = (5, 1, 0)
diff --git a/reversion/admin.py b/reversion/admin.py
index 2bf7d553..5ab748a1 100644
--- a/reversion/admin.py
+++ b/reversion/admin.py
@@ -1,5 +1,5 @@
from contextlib import contextmanager
-from django.db import models, transaction, connection
+from django.db import models, transaction, connections
from django.contrib import admin, messages
from django.contrib.admin import options
from django.contrib.admin.utils import unquote, quote
@@ -10,13 +10,18 @@
from django.urls import reverse, re_path
from django.utils.text import capfirst
from django.utils.timezone import template_localtime
-from django.utils.translation import ugettext as _
+from django.utils.translation import gettext as _
from django.utils.encoding import force_str
from django.utils.formats import localize
from reversion.errors import RevertError
from reversion.models import Version
from reversion.revisions import is_active, register, is_registered, set_comment, create_revision, set_user
-from reversion.views import _RollBackRevisionView
+
+
+class _RollBackRevisionView(Exception):
+
+ def __init__(self, response):
+ self.response = response
class VersionAdmin(admin.ModelAdmin):
@@ -48,8 +53,8 @@ def create_revision(self, request):
def _reversion_get_template_list(self, template_name):
opts = self.model._meta
return (
- "reversion/%s/%s/%s" % (opts.app_label, opts.object_name.lower(), template_name),
- "reversion/%s/%s" % (opts.app_label, template_name),
+ f"reversion/{opts.app_label}/{opts.object_name.lower()}/{template_name}",
+ f"reversion/{opts.app_label}/{template_name}",
"reversion/%s" % template_name,
)
@@ -111,7 +116,7 @@ def _reversion_introspect_inline_admin(self, inline):
):
fk_name = field.name
break
- if fk_name and not inline_model._meta.get_field(fk_name).remote_field.is_hidden():
+ if fk_name and not inline_model._meta.get_field(fk_name).remote_field.hidden:
field = inline_model._meta.get_field(fk_name)
accessor = field.remote_field.get_accessor_name()
follow_field = accessor
@@ -158,7 +163,7 @@ def change_view(self, request, object_id, form_url='', extra_context=None):
def _reversion_revisionform_view(self, request, version, template_name, extra_context=None):
# Check that database transactions are supported.
- if not connection.features.uses_savepoints:
+ if not connections[version.db].features.uses_savepoints:
raise ImproperlyConfigured("Cannot use VersionAdmin with a database that does not support savepoints.")
# Run the view.
try:
@@ -173,14 +178,18 @@ def _reversion_revisionform_view(self, request, version, template_name, extra_co
set_comment(_("Reverted to previous version, saved on %(datetime)s") % {
"datetime": localize(template_localtime(version.revision.date_created)),
})
- else:
+ elif response.status_code == 200:
response.template_name = template_name # Set the template name to the correct template.
response.render() # Eagerly render the response, so it's using the latest version.
raise _RollBackRevisionView(response) # Raise exception to undo the transaction and revision.
+ else:
+ raise RevertError(_("Could not load %(object_repr)s version - not found") % {
+ "object_repr": version.object_repr,
+ })
except (RevertError, models.ProtectedError) as ex:
opts = self.model._meta
messages.error(request, force_str(ex))
- return redirect("{}:{}_{}_changelist".format(self.admin_site.name, opts.app_label, opts.model_name))
+ return redirect(f"{self.admin_site.name}:{opts.app_label}_{opts.model_name}_changelist")
except _RollBackRevisionView as ex:
return ex.response
return response
@@ -236,7 +245,9 @@ def recoverlist_view(self, request, extra_context=None):
raise PermissionDenied
model = self.model
opts = model._meta
- deleted = self._reversion_order_version_queryset(Version.objects.get_deleted(self.model))
+ deleted = self._reversion_order_version_queryset(
+ Version.objects.get_deleted(self.model).select_related("revision")
+ )
# Set the app name.
request.current_app = self.admin_site.name
# Get the rest of the context.
@@ -270,7 +281,7 @@ def history_view(self, request, object_id, extra_context=None):
{
"revision": version.revision,
"url": reverse(
- "%s:%s_%s_revision" % (self.admin_site.name, opts.app_label, opts.model_name),
+ f"{self.admin_site.name}:{opts.app_label}_{opts.model_name}_revision",
args=(quote(version.object_id), version.id)
),
}
diff --git a/reversion/apps.py b/reversion/apps.py
new file mode 100644
index 00000000..04c88128
--- /dev/null
+++ b/reversion/apps.py
@@ -0,0 +1,8 @@
+from django.apps import AppConfig
+from django.utils.translation import gettext_lazy as _
+
+
+class ReversionConfig(AppConfig):
+ name = 'reversion'
+ verbose_name = _('Reversion')
+ default_auto_field = 'django.db.models.AutoField'
diff --git a/reversion/locale/ar/LC_MESSAGES/django.mo b/reversion/locale/ar/LC_MESSAGES/django.mo
index 1547399b..1cdd8ca9 100644
Binary files a/reversion/locale/ar/LC_MESSAGES/django.mo and b/reversion/locale/ar/LC_MESSAGES/django.mo differ
diff --git a/reversion/locale/cs/LC_MESSAGES/django.mo b/reversion/locale/cs/LC_MESSAGES/django.mo
index f060570d..3c3988c7 100644
Binary files a/reversion/locale/cs/LC_MESSAGES/django.mo and b/reversion/locale/cs/LC_MESSAGES/django.mo differ
diff --git a/reversion/locale/cs/LC_MESSAGES/django.po b/reversion/locale/cs/LC_MESSAGES/django.po
index 94bc3547..f7ffa009 100644
--- a/reversion/locale/cs/LC_MESSAGES/django.po
+++ b/reversion/locale/cs/LC_MESSAGES/django.po
@@ -14,7 +14,8 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"Plural-Forms: nplurals=3; plural=n==1 ? 0 : n>1 && n<5 ? 1 : 2;\n"
+"Plural-Forms: nplurals=4; plural=(n == 1 && n % 1 == 0) ? 0 : (n >= 2 && n "
+"<= 4 && n % 1 == 0) ? 1: (n % 1 != 0 ) ? 2 : 3;\n"
#: admin.py:112 templates/reversion/change_list.html:8
#: templates/reversion/recover_list.html:10
diff --git a/reversion/locale/da/LC_MESSAGES/django.mo b/reversion/locale/da/LC_MESSAGES/django.mo
index bc359e0e..7ab61bb5 100644
Binary files a/reversion/locale/da/LC_MESSAGES/django.mo and b/reversion/locale/da/LC_MESSAGES/django.mo differ
diff --git a/reversion/locale/de/LC_MESSAGES/django.mo b/reversion/locale/de/LC_MESSAGES/django.mo
index 807f2b26..cfb9aec4 100644
Binary files a/reversion/locale/de/LC_MESSAGES/django.mo and b/reversion/locale/de/LC_MESSAGES/django.mo differ
diff --git a/reversion/locale/de/LC_MESSAGES/django.po b/reversion/locale/de/LC_MESSAGES/django.po
index dee2e8f5..13e1a339 100644
--- a/reversion/locale/de/LC_MESSAGES/django.po
+++ b/reversion/locale/de/LC_MESSAGES/django.po
@@ -14,6 +14,7 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: admin.py:122 templates/reversion/change_list.html:8
#: templates/reversion/recover_list.html:9
diff --git a/reversion/locale/es/LC_MESSAGES/django.mo b/reversion/locale/es/LC_MESSAGES/django.mo
index 520f0fab..78179f3d 100644
Binary files a/reversion/locale/es/LC_MESSAGES/django.mo and b/reversion/locale/es/LC_MESSAGES/django.mo differ
diff --git a/reversion/locale/es_AR/LC_MESSAGES/django.mo b/reversion/locale/es_AR/LC_MESSAGES/django.mo
index fd04fc8f..f1ad1a55 100644
Binary files a/reversion/locale/es_AR/LC_MESSAGES/django.mo and b/reversion/locale/es_AR/LC_MESSAGES/django.mo differ
diff --git a/reversion/locale/fr/LC_MESSAGES/django.mo b/reversion/locale/fr/LC_MESSAGES/django.mo
index f25ee021..15aa0775 100644
Binary files a/reversion/locale/fr/LC_MESSAGES/django.mo and b/reversion/locale/fr/LC_MESSAGES/django.mo differ
diff --git a/reversion/locale/fr/LC_MESSAGES/django.po b/reversion/locale/fr/LC_MESSAGES/django.po
index d51daa15..c05927e6 100644
--- a/reversion/locale/fr/LC_MESSAGES/django.po
+++ b/reversion/locale/fr/LC_MESSAGES/django.po
@@ -16,7 +16,7 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"Plural-Forms: nplurals=2; plural=n>1;\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#: admin.py:143 templates/reversion/change_list.html:7
#: templates/reversion/recover_form.html:10
diff --git a/reversion/locale/he/LC_MESSAGES/django.mo b/reversion/locale/he/LC_MESSAGES/django.mo
index 09271c37..2257367a 100644
Binary files a/reversion/locale/he/LC_MESSAGES/django.mo and b/reversion/locale/he/LC_MESSAGES/django.mo differ
diff --git a/reversion/locale/he/LC_MESSAGES/django.po b/reversion/locale/he/LC_MESSAGES/django.po
index 3624f026..1a9b7097 100644
--- a/reversion/locale/he/LC_MESSAGES/django.po
+++ b/reversion/locale/he/LC_MESSAGES/django.po
@@ -14,6 +14,8 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=4; plural=(n == 1 && n % 1 == 0) ? 0 : (n == 2 && n % "
+"1 == 0) ? 1: (n % 10 == 0 && n % 1 == 0 && n > 10) ? 2 : 3;\n"
#: admin.py:112 templates/reversion/change_list.html:7
#: templates/reversion/recover_list.html:10
diff --git a/reversion/locale/it/LC_MESSAGES/django.mo b/reversion/locale/it/LC_MESSAGES/django.mo
index cb1b6a3e..4467379b 100644
Binary files a/reversion/locale/it/LC_MESSAGES/django.mo and b/reversion/locale/it/LC_MESSAGES/django.mo differ
diff --git a/reversion/locale/it/LC_MESSAGES/django.po b/reversion/locale/it/LC_MESSAGES/django.po
index 13a1526f..af86e1c3 100644
--- a/reversion/locale/it/LC_MESSAGES/django.po
+++ b/reversion/locale/it/LC_MESSAGES/django.po
@@ -14,6 +14,7 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: .\admin.py:128
#: .\templates\reversion\change_list.html.py:7
diff --git a/reversion/locale/nb/LC_MESSAGES/django.mo b/reversion/locale/nb/LC_MESSAGES/django.mo
index af21cdd4..4dbe5ca8 100644
Binary files a/reversion/locale/nb/LC_MESSAGES/django.mo and b/reversion/locale/nb/LC_MESSAGES/django.mo differ
diff --git a/reversion/locale/nl/LC_MESSAGES/django.mo b/reversion/locale/nl/LC_MESSAGES/django.mo
index 9a0bc83b..6b195b4a 100644
Binary files a/reversion/locale/nl/LC_MESSAGES/django.mo and b/reversion/locale/nl/LC_MESSAGES/django.mo differ
diff --git a/reversion/locale/pl/LC_MESSAGES/django.mo b/reversion/locale/pl/LC_MESSAGES/django.mo
index 3fe12d28..1ee671a6 100644
Binary files a/reversion/locale/pl/LC_MESSAGES/django.mo and b/reversion/locale/pl/LC_MESSAGES/django.mo differ
diff --git a/reversion/locale/pl/LC_MESSAGES/django.po b/reversion/locale/pl/LC_MESSAGES/django.po
index 2b2b2b38..5ff24a14 100644
--- a/reversion/locale/pl/LC_MESSAGES/django.po
+++ b/reversion/locale/pl/LC_MESSAGES/django.po
@@ -15,6 +15,9 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n"
+"%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n"
+"%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n"
#: admin.py:100
msgid "Initial version."
diff --git a/reversion/locale/pt_BR/LC_MESSAGES/django.mo b/reversion/locale/pt_BR/LC_MESSAGES/django.mo
index a8754138..61c4708f 100644
Binary files a/reversion/locale/pt_BR/LC_MESSAGES/django.mo and b/reversion/locale/pt_BR/LC_MESSAGES/django.mo differ
diff --git a/reversion/locale/pt_BR/LC_MESSAGES/django.po b/reversion/locale/pt_BR/LC_MESSAGES/django.po
index 76c4679c..b9af526d 100644
--- a/reversion/locale/pt_BR/LC_MESSAGES/django.po
+++ b/reversion/locale/pt_BR/LC_MESSAGES/django.po
@@ -14,6 +14,7 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#: .\admin.py:128
#: .\templates\reversion\change_list.html.py:7
diff --git a/reversion/locale/ru/LC_MESSAGES/django.mo b/reversion/locale/ru/LC_MESSAGES/django.mo
index c78390c6..741a7b4a 100644
Binary files a/reversion/locale/ru/LC_MESSAGES/django.mo and b/reversion/locale/ru/LC_MESSAGES/django.mo differ
diff --git a/reversion/locale/ru/LC_MESSAGES/django.po b/reversion/locale/ru/LC_MESSAGES/django.po
index 68f15ca4..3ccc3aab 100644
--- a/reversion/locale/ru/LC_MESSAGES/django.po
+++ b/reversion/locale/ru/LC_MESSAGES/django.po
@@ -16,6 +16,9 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"X-Poedit-Language: Russian\n"
"X-Poedit-Country: RUSSIAN FEDERATION\n"
+"Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
+"%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n"
+"%100>=11 && n%100<=14)? 2 : 3);\n"
#: admin.py:122
#: templates/reversion/change_list.html:8
diff --git a/reversion/locale/sk/LC_MESSAGES/django.mo b/reversion/locale/sk/LC_MESSAGES/django.mo
index cb6a257a..250bf5fb 100644
Binary files a/reversion/locale/sk/LC_MESSAGES/django.mo and b/reversion/locale/sk/LC_MESSAGES/django.mo differ
diff --git a/reversion/locale/sk/LC_MESSAGES/django.po b/reversion/locale/sk/LC_MESSAGES/django.po
index f6750fb6..106d782d 100644
--- a/reversion/locale/sk/LC_MESSAGES/django.po
+++ b/reversion/locale/sk/LC_MESSAGES/django.po
@@ -16,7 +16,8 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n"
+"Plural-Forms: nplurals=4; plural=(n % 1 == 0 && n == 1 ? 0 : n % 1 == 0 && n "
+">= 2 && n <= 4 ? 1 : n % 1 != 0 ? 2: 3);\n"
#: admin.py:153
msgid "Initial version."
diff --git a/reversion/locale/sl_SI/LC_MESSAGES/django.mo b/reversion/locale/sl_SI/LC_MESSAGES/django.mo
index 0653addb..6ba9b3e3 100644
Binary files a/reversion/locale/sl_SI/LC_MESSAGES/django.mo and b/reversion/locale/sl_SI/LC_MESSAGES/django.mo differ
diff --git a/reversion/locale/sl_SI/LC_MESSAGES/django.po b/reversion/locale/sl_SI/LC_MESSAGES/django.po
index 33353fbb..366cbb82 100644
--- a/reversion/locale/sl_SI/LC_MESSAGES/django.po
+++ b/reversion/locale/sl_SI/LC_MESSAGES/django.po
@@ -16,6 +16,8 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=4; plural=(n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n"
+"%100==4 ? 2 : 3);\n"
#: admin.py:66
msgid "Initial version."
diff --git a/reversion/locale/sv/LC_MESSAGES/django.mo b/reversion/locale/sv/LC_MESSAGES/django.mo
index 5aefd033..e3f29e23 100644
Binary files a/reversion/locale/sv/LC_MESSAGES/django.mo and b/reversion/locale/sv/LC_MESSAGES/django.mo differ
diff --git a/reversion/locale/uk/LC_MESSAGES/django.mo b/reversion/locale/uk/LC_MESSAGES/django.mo
index e8baa9ee..15d54c84 100644
Binary files a/reversion/locale/uk/LC_MESSAGES/django.mo and b/reversion/locale/uk/LC_MESSAGES/django.mo differ
diff --git a/reversion/locale/uk/LC_MESSAGES/django.po b/reversion/locale/uk/LC_MESSAGES/django.po
index 93a1db22..e7bdf6c4 100644
--- a/reversion/locale/uk/LC_MESSAGES/django.po
+++ b/reversion/locale/uk/LC_MESSAGES/django.po
@@ -13,8 +13,10 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
-"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
+"Plural-Forms: nplurals=4; plural=(n % 1 == 0 && n % 10 == 1 && n % 100 != "
+"11 ? 0 : n % 1 == 0 && n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 12 || n % "
+"100 > 14) ? 1 : n % 1 == 0 && (n % 10 ==0 || (n % 10 >=5 && n % 10 <=9) || "
+"(n % 100 >=11 && n % 100 <=14 )) ? 2: 3);\n"
#: reversion/admin.py:83
msgid "Initial version."
msgstr "Початкова версія."
diff --git a/reversion/locale/zh_CN/LC_MESSAGES/django.mo b/reversion/locale/zh_CN/LC_MESSAGES/django.mo
deleted file mode 100644
index 6b0fa29e..00000000
Binary files a/reversion/locale/zh_CN/LC_MESSAGES/django.mo and /dev/null differ
diff --git a/reversion/locale/zh_CN/LC_MESSAGES/django.po b/reversion/locale/zh_CN/LC_MESSAGES/django.po
deleted file mode 100644
index 75e72312..00000000
--- a/reversion/locale/zh_CN/LC_MESSAGES/django.po
+++ /dev/null
@@ -1,121 +0,0 @@
-# SOME DESCRIPTIVE TITLE.
-# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
-# This file is distributed under the same license as the PACKAGE package.
-# FIRST AUTHOR , YEAR.
-#
-#, fuzzy
-msgid ""
-msgstr ""
-"Project-Id-Version: PACKAGE VERSION\n"
-"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2014-06-12 14:21+0800\n"
-"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
-"Last-Translator: FULL NAME \n"
-"Language-Team: LANGUAGE \n"
-"Language: \n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"Plural-Forms: nplurals=1; plural=0;\n"
-
-#: admin.py:160
-msgid "Initial version."
-msgstr "初始版本"
-
-#: admin.py:194 templates/reversion/change_list.html:7
-#: templates/reversion/recover_form.html:11
-#: templates/reversion/recover_list.html:11
-#, python-format
-msgid "Recover deleted %(name)s"
-msgstr "恢复已删除的 %(name)s"
-
-#: admin.py:311
-#, python-format
-msgid "Reverted to previous version, saved on %(datetime)s"
-msgstr "恢复到 %(datetime)s 的版本"
-
-#: admin.py:313
-#, python-format
-msgid ""
-"The %(model)s \"%(name)s\" was reverted successfully. You may edit it again "
-"below."
-msgstr "%(model)s \"%(name)s\" 已成功恢复,你可以在下面在此编辑它。"
-
-#: admin.py:398
-#, python-format
-msgid "Recover %(name)s"
-msgstr "恢复 %(name)s"
-
-#: admin.py:412
-#, python-format
-msgid "Revert %(name)s"
-msgstr "恢复 %(name)s"
-
-#: models.py:55
-msgid "date created"
-msgstr "创建日期"
-
-#: models.py:62
-msgid "user"
-msgstr "用户"
-
-#: models.py:66
-msgid "comment"
-msgstr "评论"
-
-#: templates/reversion/object_history.html:8
-msgid ""
-"Choose a date from the list below to revert to a previous version of this "
-"object."
-msgstr "单击下方的日期以恢复当前对象到之前的版本。"
-
-#: templates/reversion/object_history.html:15
-#: templates/reversion/recover_list.html:24
-msgid "Date/time"
-msgstr "时间"
-
-#: templates/reversion/object_history.html:16
-msgid "User"
-msgstr "用户"
-
-#: templates/reversion/object_history.html:17
-msgid "Comment"
-msgstr "评论"
-
-#: templates/reversion/object_history.html:38
-msgid ""
-"This object doesn't have a change history. It probably wasn't added via this "
-"admin site."
-msgstr "此对象不存在任何变更历史,它可能不是通过管理站点添加的。"
-
-#: templates/reversion/recover_form.html:8
-#: templates/reversion/recover_list.html:8
-#: templates/reversion/revision_form.html:8
-msgid "Home"
-msgstr "首页"
-
-#: templates/reversion/recover_form.html:18
-msgid "Press the save button below to recover this version of the object."
-msgstr "单击保存按钮以恢复为此版本。"
-
-#: templates/reversion/recover_list.html:18
-msgid ""
-"Choose a date from the list below to recover a deleted version of an object."
-msgstr "单击下方的日期以恢复一个已删除的对象。"
-
-#: templates/reversion/recover_list.html:38
-msgid "There are no deleted objects to recover."
-msgstr "没有可供恢复的已删除对象。"
-
-#: templates/reversion/revision_form.html:12
-msgid "History"
-msgstr "历史"
-
-#: templates/reversion/revision_form.html:13
-#, python-format
-msgid "Revert %(verbose_name)s"
-msgstr "恢复 %(verbose_name)s"
-
-#: templates/reversion/revision_form.html:26
-msgid "Press the save button below to revert to this version of the object."
-msgstr "单击保存按钮将此对象恢复到此版本。"
diff --git a/reversion/locale/zh_Hans/LC_MESSAGES/django.mo b/reversion/locale/zh_Hans/LC_MESSAGES/django.mo
index 6b0fa29e..490a373c 100644
Binary files a/reversion/locale/zh_Hans/LC_MESSAGES/django.mo and b/reversion/locale/zh_Hans/LC_MESSAGES/django.mo differ
diff --git a/reversion/locale/zh_Hans/LC_MESSAGES/django.po b/reversion/locale/zh_Hans/LC_MESSAGES/django.po
index 75e72312..b70ee59b 100644
--- a/reversion/locale/zh_Hans/LC_MESSAGES/django.po
+++ b/reversion/locale/zh_Hans/LC_MESSAGES/django.po
@@ -2,13 +2,13 @@
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR , YEAR.
-#
+#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2014-06-12 14:21+0800\n"
+"POT-Creation-Date: 2024-01-25 16:26+0800\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME \n"
"Language-Team: LANGUAGE \n"
@@ -18,104 +18,138 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
-#: admin.py:160
+#: .\admin.py:70
msgid "Initial version."
msgstr "初始版本"
-#: admin.py:194 templates/reversion/change_list.html:7
-#: templates/reversion/recover_form.html:11
-#: templates/reversion/recover_list.html:11
-#, python-format
-msgid "Recover deleted %(name)s"
-msgstr "恢复已删除的 %(name)s"
-
-#: admin.py:311
+#: .\admin.py:178
#, python-format
msgid "Reverted to previous version, saved on %(datetime)s"
msgstr "恢复到 %(datetime)s 的版本"
-#: admin.py:313
+#: .\admin.py:186
#, python-format
-msgid ""
-"The %(model)s \"%(name)s\" was reverted successfully. You may edit it again "
-"below."
-msgstr "%(model)s \"%(name)s\" 已成功恢复,你可以在下面在此编辑它。"
+msgid "Could not load %(object_repr)s version - not found"
+msgstr "无法载入 %(object_repr)s 版本 - 未找到"
-#: admin.py:398
+#: .\admin.py:206
#, python-format
msgid "Recover %(name)s"
msgstr "恢复 %(name)s"
-#: admin.py:412
+#: .\admin.py:222
#, python-format
msgid "Revert %(name)s"
msgstr "恢复 %(name)s"
-#: models.py:55
+#: .\admin.py:259 .\templates\reversion\change_list.html:7
+#: .\templates\reversion\recover_form.html:10
+#: .\templates\reversion\recover_list.html:10
+#, python-format
+msgid "Recover deleted %(name)s"
+msgstr "恢复已删除的 %(name)s"
+
+#: .\apps.py:7
+msgid "Reversion"
+msgstr "版本记录"
+
+#: .\models.py:39
+#, python-format
+msgid "Could not save %(object_repr)s version - missing dependency."
+msgstr "无法保存 %(object_repr)s 版本 - 缺少依赖"
+
+#: .\models.py:52
msgid "date created"
msgstr "创建日期"
-#: models.py:62
+#: .\models.py:61
msgid "user"
msgstr "用户"
-#: models.py:66
+#: .\models.py:67
msgid "comment"
msgstr "评论"
-#: templates/reversion/object_history.html:8
+#: .\models.py:116
+msgid "revision"
+msgstr "修改"
+
+#: .\models.py:117
+msgid "revisions"
+msgstr "修改"
+
+#: .\models.py:275
+#, python-format
+msgid "Could not load %(object_repr)s version - incompatible version data."
+msgstr "无法载入 %(object_repr)s 版本 - 不兼容的版本数据。"
+
+#: .\models.py:279
+#, python-format
+msgid "Could not load %(object_repr)s version - unknown serializer %(format)s."
+msgstr "无法载入 %(object_repr)s 版本 - 未知的序列化 %(format)s。"
+
+#: .\models.py:335
+#, fuzzy
+msgid "version"
+msgstr "版本"
+
+#: .\models.py:336
+msgid "versions"
+msgstr "版本"
+
+#: .\templates\reversion\object_history.html:8
msgid ""
"Choose a date from the list below to revert to a previous version of this "
"object."
msgstr "单击下方的日期以恢复当前对象到之前的版本。"
-#: templates/reversion/object_history.html:15
-#: templates/reversion/recover_list.html:24
+#: .\templates\reversion\object_history.html:15
+#: .\templates\reversion\recover_list.html:23
msgid "Date/time"
msgstr "时间"
-#: templates/reversion/object_history.html:16
+#: .\templates\reversion\object_history.html:16
msgid "User"
msgstr "用户"
-#: templates/reversion/object_history.html:17
-msgid "Comment"
-msgstr "评论"
+#: .\templates\reversion\object_history.html:17
+msgid "Action"
+msgstr "操作"
-#: templates/reversion/object_history.html:38
+#: .\templates\reversion\object_history.html:38
msgid ""
"This object doesn't have a change history. It probably wasn't added via this "
"admin site."
msgstr "此对象不存在任何变更历史,它可能不是通过管理站点添加的。"
-#: templates/reversion/recover_form.html:8
-#: templates/reversion/recover_list.html:8
-#: templates/reversion/revision_form.html:8
+#: .\templates\reversion\recover_form.html:7
+#: .\templates\reversion\recover_list.html:7
+#: .\templates\reversion\revision_form.html:7
msgid "Home"
msgstr "首页"
-#: templates/reversion/recover_form.html:18
+#: .\templates\reversion\recover_form.html:20
msgid "Press the save button below to recover this version of the object."
msgstr "单击保存按钮以恢复为此版本。"
-#: templates/reversion/recover_list.html:18
+#: .\templates\reversion\recover_list.html:17
msgid ""
"Choose a date from the list below to recover a deleted version of an object."
msgstr "单击下方的日期以恢复一个已删除的对象。"
-#: templates/reversion/recover_list.html:38
+#: .\templates\reversion\recover_list.html:37
msgid "There are no deleted objects to recover."
msgstr "没有可供恢复的已删除对象。"
-#: templates/reversion/revision_form.html:12
+#: .\templates\reversion\revision_form.html:11
msgid "History"
msgstr "历史"
-#: templates/reversion/revision_form.html:13
+#: .\templates\reversion\revision_form.html:12
#, python-format
msgid "Revert %(verbose_name)s"
msgstr "恢复 %(verbose_name)s"
-#: templates/reversion/revision_form.html:26
+#: .\templates\reversion\revision_form.html:21
msgid "Press the save button below to revert to this version of the object."
msgstr "单击保存按钮将此对象恢复到此版本。"
diff --git a/reversion/management/commands/__init__.py b/reversion/management/commands/__init__.py
index 87253a41..cb5a14ea 100644
--- a/reversion/management/commands/__init__.py
+++ b/reversion/management/commands/__init__.py
@@ -43,7 +43,7 @@ def get_models(self, options):
try:
model = apps.get_model(label)
except LookupError:
- raise CommandError("Unknown model: {}".format(label))
+ raise CommandError(f"Unknown model: {label}")
selected_models.add(model)
else:
# This is just an app - no model qualifier.
@@ -51,7 +51,7 @@ def get_models(self, options):
try:
app = apps.get_app_config(app_label)
except LookupError:
- raise CommandError("Unknown app: {}".format(app_label))
+ raise CommandError(f"Unknown app: {app_label}")
selected_models.update(app.get_models())
for model in selected_models:
if is_registered(model):
diff --git a/reversion/management/commands/createinitialrevisions.py b/reversion/management/commands/createinitialrevisions.py
index 628273a8..9d648fa1 100644
--- a/reversion/management/commands/createinitialrevisions.py
+++ b/reversion/management/commands/createinitialrevisions.py
@@ -2,7 +2,7 @@
from django.apps import apps
from django.core.management import CommandError
-from django.db import reset_queries, transaction, router
+from django.db import connections, reset_queries, transaction, router
from reversion.models import Revision, Version, _safe_subquery
from reversion.management.commands import BaseRevisionCommand
from reversion.revisions import create_revision, set_comment, add_to_revision, add_meta
@@ -48,10 +48,13 @@ def handle(self, *app_labels, **options):
model = apps.get_model(label)
meta_models.append(model)
except LookupError:
- raise CommandError("Unknown model: {}".format(label))
+ raise CommandError(f"Unknown model: {label}")
meta_values = meta.values()
- # Create revisions.
+ # Determine if we should use queryset.iterator()
using = using or router.db_for_write(Revision)
+ server_side_cursors = not connections[using].settings_dict.get('DISABLE_SERVER_SIDE_CURSORS')
+ use_iterator = connections[using].vendor in ("postgresql",) and server_side_cursors
+ # Create revisions.
with transaction.atomic(using=using):
for model in self.get_models(options):
# Check all models for empty revisions.
@@ -70,28 +73,48 @@ def handle(self, *app_labels, **options):
),
"object_id",
)
+ live_objs = live_objs.order_by()
# Save all the versions.
- ids = list(live_objs.values_list("pk", flat=True).order_by())
- total = len(ids)
- for i in range(0, total, batch_size):
- chunked_ids = ids[i:i+batch_size]
- objects = live_objs.in_bulk(chunked_ids)
- for obj in objects.values():
- with create_revision(using=using):
- if meta:
- for model, values in zip(meta_models, meta_values):
- add_meta(model, **values)
- set_comment(comment)
- add_to_revision(obj, model_db=model_db)
- created_count += 1
- reset_queries()
- if verbosity >= 2:
- self.stdout.write("- Created {created_count} / {total}".format(
- created_count=created_count,
- total=total,
- ))
+ if use_iterator:
+ total = live_objs.count()
+ if total:
+ for obj in live_objs.iterator(batch_size):
+ self.create_revision(obj, using, meta, meta_models, meta_values, comment, model_db)
+ created_count += 1
+ # Print out a message every batch_size if feeling extra verbose
+ if not created_count % batch_size:
+ self.batch_complete(verbosity, created_count, total)
+ else:
+ # Save all the versions.
+ ids = list(live_objs.values_list("pk", flat=True))
+ total = len(ids)
+ for i in range(0, total, batch_size):
+ chunked_ids = ids[i:i+batch_size]
+ objects = live_objs.in_bulk(chunked_ids)
+ for obj in objects.values():
+ self.create_revision(obj, using, meta, meta_models, meta_values, comment, model_db)
+ created_count += 1
+ # Print out a message every batch_size if feeling extra verbose
+ self.batch_complete(verbosity, created_count, total)
+
# Print out a message, if feeling verbose.
if verbosity >= 1:
self.stdout.write("- Created {total} / {total}".format(
total=total,
))
+
+ def create_revision(self, obj, using, meta, meta_models, meta_values, comment, model_db):
+ with create_revision(using=using):
+ if meta:
+ for model, values in zip(meta_models, meta_values):
+ add_meta(model, **values)
+ set_comment(comment)
+ add_to_revision(obj, model_db=model_db)
+
+ def batch_complete(self, verbosity, created_count, total):
+ reset_queries()
+ if verbosity >= 2:
+ self.stdout.write("- Created {created_count} / {total}".format(
+ created_count=created_count,
+ total=total,
+ ))
diff --git a/reversion/migrations/0001_squashed_0004_auto_20160611_1202.py b/reversion/migrations/0001_squashed_0004_auto_20160611_1202.py
index f35f5a33..afb1618c 100644
--- a/reversion/migrations/0001_squashed_0004_auto_20160611_1202.py
+++ b/reversion/migrations/0001_squashed_0004_auto_20160611_1202.py
@@ -23,7 +23,9 @@ class Migration(migrations.Migration):
('user', models.ForeignKey(blank=True, help_text='The user who created this revision.', null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='user')),
],
options={
- "ordering": ("-pk",)
+ "ordering": ("-pk",),
+ 'verbose_name': 'revision',
+ 'verbose_name_plural': 'revisions',
},
),
migrations.CreateModel(
@@ -39,11 +41,13 @@ class Migration(migrations.Migration):
('db', models.CharField(help_text='The database the model under version control is stored in.', max_length=191)),
],
options={
- "ordering": ("-pk",)
+ "ordering": ("-pk",),
+ 'verbose_name': 'version',
+ 'verbose_name_plural': 'versions',
},
),
migrations.AlterUniqueTogether(
name='version',
- unique_together=set([('db', 'content_type', 'object_id', 'revision')]),
+ unique_together={('db', 'content_type', 'object_id', 'revision')},
),
]
diff --git a/reversion/migrations/0005_add_index_on_version_for_content_type_and_db.py b/reversion/migrations/0005_add_index_on_version_for_content_type_and_db.py
new file mode 100644
index 00000000..06296632
--- /dev/null
+++ b/reversion/migrations/0005_add_index_on_version_for_content_type_and_db.py
@@ -0,0 +1,17 @@
+# Generated by Django 3.2.6 on 2021-08-16 18:08
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('reversion', '0003_auto_20201027_1546'),
+ ]
+
+ operations = [
+ migrations.AddIndex(
+ model_name='version',
+ index=models.Index(fields=['content_type', 'db'], name='reversion_v_content_f95daf_idx'),
+ ),
+ ]
diff --git a/reversion/models.py b/reversion/models.py
index f9e61a56..7f64bb48 100644
--- a/reversion/models.py
+++ b/reversion/models.py
@@ -1,6 +1,8 @@
from collections import defaultdict
from itertools import chain, groupby
+import logging
+import django
from django.apps import apps
from django.conf import settings
from django.contrib.contenttypes.fields import GenericForeignKey
@@ -13,7 +15,7 @@
from django.db.models.functions import Cast
from django.utils.encoding import force_str
from django.utils.functional import cached_property
-from django.utils.translation import ugettext
+from django.utils.translation import gettext
from django.utils.translation import gettext_lazy as _
from encrypted_fields.fields import EncryptedTextField
@@ -22,6 +24,9 @@
_get_content_type, _get_options)
+logger = logging.getLogger(__name__)
+
+
def _safe_revert(versions):
unreverted_versions = []
for version in versions:
@@ -29,9 +34,10 @@ def _safe_revert(versions):
with transaction.atomic(using=version.db):
version.revert()
except (IntegrityError, ObjectDoesNotExist):
+ logger.warning(f'Could not revert to {version}', exc_info=True)
unreverted_versions.append(version)
if len(unreverted_versions) == len(versions):
- raise RevertError(ugettext("Could not save %(object_repr)s version - missing dependency.") % {
+ raise RevertError(gettext("Could not save %(object_repr)s version - missing dependency.") % {
"object_repr": unreverted_versions[0],
})
if unreverted_versions:
@@ -108,6 +114,8 @@ def __str__(self):
return ", ".join(force_str(version) for version in self.version_set.all())
class Meta:
+ verbose_name = _('revision')
+ verbose_name_plural = _('revisions')
app_label = "reversion"
ordering = ("-pk",)
@@ -134,20 +142,51 @@ def get_deleted(self, model, model_db=None):
model_db = model_db or router.db_for_write(model)
connection = connections[self.db]
if self.db == model_db and connection.vendor in ("sqlite", "postgresql", "oracle"):
- model_qs = (
- model._default_manager
- .using(model_db)
- .annotate(_pk_to_object_id=Cast("pk", Version._meta.get_field("object_id")))
- .filter(_pk_to_object_id=models.OuterRef("object_id"))
- )
- subquery = (
- self.get_for_model(model, model_db=model_db)
- .annotate(pk_not_exists=~models.Exists(model_qs))
- .filter(pk_not_exists=True)
- .values("object_id")
- .annotate(latest_pk=models.Max("pk"))
- .values("latest_pk")
- )
+ pk_field_name = model._meta.pk.name
+ object_id_cast_target = model._meta.get_field(pk_field_name)
+ if django.VERSION >= (2, 1):
+ # django 2.0 contains a critical bug that doesn't allow the code below to work,
+ # fallback to casting primary keys then
+ # see https://code.djangoproject.com/ticket/29142
+ if django.VERSION < (2, 2):
+ # properly cast autofields for django before 2.2 as it was fixed in django itself later
+ # see https://github.com/django/django/commit/ac25dd1f8d48accc765c05aebb47c427e51f3255
+ object_id_cast_target = {
+ "AutoField": models.IntegerField(),
+ "BigAutoField": models.BigIntegerField(),
+ }.get(object_id_cast_target.__class__.__name__, object_id_cast_target)
+ casted_object_id = Cast(models.OuterRef("object_id"), object_id_cast_target)
+ model_qs = (
+ model._default_manager
+ .using(model_db)
+ .filter(**{pk_field_name: casted_object_id})
+ )
+ else:
+ model_qs = (
+ model._default_manager
+ .using(model_db)
+ .annotate(_pk_to_object_id=Cast("pk", Version._meta.get_field("object_id")))
+ .filter(_pk_to_object_id=models.OuterRef("object_id"))
+ )
+ # conditional expressions are being supported since django 3.0
+ # DISTINCT ON works only for Postgres DB
+ if connection.vendor == "postgresql" and django.VERSION >= (3, 0):
+ subquery = (
+ self.get_for_model(model, model_db=model_db)
+ .filter(~models.Exists(model_qs))
+ .order_by("object_id", "-pk")
+ .distinct("object_id")
+ .values("pk")
+ )
+ else:
+ subquery = (
+ self.get_for_model(model, model_db=model_db)
+ .annotate(pk_not_exists=~models.Exists(model_qs))
+ .filter(pk_not_exists=True)
+ .values("object_id")
+ .annotate(latest_pk=models.Max("pk"))
+ .values("latest_pk")
+ )
else:
# We have to use a slow subquery.
subquery = self.get_for_model(model, model_db=model_db).exclude(
@@ -158,7 +197,8 @@ def get_deleted(self, model, model_db=None):
latest_pk=models.Max("pk")
).order_by().values_list("latest_pk", flat=True)
# Perform the subquery.
- return self.filter(pk__in=subquery)
+ # Filter by model to reduce query execution time.
+ return self.get_for_model(model, model_db=model_db).filter(pk__in=subquery)
def get_unique(self):
last_key = None
@@ -233,11 +273,11 @@ def _object_version(self):
return list(serializers.deserialize(self.format, data, ignorenonexistent=True,
use_natural_foreign_keys=version_options.use_natural_foreign_keys))[0]
except DeserializationError:
- raise RevertError(ugettext("Could not load %(object_repr)s version - incompatible version data.") % {
+ raise RevertError(gettext("Could not load %(object_repr)s version - incompatible version data.") % {
"object_repr": self.object_repr,
})
except serializers.SerializerDoesNotExist:
- raise RevertError(ugettext("Could not load %(object_repr)s version - unknown serializer %(format)s.") % {
+ raise RevertError(gettext("Could not load %(object_repr)s version - unknown serializer %(format)s.") % {
"object_repr": self.object_repr,
"format": self.format,
})
@@ -293,10 +333,17 @@ def __str__(self):
return self.object_repr
class Meta:
+ verbose_name = _('version')
+ verbose_name_plural = _('versions')
app_label = 'reversion'
unique_together = (
("db", "content_type", "object_id", "revision"),
)
+ indexes = (
+ models.Index(
+ fields=["content_type", "db"]
+ ),
+ )
ordering = ("-pk",)
@@ -329,25 +376,25 @@ def _safe_subquery(method, left_query, left_field_name, right_subquery, right_fi
)
):
return getattr(left_query, method)(**{
- "{}__in".format(left_field_name): list(right_subquery.iterator()),
+ f"{left_field_name}__in": list(right_subquery.iterator()),
})
else:
# If the left hand side is not a text field, we need to cast it.
if not isinstance(left_field, (models.CharField, models.TextField)):
- left_field_name_str = "{}_str".format(left_field_name)
+ left_field_name_str = f"{left_field_name}_str"
left_query = left_query.annotate(**{
left_field_name_str: _Str(left_field_name),
})
left_field_name = left_field_name_str
# If the right hand side is not a text field, we need to cast it.
if not isinstance(right_field, (models.CharField, models.TextField)):
- right_field_name_str = "{}_str".format(right_field_name)
+ right_field_name_str = f"{right_field_name}_str"
right_subquery = right_subquery.annotate(**{
right_field_name_str: _Str(right_field_name),
}).values_list(right_field_name_str, flat=True)
right_field_name = right_field_name_str
# Use Exists if running on the same DB, it is much much faster
- exist_annotation_name = "{}_annotation_str".format(right_subquery.model._meta.db_table)
+ exist_annotation_name = f"{right_subquery.model._meta.db_table}_annotation_str"
right_subquery = right_subquery.filter(**{right_field_name: models.OuterRef(left_field_name)})
left_query = left_query.annotate(**{exist_annotation_name: models.Exists(right_subquery)})
return getattr(left_query, method)(**{exist_annotation_name: True})
diff --git a/reversion/revisions.py b/reversion/revisions.py
index 6a8dafd2..5e2ed4f6 100644
--- a/reversion/revisions.py
+++ b/reversion/revisions.py
@@ -1,11 +1,11 @@
+from contextvars import ContextVar
from collections import namedtuple, defaultdict
from contextlib import contextmanager
from functools import wraps
-from threading import local
from django.apps import apps
from django.core import serializers
from django.core.exceptions import ObjectDoesNotExist
-from django.db import models, transaction, router
+from django.db import models, transaction, router, connections
from django.db.models.query import QuerySet
from django.db.models.signals import post_save, m2m_changed
from django.utils.encoding import force_str
@@ -34,23 +34,17 @@
))
-class _Local(local):
-
- def __init__(self):
- self.stack = ()
-
-
-_local = _Local()
+_stack = ContextVar("reversion-stack", default=[])
def is_active():
- return bool(_local.stack)
+ return bool(_stack.get())
def _current_frame():
if not is_active():
raise RevisionManagementError("There is no active revision for this thread")
- return _local.stack[-1]
+ return _stack.get()[-1]
def _copy_db_versions(db_versions):
@@ -79,16 +73,17 @@ def _push_frame(manage_manually, using):
db_versions={using: {}},
meta=(),
)
- _local.stack += (stack_frame,)
+ _stack.set(_stack.get() + [stack_frame])
def _update_frame(**kwargs):
- _local.stack = _local.stack[:-1] + (_current_frame()._replace(**kwargs),)
+ _stack.get()[-1] = _current_frame()._replace(**kwargs)
def _pop_frame():
prev_frame = _current_frame()
- _local.stack = _local.stack[:-1]
+ stack = _stack.get()
+ del stack[-1]
if is_active():
current_frame = _current_frame()
db_versions = {
@@ -147,8 +142,7 @@ def _follow_relations(obj):
if isinstance(follow_obj, models.Model):
yield follow_obj
elif isinstance(follow_obj, (models.Manager, QuerySet)):
- for follow_obj_instance in follow_obj.all():
- yield follow_obj_instance
+ yield from follow_obj.all()
elif follow_obj is not None:
raise RegistrationError("{name}.{follow_name} should be a Model or QuerySet".format(
name=obj.__class__.__name__,
@@ -217,6 +211,7 @@ def add_to_revision(obj, model_db=None):
def _save_revision(versions, user=None, comment="", meta=(), date_created=None, using=None):
from reversion.models import Revision
+ from reversion.models import Version
# Only save versions that exist in the database.
# Use _base_manager so we don't have problems when _default_manager is overriden
model_db_pks = defaultdict(lambda: defaultdict(set))
@@ -254,9 +249,17 @@ def _save_revision(versions, user=None, comment="", meta=(), date_created=None,
# Save the revision.
revision.save(using=using)
# Save version models.
+
+ can_use_bulk_create = connections[using].features.can_return_rows_from_bulk_insert
+
for version in versions:
version.revision = revision
- version.save(using=using)
+ if not can_use_bulk_create:
+ version.save(using=using)
+
+ if can_use_bulk_create:
+ Version.objects.using(using).bulk_create(versions)
+
# Save the meta information.
for meta_model, meta_fields in meta:
meta_model._base_manager.db_manager(using=using).create(
@@ -283,8 +286,15 @@ def _create_revision_context(manage_manually, using, atomic):
_push_frame(manage_manually, using)
try:
yield
+ if transaction.get_connection(using).in_atomic_block and transaction.get_rollback(using):
+ # Transaction is in invalid state due to catched exception within yield statement.
+ # Do not try to create Revision, otherwise it would lead to the transaction management error.
+ #
+ # Atomic block could be called manually around `create_revision` context manager.
+ # That's why we have to check connection flag instead of `atomic` variable value.
+ return
# Only save for a db if that's the last stack frame for that db.
- if not any(using in frame.db_versions for frame in _local.stack[:-1]):
+ if not any(using in frame.db_versions for frame in _stack.get()[:-1]):
current_frame = _current_frame()
_save_revision(
versions=current_frame.db_versions[using].values(),
@@ -304,7 +314,7 @@ def create_revision(manage_manually=False, using=None, atomic=True):
return _ContextWrapper(_create_revision_context, (manage_manually, using, atomic))
-class _ContextWrapper(object):
+class _ContextWrapper:
def __init__(self, func, args):
self._func = func
diff --git a/reversion/views.py b/reversion/views.py
index 7b7e8334..f5105517 100644
--- a/reversion/views.py
+++ b/reversion/views.py
@@ -3,12 +3,6 @@
from reversion.revisions import create_revision as create_revision_base, set_user, get_user
-class _RollBackRevisionView(Exception):
-
- def __init__(self, response):
- self.response = response
-
-
def _request_creates_revision(request):
return request.method not in ("OPTIONS", "GET", "HEAD")
@@ -30,23 +24,16 @@ def decorator(func):
@wraps(func)
def do_revision_view(request, *args, **kwargs):
if request_creates_revision(request):
- try:
- with create_revision_base(manage_manually=manage_manually, using=using, atomic=atomic):
- response = func(request, *args, **kwargs)
- # Check for an error response.
- if response.status_code >= 400:
- raise _RollBackRevisionView(response)
- # Otherwise, we're good.
- _set_user_from_request(request)
- return response
- except _RollBackRevisionView as ex:
- return ex.response
+ with create_revision_base(manage_manually=manage_manually, using=using, atomic=atomic):
+ response = func(request, *args, **kwargs)
+ _set_user_from_request(request)
+ return response
return func(request, *args, **kwargs)
return do_revision_view
return decorator
-class RevisionMixin(object):
+class RevisionMixin:
"""
A class-based view mixin that wraps the request in a revision.
diff --git a/setup.py b/setup.py
index 4ccb7184..1a72d8bf 100644
--- a/setup.py
+++ b/setup.py
@@ -1,10 +1,11 @@
-from setuptools import setup, find_packages
-from reversion import __version__
+from setuptools import find_packages, setup
+from reversion import __version__
# Load in babel support, if available.
try:
from babel.messages import frontend as babel
+
cmdclass = {
"compile_catalog": babel.compile_catalog,
"extract_messages": babel.extract_messages,
@@ -16,13 +17,13 @@
def read(filepath):
- with open(filepath, "r", encoding="utf-8") as f:
+ with open(filepath, encoding="utf-8") as f:
return f.read()
setup(
name="encrypted-django-reversion",
- version='.'.join(str(x) for x in __version__),
+ version=".".join(str(x) for x in __version__),
license="BSD",
description="An extension to the Django web framework that provides version control for model instances.",
long_description=read('README.rst'),
@@ -32,13 +33,14 @@ def read(filepath):
zip_safe=False,
packages=find_packages(),
package_data={
- "reversion": ["locale/*/LC_MESSAGES/django.*", "templates/reversion/*.html"]},
+ "reversion": ["locale/*/LC_MESSAGES/django.*", "templates/reversion/*.html"]
+ },
cmdclass=cmdclass,
install_requires=[
- "django>=1.11",
+ "django>=4.2",
"django-searchable-encrypted-fields>=0.1",
],
- python_requires='>=3.6',
+ python_requires=">=3.8",
classifiers=[
"Development Status :: 5 - Production/Stable",
"Environment :: Web Environment",
@@ -46,9 +48,11 @@ def read(filepath):
"License :: OSI Approved :: BSD License",
"Operating System :: OS Independent",
"Programming Language :: Python",
- 'Programming Language :: Python :: 3.6',
- 'Programming Language :: Python :: 3.7',
- 'Programming Language :: Python :: 3.8',
+ "Programming Language :: Python :: 3.8",
+ "Programming Language :: Python :: 3.9",
+ "Programming Language :: Python :: 3.10",
+ "Programming Language :: Python :: 3.11",
+ "Programming Language :: Python :: 3.12",
"Framework :: Django",
- ]
+ ],
)
diff --git a/tests/test_app/migrations/0001_initial.py b/tests/test_app/migrations/0001_initial.py
index c2020c9b..0b0a0cbd 100644
--- a/tests/test_app/migrations/0001_initial.py
+++ b/tests/test_app/migrations/0001_initial.py
@@ -108,4 +108,11 @@ class Migration(migrations.Migration):
('revision', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='reversion.revision')),
],
),
+ migrations.CreateModel(
+ name='TestModelWithUniqueConstraint',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('name', models.CharField(max_length=191, unique=True)),
+ ],
+ ),
]
diff --git a/tests/test_app/migrations/0002_alter_testmodel_related_and_more.py b/tests/test_app/migrations/0002_alter_testmodel_related_and_more.py
new file mode 100644
index 00000000..d02186f9
--- /dev/null
+++ b/tests/test_app/migrations/0002_alter_testmodel_related_and_more.py
@@ -0,0 +1,23 @@
+# Generated by Django 5.0.1 on 2024-01-30 19:07
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('test_app', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='testmodel',
+ name='related',
+ field=models.ManyToManyField(blank=True, related_name='+', to='test_app.testmodelrelated'),
+ ),
+ migrations.AlterField(
+ model_name='testmodel',
+ name='related_through',
+ field=models.ManyToManyField(blank=True, related_name='+', through='test_app.TestModelThrough', to='test_app.testmodelrelated'),
+ ),
+ ]
diff --git a/tests/test_app/models.py b/tests/test_app/models.py
index a21e94f8..bfb3a16c 100644
--- a/tests/test_app/models.py
+++ b/tests/test_app/models.py
@@ -142,3 +142,11 @@ class TestModelInlineByNaturalKey(models.Model):
TestModelWithNaturalKey,
on_delete=models.CASCADE,
)
+
+
+class TestModelWithUniqueConstraint(models.Model):
+
+ name = models.CharField(
+ max_length=191,
+ unique=True,
+ )
diff --git a/tests/test_app/tests/base.py b/tests/test_app/tests/base.py
index a477fbad..75a8bd40 100644
--- a/tests/test_app/tests/base.py
+++ b/tests/test_app/tests/base.py
@@ -17,9 +17,9 @@
# Test helpers.
-class TestBaseMixin(object):
+class TestBaseMixin:
- multi_db = True
+ databases = "__all__"
def reloadUrls(self):
reload(import_module(settings.ROOT_URLCONF))
@@ -76,7 +76,7 @@ class TestBaseTransaction(TestBaseMixin, TransactionTestCase):
pass
-class TestModelMixin(object):
+class TestModelMixin:
def setUp(self):
super().setUp()
diff --git a/tests/test_app/tests/test_admin.py b/tests/test_app/tests/test_admin.py
index bad4c421..71e4f08f 100644
--- a/tests/test_app/tests/test_admin.py
+++ b/tests/test_app/tests/test_admin.py
@@ -218,7 +218,7 @@ def testHistoryWithQuotedPrimaryKey(self):
response = self.client.get(history_url)
self.assertContains(response, revision_url)
response = self.client.get(revision_url)
- self.assertContains(response, 'value="{}"'.format(pk))
+ self.assertContains(response, f'value="{pk}"')
class TestModelInlineAdmin(admin.TabularInline):
diff --git a/tests/test_app/tests/test_api.py b/tests/test_app/tests/test_api.py
index 500d1def..16ef56d7 100644
--- a/tests/test_app/tests/test_api.py
+++ b/tests/test_app/tests/test_api.py
@@ -32,7 +32,7 @@ def testIsRegisteredFalse(self):
class GetRegisteredModelsTest(TestModelMixin, TestBase):
def testGetRegisteredModels(self):
- self.assertEqual(set(reversion.get_registered_models()), set((TestModel,)))
+ self.assertEqual(set(reversion.get_registered_models()), {TestModel})
class RegisterTest(TestBase):
diff --git a/tests/test_app/tests/test_models.py b/tests/test_app/tests/test_models.py
index 601f2e5f..2d2ca0a9 100644
--- a/tests/test_app/tests/test_models.py
+++ b/tests/test_app/tests/test_models.py
@@ -4,6 +4,7 @@
TestModel, TestModelRelated, TestModelParent, TestModelInline,
TestModelNestedInline,
TestModelInlineByNaturalKey, TestModelWithNaturalKey,
+ TestModelWithUniqueConstraint,
)
from test_app.tests.base import TestBase, TestModelMixin, TestModelParentMixin
import json
@@ -317,7 +318,7 @@ def testM2MSave(self):
obj.related.add(v1)
obj.related.add(v2)
version = Version.objects.get_for_object(obj).first()
- self.assertEqual(set(version.field_dict["related"]), set((v1.pk, v2.pk,)))
+ self.assertEqual(set(version.field_dict["related"]), {v1.pk, v2.pk})
class RevertTest(TestModelMixin, TestBase):
@@ -443,3 +444,17 @@ def testNaturalKeyInline(self):
'test_model_id': 1,
'id': 1,
})
+
+
+class TransactionRollbackTest(TestBase):
+
+ def setUp(self):
+ reversion.register(TestModelWithUniqueConstraint)
+
+ def testTransactionInRollbackState(self):
+ with reversion.create_revision():
+ try:
+ TestModelWithUniqueConstraint.objects.create(name='A')
+ TestModelWithUniqueConstraint.objects.create(name='A')
+ except Exception:
+ pass
diff --git a/tests/test_app/views.py b/tests/test_app/views.py
index d645ad06..90fbca2b 100644
--- a/tests/test_app/views.py
+++ b/tests/test_app/views.py
@@ -1,3 +1,4 @@
+from django.db import transaction
from django.http import HttpResponse
from django.views.generic.base import View
from reversion.views import create_revision, RevisionMixin
@@ -9,8 +10,9 @@ def save_obj_view(request):
def save_obj_error_view(request):
- TestModel.objects.create()
- raise Exception("Boom!")
+ with transaction.atomic():
+ TestModel.objects.create()
+ raise Exception("Boom!")
@create_revision()
@@ -21,7 +23,7 @@ def create_revision_view(request):
class RevisionMixinView(RevisionMixin, View):
def revision_request_creates_revision(self, request):
- silent = request.META.get("HTTP_X_NOREVISION", "false") == "true"
+ silent = request.headers.get('X-Norevision', "false") == "true"
return super().revision_request_creates_revision(request) and not silent
def dispatch(self, request):
diff --git a/tests/test_project/settings.py b/tests/test_project/settings.py
index d9e7e46e..ec05a201 100644
--- a/tests/test_project/settings.py
+++ b/tests/test_project/settings.py
@@ -81,25 +81,28 @@
"ENGINE": "django.db.backends.sqlite3",
"NAME": os.path.join(BASE_DIR, "db.sqlite3"),
},
-
"postgres": {
+ "HOST": os.environ.get("DJANGO_DATABASE_HOST_POSTGRES", ""),
"ENGINE": "django.db.backends.sqlite3",
- "NAME": os.path.join(BASE_DIR, "test_project_postgres.sqlite3"),
# "ENGINE": "django.db.backends.postgresql_psycopg2",
+ "NAME": os.path.join(BASE_DIR, "test_project_postgres.sqlite3"),
# "NAME": os.environ.get("DJANGO_DATABASE_NAME_POSTGRES", "test_project"),
"USER": os.environ.get("DJANGO_DATABASE_USER_POSTGRES", getpass.getuser()),
"PASSWORD": os.environ.get("DJANGO_DATABASE_PASSWORD_POSTGRES", ""),
},
"mysql": {
- "ENGINE": "django.db.backends.sqlite3",
- "NAME": os.path.join(BASE_DIR, "test_project_mysql.sqlite3"),
# "ENGINE": "django.db.backends.mysql",
# "NAME": os.environ.get("DJANGO_DATABASE_NAME_MYSQL", "test_project"),
+ "ENGINE": "django.db.backends.sqlite3",
+ "NAME": os.path.join(BASE_DIR, "test_project_mysql.sqlite3"),
+
+ "HOST": os.environ.get("DJANGO_DATABASE_HOST_MYSQL", ""),
"USER": os.environ.get("DJANGO_DATABASE_USER_MYSQL", getpass.getuser()),
"PASSWORD": os.environ.get("DJANGO_DATABASE_PASSWORD_MYSQL", ""),
},
}
+DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
# Password validation
# https://docs.djangoproject.com/en/dev/ref/settings/#auth-password-validators
diff --git a/tests/test_project/urls.py b/tests/test_project/urls.py
index ed210aa9..23f0d3c0 100644
--- a/tests/test_project/urls.py
+++ b/tests/test_project/urls.py
@@ -1,4 +1,4 @@
-from django.conf.urls import include
+from django.urls import include
from django.urls import path
from django.contrib import admin