Compare commits

...

No commits in common. "master" and "1.8" have entirely different histories.
master ... 1.8

162 changed files with 5125 additions and 6358 deletions

View File

@ -1,21 +0,0 @@
[run]
source =
reversion
test_app
test_project
[report]
exclude_lines =
# Have to re-enable the standard pragma
pragma: no cover
# Don't complain if tests don't hit defensive assertion code:
raise AssertionError
raise NotImplementedError
assert False
# Don't complain if tests don't hit model __str__ methods.
def __str__
show_missing = True
skip_covered = True

5
.gitignore vendored
View File

@ -8,8 +8,5 @@
dist
build
MANIFEST
*.egg-info/
src/django_reversion.egg-info/
docs/_build
.coverage
*.sqlite3
.tox

View File

@ -1,39 +1,24 @@
sudo: false
language: python
python:
- 3.6
addons:
apt:
packages:
- libmysqlclient-dev
cache:
directories:
- "$HOME/.cache/pip"
- 2.6
- 2.7
- 3.2
- 3.3
- 3.4
env:
global:
- PYTHONWARNINGS=default,ignore::PendingDeprecationWarning,ignore::ResourceWarning
- DJANGO_DATABASE_USER_POSTGRES=postgres
- DJANGO_DATABASE_USER_MYSQL=travis
- DJANGO=django==1.6.11
- DJANGO=django==1.7.7
- DJANGO=django==1.8.1
matrix:
exclude:
- python: 2.6
env: DJANGO=django==1.7.7
- python: 2.6
env: DJANGO=django==1.8.1
fast_finish: true
services:
- postgresql
- mysql
install:
- pyenv shell 2.7 3.5 3.6
- pip install 'tox>=2.3.1'
before_script:
- mysql -e 'create database test_project'
- psql -c 'create database test_project;' -U postgres;
script: tox
deploy:
provider: pypi
user: etianen
password:
secure: XW4/9HiChbPJSJe4d/MRcO+ViPGhW1iQ8kVi814KJh7mCxOAKijpW5hfdc9oSKB6d8iYB3OzZ7naIUU9GMce40bpeTgPDLVBLCSYKRNLuVoJdh+Q6ItGUiFf8kAJz5jgopG80QnCpLA9JvYxKVJ4amfYWWm204eQmIEnRRAd+Jk=
on:
tags: true
distributions: sdist bdist_wheel
repo: etianen/django-reversion
- travis_retry pip install $DJANGO
- pip install -e .
script: python src/tests/runtests.py
notifications:
email: false

View File

@ -1,637 +1,203 @@
.. _changelog:
django-reversion changelog
==========================
2.0.12 - 05/12/2017
-------------------
- Fixed MySQL error in ``get_deleted()``.
2.0.11 - 27/11/2017
-------------------
- Dramatically improved performance of ``get_deleted()`` over large datasets (@alexey-v-paramonov, @etianen).
- Ukranian translation (@illia-v).
- Bugfixes (@achidlow, @claudep, @etianen).
2.0.10 - 18/08/2017
-------------------
- Bugfix: Handling case of `None` user in request (@pawelad).
- Documentation corrections (@danielquinn).
- Bugfix: "invalid literal for int() with base 10: 'None'" for unversioned admin inline relations.
If, after updating, you still experience this issue, run the following in a Django shell:
.. code::
from reversion.models import Version
Version.objects.filter(object_id="None").delete()
**Important:** Ensure that none of your versioned models contain a string primary key where `"None"` is a valid value
before running this snippet!
2.0.9 - 19/06/2017
------------------
- Bugfix: Deleted inline admin instances no longer added to revision.
- Bugfix: M2M relations correctly added to revision (@etianen, @claudep).
- Improved performance of 0003 migration (@mkurek).
- Documentation improvements (@orlra, @guettli, @meilinger).
- Django 1.11 support (@claudep).
- Added ``atomic=True`` parameter to ``create_revision`` (Ernesto Ferro).
2.0.8 - 28/11/2016
------------------
- Setting ``revision.user`` in ``process_response`` for middleware (@etianen).
- Fixing localization of model primary keys in `recover_list.html` (@w4rri0r3k).
- Documentation tweaks (@jaywink).
2.0.7 - 31/10/2016
------------------
- Database migrations now db-aware (@alukach).
- Added "revert" and "recover" context variables to admin templates (@kezabelle).
- Added ``post_revision_commit`` and ``pre_revision_commit`` signals back in (@carlosxl).
- Fixing datetime in admin change message (@arogachev).
- Fixing performance bug in postgres (@st4lk).
- Fixing admin change messages in Django 1.10+ (@claudep).
- Fixing revision middleware behavior in Django 1.10+ (@etianen).
- Documentation tweaks (@jschneier).
- Deprecation fixes (@KhasanovBI, @zsiciarz, @claudep).
- Releasing as a universal wheel (@adamchainz).
2.0.6 - 21/07/2016
------------------
- Fixed ``RevisionMiddleware`` always rolling back transactions in gunicorn (@stebunovd, @etianen).
- Tweaks and minor bugfixes (@SahilMak).
2.0.5 - 29/06/2016
------------------
- Fixed LookupError when running migration 0003 with stale content types (@etianen).
2.0.4 - 20/06/2016
------------------
- Fixed LookupError when running migration 0003 (@etianen).
- Fixed duplicate versions using ``get_deleted()`` (@etianen).
- Fixed unexpected deletion of underflowing revisions when using ``--keep`` switch with ``deleterevisions`` (@etianen).
2.0.3 - 14/06/2016
------------------
- Added support for m2m fields with a custom ``through`` model (@etianen).
2.0.2 - 13/06/2016
------------------
- Fixing migration 0003 in MySQL (@etianen).
2.0.1 - 13/06/2016
------------------
- Improved performance of migration 0003 (@BertrandBordage).
- De-duplicating ``Version`` table before applying migration 0004 (@BertrandBordage, @etianen).
2.0.0 - 11/06/2016
------------------
django-reversion was first released in May 2008, and has been in active development ever since. Over this time it's developed a certain amount of cruft from legacy and unused features, resulting in needless complexity and multiple ways of achieving the same task.
This release substantially cleans and refactors the codebase. Much of the top-level functionality remains unchanged or is very similar. The release notes are divided into subsections to make it easier to find out where you need to update your code.
This release includes a migration for the ``Version`` model that may take some time to complete.
General improvements
^^^^^^^^^^^^^^^^^^^^
* Dramatically improved performance of version lookup for models with a non-integer primary key (@etianen, @mshannon1123).
* Documentation refactor (@etianen).
* Test refactor (@etianen).
* Minor tweaks and bugfixes (@etianen, @bmarika, @ticosax).
Admin
^^^^^
* Fixed issue with empty revisions being created in combination with ``RevisionMiddleware`` (@etianen).
* **Breaking:** Removed ``reversion_format`` property from ``VersionAdmin`` (@etianen).
Use ``VersionAdmin.reversion_register`` instead.
.. code::
class YourVersionAdmin(VersionAdmin):
def reversion_register(self, model, **options):
options["format"] = "yaml"
super(YourVersionAdmin, self).reversion_register(model, **options)
* **Breaking:** Removed ``ignore_duplicate_revisions`` property from ``VersionAdmin`` (@etianen).
Use ``VersionAdmin.reversion_register`` instead.
.. code::
class YourVersionAdmin(VersionAdmin):
def reversion_register(self, model, **options):
options["ignore_duplicate_revisions"] = True
super(YourVersionAdmin, self).reversion_register(model, **options)
Management commands
^^^^^^^^^^^^^^^^^^^
* **Breaking:** Refactored arguments to ``createinitialrevisions`` (@etianen).
All existing functionality should still be supported, but several parameter names have been updated to match Django coding conventions.
Check the command ``--help`` for details.
* **Breaking:** Refactored arguments to ``deleterevisions`` (@etianen).
All existing functionality should still be supported, but several parameter names have been updated to match Django coding conventions, and some duplicate parameters have been removed. The confirmation prompt has been removed entirely, and the command now always runs in the ``--force`` mode from the previous version.
Check the command ``--help`` for details.
Middleware
^^^^^^^^^^
* Added support for using ``RevisionMiddleware`` with new-style Django 1.10 ``MIDDLEWARE`` (@etianen).
* Middleware wraps entire request in ``transaction.atomic()`` to preserve transactional integrity of revision and models (@etianen).
View helpers
^^^^^^^^^^^^
* Added ``reversion.views.create_revision`` view decorator (@etianen).
* Added ``reversion.views.RevisionMixin`` class-based view mixin (@etianen).
Low-level API
^^^^^^^^^^^^^
* Restored many of the django-reversion API methods back to the top-level namespace (@etianen).
* Revision blocks are now automatically wrapped in ``transaction.atomic()`` (@etianen).
* Added ``for_concrete_model`` argument to ``reversion.register()`` (@etianen).
* Added ``Version.objects.get_for_model()`` lookup function (@etianen).
* Added ``reversion.add_to_revision()`` for manually adding model instances to an active revision (@etianen).
* Removed ``Version.object_id_int`` field, in favor of a unified ``Version.object_id`` field for all primary key types (@etianen).
* **Breaking:** ``reversion.get_for_object_reference()`` has been moved to ``Version.objects.get_for_object_reference()`` (@etianen).
* **Breaking:** ``reversion.get_for_object()`` has been moved to ``Version.objects.get_for_object()`` (@etianen).
* **Breaking:** ``reversion.get_deleted()`` has been moved to ``Version.objects.get_deleted()`` (@etianen).
* **Breaking:** ``Version.object_version`` has been renamed to ``Version._object_version`` (@etianen).
* **Breaking:** Refactored multi-db support (@etianen).
django-reversion now supports restoring model instances to their original database automatically. Several parameter names have also be updated to match Django coding conventions.
If you made use of the previous multi-db functionality, check the latest docs for details. Otherwise, everything should *just work*.
* **Breaking:** Removed ``get_ignore_duplicates`` and ``set_ignore_duplicates`` (@etianen).
``ignore_duplicates`` is now set in reversion.register() on a per-model basis.
* **Breaking:** Removed ``get_for_date()`` function (@etianen).
Use ``get_for_object().filter(revision__date_created__lte=date)`` instead.
* **Breaking:** Removed ``get_unique_for_object()`` function (@etianen).
Use ``get_for_object().get_unique()`` instead.
* **Breaking:** Removed ``signal`` and ``eager_signals`` argument from ``reversion.register()`` (@etianen).
To create revisions on signals other than ``post_save`` and ``m2m_changed``, call ``reversion.add_to_revision()`` in a signal handler for the appropriate signal.
.. code:: python
from django.dispatch import receiver
import reversion
from your_app import your_custom_signal
@reciever(your_custom_signal)
def your_custom_signal_handler(instance, **kwargs):
if reversion.is_active():
reversion.add_to_revision(instance)
This approach will work for both eager and non-eager signals.
* **Breaking:** Removed ``adapter_cls`` argument from ``reversion.register()`` (@etianen).
* **Breaking:** Removed ``reversion.save_revision()`` (@etianen).
Use reversion.add_to_revision() instead.
.. code:: python
import reversion
with reversion.create_revision():
reversion.add_to_revision(your_obj)
Signals
^^^^^^^
* **Breaking:** Removed ``pre_revision_commit`` signal (@etianen).
Use the Django standard ``pre_save`` signal for ``Revision`` instead.
* **Breaking:** Removed ``post_revision_commit`` signal (@etianen).
Use the Django standard ``post_save`` signal for ``Revision`` instead.
Helpers
^^^^^^^
* **Breaking:** Removed ``patch_admin`` function (@etianen).
Use ``VersionAdmin`` as a mixin to 3rd party ModelAdmins instead.
.. code::
@admin.register(SomeModel)
class YourModelAdmin(VersionAdmin, SomeModelAdmin):
pass
* **Breaking:** Removed ``generate_diffs`` function (@etianen).
django-reversion no supports an official diff helper. There are much better ways of achieving this now, such as `django-reversion-compare <https://github.com/jedie/django-reversion-compare>`_.
The old implementation is available for reference from the `previous release <https://github.com/etianen/django-reversion/blob/release-1.10.2/src/reversion/helpers.py>`_.
* **Breaking:** Removed ``generate_patch`` function (@etianen).
django-reversion no supports an official diff helper. There are much better ways of achieving this now, such as `django-reversion-compare <https://github.com/jedie/django-reversion-compare>`_.
The old implementation is available for reference from the `previous release <https://github.com/etianen/django-reversion/blob/release-1.10.2/src/reversion/helpers.py>`_.
* **Breaking:** Removed ``generate_patch_html`` function (@etianen).
django-reversion no supports an official diff helper. There are much better ways of achieving this now, such as `django-reversion-compare <https://github.com/jedie/django-reversion-compare>`_.
The old implementation is available for reference from the `previous release <https://github.com/etianen/django-reversion/blob/release-1.10.2/src/reversion/helpers.py>`_.
Models
^^^^^^
* **Breaking:** Ordering of ``-pk`` added to models ``Revision`` and ``Version``. Previous was the default ``pk``.
1.10.2 - 18/04/2016
-------------------
* Fixing deprecation warnings (@claudep).
* Minor tweaks and bug fixes (@fladi, @claudep, @etianen).
1.10.1 - 27/01/2016
-------------------
* Fixing some deprecation warnings (@ticosax).
* Minor tweaks (@claudep, @etianen).
1.10 - 02/12/2015
-----------------
* **Breaking:** Updated the location of ``VersionAdmin``.
Prior to this change, you could access the ``VersionAdmin`` class using the following import:
.. code:: python
# Old-style import for accessing the admin class.
import reversion
# Access admin class from the reversion namespace.
class YourModelAdmin(reversion.VersionAdmin):
pass
In order to support Django 1.9, the admin class has been moved to the following
import:
.. code:: python
# New-style import for accesssing admin class.
from reversion.admin import VersionAdmin
# Use the admin class directly.
class YourModelAdmin(VersionAdmin):
pass
* **Breaking:** Updated the location of low-level API methods.
Prior to this change, you could access the low-level API using the following import:
.. code:: python
# Old-style import for accessing the low-level API.
import reversion
# Use low-level API methods from the reversion namespace.
@reversion.register
class YourModel(models.Model):
pass
In order to support Django 1.9, the low-level API
methods have been moved to the following import:
.. code:: python
# New-style import for accesssing the low-level API.
from reversion import revisions as reversion
# Use low-level API methods from the revisions namespace.
@reversion.register
class YourModel(models.Model):
pass
* **Breaking:** Updated the location of http://django-reversion.readthedocs.org/en/latest/signals.html.
Prior to this change, you could access the reversion signals using the following import:
.. code:: python
# Old-style import for accessing the reversion signals
import reversion
# Use signals from the reversion namespace.
reversion.post_revision_commit.connect(...)
In order to support Django 1.9, the reversion signals have been moved to the following
import:
.. code:: python
# New-style import for accesssing the reversion signals.
from reversion.signals import pre_revision_commit, post_revision_commit
# Use reversion signals directly.
post_revision_commit.connect(...)
* Django 1.9 compatibility (@etianen).
* Added spanish (argentina) translation (@gonzalobustos).
* Minor bugfixes and tweaks (@Blitzstok, @IanLee1521, @lutoma, @siamalekpour, @etianen).
1.9.3 - 07/08/2015
------------------
* Fixing regression with admin redirects following save action (@etianen).
1.9.2 - 07/08/2015
------------------
* Fixing regression with "delete", "save as new" and "save and continue" button being shown in recover and revision admin views (@etianen).
* Fixing regression where VersionAdmin.ignore_duplicate_revisions was ignored (@etianen).
1.9.1 - 04/08/2015
------------------
* Fixing packaging error that rendered the 1.9.0 release unusable. No way to cover up the mistake, so here's a brand new bugfix release! (@etianen).
1.9.0 - 04/08/2015
------------------
* Using database transactions do render consistent views of past revisions in database admin, fixing a lot of lingering minor issues (@etianen).
* Correct handling of readonly fields in admin (@etianen).
* Updates to Czech translation (@cuchac).
* Arabic translation (@RamezIssac).
* Fixing deleterevisions to work with Python2 (@jmurty).
* Fixing edge-cases where an object does not have a PK (@johnfraney).
* Tweaks, code cleanups and documentation fixes (@claudep, @johnfraney, @podloucky-init, Drew Hubl, @JanMalte, @jmurty, @etianen).
1.8.7 - 21/05/2015
------------------
* Fixing deleterevisions command on Python 3 (@davidfsmith).
* Fixing Django 1.6 compatibility (@etianen).
* Removing some Django 1.9 deprecation warnings (@BATCOH, @niknokseyer).
* Minor tweaks (@nikolas, @etianen).
- Fixing deleterevisions command on Python 3 (@davidfsmith).
- Fixing Django 1.6 compatibility (@etianen).
- Removing some Django 1.9 deprecation warnings (@BATCOH, @niknokseyer).
- Minor tweaks (@nikolas, @etianen).
1.8.6 - 13/04/2015
------------------
* Support for MySQL utf8mb4 (@alexhayes).
* Fixing some Django deprecation warnings (Drew Hubl, @khakulov, @adonm).
* Versions passed through by reversion.post_revision_commit now contain a primary key (@joelarson).
- Support for MySQL utf8mb4 (@alexhayes).
- Fixing some Django deprecation warnings (Drew Hubl, @khakulov, @adonm).
- Versions passed through by reversion.post_revision_commit now contain a primary key (@joelarson).
1.8.5 - 31/10/2014
------------------
* Added support for proxy models (@AgDude, @bourivouh).
* Allowing registration of models with django-reversion using custom signals (@ErwinJunge).
* Fixing some Django deprecation warnings (@skipp, @narrowfail).
- Added support for proxy models (@AgDude, @bourivouh).
- Allowing registration of models with django-reversion using custom signals (@ErwinJunge).
- Fixing some Django deprecation warnings (@skipp, @narrowfail).
1.8.4 - 07/09/2014
------------------
* Fixing including legacy south migrations in PyPi package (@GeyseR).
- Fixing including legacy south migrations in PyPi package (@GeyseR).
1.8.3 - 06/09/2014
------------------
* Provisional Django 1.7 support (@etianen).
* Multi-db and multi-manager support to management commands (@marekmalek).
* Added index on reversion.date_created (@rkojedzinszky).
* Minor bugfixes and documentation improvements (@coagulant).
- Provisional Django 1.7 support (@etianen).
- Multi-db and multi-manager support to management commands (@marekmalek).
- Added index on reversion.date_created (@rkojedzinszky).
- Minor bugfixes and documentation improvements (@coagulant).
1.8.2 - 01/08/2014
------------------
* reversion.register() can now be used as a class decorator (@aquavitae).
* Danish translation (@Vandborg).
* Improvements to Travis CI integration (@thedrow).
* Simplified Chinese translation (@QuantumGhost).
* Minor bugfixes and documentation improvements (@marekmalek, @dhoffman34, @mauricioabreu, @mark0978).
- reversion.register() can now be used as a class decorator (@aquavitae).
- Danish translation (@Vandborg).
- Improvements to Travis CI integration (@thedrow).
- Simplified Chinese translation (@QuantumGhost).
- Minor bugfixes and documentation improvements (@marekmalek, @dhoffman34, @mauricioabreu, @mark0978).
1.8.1 - 29/05/2014
------------------
* Slovak translation (@jbub).
* Deleting a user no longer deletes the associated revisions (@daaray).
* Improving handling of inline models in admin integration (@blueyed).
* Improving error messages for proxy model registration (@blueyed).
* Improvements to using migrations with custom user model (@aivins).
* Removing sys.exit() in deleterevisions management command, allowing it to be used internally by Django projects (@tongwang).
* Fixing some backwards-compatible admin deprecation warnings (Thomas Schreiber).
* Fixing tests if RevisionMiddleware is used as a decorator in the parent project (@jmoldow).
* Derived models, such as those generated by deferred querysets, now work.
* Removed deprecated low-level API methods.
- Slovak translation (@jbub).
- Deleting a user no longer deletes the associated revisions (@daaray).
- Improving handling of inline models in admin integration (@blueyed).
- Improving error messages for proxy model registration (@blueyed).
- Improvements to using migrations with custom user model (@aivins).
- Removing sys.exit() in deleterevisions management command, allowing it to be used internally by Django projects (@tongwang).
- Fixing some backwards-compatible admin deprecation warnings (Thomas Schreiber).
- Fixing tests if RevisionMiddleware is used as a decorator in the parent project (@jmoldow).
- Derived models, such as those generated by deferred querysets, now work.
- Removed deprecated low-level API methods.
1.8.0 - 01/11/2013
------------------
* Django 1.6 compatibility (@niwibe & @meshy).
* Removing type flag from Version model.
* Using bulk_create to speed up revision creation.
* Including docs in source distribution (@pquentin & @fladi).
* Spanish translation (@alexander-ae).
* Fixing edge-case bugs in revision middleware (@pricem & @oppianmatt).
- Django 1.6 compatibility (@niwibe & @meshy).
- Removing type flag from Version model.
- Using bulk_create to speed up revision creation.
- Including docs in source distribution (@pquentin & @fladi).
- Spanish translation (@alexander-ae).
- Fixing edge-case bugs in revision middleware (@pricem & @oppianmatt).
1.7.1 - 26/06/2013
------------------
* Bugfixes when using a custom User model.
* Minor bugfixes.
- Bugfixes when using a custom User model.
- Minor bugfixes.
1.7 - 27/02/2013
----------------
* Django 1.5 compatibility.
* Experimantal Python 3.3 compatibility!
- Django 1.5 compatibility.
- Experimantal Python 3.3 compatibility!
1.6.6 - 12/02/2013
------------------
* Removing version checking code. It's more trouble than it's worth.
* Dutch translation improvements.
- Removing version checking code. It's more trouble than it's worth.
- Dutch translation improvements.
1.6.5 - 12/12/2012
------------------
* Support for Django 1.4.3.
- Support for Django 1.4.3.
1.6.4 - 28/10/2012
------------------
* Support for Django 1.4.2.
- Support for Django 1.4.2.
1.6.3 - 05/09/2012
------------------
* Fixing issue with reverting models with unique constraints in the admin.
* Enforcing permissions in admin views.
- Fixing issue with reverting models with unique constraints in the admin.
- Enforcing permissions in admin views.
1.6.2 - 31/07/2012
------------------
* Batch saving option in createinitialrevisions.
* Suppressing warning for Django 1.4.1.
- Batch saving option in createinitialrevisions.
- Suppressing warning for Django 1.4.1.
1.6.1 - 20/06/2012
------------------
* Swedish translation.
* Fixing formating for PyPi readme and license.
* Minor features and bugfixes.
- Swedish translation.
- Fixing formating for PyPi readme and license.
- Minor features and bugfixes.
1.6 - 27/03/2012
----------------
* Django 1.4 compatibility.
- Django 1.4 compatibility.
1.5.2 - 27/03/2012
------------------
* Multi-db support.
* Brazillian Portuguese translation.
* New manage_manually revision mode.
- Multi-db support.
- Brazillian Portuguese translation.
- New manage_manually revision mode.
1.5.1 - 20/10/2011
-------------------
------------------
* Polish translation.
* Minor bug fixes.
- Polish translation.
- Minor bug fixes.
1.5 - 04/09/2011
----------------
* Added in simplified low level API methods, and deprecated old low level API methods.
* Added in support for multiple revision managers running in the same project.
* Added in significant speedups for models with integer primary keys.
* Added in cleanup improvements to patch generation helpers.
* Minor bug fixes.
- Added in simplified low level API methods, and deprecated old low level API methods.
- Added in support for multiple revision managers running in the same project.
- Added in significant speedups for models with integer primary keys.
- Added in cleanup improvements to patch generation helpers.
- Minor bug fixes.
1.4 - 27/04/2011
----------------
* Added in a version flag for add / change / delete annotations.
* Added experimental deleterevisions management command.
* Added a --comment option to createinitialrevisions management command.
* Django 1.3 compatibility.
- Added in a version flag for add / change / delete annotations.
- Added experimental deleterevisions management command.
- Added a --comment option to createinitialrevisions management command.
- Django 1.3 compatibility.
1.3.3 - 05/03/2011
------------------
* Improved resilience of revert() to database integrity errors.
* Added in Czech translation.
* Added ability to only save revisions if there is no change.
* Fixed long-running bug with file fields in inline related admin models.
* Easier debugging for createinitialrevisions command.
* Improved compatibility with Oracle database backend.
* Fixed error in MySQL tests.
* Greatly improved performance of get_deleted() Version manager method.
* Fixed an edge-case UnicodeError.
- Improved resilience of revert() to database integrity errors.
- Added in Czech translation.
- Added ability to only save revisions if there is no change.
- Fixed long-running bug with file fields in inline related admin models.
- Easier debugging for createinitialrevisions command.
- Improved compatibility with Oracle database backend.
- Fixed error in MySQL tests.
- Greatly improved performance of get_deleted() Version manager method.
- Fixed an edge-case UnicodeError.
1.3.2 - 22/10/2010
------------------
* Added Polish translation.
* Added French translation.
* Improved resilience of unit tests.
* Improved scaleability of Version.object.get_deleted() method.
* Improved scaleability of createinitialrevisions command.
* Removed post_syncdb hook.
* Added new createinitialrevisions management command.
* Fixed DoesNotExistError with OneToOneFields and follow.
- Added Polish translation.
- Added French translation.
- Improved resilience of unit tests.
- Improved scaleability of Version.object.get_deleted() method.
- Improved scaleability of createinitialrevisions command.
- Removed post_syncdb hook.
- Added new createinitialrevisions management command.
- Fixed DoesNotExistError with OneToOneFields and follow.
1.3.1 - 31/05/2010
@ -639,7 +205,7 @@ Models
This release is compatible with Django 1.2.1.
* Django 1.2.1 admin compatibility.
- Django 1.2.1 admin compatibility.
1.2.1 - 03/03/2010
@ -647,11 +213,11 @@ This release is compatible with Django 1.2.1.
This release is compatible with Django 1.1.1.
* The django syncdb command will now automatically populate any
version-controlled models with an initial revision. This ensures existing
projects that integrate Reversion won't get caught out.
* Reversion now works with SQLite for tables over 999 rows.
* Added Hebrew translation.
- The django syncdb command will now automatically populate any
version-controlled models with an initial revision. This ensures existing
projects that integrate Reversion won't get caught out.
- Reversion now works with SQLite for tables over 999 rows.
- Added Hebrew translation.
1.2 - 12/10/2009
@ -659,7 +225,7 @@ This release is compatible with Django 1.1.1.
This release is compatible with Django 1.1.
* Django 1.1 admin compatibility.
- Django 1.1 admin compatibility.
1.1.2 - 23/07/2009
@ -667,20 +233,20 @@ This release is compatible with Django 1.1.
This release is compatible with Django 1.0.4.
* Doc tests.
* German translation update.
* Better compatibility with the Django trunk.
* The ability to specify a serialization format used by the ReversionAdmin
class when models are auto-registered.
* Reduction in the number of database queries performed by the Reversion
* admin interface.
- Doc tests.
- German translation update.
- Better compatibility with the Django trunk.
- The ability to specify a serialization format used by the ReversionAdmin
class when models are auto-registered.
- Reduction in the number of database queries performed by the Reversion
- admin interface.
1.1.1 - 25/03/2010
------------------
This release is compatible with Django 1.0.2.
* German and Italian translations.
* Helper functions for generating diffs.
* Improved handling of one-to-many relationships in the admin.
- German and Italian translations.
- Helper functions for generating diffs.
- Improved handling of one-to-many relationships in the admin.

View File

@ -1,9 +1,8 @@
include reversion/templates/reversion/*.html
include reversion/locale/*/LC_MESSAGES/django.*
include src/reversion/templates/reversion/*.html
include src/reversion/locale/*/LC_MESSAGES/django.*
include LICENSE
include README.rst
include CHANGELOG.rst
include MANIFEST.in
recursive-include docs *
recursive-include tests *.py
prune docs/_build
prune docs/_build

View File

@ -2,24 +2,32 @@ django-reversion
================
**django-reversion** is an extension to the Django web framework that provides
version control for model instances.
comprehensive version control facilities.
Features
--------
- Roll back to any point in a model instance's history.
- Recover deleted model instances.
- Simple admin integration.
- Roll back to any point in a model's history - an unlimited undo facility!
- Recover deleted models - never lose data again!
- Admin integration for maximum usability.
- Group related changes into revisions that can be rolled back in a single
transaction.
- Automatically save a new version whenever your model changes using Django's
flexible signalling framework.
- Automate your revision management with easy-to-use middleware.
**django-reversion** can be easily added to your existing Django project with an
absolute minimum of code changes.
Documentation
-------------
Please read the `Getting Started <https://django-reversion.readthedocs.io/>`_
Please read the `Getting Started <http://django-reversion.readthedocs.org/en/latest/>`_
guide for more information.
Issue tracking and source code can be found at the
`main project website <http://github.com/etianen/django-reversion>`_.
Download instructions, bug reporting and links to full documentation can be
found at the `main project website <http://github.com/etianen/django-reversion>`_.
You can keep up to date with the latest announcements by joining the
`django-reversion discussion group <http://groups.google.com/group/django-reversion>`_.
@ -28,8 +36,14 @@ You can keep up to date with the latest announcements by joining the
Upgrading
---------
Please check the `Changelog <https://github.com/etianen/django-reversion/blob/master/CHANGELOG.rst>`_ before upgrading
your installation of django-reversion.
If you're upgrading your existing installation of django-reversion, please check
the `Schema Migrations <http://django-reversion.readthedocs.org/en/latest/migrations.html>`_
documentation for information on any database changes and how to upgrade. If you're using
South to manage database migrations in your project, then upgrading is as easy as running
a few django management commands.
It's always worth checking the `CHANGELOG <https://github.com/etianen/django-reversion/blob/master/CHANGELOG.rst>`_
before upgrading too, just in case you get caught off-guard by a minor upgrade to the library.
Contributing
@ -40,21 +54,29 @@ Bug reports, bug fixes, and new features are always welcome. Please raise issues
pull requests for any new code.
You can run the test suite yourself from within a virtual environment with the following
commands. The test suite requires that both MySQL and PostgreSQL be installed.
commands:
.. code:: bash
::
pip install 'tox>=2.3.1'
tox
$ pip install django
$ pip install -e .
$ python src/tests/runtests.py
The django-reversion project is built on every push with `Travis CI <https://travis-ci.org/etianen/django-reversion>`_.
.. image:: https://travis-ci.org/etianen/django-reversion.svg?branch=master
:target: https://travis-ci.org/etianen/django-reversion
More information
----------------
Contributors
------------
The django-reversion project was developed by Dave Hall. You can get the code
from the `django-reversion project site <http://github.com/etianen/django-reversion>`_.
Dave Hall is a freelance web developer, based in Cambridge, UK. You can usually
find him on the Internet in a number of different places:
The django-reversion project was developed by `Dave Hall <http://www.etianen.com/>`_ and contributed
to by `many other people <https://github.com/etianen/django-reversion/graphs/contributors>`_.
- `Website <http://www.etianen.com/>`_
- `Twitter <http://twitter.com/etianen>`_
- `Google Profile <http://www.google.com/profiles/david.etianen>`_

75
debian/changelog vendored
View File

@ -1,78 +1,3 @@
django-reversion (2.0.12-1) unstable; urgency=low
* New upstream release (Closes: #873910).
* Refresh patches.
* Bump Standards-Version to 4.1.1.
* Run wrap-and-sort -bast to reduce diff size of future changes.
* Enable autopkgtest-pkg-python testsuite.
-- Michael Fladischer <fladi@debian.org> Fri, 08 Dec 2017 19:55:32 +0100
django-reversion (2.0.10-1) unstable; urgency=low
* New upstream release.
* Refresh patches after git-dpm to gbp pq conversion
* Bump Standards-Version to 4.1.0.
-- Michael Fladischer <fladi@debian.org> Thu, 31 Aug 2017 21:21:22 +0200
django-reversion (2.0.9-1) unstable; urgency=low
* New upstream release.
* Bump Standards-Version to 4.0.0.
* Add patch to fix babel build isues.
-- Michael Fladischer <fladi@debian.org> Thu, 22 Jun 2017 10:10:46 +0200
django-reversion (2.0.8-1) unstable; urgency=low
* New upstream release.
* Update d/watch to match new upstream versioning scheme.
* Drop intersphinx patch as upstream no longer uses it in their
documentation.
* Adapt d/rules to new upstream source layout.
* Add python(3)-mock to Build-Depends as they are required to run the
tests.
* Change name of upstream changelog to CHANGELOG.rst.
* Use https:// for copyright-format 1.0 URL.
-- Michael Fladischer <fladi@debian.org> Tue, 29 Nov 2016 23:05:36 +0100
django-reversion (1.10.2-1) unstable; urgency=low
[ Ondřej Nový ]
* Fixed VCS URL (https)
[ Michael Fladischer ]
* New upstream release.
* Switch to python3-sphinx and drop versioned Build-Depends.
* Bump Standards-Version to 3.9.8.
-- Michael Fladischer <fladi@debian.org> Fri, 22 Apr 2016 22:36:16 +0200
django-reversion (1.10.0-1) unstable; urgency=low
[ Michael Fladischer ]
* New upstream release.
* Update Source filed in d/copyright to point to github repository.
* CHANGELOG.rst has been renamed to CHANGELOG.md.
[ SVN-Git Migration ]
* git-dpm config
* Update Vcs fields for git migration
-- Michael Fladischer <fladi@debian.org> Fri, 04 Dec 2015 17:22:55 +0100
django-reversion (1.9.3-1) unstable; urgency=low
* New upstream release.
* Use pybuild to execute tests.
* Add python(3)-setuptools to Build-Depends.
* Clean metadata files to allow two builds in a row.
* Bump debhelper compatibility level to 9.
-- Michael Fladischer <fladi@debian.org> Tue, 11 Aug 2015 09:31:12 +0200
django-reversion (1.8.7-1) unstable; urgency=medium
* New upstream release.

8
debian/clean vendored
View File

@ -1,7 +1 @@
reversion/locale/*/LC_MESSAGES/django.mo
django_reversion.egg-info/PKG-INFO
django_reversion.egg-info/SOURCES.txt
django_reversion.egg-info/dependency_links.txt
django_reversion.egg-info/not-zip-safe
django_reversion.egg-info/requires.txt
django_reversion.egg-info/top_level.txt
src/reversion/locale/*/LC_MESSAGES/django.mo

2
debian/compat vendored
View File

@ -1 +1 @@
9
7

56
debian/control vendored
View File

@ -2,31 +2,25 @@ Source: django-reversion
Section: python
Priority: optional
Maintainer: Debian Python Modules Team <python-modules-team@lists.alioth.debian.org>
Uploaders:
Michael Fladischer <fladi@debian.org>,
Build-Depends:
debhelper (>= 9),
dh-python,
python-all,
python-babel,
python-django (>= 1.7),
python-mock,
python-setuptools
Standards-Version: 4.1.1
X-Python-Version: >= 2.5
Uploaders: Michael Fladischer <fladi@debian.org>
Build-Depends: debhelper (>= 8.1.0~),
dh-python,
python-all,
python-babel,
python-django (>= 1.7),
python-sphinx (>= 1.0.7+dfsg)
Standards-Version: 3.9.6
X-Python-Version: >= 2.7
Homepage: https://github.com/etianen/django-reversion
Vcs-Git: https://anonscm.debian.org/git/python-modules/packages/django-reversion.git
Vcs-Browser: https://anonscm.debian.org/cgit/python-modules/packages/django-reversion.git
Testsuite: autopkgtest-pkg-python
Vcs-Svn: svn://anonscm.debian.org/python-modules/packages/django-reversion/trunk/
Vcs-Browser: http://anonscm.debian.org/viewvc/python-modules/packages/django-reversion/trunk/
Package: python-django-reversion
Architecture: all
Depends:
python-django (>= 1.7),
${misc:Depends},
${python:Depends},
Suggests:
python-django-reversion-doc,
Depends: python-django (>= 1.7),
${misc:Depends},
${python:Depends}
Suggests: python-django-reversion-doc
Description: Provides comprehensive version control facilities for Django
Reversion is an extension to the Django web framework that provides
comprehensive version control facilities.
@ -40,3 +34,23 @@ Description: Provides comprehensive version control facilities for Django
* Automatically save a new version whenever your model changes using Django's
flexible signalling framework.
* Automate your revision management with easy-to-use middleware.
Package: python-django-reversion-doc
Section: doc
Architecture: all
Depends: ${misc:Depends}, ${sphinxdoc:Depends}
Description: Provides comprehensive version control facilities for Django (Documentation)
Reversion is an extension to the Django web framework that provides
comprehensive version control facilities.
.
Features:
* Roll back to any point in a model's history - an unlimited undo facility!
* Recover deleted models - never lose data again!
* Admin integration for maximum usability.
* Group related changes into revisions that can be rolled back in a single
transaction.
* Automatically save a new version whenever your model changes using Django's
flexible signalling framework.
* Automate your revision management with easy-to-use middleware.
.
This package contains the documentation.

4
debian/copyright vendored
View File

@ -1,7 +1,7 @@
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: django-reversion
Upstream-Contact: David Hall <david@etianen.com>
Source: https://github.com/etianen/django-reversion
Source: https://code.google.com/p/django-reversion/downloads/list
Files: *
Copyright: 2009-2013, David Hall <david@etianen.com>

2
debian/gbp.conf vendored
View File

@ -1,2 +0,0 @@
[DEFAULT]
debian-branch=debian/master

View File

@ -1,312 +0,0 @@
From: Michael Fladischer <FladischerMichael@fladi.at>
Date: Tue, 29 Nov 2016 13:34:15 +0100
Subject: Skip postgresql and mysql tests.
---
tests/test_app/tests/test_api.py | 15 +++++++++++++++
tests/test_app/tests/test_commands.py | 18 ++++++++++++++++++
tests/test_app/tests/test_models.py | 24 ++++++++++++++++++++++++
tests/test_project/settings.py | 27 +++++++++++++++++++++------
4 files changed, 78 insertions(+), 6 deletions(-)
diff --git a/tests/test_app/tests/test_api.py b/tests/test_app/tests/test_api.py
index e12b102..f2a54ee 100644
--- a/tests/test_app/tests/test_api.py
+++ b/tests/test_app/tests/test_api.py
@@ -1,3 +1,4 @@
+import unittest
from datetime import timedelta
from django.contrib.auth.models import User
from django.db import models
@@ -12,6 +13,16 @@ try:
except ImportError:
from mock import MagicMock
+try:
+ import psycopg2
+except ImportError:
+ psycopg2 = None
+
+try:
+ import MySQLdb
+except ImportError:
+ MySQLdb = None
+
class SaveTest(TestModelMixin, TestBase):
@@ -159,6 +170,8 @@ class CreateRevisionManageManuallyTest(TestModelMixin, TestBase):
class CreateRevisionDbTest(TestModelMixin, TestBase):
+ @unittest.skipIf(not psycopg2, "psycopg2 not installed")
+ @unittest.skipIf(not MySQLdb, "MySQLdb not installed")
def testCreateRevisionMultiDb(self):
with reversion.create_revision(using="mysql"), reversion.create_revision(using="postgres"):
obj = TestModel.objects.create()
@@ -319,6 +332,8 @@ class AddMetaTest(TestModelMixin, TestBase):
with self.assertRaises(reversion.RevisionManagementError):
reversion.add_meta(TestMeta, name="meta v1")
+ @unittest.skipIf(not psycopg2, "psycopg2 not installed")
+ @unittest.skipIf(not MySQLdb, "MySQLdb not installed")
def testAddMetaMultDb(self):
with reversion.create_revision(using="mysql"), reversion.create_revision(using="postgres"):
obj = TestModel.objects.create()
diff --git a/tests/test_app/tests/test_commands.py b/tests/test_app/tests/test_commands.py
index bb950ad..0cc406f 100644
--- a/tests/test_app/tests/test_commands.py
+++ b/tests/test_app/tests/test_commands.py
@@ -1,3 +1,4 @@
+import unittest
from datetime import timedelta
from django.core.management import CommandError
from django.utils import timezone
@@ -5,6 +6,16 @@ import reversion
from test_app.models import TestModel
from test_app.tests.base import TestBase, TestModelMixin
+try:
+ import psycopg2
+except ImportError:
+ psycopg2 = None
+
+try:
+ import MySQLdb
+except ImportError:
+ MySQLdb = None
+
class CreateInitialRevisionsTest(TestModelMixin, TestBase):
@@ -52,12 +63,14 @@ class CreateInitialRevisionsAppLabelTest(TestModelMixin, TestBase):
class CreateInitialRevisionsDbTest(TestModelMixin, TestBase):
+ @unittest.skipIf(not psycopg2, "psycopg2 not installed")
def testCreateInitialRevisionsDb(self):
obj = TestModel.objects.create()
self.callCommand("createinitialrevisions", using="postgres")
self.assertNoRevision()
self.assertSingleRevision((obj,), comment="Initial version.", using="postgres")
+ @unittest.skipIf(not MySQLdb, "MySQLdb not installed")
def testCreateInitialRevisionsDbMySql(self):
obj = TestModel.objects.create()
self.callCommand("createinitialrevisions", using="mysql")
@@ -67,6 +80,7 @@ class CreateInitialRevisionsDbTest(TestModelMixin, TestBase):
class CreateInitialRevisionsModelDbTest(TestModelMixin, TestBase):
+ @unittest.skipIf(not psycopg2, "psycopg2 not installed")
def testCreateInitialRevisionsModelDb(self):
obj = TestModel.objects.db_manager("postgres").create()
self.callCommand("createinitialrevisions", model_db="postgres")
@@ -125,18 +139,21 @@ class DeleteRevisionsAppLabelTest(TestModelMixin, TestBase):
class DeleteRevisionsDbTest(TestModelMixin, TestBase):
+ @unittest.skipIf(not psycopg2, "psycopg2 not installed")
def testDeleteRevisionsDb(self):
with reversion.create_revision(using="postgres"):
TestModel.objects.create()
self.callCommand("deleterevisions", using="postgres")
self.assertNoRevision(using="postgres")
+ @unittest.skipIf(not MySQLdb, "MySQLdb not installed")
def testDeleteRevisionsDbMySql(self):
with reversion.create_revision(using="mysql"):
TestModel.objects.create()
self.callCommand("deleterevisions", using="mysql")
self.assertNoRevision(using="mysql")
+ @unittest.skipIf(not psycopg2, "psycopg2 not installed")
def testDeleteRevisionsDbNoMatch(self):
with reversion.create_revision():
obj = TestModel.objects.create()
@@ -146,6 +163,7 @@ class DeleteRevisionsDbTest(TestModelMixin, TestBase):
class DeleteRevisionsModelDbTest(TestModelMixin, TestBase):
+ @unittest.skipIf(not psycopg2, "psycopg2 not installed")
def testDeleteRevisionsModelDb(self):
with reversion.create_revision():
TestModel.objects.db_manager("postgres").create()
diff --git a/tests/test_app/tests/test_models.py b/tests/test_app/tests/test_models.py
index 80f893d..04ca1aa 100644
--- a/tests/test_app/tests/test_models.py
+++ b/tests/test_app/tests/test_models.py
@@ -1,9 +1,20 @@
+import unittest
from django.utils.encoding import force_text
import reversion
from reversion.models import Version
from test_app.models import TestModel, TestModelRelated, TestModelParent
from test_app.tests.base import TestBase, TestModelMixin, TestModelParentMixin
+try:
+ import psycopg2
+except ImportError:
+ psycopg2 = None
+
+try:
+ import MySQLdb
+except ImportError:
+ MySQLdb = None
+
class GetForModelTest(TestModelMixin, TestBase):
@@ -15,11 +26,13 @@ class GetForModelTest(TestModelMixin, TestBase):
class GetForModelDbTest(TestModelMixin, TestBase):
+ @unittest.skipIf(not psycopg2, "psycopg2 not installed")
def testGetForModelDb(self):
with reversion.create_revision(using="postgres"):
obj = TestModel.objects.create()
self.assertEqual(Version.objects.using("postgres").get_for_model(obj.__class__).count(), 1)
+ @unittest.skipIf(not MySQLdb, "MySQLdb not installed")
def testGetForModelDbMySql(self):
with reversion.create_revision(using="mysql"):
obj = TestModel.objects.create()
@@ -57,12 +70,14 @@ class GetForObjectTest(TestModelMixin, TestBase):
class GetForObjectDbTest(TestModelMixin, TestBase):
+ @unittest.skipIf(not psycopg2, "psycopg2 not installed")
def testGetForObjectDb(self):
with reversion.create_revision(using="postgres"):
obj = TestModel.objects.create()
self.assertEqual(Version.objects.get_for_object(obj).count(), 0)
self.assertEqual(Version.objects.using("postgres").get_for_object(obj).count(), 1)
+ @unittest.skipIf(not MySQLdb, "MySQLdb not installed")
def testGetForObjectDbMySql(self):
with reversion.create_revision(using="mysql"):
obj = TestModel.objects.create()
@@ -72,6 +87,7 @@ class GetForObjectDbTest(TestModelMixin, TestBase):
class GetForObjectModelDbTest(TestModelMixin, TestBase):
+ @unittest.skipIf(not psycopg2, "psycopg2 not installed")
def testGetForObjectModelDb(self):
with reversion.create_revision():
obj = TestModel.objects.db_manager("postgres").create()
@@ -128,6 +144,7 @@ class GetForObjectReferenceTest(TestModelMixin, TestBase):
class GetForObjectReferenceDbTest(TestModelMixin, TestBase):
+ @unittest.skipIf(not psycopg2, "psycopg2 not installed")
def testGetForObjectReferenceModelDb(self):
with reversion.create_revision(using="postgres"):
obj = TestModel.objects.create()
@@ -137,12 +154,14 @@ class GetForObjectReferenceDbTest(TestModelMixin, TestBase):
class GetForObjectReferenceModelDbTest(TestModelMixin, TestBase):
+ @unittest.skipIf(not psycopg2, "psycopg2 not installed")
def testGetForObjectReferenceModelDb(self):
with reversion.create_revision():
obj = TestModel.objects.db_manager("postgres").create()
self.assertEqual(Version.objects.get_for_object_reference(TestModel, obj.pk).count(), 0)
self.assertEqual(Version.objects.get_for_object_reference(TestModel, obj.pk, model_db="postgres").count(), 1)
+ @unittest.skipIf(not MySQLdb, "MySQLdb not installed")
def testGetForObjectReferenceModelDbMySql(self):
with reversion.create_revision():
obj = TestModel.objects.db_manager("mysql").create()
@@ -177,6 +196,7 @@ class GetDeletedTest(TestModelMixin, TestBase):
self.assertEqual(Version.objects.get_deleted(TestModel)[0].object_id, force_text(pk_2))
self.assertEqual(Version.objects.get_deleted(TestModel)[1].object_id, force_text(pk_1))
+ @unittest.skipIf(not psycopg2, "psycopg2 not installed")
def testGetDeletedPostgres(self):
with reversion.create_revision(using="postgres"):
obj = TestModel.objects.using("postgres").create()
@@ -185,6 +205,7 @@ class GetDeletedTest(TestModelMixin, TestBase):
obj.delete()
self.assertEqual(Version.objects.using("postgres").get_deleted(TestModel, model_db="postgres").count(), 1)
+ @unittest.skipIf(not MySQLdb, "MySQLdb not installed")
def testGetDeletedMySQL(self):
with reversion.create_revision(using="mysql"):
obj = TestModel.objects.using("mysql").create()
@@ -196,6 +217,7 @@ class GetDeletedTest(TestModelMixin, TestBase):
class GetDeletedDbTest(TestModelMixin, TestBase):
+ @unittest.skipIf(not psycopg2, "psycopg2 not installed")
def testGetDeletedDb(self):
with reversion.create_revision(using="postgres"):
obj = TestModel.objects.create()
@@ -203,6 +225,7 @@ class GetDeletedDbTest(TestModelMixin, TestBase):
self.assertEqual(Version.objects.get_deleted(TestModel).count(), 0)
self.assertEqual(Version.objects.using("postgres").get_deleted(TestModel).count(), 1)
+ @unittest.skipIf(not MySQLdb, "MySQLdb not installed")
def testGetDeletedDbMySql(self):
with reversion.create_revision(using="mysql"):
obj = TestModel.objects.create()
@@ -213,6 +236,7 @@ class GetDeletedDbTest(TestModelMixin, TestBase):
class GetDeletedModelDbTest(TestModelMixin, TestBase):
+ @unittest.skipIf(not psycopg2, "psycopg2 not installed")
def testGetDeletedModelDb(self):
with reversion.create_revision():
obj = TestModel.objects.db_manager("postgres").create()
diff --git a/tests/test_project/settings.py b/tests/test_project/settings.py
index 2df52e2..79191fb 100644
--- a/tests/test_project/settings.py
+++ b/tests/test_project/settings.py
@@ -13,6 +13,17 @@ https://docs.djangoproject.com/en/dev/ref/settings/
import os
import getpass
+try:
+ import psycopg2
+except ImportError:
+ psycopg2 = None
+
+try:
+ import MySQLdb
+except ImportError:
+ MySQLdb = None
+
+
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
@@ -80,20 +91,24 @@ DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": os.path.join(BASE_DIR, "db.sqlite3"),
- },
- "postgres": {
+ }
+}
+
+if psycopg2:
+ DATABASES["postgres"] = {
"ENGINE": "django.db.backends.postgresql_psycopg2",
"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": {
+ }
+
+if MySQLdb:
+ DATABASES["mysql"] = {
"ENGINE": "django.db.backends.mysql",
"NAME": os.environ.get("DJANGO_DATABASE_NAME_MYSQL", "test_project"),
"USER": os.environ.get("DJANGO_DATABASE_USER_MYSQL", getpass.getuser()),
"PASSWORD": os.environ.get("DJANGO_DATABASE_PASSWORD_MYSQL", ""),
- },
-}
+ }
# Password validation

View File

@ -1,184 +0,0 @@
From: Michael Fladischer <FladischerMichael@fladi.at>
Date: Thu, 22 Jun 2017 10:06:02 +0200
Subject: Set missing Language property on catalogs.
This prevents babel from failing to compile during build.
---
reversion/locale/ar/LC_MESSAGES/django.po | 2 +-
reversion/locale/cs/LC_MESSAGES/django.po | 1 +
reversion/locale/da/LC_MESSAGES/django.po | 2 +-
reversion/locale/de/LC_MESSAGES/django.po | 1 +
reversion/locale/he/LC_MESSAGES/django.po | 1 +
reversion/locale/it/LC_MESSAGES/django.po | 1 +
reversion/locale/nb/LC_MESSAGES/django.po | 2 +-
reversion/locale/pt_BR/LC_MESSAGES/django.po | 1 +
reversion/locale/ru/LC_MESSAGES/django.po | 1 +
reversion/locale/sk/LC_MESSAGES/django.po | 2 +-
reversion/locale/sv/LC_MESSAGES/django.po | 2 +-
reversion/locale/zh_CN/LC_MESSAGES/django.po | 2 +-
reversion/locale/zh_Hans/LC_MESSAGES/django.po | 2 +-
13 files changed, 13 insertions(+), 7 deletions(-)
diff --git a/reversion/locale/ar/LC_MESSAGES/django.po b/reversion/locale/ar/LC_MESSAGES/django.po
index 07ebf72..7213596 100644
--- a/reversion/locale/ar/LC_MESSAGES/django.po
+++ b/reversion/locale/ar/LC_MESSAGES/django.po
@@ -11,7 +11,7 @@ msgstr ""
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
-"Language: \n"
+"Language: ar\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
diff --git a/reversion/locale/cs/LC_MESSAGES/django.po b/reversion/locale/cs/LC_MESSAGES/django.po
index 94bc354..95497d5 100644
--- a/reversion/locale/cs/LC_MESSAGES/django.po
+++ b/reversion/locale/cs/LC_MESSAGES/django.po
@@ -11,6 +11,7 @@ msgstr ""
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
+"Language: cs\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
diff --git a/reversion/locale/da/LC_MESSAGES/django.po b/reversion/locale/da/LC_MESSAGES/django.po
index 074b908..bcae256 100644
--- a/reversion/locale/da/LC_MESSAGES/django.po
+++ b/reversion/locale/da/LC_MESSAGES/django.po
@@ -12,7 +12,7 @@ msgstr ""
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
-"Language: \n"
+"Language: da\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
diff --git a/reversion/locale/de/LC_MESSAGES/django.po b/reversion/locale/de/LC_MESSAGES/django.po
index dee2e8f..adaed4e 100644
--- a/reversion/locale/de/LC_MESSAGES/django.po
+++ b/reversion/locale/de/LC_MESSAGES/django.po
@@ -11,6 +11,7 @@ msgstr ""
"PO-Revision-Date: 2009-02-03 08:41+0100\n"
"Last-Translator: Jannis Leidel <jannis@leidel.info>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
+"Language: de\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
diff --git a/reversion/locale/he/LC_MESSAGES/django.po b/reversion/locale/he/LC_MESSAGES/django.po
index 3624f02..a346736 100644
--- a/reversion/locale/he/LC_MESSAGES/django.po
+++ b/reversion/locale/he/LC_MESSAGES/django.po
@@ -11,6 +11,7 @@ msgstr ""
"PO-Revision-Date: 2009-12-10 10:45+0200\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
+"Language: he\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
diff --git a/reversion/locale/it/LC_MESSAGES/django.po b/reversion/locale/it/LC_MESSAGES/django.po
index 13a1526..545ee5f 100644
--- a/reversion/locale/it/LC_MESSAGES/django.po
+++ b/reversion/locale/it/LC_MESSAGES/django.po
@@ -11,6 +11,7 @@ msgstr ""
"PO-Revision-Date: 2009-08-29 13:44+0100\n"
"Last-Translator: Marco Beri <marcoberi@gmail.com>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
+"Language: it\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
diff --git a/reversion/locale/nb/LC_MESSAGES/django.po b/reversion/locale/nb/LC_MESSAGES/django.po
index 5de5ded..16a6aae 100644
--- a/reversion/locale/nb/LC_MESSAGES/django.po
+++ b/reversion/locale/nb/LC_MESSAGES/django.po
@@ -10,7 +10,7 @@ msgstr ""
"PO-Revision-Date: 2011-10-17 10:17+0100\n"
"Last-Translator: Sindre Sorhus <sindresorhus@gmail.com>\n"
"Language-Team: \n"
-"Language: \n"
+"Language: nb\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
diff --git a/reversion/locale/pt_BR/LC_MESSAGES/django.po b/reversion/locale/pt_BR/LC_MESSAGES/django.po
index 76c4679..f5319d2 100644
--- a/reversion/locale/pt_BR/LC_MESSAGES/django.po
+++ b/reversion/locale/pt_BR/LC_MESSAGES/django.po
@@ -11,6 +11,7 @@ msgstr ""
"PO-Revision-Date: 2009-08-29 13:44+0100\n"
"Last-Translator: Partec <beto@tangerinalab.com>\n"
"Language-Team: Tangerina Lab <beto@tangerinalab.com>\n"
+"Language: pt_BR\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
diff --git a/reversion/locale/ru/LC_MESSAGES/django.po b/reversion/locale/ru/LC_MESSAGES/django.po
index 68f15ca..8757d28 100644
--- a/reversion/locale/ru/LC_MESSAGES/django.po
+++ b/reversion/locale/ru/LC_MESSAGES/django.po
@@ -11,6 +11,7 @@ msgstr ""
"PO-Revision-Date: 2009-10-14 22:21+0300\n"
"Last-Translator: Alexander Yakovlev <ayakovlev@rambler.ru>\n"
"Language-Team: Russian <ru@li.org>\n"
+"Language: ru\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
diff --git a/reversion/locale/sk/LC_MESSAGES/django.po b/reversion/locale/sk/LC_MESSAGES/django.po
index f6750fb..a1b7d28 100644
--- a/reversion/locale/sk/LC_MESSAGES/django.po
+++ b/reversion/locale/sk/LC_MESSAGES/django.po
@@ -12,7 +12,7 @@ msgstr ""
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Juraj Bubniak <translations@jbub.eu>\n"
"Language-Team: Slovak <LL@li.org>\n"
-"Language: \n"
+"Language: sk\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
diff --git a/reversion/locale/sv/LC_MESSAGES/django.po b/reversion/locale/sv/LC_MESSAGES/django.po
index 9e696fe..44d7955 100644
--- a/reversion/locale/sv/LC_MESSAGES/django.po
+++ b/reversion/locale/sv/LC_MESSAGES/django.po
@@ -12,7 +12,7 @@ msgstr ""
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
-"Language: \n"
+"Language: sv\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
diff --git a/reversion/locale/zh_CN/LC_MESSAGES/django.po b/reversion/locale/zh_CN/LC_MESSAGES/django.po
index 75e7231..567e0b8 100644
--- a/reversion/locale/zh_CN/LC_MESSAGES/django.po
+++ b/reversion/locale/zh_CN/LC_MESSAGES/django.po
@@ -12,7 +12,7 @@ msgstr ""
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
-"Language: \n"
+"Language: zh_CN\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
diff --git a/reversion/locale/zh_Hans/LC_MESSAGES/django.po b/reversion/locale/zh_Hans/LC_MESSAGES/django.po
index 75e7231..284f980 100644
--- a/reversion/locale/zh_Hans/LC_MESSAGES/django.po
+++ b/reversion/locale/zh_Hans/LC_MESSAGES/django.po
@@ -12,7 +12,7 @@ msgstr ""
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
-"Language: \n"
+"Language: zh_Hans\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"

29
debian/patches/intersphinx.patch vendored Normal file
View File

@ -0,0 +1,29 @@
Description: use local objects.inv where possible
Upstream uses intersphinx mappings that fetch the objects.inv for Python, HTTP
from a remote host. Using the local objects.inv from Python enables the package
to build without network connection.
Author: Michael Fladischer <fladi@debian.org>
Forwarded: not-needed
Last-Update: 2015-05-19
Index: django-reversion/docs/conf.py
===================================================================
--- django-reversion.orig/docs/conf.py 2015-04-13 10:28:25.000000000 +0200
+++ django-reversion/docs/conf.py 2015-05-19 10:23:17.477027780 +0200
@@ -244,4 +244,15 @@
# Example configuration for intersphinx: refer to the Python standard library.
-intersphinx_mapping = {'http://docs.python.org/': None}
+def check_object_path(key, url, path):
+ if os.path.isfile(path):
+ return {key: (url, path)}
+ return {}
+
+intersphinx_mapping = {}
+intersphinx_mapping.update(check_object_path('python',
+ 'http://docs.python.org/',
+ '/usr/share/doc/python'
+ + '.'.join([str(x) for x in sys.version_info[0:2]])
+ + '/html/objects.inv'))
+

View File

@ -1,2 +1 @@
#0001-Skip-postgresql-and-mysql-tests.patch
#0002-Set-missing-Language-property-on-catalogs.patch
#intersphinx.patch

10
debian/rules vendored
View File

@ -6,17 +6,15 @@
export PYBUILD_NAME=django-reversion
%:
dh $@ --with python2 --buildsystem=pybuild
override_dh_auto_test:
true
dh $@ --with python2,sphinxdoc --buildsystem=pybuild
override_dh_auto_build:
set -e; \
for loc in reversion/locale/*; do \
python setup.py compile_catalog --directory reversion/locale/ --locale $$(basename $$loc) --domain django; \
for loc in src/reversion/locale/*; do \
python setup.py compile_catalog --directory src/reversion/locale/ --locale $$(basename $$loc) --domain django; \
done
dh_auto_build
PYTHONPATH=. sphinx-build -b html -d docs/.build/.doctrees -N docs docs/.build/html
override_dh_installchangelogs:
dh_installchangelogs CHANGELOG.rst

2
debian/watch vendored
View File

@ -1,3 +1,3 @@
version=3
https://github.com/etianen/django-reversion/tags \
/etianen/django-reversion/archive/(?:release-|v)([\d\.]+)\.tar\.gz
/etianen/django-reversion/archive/release-([\d\.]+)\.tar\.gz

153
docs/Makefile Normal file
View File

@ -0,0 +1,153 @@
# Makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
PAPER =
BUILDDIR = _build
# Internal variables.
PAPEROPT_a4 = -D latex_paper_size=a4
PAPEROPT_letter = -D latex_paper_size=letter
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
# the i18n builder cannot share the environment and doctrees with the others
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
help:
@echo "Please use \`make <target>' where <target> is one of"
@echo " html to make standalone HTML files"
@echo " dirhtml to make HTML files named index.html in directories"
@echo " singlehtml to make a single large HTML file"
@echo " pickle to make pickle files"
@echo " json to make JSON files"
@echo " htmlhelp to make HTML files and a HTML help project"
@echo " qthelp to make HTML files and a qthelp project"
@echo " devhelp to make HTML files and a Devhelp project"
@echo " epub to make an epub"
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
@echo " latexpdf to make LaTeX files and run them through pdflatex"
@echo " text to make text files"
@echo " man to make manual pages"
@echo " texinfo to make Texinfo files"
@echo " info to make Texinfo files and run them through makeinfo"
@echo " gettext to make PO message catalogs"
@echo " changes to make an overview of all changed/added/deprecated items"
@echo " linkcheck to check all external links for integrity"
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
clean:
-rm -rf $(BUILDDIR)/*
html:
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
dirhtml:
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
singlehtml:
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
@echo
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
pickle:
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
@echo
@echo "Build finished; now you can process the pickle files."
json:
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
@echo
@echo "Build finished; now you can process the JSON files."
htmlhelp:
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
@echo
@echo "Build finished; now you can run HTML Help Workshop with the" \
".hhp project file in $(BUILDDIR)/htmlhelp."
qthelp:
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
@echo
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/django-reversion.qhcp"
@echo "To view the help file:"
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/django-reversion.qhc"
devhelp:
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
@echo
@echo "Build finished."
@echo "To view the help file:"
@echo "# mkdir -p $$HOME/.local/share/devhelp/django-reversion"
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/django-reversion"
@echo "# devhelp"
epub:
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
@echo
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
latex:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
@echo "Run \`make' in that directory to run these through (pdf)latex" \
"(use \`make latexpdf' here to do that automatically)."
latexpdf:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through pdflatex..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
text:
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
@echo
@echo "Build finished. The text files are in $(BUILDDIR)/text."
man:
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
@echo
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
texinfo:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
@echo "Run \`make' in that directory to run these through makeinfo" \
"(use \`make info' here to do that automatically)."
info:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo "Running Texinfo files through makeinfo..."
make -C $(BUILDDIR)/texinfo info
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
gettext:
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
@echo
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
changes:
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
@echo
@echo "The overview file is in $(BUILDDIR)/changes."
linkcheck:
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
@echo
@echo "Link check complete; look for any errors in the above output " \
"or in $(BUILDDIR)/linkcheck/output.txt."
doctest:
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
@echo "Testing of doctests in the sources finished, look at the " \
"results in $(BUILDDIR)/doctest/output.txt."

View File

@ -1,13 +0,0 @@
Register your models with a subclass of :ref:`VersionAdmin`.
.. code:: python
from django.contrib import admin
from reversion.admin import VersionAdmin
@admin.register(YourModel)
class YourModelAdmin(VersionAdmin):
pass
.. include:: /_include/post-register.rst

View File

@ -1,8 +0,0 @@
``manage_manually``
.. include:: /_include/create-revision-manage-manually.rst
``using``
.. include:: /_include/create-revision-using.rst
``atomic``
.. include:: /_include/create-revision-atomic.rst

View File

@ -1 +0,0 @@
If ``True``, the revision block will be wrapped in a ``transaction.atomic()``.

View File

@ -1 +0,0 @@
If ``True``, versions will not be saved when a model's ``save()`` method is called. This allows version control to be switched off for a given revision block.

View File

@ -1 +0,0 @@
The database to save the revision data. The revision block will be wrapped in a transaction using this database. If ``None``, the default database for :ref:`Revision` will be used.

View File

@ -1,2 +0,0 @@
``model_db``
The database where the model is saved. Defaults to the default database for the model.

View File

@ -1,2 +0,0 @@
.. Hint::
Whenever you register a model with django-reversion, run :ref:`createinitialrevisions`.

View File

@ -1,8 +0,0 @@
``sender``
The ``reversion.create_revision`` object.
``revision``
The :ref:`Revision` model.
``versions``
The :ref:`Version` models in the revision.

View File

@ -1 +0,0 @@
Throws :ref:`RegistrationError` if the model has not been registered with django-reversion.

View File

@ -1 +0,0 @@
Throws :ref:`RevertError` if the model could not be deserialized or reverted, e.g. the serialized data is not compatible with the current database schema.

View File

@ -1 +0,0 @@
Throws :ref:`RevisionManagementError` if there is no active revision block.

View File

@ -3,107 +3,63 @@
Admin integration
=================
django-reversion can be used to add rollback and recovery to your admin site.
django-reversion can be used to add a powerful rollback and recovery facility to your admin site. To enable this, simply register your models with a subclass of ``reversion.VersionAdmin``.
.. 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!
::
import reversion
Overview
--------
class YourModelAdmin(reversion.VersionAdmin):
Registering models
^^^^^^^^^^^^^^^^^^
pass
admin.site.register(YourModel, YourModelAdmin)
.. include:: /_include/admin.rst
You can also use ``reversion.VersionAdmin`` as a mixin with another specialized admin class.
.. Note::
::
If you've registered your models using :ref:`reversion.register() <register>`, the admin class will use the configuration you specify there. Otherwise, the admin class will auto-register your model, following all inline model relations and parent superclasses. Customize the admin registration by overriding :ref:`VersionAdmin.register() <VersionAdmin_register>`.
Integration with 3rd party apps
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
You can use :ref:`VersionAdmin` as a mixin with a 3rd party admin class.
.. code:: python
@admin.register(SomeModel)
class YourModelAdmin(VersionAdmin, SomeModelAdmin):
class YourModelAdmin(reversion.VersionAdmin, YourBaseModelAdmin):
pass
If the 3rd party model is already registered with the Django admin, you may have to unregister it first.
If you're using an existing third party app, then you can add patch django-reversion into its admin class by using the ``reversion.helpers.patch_admin()`` method. For example, to add version control to the built-in User model:
.. code:: python
::
admin.site.unregister(SomeModel)
from reversion.helpers import patch_admin
@admin.register(SomeModel)
class YourModelAdmin(VersionAdmin, SomeModelAdmin):
pass
patch_admin(User)
.. _VersionAdmin:
Admin customizations
--------------------
reversion.admin.VersionAdmin
----------------------------
It's possible to customize the way django-reversion integrates with your admin site by specifying options on the subclass of ``reversion.VersionAdmin`` as follows:
A subclass of ``django.contrib.ModelAdmin`` providing rollback and recovery.
::
class YourModelAdmin(reversion.VersionAdmin):
option_name = option_value
The available admin options are:
* **history_latest_first:** Whether to display the available versions in reverse chronological order on the revert and recover views (default ``False``)
* **ignore_duplicate_revisions:** Whether to ignore duplicate revisions when storing version data (default ``False``)
* **recover_form_template:** The name of the template to use when rendering the recover form (default ``'reversion/recover_form.html'``)
* **reversion_format:** The name of a serialization format to use when storing version data (default ``'json'``)
* **revision_form_template:** The name of the template to use when rendering the revert form (default ``'reversion/revision_form.html'``)
* **recover_list_template:** The name of the template to use when rendering the recover list view (default ``'reversion/recover_list.html'``)
``revision_form_template = None``
Customizing admin templates
---------------------------
A custom template to render the revision form.
In addition to specifying custom templates using the options above, you can also place specially named templates on your template root to override the default templates on a per-model or per-app basis.
Alternatively, create specially named templates to override the default templates on a per-model or per-app basis.
For example, to override the recover_list template for the user model, the auth app, or all registered models, you could create a template with one of the following names:
* ``'reversion/app_label/model_name/revision_form.html'``
* ``'reversion/app_label/revision_form.html'``
* ``'reversion/revision_form.html'``
``recover_list_template = None``
A custom template to render the recover list.
Alternatively, create specially named templates to override the default templates on a per-model or per-app basis.
* ``'reversion/app_label/model_name/recover_list.html'``
* ``'reversion/app_label/recover_list.html'``
* ``'reversion/recover_list.html'``
``recover_form_template = None``
A custom template to render the recover form.
* ``'reversion/app_label/model_name/recover_form.html'``
* ``'reversion/app_label/recover_form.html'``
* ``'reversion/recover_form.html'``
``history_latest_first = False``
If ``True``, revisions will be displayed with the most recent revision first.
.. _VersionAdmin_register:
``reversion_register(model, **options)``
Callback used by the auto-registration machinery to register the model with django-reversion. Override this to customize how models are registered.
.. code:: python
def reversion_register(self, model, **options):
options["exclude"] = ("some_field",)
super(YourModelAdmin, self).reversion_register(model, **options)
``model``
The model that will be registered with django-reversion.
``options``
Registeration options, see :ref:`reversion.register() <register>`.
* ``'reversion/auth/user/recover_list.html'``
* ``'reversion/auth/recover_list.html'``
* ``'reversion/recover_list.html'``

View File

@ -1,479 +1,302 @@
.. _api:
django-reversion API
====================
Low-level API
=============
Use the django-reversion API to build version-controlled apps. See also :ref:`Views` and :ref:`Middleware`.
You can use django-reversion's API to build powerful version-controlled views outside of the built-in admin site.
Overview
--------
Registering models with django-reversion
----------------------------------------
Registering models
^^^^^^^^^^^^^^^^^^
If you're already using the :ref:`admin integration <admin>` for a model, then there's no need to register it. However, if you want to register a model without using the admin integration, then you need to use the ``reversion.register()`` method.
Models must be registered with django-reversion before they can be used with the API.
::
.. code:: python
from django.db import models
import reversion
@reversion.register()
reversion.register(YourModel)
``reversion.register`` can also be used as a class decorator, with or without arguments.
::
import reversion
@reversion.register
class YourModel(models.Model):
...
pass
@reversion.register(format='yaml')
class YourOtherModel(models.Model):
...
.. Hint::
If you're using the :ref:`admin`, model registration is automatic. If youre using django-reversion in a management command, make sure you call ``django.contrib.admin.autodiscover()`` to load the admin modules before using the django-reversion API.
**Warning:** If youre using django-reversion in an management command, and are using the automatic ``VersionAdmin`` registration method, then youll need to import the relevant ``admin.py`` file at the top of your management command file.
.. include:: /_include/post-register.rst
**Warning:** When Django starts up, some python scripts get loaded twice, which can cause 'already registered' errors to be thrown. If you place your calls to ``reversion.register()`` in the ``models.py`` file, immediately after the model definition, this problem will go away.
Creating revisions
------------------
A revision represents one or more changes made to your models, grouped together as a single unit. You create a revision by marking up a section of code to represent a revision. Whenever you call ``save()`` on a model within the scope of a revision, it will be added to that revision.
**Note:** If you call ``save()`` outside of the scope of a revision, a revision is NOT created. This means that you are in control of when to create revisions.
There are several ways to create revisions, as explained below. Although there is nothing stopping you from mixing and matching these approaches, it is recommended that you pick one of the methods and stick with it throughout your project.
reversion.create_revision() decorator
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
You can decorate any function with the ``reversion.create_revision()`` decorator. Any changes to your models that occur during this function will be grouped together into a revision.
::
@transaction.atomic()
@reversion.create_revision()
def you_view_func(request):
your_model.save()
reversion.create_revision() context manager
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
You can use a context manager to mark up a block of code. Once the block terminates, any changes made to your models will be grouped together into a revision.
::
with transaction.atomic(), reversion.create_revision():
your_model.save()
RevisionMiddleware
^^^^^^^^^^^^^^^^^^
A *revision* represents one or more changes made to your model instances, grouped together as a single unit. You create a revision by creating a *revision block*. When you call ``save()`` on a registered model inside a revision block, it will be added to that revision.
The simplest way to create revisions is to use ``reversion.middleware.RevisionMiddleware``. This will automatically wrap every request in a revision, ensuring that all changes to your models will be added to their version history.
.. code:: python
To enable the revision middleware, simply add it to your ``MIDDLEWARE_CLASSES`` setting as follows::
# Declare a revision block.
with reversion.create_revision():
MIDDLEWARE_CLASSES = (
'reversion.middleware.RevisionMiddleware',
# Other middleware goes here...
)
# Save a new model instance.
obj = YourModel()
obj.name = "obj v1"
obj.save()
**Warning**: Due to changes in the Django 1.6 transaction handling, revision data will be saved in a separate database transaction to the one used to save your models, even if you set ``ATOMIC_REQUESTS = True``. If you need to ensure that your models and revisions are saved in the save transaction, please use the ``reversion.create_revision()`` context manager or decorator in combination with ``transaction.atomic()``.
# Store some meta-information.
reversion.set_user(request.user)
reversion.set_comment("Created revision 1")
# Declare a new revision block.
with reversion.create_revision():
Version meta data
-----------------
# Update the model instance.
obj.name = "obj v2"
obj.save()
It is possible to attach a comment and a user reference to an active revision using the following method::
# Store some meta-information.
reversion.set_user(request.user)
reversion.set_comment("Created revision 2")
with transaction.atomic(), reversion.create_revision():
your_model.save()
reversion.set_user(user)
reversion.set_comment("Comment text...")
If you use ``RevisionMiddleware``, then the user will automatically be added to the revision from the incoming request.
.. Important::
Custom meta data
^^^^^^^^^^^^^^^^
Bulk actions, such as ``Queryset.update()``, do not send signals, so won't be noticed by django-reversion.
You can attach custom meta data to a revision by creating a separate django model to hold the additional fields. For example::
from reversion.models import Revision
Loading revisions
^^^^^^^^^^^^^^^^^
class VersionRating(models.Model):
revision = models.ForeignKey(Revision) # There must be a relationship with Revision
rating = models.PositiveIntegerField()
Each model instance saved in a revision block is serialized as a :ref:`Version`. All versions in a revision block are associated with a single :ref:`Revision`.
You can then attach this meta class to a revision using the following method::
You can load a :ref:`VersionQuerySet` of versions from the database. Versions are loaded with the most recent version first.
reversion.add_meta(VersionRating, rating=5)
.. code:: python
from reversion.models import Version
Reverting to previous revisions
-------------------------------
# Load a queryset of versions for a specific model instance.
versions = Version.objects.get_for_object(instance)
assert len(versions) == 2
To revert a model to a previous version, use the following method::
# Check the serialized data for the first version.
assert versions[1].field_dict["name"] = "obj v1"
your_model = YourModel.objects.get(pk=1)
# Check the serialized data for the second version.
assert versions[0].field_dict["name"] = "obj v2"
# Build a list of all previous versions, latest versions first:
version_list = reversion.get_for_object(your_model)
# Build a list of all previous versions, latest versions first, duplicates removed:
version_list = reversion.get_unique_for_object(your_model)
Revision metadata
^^^^^^^^^^^^^^^^^
# Find the most recent version for a given date:
version = reversion.get_for_date(your_model, datetime.datetime(2008, 7, 10))
:ref:`Revision` stores meta-information about the revision.
# Access the model data stored within the version:
version_data = version.field_dict
.. code:: python
# Revert all objects in this revision:
version.revision.revert()
# Check the revision metadata for the first revision.
assert versions[1].revision.comment = "Created revision 1"
assert versions[1].revision.user = request.user
assert isinstance(versions[1].revision.date_created, datetime.datetime)
# Revert all objects in this revision, deleting related objects that have been created since the revision:
version.revision.revert(delete=True)
# Check the revision metadata for the second revision.
assert versions[0].revision.comment = "Created revision 2"
assert versions[0].revision.user = request.user
assert isinstance(versions[0].revision.date_created, datetime.datetime)
# Just revert this object, leaving the rest of the revision unchanged:
version.revert()
Reverting revisions
^^^^^^^^^^^^^^^^^^^
Recovering Deleted Objects
--------------------------
Revert a :ref:`Revision` to restore the serialized model instances.
To recover a deleted object, use the following method::
.. code:: python
# Built a list of all deleted objects, latest deletions first.
deleted_list = reversion.get_deleted(YourModel)
# Revert the first revision.
versions[1].revision.revert()
# Access a specific deleted object.
delete_version = deleted_list.get(id=5)
# Check the model instance has been reverted.
obj.refresh_from_db()
assert obj.name == "version 1"
# Recover all objects in this revision:
deleted_version.revision.revert()
# Revert the second revision.
versions[0].revision.revert()
# Just recover this object, leaving the rest of the revision unchanged:
deleted_version.revert()
# Check the model instance has been reverted.
obj.refresh_from_db()
assert obj.name == "version 2"
Advanced model registration
---------------------------
Restoring deleted model instances
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Following foreign key relationships
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Reverting a :ref:`Revision` will restore any serialized model instances that have been deleted.
Normally, when you save a model it will only save the primary key of any ForeignKey or ManyToMany fields. If you also wish to include the data of the foreign key in your revisions, pass a list of relationship names to the ``reversion.register()`` method.
.. code:: python
::
# Delete the model instance, but store the pk.
pk = obj.pk
obj.delete()
reversion.register(YourModel, follow=["your_foreign_key_field"])
# Revert the second revision.
versions[0].revision.revert()
**Please note:** If you use the follow parameter, you must also ensure that the related model has been registered with django-reversion.
# Check the model has been restored to the database.
obj = YourModel.objects.get(pk=obj.pk)
assert obj.name == "version 2"
In addition to ForeignKey and ManyToMany relationships, you can also specify related names of one-to-many relationships in the follow clause. For example, given the following database models::
class Person(models.Model):
pass
.. _registration-api:
class Pet(models.Model):
person = models.ForeignKey(Person)
Registration API
----------------
reversion.register(Person, follow=["pet_set"])
reversion.register(Pet)
.. _register:
Now whenever you save a revision containing a ``Person``, all related ``Pet`` instances will be automatically saved to the same revision.
``reversion.register(model, **options)``
Multi-table inheritance
^^^^^^^^^^^^^^^^^^^^^^^
Registers a model with django-reversion.
By default, django-reversion will not save data in any parent classes of a model that uses multi-table inheritance. If you wish to also add parent models to your revision, you must explicitly add them to the follow clause when you register the model.
Throws :ref:`RegistrationError` if the model has already been registered.
For example::
``model``
The Django model to register.
class Place(models.Model):
pass
``fields=None``
An iterable of field names to include in the serialized data. If ``None``, all fields will be included.
class Restaurant(Place):
pass
``exclude=()``
An iterable of field names to exclude from the serialized data.
reversion.register(Place)
reversion.register(Restaurant, follow=["place_ptr"])
``follow=()``
An iterable of model relationships to follow when saving a version of this model. ``ForeignKey``, ``ManyToManyField`` and reversion ``ForeignKey`` relationships are supported. Any property that returns a ``Model`` or ``QuerySet`` is also supported.
``format="json"``
The name of a Django serialization format to use when saving the model instance.
Saving a subset of fields
^^^^^^^^^^^^^^^^^^^^^^^^^
``for_concrete_model=True``
If ``True`` proxy models will be saved under the same content type as their concrete model. If ``False``, proxy models will be saved under their own content type, effectively giving proxy models their own distinct history.
If you only want a subset of fields to be saved to a revision, you can specify a ``fields`` or ``exclude`` argument to the ``reversion.register()`` method.
``ignore_duplicates=False``
If ``True``, then an additional check is performed to avoid saving duplicate versions for this model.
::
Checking for duplicate revisions adds significant overhead to the process of creating a revision. Don't enable it unless you really need it!
reversion.register(YourModel, fields=["pk", "foo", "bar"])
reversion.register(YourModel, exclude=["foo"])
.. Hint::
By default, django-reversion will not register any parent classes of a model that uses multi-table inheritance. If you wish to also add parent models to your revision, you must explicitly add their ``parent_ptr`` fields to the ``follow`` parameter when you register the model.
**Please note:** If you are not careful, then it is possible to specify a combination of fields that will make the model impossible to recover. As such, approach this option with caution.
.. include:: /_include/post-register.rst
Custom serialization format
^^^^^^^^^^^^^^^^^^^^^^^^^^^
``reversion.is_registered(model)``
By default, django-reversion will serialize model data using the ``'json'`` serialization format. You can override this on a per-model basis using the format argument to the register method.
Returns whether the given model has been registered with django-reversion.
::
``model``
The Django model to check.
reversion.register(YourModel, format="yaml")
**Please note:** The named serializer must serialize model data to a utf-8 encoded character string. Please verify that your serializer is compatible before using it with django-reversion.
``reversion.unregister(model)``
Unregisters the given model from django-reversion.
Registering with custom signals
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. include:: /_include/throws-registration-error.rst
By default, django-reversion saves a new revision whenever a model is saved, using the ``post_save`` signal. However, sometimes you might need to create a revision on other signals too.
``model``
The Django model to unregister.
::
from django.db.models.signals import post_save
from your_app.signals import custom_signal
``reversion.get_registered_models()``
reversion.register(YourModel, signals=[post_save, custom_signal])
Returns an iterable of all registered models.
By default, revision data is serialized at the end of the ``reversion.create_revision()`` block, allowing foreign key references to be updated in the same block before the revision data is prepared. However, in some cases you might want to serialize the revision data immediately, such as times when the model is shortly going to be deleted.
::
.. _revision-api:
from django.db.models.signals import post_save, pre_delete
Revision API
------------
reversion.register(YourModel, signals=[post_save], eager_signals=[pre_delete])
``reversion.create_revision(manage_manually=False, using=None, atomic=True)``
**Important:** Creating revisions using the `pre_delete` signal is not recommended, as it alters the semantics of revision recovery. Only do this if you have a good understanding of the django-reversion internals.
Marks a block of code as a *revision block*. Can also be used as a decorator.
.. include:: /_include/create-revision-args.rst
Really advanced registration
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
It's possible to customize almost every aspect of model registration by registering your model with a subclass of ``reversion.VersionAdapter``. Behind the scenes, ``reversion.register()`` does this anyway, but you can explicitly provide your own VersionAdapter if you need to perform really advanced customization.
``reversion.is_active()``
::
Returns whether there is currently an active revision block.
class MyVersionAdapter(reversion.VersionAdapter):
pass # Please see the reversion source code for available methods to override.
reversion.register(MyModel, adapter_cls=MyVersionAdapter)
``reversion.is_manage_manually()``
Returns whether the current revision block is in ``manage_manually`` mode.
Automatic Registration by the Admin Interface
---------------------------------------------
As mentioned at the start of this page, the admin interface will automatically register any models that use the ``VersionAdmin`` class. The admin interface will automatically follow any InlineAdmin relationships, as well as any parent links for models that use multi-table inheritance.
``reversion.set_user(user)``
For example::
Sets the user for the current revision.
# models.py
.. include:: /_include/throws-revision-error.rst
class Place(models.Model):
pass
``user``
A ``User`` model instance (or whatever your ``settings.AUTH_USER_MODEL`` is).
class Restaurant(Place):
pass
class Meal(models.Model):
restaurant = models.ForeignKey(Restaurant)
``reversion.get_user()``
# admin.py
Returns the user for the current revision.
class MealInlineAdmin(admin.StackedInline):
model = Meal
.. include:: /_include/throws-revision-error.rst
class RestaurantAdmin(VersionAdmin):
inlines = MealInlineAdmin,
admin.site.register(Restaurant, RestaurantAdmin)
.. _set_comment:
Since ``Restaurant`` has been registered with a subclass of ``VersionAdmin``, the following registration calls will be made automatically::
``reversion.set_comment(comment)``
reversion.register(Place)
reversion.register(Restaurant, follow=("place_ptr", "meal_set"))
reversion.register(Meal)
Sets the comment for the current revision.
.. include:: /_include/throws-revision-error.rst
``comment``
The text comment for the revision.
``reversion.get_comment()``
Returns the comment for the current revision.
.. include:: /_include/throws-revision-error.rst
``reversion.set_date_created(date_created)``
Sets the creation date for the current revision.
.. include:: /_include/throws-revision-error.rst
``date_created``
The creation date for the revision.
``reversion.get_date_created()``
Returns the creation date for the current revision.
.. include:: /_include/throws-revision-error.rst
``reversion.add_meta(model, **values)``
Adds custom metadata to a revision.
.. include:: /_include/throws-revision-error.rst
``model``
A Django model to store the custom metadata. The model must have a ``ForeignKey`` or ``OneToOneField`` to :ref:`Revision`.
``**values``
Values to be stored on ``model`` when it is saved.
``reversion.add_to_revision(obj, model_db=None)``
Adds a model instance to a revision.
.. include:: /_include/throws-revision-error.rst
``obj``
A model instance to add to the revision.
.. include:: /_include/model-db-arg.rst
.. _VersionQuerySet:
reversion.models.VersionQuerySet
--------------------------------
A ``QuerySet`` of :ref:`Version`. The results are ordered with the most recent :ref:`Version` first.
``Version.objects.get_for_model(model, model_db=None)``
Returns a :ref:`VersionQuerySet` for the given model.
.. include:: /_include/throws-registration-error.rst
``model``
A registered model.
.. include:: /_include/model-db-arg.rst
``Version.objects.get_for_object(obj, model_db=None)``
Returns a :ref:`VersionQuerySet` for the given model instance.
.. include:: /_include/throws-registration-error.rst
``obj``
An instance of a registered model.
.. include:: /_include/model-db-arg.rst
``Version.objects.get_for_object_reference(model, pk, model_db=None)``
Returns a :ref:`VersionQuerySet` for the given model and primary key.
.. include:: /_include/throws-registration-error.rst
``model``
A registered model.
``pk``
The database primary key of a model instance.
.. include:: /_include/model-db-arg.rst
``Version.objects.get_deleted(model, model_db=None)``
Returns a :ref:`VersionQuerySet` for the given model containing versions where the serialized model no longer exists in the database.
.. include:: /_include/throws-registration-error.rst
``model``
A registered model.
``db``
The database to load the versions from.
.. include:: /_include/model-db-arg.rst
``Version.objects.get_unique()``
Returns an iterable of :ref:`Version`, where each version is unique for a given database, model instance, and set of serialized fields.
.. _Version:
reversion.models.Version
------------------------
Represents a single model instance serialized in a revision.
``Version.id``
The database primary key of the :ref:`Version`.
``Version.revision``
A ``ForeignKey`` to a :ref:`Revision` instance.
``Version.content_type``
The ``ContentType`` of the serialized model instance.
``Version.object_id``
The string representation of the serialized model instance's primary key.
``Version.db``
The Django database alias where the serialized model was saved.
``Version.format``
The name of the Django serialization format used to serialize the model instance.
``Version.serialized_data``
The raw serialized data of the model instance.
``Version.object_repr``
The stored snapshot of the model instance's ``__str__`` method when the instance was serialized.
``Version.field_dict``
A dictionary of stored model fields. This includes fields from any parent models in the same revision.
.. include:: /_include/throws-revert-error.rst
``Version.revert()``
Restores the serialized model instance to the database. To restore the entire revision, use :ref:`Revision.revert() <Revision-revert>`.
.. include:: /_include/throws-revert-error.rst
.. _Revision:
reversion.models.Revision
-------------------------
Contains metadata about a revision, and groups together all :ref:`Version` instances created in that revision.
``Revision.id``
The database primary key of the :ref:`Revision`.
``Revision.date_created``
A ``datetime`` when the revision was created.
``Revision.user``
The ``User`` that created the revision, or None.
``Revision.comment``
A text comment on the revision.
.. _Revision-revert:
``Revision.revert(delete=False)``
Restores all contained serialized model instances to the database.
.. include:: /_include/throws-revert-error.rst
``delete``
If ``True``, any model instances which have been created and are reachable by the ``follow`` clause of any model instances in this revision will be deleted. This effectively restores a group of related models to the state they were in when the revision was created.
It is only necessary to manually register these models if you wish to override the default registration parameters. In most cases, however, the defaults will suit just fine.

View File

@ -1 +0,0 @@
.. include:: ../CHANGELOG.rst

View File

@ -3,43 +3,15 @@
Management commands
===================
django-reversion includes a number of ``django-admin.py`` management commands.
.. _createinitialrevisions:
django-reversion comes with a number of additional django-admin.py management commands, detailed below.
createinitialrevisions
----------------------
Creates an initial revision for all registered models in your project. It should be run after installing django-reversion, or registering a new model with django-reversion.
This command is used to create a single, base revision for all registered models in your project. It should be run after installing django-reversion, or registering a new model with django-reversion. If your project contains a lot of version-controlled data, then this might take a while to complete.
.. code:: bash
::
./manage.py createinitialrevisions
./manage.py createinitialrevisions your_app.YourModel --comment="Initial revision."
Run ``./manage.py createinitialrevisions --help`` for more information.
.. Warning::
For large databases, this command can take a long time to run.
deleterevisions
---------------
Deletes old revisions. It can be run regularly to keep revision history manageable.
.. code:: bash
./manage.py deleterevisions
# keep any changes from last 30 days
./manage.py deleterevisions your_app.YourModel --days=30
# keep 30 most recent changes for each item.
./manage.py deleterevisions your_app.YourModel --keep=30
# Keep anything from last 30 days and at least 3 from older changes.
./manage.py deleterevisions your_app.YourModel --keep=3 --days=30
Run ``./manage.py deleterevisions --help`` for more information.
.. Warning::
With no arguments, this command will delete your entire revision history! Read the command help for ways to limit which revisions should be deleted.
django-admin.py createinitialrevisions
django-admin.py createinitialrevisions someapp
django-admin.py createinitialrevisions someapp.SomeModel

View File

@ -1,12 +0,0 @@
.. _common-problems:
Common problems
===============
RegistrationError: class 'myapp.MyModel' has already been registered with Reversion
-----------------------------------------------------------------------------------
This is caused by your ``models.py`` file being imported twice, resulting in ``reversion.register()`` being called twice for the same model.
This problem is almost certainly due to relative import statements in your codebase. Try converting all your relative imports into absolute imports.

View File

@ -2,10 +2,9 @@
# -*- coding: utf-8 -*-
#
# django-reversion documentation build configuration file, created by
# sphinx-quickstart on Thu Jun 2 08:41:36 2016.
# sphinx-quickstart on Thu Aug 29 09:17:37 2013.
#
# This file is execfile()d with the current directory set to its
# containing dir.
# This file is execfile()d with the current directory set to its containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
@ -13,323 +12,247 @@
# All configuration values have a default; values that are commented out
# serve to show the default.
import sys, os
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
# import os
# import sys
# sys.path.insert(0, os.path.abspath('.'))
#sys.path.insert(0, os.path.abspath('.'))
from reversion import __version__
# -- General configuration ------------------------------------------------
# -- General configuration -----------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
#
# needs_sphinx = '1.0'
#needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = []
# Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = ['sphinx.ext.intersphinx']
# Add any paths that contain templates here, relative to this directory.
templates_path = []
templates_path = ['_templates']
# The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string:
#
# source_suffix = ['.rst', '.md']
# The suffix of source filenames.
source_suffix = '.rst'
# The encoding of source files.
#
# source_encoding = 'utf-8-sig'
#source_encoding = 'utf-8-sig'
# The master toctree document.
master_doc = 'index'
# General information about the project.
project = 'django-reversion'
copyright = '2016, Dave Hall'
author = 'Dave Hall'
copyright = '2013, Dave Hall'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = '.'.join(str(x) for x in __version__[:2])
version = '1.8'
# The full version, including alpha/beta/rc tags.
release = '.'.join(str(x) for x in __version__)
release = '1.8.7'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#
# 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 = None
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
#
# today = ''
#
#today = ''
# Else, today_fmt is used as the format for a strftime call.
#
# today_fmt = '%B %d, %Y'
#today_fmt = '%B %d, %Y'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This patterns also effect to html_static_path and html_extra_path
exclude_patterns = ['_build', '_include', 'Thumbs.db', '.DS_Store']
exclude_patterns = ['_build']
# The reST default role (used for this markup: `text`) to use for all
# documents.
#
# default_role = None
# The reST default role (used for this markup: `text`) to use for all documents.
#default_role = None
# If true, '()' will be appended to :func: etc. cross-reference text.
#
# add_function_parentheses = True
#add_function_parentheses = True
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
#
# add_module_names = True
#add_module_names = True
# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
#
# show_authors = False
#show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# A list of ignored prefixes for module index sorting.
# modindex_common_prefix = []
# If true, keep warnings as "system message" paragraphs in the built documents.
# keep_warnings = False
# If true, `todo` and `todoList` produce output, else they produce nothing.
todo_include_todos = False
#modindex_common_prefix = []
# -- Options for HTML output ----------------------------------------------
# -- 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'
html_theme = 'default'
# 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 = {}
#html_theme_options = {}
# Add any paths that contain custom themes here, relative to this directory.
# html_theme_path = []
#html_theme_path = []
# The name for this set of Sphinx documents.
# "<project> v<release> documentation" by default.
#
# html_title = 'django-reversion v1.10.3'
# The name for this set of Sphinx documents. If None, it defaults to
# "<project> v<release> documentation".
#html_title = None
# A shorter title for the navigation bar. Default is the same as html_title.
#
# html_short_title = None
#html_short_title = None
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
#
# html_logo = None
#html_logo = None
# The name of an image file (relative to this directory) to use as a favicon of
# the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# The name of an image file (within the static path) to use as favicon of the
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
#
# html_favicon = None
#html_favicon = None
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = []
# Add any extra paths that contain custom files (such as robots.txt or
# .htaccess) here, relative to this directory. These files are copied
# directly to the root of the documentation.
#
# html_extra_path = []
# If not None, a 'Last updated on:' timestamp is inserted at every page
# bottom, using the given strftime format.
# The empty string is equivalent to '%b %d, %Y'.
#
# html_last_updated_fmt = None
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.
#html_last_updated_fmt = '%b %d, %Y'
# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
#
# html_use_smartypants = True
#html_use_smartypants = True
# Custom sidebar templates, maps document names to template names.
#
# html_sidebars = {}
#html_sidebars = {}
# Additional templates that should be rendered to pages, maps page names to
# template names.
#
# html_additional_pages = {}
#html_additional_pages = {}
# If false, no module index is generated.
#
# html_domain_indices = True
#html_domain_indices = True
# If false, no index is generated.
#
# html_use_index = True
#html_use_index = True
# If true, the index is split into individual pages for each letter.
#
# html_split_index = False
#html_split_index = False
# If true, links to the reST sources are added to the pages.
#
# html_show_sourcelink = True
#html_show_sourcelink = True
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
#
# html_show_sphinx = True
#html_show_sphinx = True
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
#
# html_show_copyright = True
#html_show_copyright = True
# If true, an OpenSearch description file will be output, and all pages will
# contain a <link> tag referring to it. The value of this option must be the
# base URL from which the finished HTML is served.
#
# html_use_opensearch = ''
#html_use_opensearch = ''
# This is the file name suffix for HTML files (e.g. ".xhtml").
# html_file_suffix = None
# Language to be used for generating the HTML full-text search index.
# Sphinx supports the following languages:
# 'da', 'de', 'en', 'es', 'fi', 'fr', 'h', 'it', 'ja'
# 'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr', 'zh'
#
# html_search_language = 'en'
# A dictionary with options for the search language support, empty by default.
# 'ja' uses this config value.
# 'zh' user can custom change `jieba` dictionary path.
#
# html_search_options = {'type': 'default'}
# The name of a javascript file (relative to the configuration directory) that
# implements a search results scorer. If empty, the default will be used.
#
# html_search_scorer = 'scorer.js'
#html_file_suffix = None
# Output file base name for HTML help builder.
htmlhelp_basename = 'django-reversiondoc'
# -- Options for LaTeX output ---------------------------------------------
# -- Options for LaTeX output --------------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#
# 'papersize': 'letterpaper',
# The paper size ('letterpaper' or 'a4paper').
#'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#
# 'pointsize': '10pt',
# The font size ('10pt', '11pt' or '12pt').
#'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#
# 'preamble': '',
# Latex figure (float) alignment
#
# 'figure_align': 'htbp',
# Additional stuff for the LaTeX preamble.
#'preamble': '',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
# (source start file, target name, title, author, documentclass [howto/manual]).
latex_documents = [
(master_doc, 'django-reversion.tex', 'django-reversion Documentation',
'Dave Hall', 'manual'),
('index', 'django-reversion.tex', 'django-reversion Documentation',
'Dave Hall', 'manual'),
]
# The name of an image file (relative to this directory) to place at the top of
# the title page.
#
# latex_logo = None
#latex_logo = None
# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
#
# latex_use_parts = False
#latex_use_parts = False
# If true, show page references after internal links.
#
# latex_show_pagerefs = False
#latex_show_pagerefs = False
# If true, show URL addresses after external links.
#
# latex_show_urls = False
#latex_show_urls = False
# Documents to append as an appendix to all manuals.
#
# latex_appendices = []
#latex_appendices = []
# If false, no module index is generated.
#
# latex_domain_indices = True
#latex_domain_indices = True
# -- Options for manual page output ---------------------------------------
# -- Options for manual page output --------------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
(master_doc, 'django-reversion', 'django-reversion Documentation',
[author], 1)
('index', 'django-reversion', 'django-reversion Documentation',
['Dave Hall'], 1)
]
# If true, show URL addresses after external links.
#
# man_show_urls = False
#man_show_urls = False
# -- Options for Texinfo output -------------------------------------------
# -- Options for Texinfo output ------------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
(master_doc, 'django-reversion', 'django-reversion Documentation',
author, 'django-reversion', 'One line description of project.',
'Miscellaneous'),
('index', 'django-reversion', 'django-reversion Documentation',
'Dave Hall', 'django-reversion', 'One line description of project.',
'Miscellaneous'),
]
# Documents to append as an appendix to all manuals.
#
# texinfo_appendices = []
#texinfo_appendices = []
# If false, no module index is generated.
#
# texinfo_domain_indices = True
#texinfo_domain_indices = True
# How to display URL addresses: 'footnote', 'no', or 'inline'.
#
# texinfo_show_urls = 'footnote'
#texinfo_show_urls = 'footnote'
# Example configuration for intersphinx: refer to the Python standard library.
def check_object_path(key, url, path):
if os.path.isfile(path):
return {key: (url, path)}
return {}
intersphinx_mapping = {}
intersphinx_mapping.update(check_object_path('python',
'http://docs.python.org/',
'/usr/share/doc/python'
+ '.'.join([str(x) for x in sys.version_info[0:2]])
+ '/html/objects.inv'))
# If true, do not generate a @detailmenu in the "Top" node's menu.
#
# texinfo_no_detailmenu = False

56
docs/diffs.rst Normal file
View File

@ -0,0 +1,56 @@
.. _diffs:
Generating Diffs
================
A common problem when dealing with version-controlled text is generating diffs to highlight changes between different versions.
django-reversion comes with a number of helper functions that make generating diffs easy. They all rely on the `google-diff-match-patch <http://code.google.com/p/google-diff-match-patch/>`_ library, so make sure you have this installed before trying to use the functions.
Low-Level API
-------------
It is possible to generate two types of diff using the diff helper functions. For the purpose of these examples, it is assumed that you have created a model called ``Page``, which contains a text field called ``content``.
First of all, you need to use the :ref:`low level API <api>` to retrieve the versions you want to compare.
::
from reversion.helpers import generate_patch
# Get the page object to generate diffs for.
page = Page.objects.all()[0]
# Get the two versions to compare.
available_versions = reversion.get_for_object(page)
old_version = available_versions[0]
new_version = available_versions[1]
Now, in order to generate a text patch::
from reversion.helpers import generate_patch
patch = generate_patch(old_version, new_version, "content")
Or, to generate a pretty HTML patch::
from reversion.helpers import generate_patch_html
patch_html = generate_patch_html(old_version, new_version, "content")
Because text diffs can often be fragmented and hard to read, an optional ``cleanup`` parameter may be passed to generate friendlier diffs.
::
patch_html = generate_patch_html(old_version, new_version, "content", cleanup="semantic")
patch_html = generate_patch_html(old_version, new_version, "content", cleanup="efficiency")
Of the two cleanup styles, the one that generally produces the best result is 'semantic'.
Admin Integration
-----------------
The admin integration for django-reversion does not currently support diff generation. This is a deliberate design decision, as it would make the framework a lot more heavyweight, as well as carrying the risk of confusing non-technical end users.
While future versions may support a more advanced admin class, for the time being it is left up to your own imagination for ways in which to integrate diffs with your project.

View File

@ -1,19 +1,46 @@
.. _django-versions:
Compatible Django versions
Compatible Django Versions
==========================
django-reversion aims to stay compatible with the latest LTS release of Django, along with more recent releases. See :ref:`changelog`.
django-reversion is an actively-maintained project, and aims to stay compatible with the latest version of Django. Unfortunately, this means that the latest release of django-reversion might not work with older versions of Django.
Older versions of Django require an older version of django-reversion to be installed.
If you are using anything other than the latest release of Django, it is important that you check the table below to ensure that your django-reversion download will be compatible.
============== =================
Django version Reversion release
============== =================
1.8 - current 2.0.0
1.7 1.10.x
1.6 1.8.x
1.7+ 1.8.7
1.6+ 1.8.5
1.5.1+ 1.7.1
1.5 1.7
1.4.4+ 1.6.6
1.4.3 1.6.5
1.4.2 1.6.4
1.4.1 1.6.3
1.4 1.6.1
1.3.6 1.5.7
1.3.5 1.5.6
1.3.4 1.5.5
1.3.3 1.5.4
1.3.2 1.5.3
1.3.1 1.5.2
1.3 1.5
1.2.5 1.3.3
1.2.4 1.3.3
1.2.3 1.3.2
1.2 1.3
1.1.1 1.2.1
1.1 1.2
1.0.4 1.1.2
1.0.3 1.1.2
1.0.2 1.1.1
============== =================
.. Warning::
Older versions of django-reversion receive very limited support. It's advised to upgrade your Django to remain compatible with the latest release of django-reversion.
Getting the code
----------------
All django-reversion releases are available from the `project downloads area <http://github.com/etianen/django-reversion/downloads>`_. You can also use Git to checkout tags from the `public git repository <http://github.com/etianen/django-reversion>`_.
There are a number of alternative methods you can use when installing django-reversion. Please check the :ref:`installation methods <installation>` page for more information.

View File

@ -1,30 +0,0 @@
.. _errors:
Errors
======
django-reversion defines several custom errors.
.. _RegistrationError:
reversion.RegistrationError
---------------------------
Something went wrong with the :ref:`registration-api`.
.. _RevisionManagementError:
reversion.RevisionManagementError
---------------------------------
Something went wrong using the :ref:`revision-api`.
.. _RevertError:
reversion.RevertError
---------------------
Something went wrong reverting a revision.

46
docs/how-it-works.rst Normal file
View File

@ -0,0 +1,46 @@
.. _how-it-works:
How it works
============
Saving Revisions
----------------
Enabling version control for a model is achieved using the ``reversion.register`` method. This registers the version control machinery with the ``post_save`` signal for that model, allowing new changes to the model to be caught.
::
import reversion
reversion.register(YourModel)
Any models that use subclasses of ``VersionAdmin`` in the admin interface will be automatically registered with django-reversion. As such, it is only necessary to manually register these models if you wish to override the default registration settings.
Whenever you save changes to a model, it is serialized using the Django serialization framework into a JSON string. This is saved to the database as a ``reversion.models.Version`` model. Each ``Version`` model is linked to a model instance using a ``GenericForeignKey``.
Foreign keys and many-to-many relationships are normally saved as their primary keys only. However, the ``reversion.register`` method takes an optional follow clause allowing these relationships to be automatically added to revisions. Please see :ref:`Low Level API <api>` for more information.
Reverting Versions
------------------
Reverting a version is simply a matter of loading the appropriate ``Version`` model from the database, deserializing the model data, and re-saving the old data.
There are a number of utility methods present on the ``Version`` object manager to assist this process. Please see :ref:`Low Level API <api>` for more information.
Revision Management
-------------------
Related changes to models are grouped together in revisions. This allows for atomic rollback from one revision to another. You can automate revision management using either ``reversion.middleware.RevisionMiddleware``, or the ``reversion.create_revision`` decorator.
For more information on creating revisions, please see :ref:`Low Level API <api>`.
Admin Integration
-----------------
Full admin integration is achieved using the ``reversion.admin.VersionAdmin`` class. This will create a new revision whenever a model is edited using the admin interface. Any models registered for version control, including inline models, will be included in this revision.
The ``object_history`` view is extended to make each ``LogEntry`` a link that can be used to revert the model back to the most recent version at the time the ``LogEntry`` was created.
Choosing to revert a model will display the standard model change form. The fields in this form are populated using the data contained in the revision corresponding to the chosen ``LogEntry``. Saving this form will result in a new revision being created containing the new model data.
For most projects, simply registering a model with a subclass of ``VersionAdmin`` is enough to satisfy all its version-control needs.

View File

@ -1,47 +1,44 @@
.. _index:
django-reversion
================
django-reversion documentation
==============================
**django-reversion** is an extension to the Django web framework that provides
version control for model instances.
Getting started with django-reversion
-------------------------------------
Features
--------
- Roll back to any point in a model instance's history.
- Recover deleted model instances.
- Simple admin integration.
Installation
------------
To install django-reversion:
To install django-reversion, follow these steps:
1. Install with pip: ``pip install django-reversion``.
2. Add ``'reversion'`` to ``INSTALLED_APPS``.
3. Run ``manage.py migrate``.
3. Run ``manage.py syncdb``.
.. Important::
See :ref:`django-versions` if you're not using the latest release of Django.
The latest release (1.8.7) of django-reversion is designed to work with Django 1.8. If you have installed anything other than the latest version of Django, please check the :ref:`compatible Django versions <django-versions>` page before installing django-reversion.
There are a number of alternative methods you can use when installing django-reversion. Please check the :ref:`installation methods <installation>` page for more information.
Admin integration
-----------------
django-reversion can be used to add rollback and recovery to your admin site.
django-reversion can be used to add a powerful rollback and recovery facility to your admin site. To enable this, simply register your models with a subclass of ``reversion.VersionAdmin``::
.. include:: /_include/admin.rst
import reversion
For more information about admin integration, see :ref:`admin`.
class YourModelAdmin(reversion.VersionAdmin):
pass
admin.site.register(YourModel, YourModelAdmin)
Whenever you register a model with the ``VersionAdmin`` class, be sure to run the ``./manage.py createinitialrevisions`` command to populate the version database with an initial set of model data. Depending on the number of rows in your database, this command could take a while to execute.
For more information about admin integration, please read the :ref:`admin integration <admin>` documentation.
Low-level API
Low Level API
-------------
You can use the django-reversion API to build version-controlled applications. See :ref:`api`.
You can use django-reversion's API to build powerful version-controlled views. For more information, please read the :ref:`low level API <api>` documentation.
More information
@ -53,21 +50,19 @@ Installation
.. toctree::
:maxdepth: 1
installation
django-versions
common-problems
changelog
migrations
admin
Usage
^^^^^
Further reading
^^^^^^^^^^^^^^^
.. toctree::
:maxdepth: 2
:maxdepth: 1
admin
commands
api
views
middleware
errors
commands
signals
how-it-works
diffs

35
docs/installation.rst Normal file
View File

@ -0,0 +1,35 @@
.. _installation:
Installation methods
====================
**Note:** It is recommended that you always use the latest release of django-reversion with the latest release of Django. If you are using an older version of Django, then please check out the :ref:`Compatible Django Versions <django-versions>` page for more information.
For information on configuring django-reversion, see the :ref:`getting started <index>` guide.
pip
---
You can install django-reversion into your system, or virtual environment, by running the following command in a terminal::
$ pip install django-reversion
easy_install
------------
The popular easy_install utility can be used to install the latest django-reversion release from the Python Package Index. Simply run the following command in a terminal::
$ sudo easy_install django-reversion
Git
---
Using Git to install django-reversion provides an easy way of upgrading your installation at a later date. Simply clone the `public git repository <http://github.com/etianen/django-reversion>`_ and symlink the ``src/reversion`` directory into your ``PYTHONPATH``::
$ git clone git://github.com/etianen/django-reversion.git
$ cd django-reversion.git
$ git checkout release-1.8.7
$ ln -s src/reversion /your/pythonpath/location/reversion

190
docs/make.bat Normal file
View File

@ -0,0 +1,190 @@
@ECHO OFF
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set BUILDDIR=_build
set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
set I18NSPHINXOPTS=%SPHINXOPTS% .
if NOT "%PAPER%" == "" (
set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS%
)
if "%1" == "" goto help
if "%1" == "help" (
:help
echo.Please use `make ^<target^>` where ^<target^> is one of
echo. html to make standalone HTML files
echo. dirhtml to make HTML files named index.html in directories
echo. singlehtml to make a single large HTML file
echo. pickle to make pickle files
echo. json to make JSON files
echo. htmlhelp to make HTML files and a HTML help project
echo. qthelp to make HTML files and a qthelp project
echo. devhelp to make HTML files and a Devhelp project
echo. epub to make an epub
echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
echo. text to make text files
echo. man to make manual pages
echo. texinfo to make Texinfo files
echo. gettext to make PO message catalogs
echo. changes to make an overview over all changed/added/deprecated items
echo. linkcheck to check all external links for integrity
echo. doctest to run all doctests embedded in the documentation if enabled
goto end
)
if "%1" == "clean" (
for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
del /q /s %BUILDDIR%\*
goto end
)
if "%1" == "html" (
%SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The HTML pages are in %BUILDDIR%/html.
goto end
)
if "%1" == "dirhtml" (
%SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
goto end
)
if "%1" == "singlehtml" (
%SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
goto end
)
if "%1" == "pickle" (
%SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can process the pickle files.
goto end
)
if "%1" == "json" (
%SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can process the JSON files.
goto end
)
if "%1" == "htmlhelp" (
%SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can run HTML Help Workshop with the ^
.hhp project file in %BUILDDIR%/htmlhelp.
goto end
)
if "%1" == "qthelp" (
%SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can run "qcollectiongenerator" with the ^
.qhcp project file in %BUILDDIR%/qthelp, like this:
echo.^> qcollectiongenerator %BUILDDIR%\qthelp\django-reversion.qhcp
echo.To view the help file:
echo.^> assistant -collectionFile %BUILDDIR%\qthelp\django-reversion.ghc
goto end
)
if "%1" == "devhelp" (
%SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
if errorlevel 1 exit /b 1
echo.
echo.Build finished.
goto end
)
if "%1" == "epub" (
%SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The epub file is in %BUILDDIR%/epub.
goto end
)
if "%1" == "latex" (
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
if errorlevel 1 exit /b 1
echo.
echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
goto end
)
if "%1" == "text" (
%SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The text files are in %BUILDDIR%/text.
goto end
)
if "%1" == "man" (
%SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The manual pages are in %BUILDDIR%/man.
goto end
)
if "%1" == "texinfo" (
%SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo.
goto end
)
if "%1" == "gettext" (
%SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The message catalogs are in %BUILDDIR%/locale.
goto end
)
if "%1" == "changes" (
%SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
if errorlevel 1 exit /b 1
echo.
echo.The overview file is in %BUILDDIR%/changes.
goto end
)
if "%1" == "linkcheck" (
%SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
if errorlevel 1 exit /b 1
echo.
echo.Link check complete; look for any errors in the above output ^
or in %BUILDDIR%/linkcheck/output.txt.
goto end
)
if "%1" == "doctest" (
%SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
if errorlevel 1 exit /b 1
echo.
echo.Testing of doctests in the sources finished, look at the ^
results in %BUILDDIR%/doctest/output.txt.
goto end
)
:end

View File

@ -1,34 +0,0 @@
.. _middleware:
Middleware
==========
Shortcuts when using django-reversion in views.
reversion.middleware.RevisionMiddleware
---------------------------------------
Wrap the every request that isn't ``GET``, ``HEAD`` or ``OPTIONS`` in a revision block.
The request user will also be added to the revision metadata.
To enable ``RevisionMiddleware``, add ``'reversion.middleware.RevisionMiddleware'`` to your ``MIDDLEWARE_CLASSES`` setting. For Django >= 1.10, add it to your ``MIDDLEWARE`` setting.
.. Warning::
This will wrap every request that isn't ``GET``, ``HEAD`` or ``OPTIONS`` in a database transaction. For best performance, consider marking individual views instead.
``RevisionMiddleware.manage_manually = False``
.. include:: /_include/create-revision-manage-manually.rst
``RevisionMiddleware.using = None``
.. include:: /_include/create-revision-using.rst
``RevisionMiddleware.atomic = True``
.. include:: /_include/create-revision-atomic.rst

72
docs/migrations.rst Normal file
View File

@ -0,0 +1,72 @@
.. _migrations:
Schema migrations
=================
This page describes the schema migrations that have taken place over the lifetime of django-reversion, along with a how-to guide for updating your schema.
django-reversion 1.8.3
----------------------
This release adds an index to the ``date_created`` column on the ``reversion_revision`` table.
In order to apply this migration using south, simply run::
./manage.py migrate reversion
**Important:** South 1.0 or greater is required to run these migrations.
This release also starts using the django core `migrations framework <https://docs.djangoproject.com/en/dev/topics/migrations/>`_, which is intended to be used as the community standard going forwards. To `upgrade from south <https://docs.djangoproject.com/en/dev/topics/migrations/#upgrading-from-south>`_, please complete the following steps:
1. Ensure that your app is up-to-date with all django-reversion migrations.
2. Upgrade to Django 1.7 or greater.
3. Remove ``'south'`` from ``INSTALLED_APPS``.
4. Run ``./manage.py migrate reversion``.
The legacy south migrations will be removed from django-reversion in release 1.9.
django-reversion 1.8
--------------------
This release removes ``type`` column from ``reversion_version`` table.
In order to apply this migration using south, simply run::
./manage.py migrate reversion
django-reversion 1.5
--------------------
This release adds in significant speedups for models with integer primary keys.
In order to apply this migration using south, simply run::
./manage.py migrate reversion
If you have a large amount of existing version data, then this command might take a little while to run while the database tables are updated.
django-reversion 1.4
--------------------
This release added a much-requested 'type' field to Version models, allows statistic to be gathered about the number of additions, changes and deletions that have been applied to a model.
In order to apply this migration, it is first necessary to install South.
1. Add 'south' to your ``INSTALLED_APPS`` setting.
2. Run ``./manage.py syncdb``
You then need to run the following two commands to complete the migration::
./manage.py migrate reversion 0001 --fake
./manage.py migrate reversion
django-reversion 1.3.3
----------------------
No migration needed.

View File

@ -1,22 +1,45 @@
.. _signals:
Signals
=======
Signals sent by django-reversion
================================
django-reversion provides two custom signals.
django-reversion provides a number of custom signals that can be used to tie-in additional functionality to the version creation mechanism.
**Important:** Don't connect to the pre_save or post_save signals of the Version or Revision models directly, use the signals outlined below instead. The pre_save and post_save signals are no longer sent by the Version or Revision models since django-reversion 1.7.
reversion.pre_revision_commit
-----------------------------
This signal is triggered just before a revision is saved to the database. It receives the following keyword arguments:
* **instances** - A list of the model instances in the revision.
* **revision** - The unsaved Revision model.
* **versions** - The unsaved Version models in the revision.
reversion.signals.pre_revision_commit
-------------------------------------
reversion.post_revision_commit
------------------------------
Sent just before a revision is saved to the database.
This signal is triggered just after a revision is saved to the database. It receives the following keyword arguments:
.. include:: /_include/signal-args.rst
* **instances** - A list of the model instances in the revision.
* **revision** - The saved Revision model.
* **versions** - The saved Version models in the revision.
reversion.signals.post_revision_commit
--------------------------------------
Connecting to signals
---------------------
Sent just after a revision and its related versions are saved to the database.
The signals listed above are sent only once *per revision*, rather than once *per model in the revision*. In practice, this means that you should connect to the signals without specifying a `sender`, as below::
.. include:: /_include/signal-args.rst
def on_revision_commit(**kwargs):
pass # Your signal handler code here.
reversion.post_revision_commit.connect(on_revision_commit)
To execute code only when a revision has been saved for a particular Model, you should inspect the contents of the `instances` parameter, as below::
def on_revision_commit(instances, **kwargs):
for instance in instances:
if isinstance(instance, MyModel):
pass # Your signal handler code here.
reversion.post_revision_commit.connect(on_revision_commit)

View File

@ -1,45 +0,0 @@
.. _views:
Views
=====
Shortcuts when using django-reversion in views.
Decorators
----------
``reversion.views.create_revision(manage_manually=False, using=None, atomic=True)``
Decorates a view to wrap every request that isn't ``GET``, ``HEAD`` or ``OPTIONS`` in a revision block.
The request user will also be added to the revision metadata. You can set the revision comment by calling :ref:`reversion.set_comment() <set_comment>` within your view.
.. include:: /_include/create-revision-args.rst
reversion.views.RevisionMixin
-----------------------------
Mixin a class-based view to wrap every request that isn't ``GET``, ``HEAD`` or ``OPTIONS`` in a revision block.
The request user will also be added to the revision metadata. You can set the revision comment by calling :ref:`reversion.set_comment() <set_comment>` within your view.
.. code:: python
from django.contrib.auth.views import FormView
from reversion.views import RevisionMixin
class RevisionFormView(RevisionMixin, FormView):
pass
``RevisionMixin.revision_manage_manually = False``
.. include:: /_include/create-revision-manage-manually.rst
``RevisionMixin.revision_using = None``
.. include:: /_include/create-revision-using.rst

View File

@ -1,39 +0,0 @@
"""
An extension to the Django web framework that provides version control for model instances.
Developed by Dave Hall.
<http://www.etianen.com/>
"""
try:
import django # noqa
except ImportError: # pragma: no cover
# The top-level API requires Django, which might not be present if setup.py
# is importing reversion to get __version__.
pass
else:
from reversion.errors import ( # noqa
RevertError,
RevisionManagementError,
RegistrationError,
)
from reversion.revisions import ( # noqa
is_active,
is_manage_manually,
get_user,
set_user,
get_comment,
set_comment,
get_date_created,
set_date_created,
add_meta,
add_to_revision,
create_revision,
register,
is_registered,
unregister,
get_registered_models,
)
__version__ = VERSION = (2, 0, 12)

View File

@ -1,305 +0,0 @@
from __future__ import unicode_literals
import json
from contextlib import contextmanager
from django.db import models, transaction, connection
from django.conf.urls import url
from django.contrib import admin, messages
from django.contrib.admin import options
from django.contrib.admin.models import LogEntry
from django.contrib.admin.utils import unquote, quote
try:
from django.contrib.contenttypes.admin import GenericInlineModelAdmin
from django.contrib.contenttypes.fields import GenericRelation
except ImportError: # Django < 1.9 pragma: no cover
from django.contrib.contenttypes.generic import GenericInlineModelAdmin, GenericRelation
try:
from django.urls import reverse
except ImportError: # Django < 1.10 pragma: no cover
from django.core.urlresolvers import reverse
from django.core.exceptions import PermissionDenied, ImproperlyConfigured
from django.shortcuts import get_object_or_404, render, redirect
from django.utils.text import capfirst
from django.utils.timezone import template_localtime
from django.utils.translation import ugettext as _
from django.utils.encoding import force_text
from django.utils.formats import localize
from reversion.compat import remote_field, remote_model
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
def private_fields(meta):
try:
return meta.private_fields
except AttributeError: # Django < 1.10 pragma: no cover
return meta.virtual_fields
class VersionAdmin(admin.ModelAdmin):
object_history_template = "reversion/object_history.html"
change_list_template = "reversion/change_list.html"
revision_form_template = None
recover_list_template = None
recover_form_template = None
history_latest_first = False
def reversion_register(self, model, **kwargs):
"""Registers the model with reversion."""
register(model, **kwargs)
@contextmanager
def create_revision(self, request):
with create_revision():
set_user(request.user)
yield
# Revision helpers.
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),
"reversion/%s" % template_name,
)
def _reversion_order_version_queryset(self, queryset):
"""Applies the correct ordering to the given version queryset."""
if not self.history_latest_first:
queryset = queryset.order_by("pk")
return queryset
# Messages.
def log_addition(self, request, object, change_message=None):
change_message = change_message or _("Initial version.")
if is_active():
# If https://code.djangoproject.com/ticket/27218 is implemented, we
# could first call super() and get the change_message from the returned
# LogEntry.
if isinstance(change_message, list):
set_comment(LogEntry(change_message=json.dumps(change_message)).get_change_message())
else:
set_comment(change_message)
try:
super(VersionAdmin, self).log_addition(request, object, change_message)
except TypeError: # Django < 1.9 pragma: no cover
super(VersionAdmin, self).log_addition(request, object)
def log_change(self, request, object, message):
if is_active():
if isinstance(message, list):
set_comment(LogEntry(change_message=json.dumps(message)).get_change_message())
else:
set_comment(message)
super(VersionAdmin, self).log_change(request, object, message)
# Auto-registration.
def _reversion_autoregister(self, model, follow):
if not is_registered(model):
for parent_model, field in model._meta.concrete_model._meta.parents.items():
follow += (field.name,)
self._reversion_autoregister(parent_model, ())
self.reversion_register(model, follow=follow)
def _reversion_introspect_inline_admin(self, inline):
inline_model = None
follow_field = None
fk_name = None
if issubclass(inline, GenericInlineModelAdmin):
inline_model = inline.model
ct_field = inline.ct_field
fk_name = inline.ct_fk_field
for field in private_fields(self.model._meta):
if (
isinstance(field, GenericRelation) and
remote_model(field) == inline_model and
field.object_id_field_name == fk_name and
field.content_type_field_name == ct_field
):
follow_field = field.name
break
elif issubclass(inline, options.InlineModelAdmin):
inline_model = inline.model
fk_name = inline.fk_name
if not fk_name:
for field in inline_model._meta.get_fields():
if (
isinstance(field, (models.ForeignKey, models.OneToOneField)) and
issubclass(self.model, remote_model(field))
):
fk_name = field.name
break
if fk_name and not remote_field(inline_model._meta.get_field(fk_name)).is_hidden():
field = inline_model._meta.get_field(fk_name)
accessor = remote_field(field).get_accessor_name()
follow_field = accessor
return inline_model, follow_field
def __init__(self, *args, **kwargs):
super(VersionAdmin, self).__init__(*args, **kwargs)
# Automatically register models if required.
if not is_registered(self.model):
inline_fields = ()
for inline in self.inlines:
inline_model, follow_field = self._reversion_introspect_inline_admin(inline)
if inline_model:
self._reversion_autoregister(inline_model, ())
if follow_field:
inline_fields += (follow_field,)
self._reversion_autoregister(self.model, inline_fields)
def get_urls(self):
urls = super(VersionAdmin, self).get_urls()
admin_site = self.admin_site
opts = self.model._meta
info = opts.app_label, opts.model_name,
reversion_urls = [
url("^recover/$", admin_site.admin_view(self.recoverlist_view), name='%s_%s_recoverlist' % info),
url("^recover/(\d+)/$", admin_site.admin_view(self.recover_view), name='%s_%s_recover' % info),
url("^([^/]+)/history/(\d+)/$", admin_site.admin_view(self.revision_view), name='%s_%s_revision' % info),
]
return reversion_urls + urls
# Views.
def add_view(self, request, form_url='', extra_context=None):
with self.create_revision(request):
return super(VersionAdmin, self).add_view(request, form_url, extra_context)
def change_view(self, request, object_id, form_url='', extra_context=None):
with self.create_revision(request):
return super(VersionAdmin, self).change_view(request, object_id, form_url, extra_context)
def _reversion_revisionform_view(self, request, version, template_name, extra_context=None):
# Check that database transactions are supported.
if not connection.features.uses_savepoints:
raise ImproperlyConfigured("Cannot use VersionAdmin with a database that does not support savepoints.")
# Run the view.
try:
with transaction.atomic(using=version.db):
# Revert the revision.
version.revision.revert(delete=True)
# Run the normal changeform view.
with self.create_revision(request):
response = self.changeform_view(request, quote(version.object_id), request.path, extra_context)
# Decide on whether the keep the changes.
if request.method == "POST" and response.status_code == 302:
set_comment(_("Reverted to previous version, saved on %(datetime)s") % {
"datetime": localize(template_localtime(version.revision.date_created)),
})
else:
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.
except RevertError as ex:
opts = self.model._meta
messages.error(request, force_text(ex))
return redirect("{}:{}_{}_changelist".format(self.admin_site.name, opts.app_label, opts.model_name))
except _RollBackRevisionView as ex:
return ex.response
return response
def recover_view(self, request, version_id, extra_context=None):
"""Displays a form that can recover a deleted model."""
# The revisionform view will check for change permission (via changeform_view),
# but we also need to check for add permissions here.
if not self.has_add_permission(request):
raise PermissionDenied
# Render the recover view.
version = get_object_or_404(Version, pk=version_id)
context = {
"title": _("Recover %(name)s") % {"name": version.object_repr},
"recover": True,
}
context.update(extra_context or {})
return self._reversion_revisionform_view(
request,
version,
self.recover_form_template or self._reversion_get_template_list("recover_form.html"),
context,
)
def revision_view(self, request, object_id, version_id, extra_context=None):
"""Displays the contents of the given revision."""
object_id = unquote(object_id) # Underscores in primary key get quoted to "_5F"
version = get_object_or_404(Version, pk=version_id, object_id=object_id)
context = {
"title": _("Revert %(name)s") % {"name": version.object_repr},
"revert": True,
}
context.update(extra_context or {})
return self._reversion_revisionform_view(
request,
version,
self.revision_form_template or self._reversion_get_template_list("revision_form.html"),
context,
)
def changelist_view(self, request, extra_context=None):
with self.create_revision(request):
context = {
"has_change_permission": self.has_change_permission(request),
}
context.update(extra_context or {})
return super(VersionAdmin, self).changelist_view(request, context)
def recoverlist_view(self, request, extra_context=None):
"""Displays a deleted model to allow recovery."""
# Check if user has change and add permissions for model
if not self.has_change_permission(request) or not self.has_add_permission(request):
raise PermissionDenied
model = self.model
opts = model._meta
deleted = self._reversion_order_version_queryset(Version.objects.get_deleted(self.model))
# Set the app name.
request.current_app = self.admin_site.name
# Get the rest of the context.
context = dict(
self.admin_site.each_context(request),
opts=opts,
app_label=opts.app_label,
module_name=capfirst(opts.verbose_name),
title=_("Recover deleted %(name)s") % {"name": force_text(opts.verbose_name_plural)},
deleted=deleted,
)
context.update(extra_context or {})
return render(
request,
self.recover_list_template or self._reversion_get_template_list("recover_list.html"),
context,
)
def history_view(self, request, object_id, extra_context=None):
"""Renders the history view."""
# Check if user has change permissions for model
if not self.has_change_permission(request):
raise PermissionDenied
opts = self.model._meta
action_list = [
{
"revision": version.revision,
"url": reverse(
"%s:%s_%s_revision" % (self.admin_site.name, opts.app_label, opts.model_name),
args=(quote(version.object_id), version.id)
),
}
for version
in self._reversion_order_version_queryset(Version.objects.get_for_object_reference(
self.model,
unquote(object_id), # Underscores in primary key get quoted to "_5F"
).select_related("revision__user"))
]
# Compile the context.
context = {"action_list": action_list}
context.update(extra_context or {})
return super(VersionAdmin, self).history_view(request, object_id, context)

View File

@ -1,17 +0,0 @@
import django
def remote_field(field):
# remote_field is new in Django 1.9
return field.remote_field if hasattr(field, 'remote_field') else field.rel
def remote_model(field):
# remote_field is new in Django 1.9
return field.remote_field.model if hasattr(field, 'remote_field') else field.rel.to
def is_authenticated(user):
if django.VERSION < (1, 10):
return user.is_authenticated()
return user.is_authenticated

View File

@ -1,13 +0,0 @@
class RevertError(Exception):
"""Exception thrown when something goes wrong with reverting a model."""
class RevisionManagementError(Exception):
"""Exception that is thrown when something goes wrong with revision managment."""
class RegistrationError(Exception):
"""Exception thrown when registration with django-reversion goes wrong."""

View File

@ -1,125 +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 <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2015-06-15 01:49+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: ar\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 "
"&& n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;\n"
#: reversion/admin.py:161
msgid "Initial version."
msgstr "النسخة الأولية"
#: reversion/admin.py:195 reversion/templates/reversion/change_list.html:7
#: reversion/templates/reversion/recover_form.html:10
#: reversion/templates/reversion/recover_list.html:10
#, python-format
msgid "Recover deleted %(name)s"
msgstr "أستعيد المحذوف من %(name)s"
#: reversion/admin.py:312
#, python-format
msgid "Reverted to previous version, saved on %(datetime)s"
msgstr "اُعيد لنسخه سابقه، حُفظ في %(datetime)s"
#: reversion/admin.py:314
#, python-format
msgid ""
"The %(model)s \"%(name)s\" was reverted successfully. You may edit it again "
"below."
msgstr ""
"تم أعاده %(model)s \"%(name)s\" بنجاح ، يمكنك/ي التعديل مجددا"
""
#: reversion/admin.py:399
#, python-format
msgid "Recover %(name)s"
msgstr "إستعيد %(name)s"
#: reversion/admin.py:413
#, python-format
msgid "Revert %(name)s"
msgstr "أعد %(name)s"
#: reversion/models.py:59
msgid "date created"
msgstr "تاريخ الأنشاء"
#: reversion/models.py:66
msgid "user"
msgstr "المستخدم"
#: reversion/models.py:70
msgid "comment"
msgstr "التعليق"
#: reversion/templates/reversion/object_history.html:8
msgid ""
"Choose a date from the list below to revert to a previous version of this "
"object."
msgstr "أختر تاريخ من القائمه أدناه لأعاده نسخه سابقه من هذا الكيان"
#: reversion/templates/reversion/object_history.html:15
#: reversion/templates/reversion/recover_list.html:23
msgid "Date/time"
msgstr "التاريخ/ الوقت"
#: reversion/templates/reversion/object_history.html:16
msgid "User"
msgstr "المستخدم"
#: reversion/templates/reversion/object_history.html:17
msgid "Comment"
msgstr "التعليق"
#: reversion/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 "لا يوجد تاريخ تعديل لهذا الكيان. ربما لم يُنشأ من موقع الإداره"
#: reversion/templates/reversion/recover_form.html:7
#: reversion/templates/reversion/recover_list.html:7
#: reversion/templates/reversion/revision_form.html:7
msgid "Home"
msgstr "الرئيسيه"
#: reversion/templates/reversion/recover_form.html:17
msgid "Press the save button below to recover this version of the object."
msgstr "أنقر على حفظ أدناه لأسترجاع هذه النسخه"
#: reversion/templates/reversion/recover_list.html:17
msgid ""
"Choose a date from the list below to recover a deleted version of an object."
msgstr "أختر تاريخ من القائمه أدناه لإسترجاع نسخه سابقه من هذا الكيان"
#: reversion/templates/reversion/recover_list.html:37
msgid "There are no deleted objects to recover."
msgstr "لا يوجد كيانات محذوفه لإسترجاعها"
#: reversion/templates/reversion/revision_form.html:11
msgid "History"
msgstr "التاريخ"
#: reversion/templates/reversion/revision_form.html:12
#, python-format
msgid "Revert %(verbose_name)s"
msgstr "إسترجع %(verbose_name)s"
#: reversion/templates/reversion/revision_form.html:25
msgid "Press the save button below to revert to this version of the object."
msgstr "أنقر على حفظ أدناه لإعاده هذه النسخه"

View File

@ -1,134 +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.
# Gonzalo Bustos, 2015.
#
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2015-10-11 19:10-0300\n"
"PO-Revision-Date: 2015-10-11 19:12-0300\n"
"Last-Translator: Gonzalo Bustos\n"
"Language-Team: Spanish (Argentina)\n"
"Language: es_AR\n"
"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"
"X-Generator: Poedit 1.6.10\n"
#: admin.py:144
msgid "Initial version."
msgstr "Versión inicial."
#: admin.py:166
#, python-format
msgid "Deleted %(verbose_name)s."
msgstr "%(verbose_name)s eliminados"
#: admin.py:189 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 "Restaurar %(name)s eliminados"
#: admin.py:304
#, python-format
msgid "Reverted to previous version, saved on %(datetime)s"
msgstr "Revertido a una versión anterior, guardada el %(datetime)s"
#: admin.py:306
#, python-format
msgid ""
"The %(model)s \"%(name)s\" was reverted successfully. You may edit it again "
"below."
msgstr ""
"El %(model)s \"%(name)s\" fue revertido con éxito. Puede editarlo "
"nuevamente a continuación."
#: admin.py:392
#, python-format
msgid "Recover %(name)s"
msgstr "Restaurar %(name)s"
#: admin.py:406
#, python-format
msgid "Revert %(name)s"
msgstr "Revertir %(name)s"
#: models.py:59
msgid "date created"
msgstr "fecha de creación"
#: models.py:65
msgid "user"
msgstr "usuario"
#: models.py:69
msgid "comment"
msgstr "comentario"
#: templates/reversion/object_history.html:8
msgid ""
"Choose a date from the list below to revert to a previous version of this "
"object."
msgstr ""
"Elija una fecha del listado a continuación para revertir a una versión "
"anterior de este objeto"
#: templates/reversion/object_history.html:15
#: templates/reversion/recover_list.html:24
msgid "Date/time"
msgstr "Fecha/hora"
#: templates/reversion/object_history.html:16
msgid "User"
msgstr "Usuario"
#: templates/reversion/object_history.html:17
msgid "Comment"
msgstr "Comentario"
#: templates/reversion/object_history.html:36
msgid ""
"This object doesn't have a change history. It probably wasn't added via this "
"admin site."
msgstr ""
"Este objeto no tiene un historial de cambios. Es probable que no haya sido "
"agregado a través del sitio de administración."
#: templates/reversion/recover_form.html:8
#: templates/reversion/recover_list.html:8
#: templates/reversion/revision_form.html:8
msgid "Home"
msgstr "Inicio"
#: templates/reversion/recover_form.html:18
msgid "Press the save button below to recover this version of the object."
msgstr "Presione el botón guardar para restaurar esta versión del objeto."
#: templates/reversion/recover_list.html:18
msgid ""
"Choose a date from the list below to recover a deleted version of an object."
msgstr ""
"Elija una fecha del listado a continuación para restaurar una versión "
"eliminada del objeto."
#: templates/reversion/recover_list.html:38
msgid "There are no deleted objects to recover."
msgstr "No hay objetos eliminados para restaurar."
#: templates/reversion/revision_form.html:12
msgid "History"
msgstr "Historial"
#: templates/reversion/revision_form.html:13
#, python-format
msgid "Revert %(verbose_name)s"
msgstr "Revertir %(verbose_name)s"
#: templates/reversion/revision_form.html:26
msgid "Press the save button below to revert to this version of the object."
msgstr "Presione el botón guardar para revertir a esta versión del objeto."

View File

@ -1,134 +0,0 @@
# Translation of django-reversion into Ukrainian.
# This file is distributed under the same license as the django-reversion package.
# Illia Volochii <illia.volochii@gmail.com>, 2017.
msgid ""
msgstr ""
"Project-Id-Version: django-reversion\n"
"Report-Msgid-Bugs-To: https://github.com/etianen/django-reversion/issues\n"
"POT-Creation-Date: 2017-11-03 12:02+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Illia Volochii <illia.volochii@gmail.com>\n"
"Language-Team: Ukrainian\n"
"Language: uk\n"
"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"
#: reversion/admin.py:83
msgid "Initial version."
msgstr "Початкова версія."
#: reversion/admin.py:197
#, python-format
msgid "Reverted to previous version, saved on %(datetime)s"
msgstr "Повернуто до попередньої версії, яка збережена %(datetime)s"
#: reversion/admin.py:221
#, python-format
msgid "Recover %(name)s"
msgstr "Відновити %(name)s"
#: reversion/admin.py:237
#, python-format
msgid "Revert %(name)s"
msgstr "Повернути %(name)s"
#: reversion/admin.py:272 reversion/templates/reversion/change_list.html:7
#: reversion/templates/reversion/recover_form.html:10
#: reversion/templates/reversion/recover_list.html:10
#, python-format
msgid "Recover deleted %(name)s"
msgstr "Відновити видалені %(name)s"
#: reversion/models.py:31
#, python-format
msgid "Could not save %(object_repr)s version - missing dependency."
msgstr "Неможливо зберегти версію \"%(object_repr)s\" - відсутня залежність."
#: reversion/models.py:45
msgid "date created"
msgstr "дата створення"
#: reversion/models.py:54
msgid "user"
msgstr "користувач"
#: reversion/models.py:60
msgid "comment"
msgstr "коментар"
#: reversion/models.py:242
#, python-format
msgid "Could not load %(object_repr)s version - incompatible version data."
msgstr ""
"Неможливо завантажити версію \"%(object_repr)s\" - несумісні дані версій."
#: reversion/models.py:246
#, python-format
msgid "Could not load %(object_repr)s version - unknown serializer %(format)s."
msgstr ""
"Неможливо завантажити версію \"%(object_repr)s\" - невідомий серіалізатор "
"%(format)s."
#: reversion/templates/reversion/object_history.html:8
msgid ""
"Choose a date from the list below to revert to a previous version of this "
"object."
msgstr ""
"Виберіть дату із списку нижче, щоб повернутися до попередньої версії цього "
"об'єкта."
#: reversion/templates/reversion/object_history.html:15
#: reversion/templates/reversion/recover_list.html:23
msgid "Date/time"
msgstr "Дата/час"
#: reversion/templates/reversion/object_history.html:16
msgid "User"
msgstr "Користувач"
#: reversion/templates/reversion/object_history.html:17
msgid "Action"
msgstr "Дія"
#: reversion/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 ""
"Цей об'єкт не має історії змін. Напевно, він був доданий не через цей сайт "
"адміністрування."
#: reversion/templates/reversion/recover_form.html:7
#: reversion/templates/reversion/recover_list.html:7
#: reversion/templates/reversion/revision_form.html:7
msgid "Home"
msgstr "Домівка"
#: reversion/templates/reversion/recover_form.html:20
msgid "Press the save button below to recover this version of the object."
msgstr "Натисніть кнопку \"Зберегти\" нижче, щоб відновити цю версію об'єкта."
#: reversion/templates/reversion/recover_list.html:17
msgid ""
"Choose a date from the list below to recover a deleted version of an object."
msgstr "Виберіть дату із списку нижче, щоб відновити видалену версію об'єкта."
#: reversion/templates/reversion/recover_list.html:37
msgid "There are no deleted objects to recover."
msgstr "Не знайдено видалених об'єктів для відновлення."
#: reversion/templates/reversion/revision_form.html:11
msgid "History"
msgstr "Історія"
#: reversion/templates/reversion/revision_form.html:12
#, python-format
msgid "Revert %(verbose_name)s"
msgstr "Повернути %(verbose_name)s"
#: reversion/templates/reversion/revision_form.html:21
msgid "Press the save button below to revert to this version of the object."
msgstr ""
"Натисніть кнопку \"Зберегти\" нижче, щоб повернутися до цієї версії об'єкта."

View File

@ -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 <EMAIL@ADDRESS>, 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 <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: zh_Hans\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 "单击保存按钮将此对象恢复到此版本。"

View File

@ -1,57 +0,0 @@
from __future__ import unicode_literals
from django.apps import apps
from django.contrib import admin
from django.core.management.base import BaseCommand, CommandError
from reversion.revisions import is_registered
class BaseRevisionCommand(BaseCommand):
def add_arguments(self, parser):
super(BaseRevisionCommand, self).add_arguments(parser)
parser.add_argument(
"app_label",
metavar="app_label",
nargs="*",
help="Optional app_label or app_label.model_name list.",
)
parser.add_argument(
"--using",
default=None,
help="The database to query for revision data.",
)
parser.add_argument(
"--model-db",
default=None,
help="The database to query for model data.",
)
def get_models(self, options):
# Load admin classes.
admin.autodiscover()
# Get options.
app_labels = options["app_label"]
# Parse model classes.
if len(app_labels) == 0:
selected_models = apps.get_models()
else:
selected_models = set()
for label in app_labels:
if "." in label:
# This is an app.Model specifier.
try:
model = apps.get_model(label)
except LookupError:
raise CommandError("Unknown model: {}".format(label))
selected_models.add(model)
else:
# This is just an app - no model qualifier.
app_label = label
try:
app = apps.get_app_config(app_label)
except LookupError:
raise CommandError("Unknown app: {}".format(app_label))
selected_models.update(app.get_models())
for model in selected_models:
if is_registered(model):
yield model

View File

@ -1,74 +0,0 @@
from __future__ import unicode_literals
from django.db import 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
class Command(BaseRevisionCommand):
help = "Creates initial revisions for a given app [and model]."
def add_arguments(self, parser):
super(Command, self).add_arguments(parser)
parser.add_argument(
"--comment",
action="store",
default="Initial version.",
help="Specify the comment to add to the revisions. Defaults to 'Initial version'.")
parser.add_argument(
"--batch-size",
action="store",
type=int,
default=500,
help="For large sets of data, revisions will be populated in batches. Defaults to 500.",
)
def handle(self, *app_labels, **options):
verbosity = options["verbosity"]
using = options["using"]
model_db = options["model_db"]
comment = options["comment"]
batch_size = options["batch_size"]
# Create revisions.
using = using or router.db_for_write(Revision)
with transaction.atomic(using=using):
for model in self.get_models(options):
# Check all models for empty revisions.
if verbosity >= 1:
self.stdout.write("Creating revisions for {name}".format(
name=model._meta.verbose_name,
))
created_count = 0
live_objs = _safe_subquery(
"exclude",
model._default_manager.using(model_db),
model._meta.pk.name,
Version.objects.using(using).get_for_model(
model,
model_db=model_db,
),
"object_id",
)
# 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):
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,
))
# Print out a message, if feeling verbose.
if verbosity >= 1:
self.stdout.write("- Created {total} / {total}".format(
total=total,
))

View File

@ -1,95 +0,0 @@
from __future__ import unicode_literals
from datetime import timedelta
from django.db import transaction, models, router
from django.utils import timezone
from reversion.models import Revision, Version
from reversion.management.commands import BaseRevisionCommand
class Command(BaseRevisionCommand):
help = "Deletes revisions for a given app [and model]."
def add_arguments(self, parser):
super(Command, self).add_arguments(parser)
parser.add_argument(
"--days",
default=0,
type=int,
help="Delete only revisions older than the specified number of days.",
)
parser.add_argument(
"--keep",
default=0,
type=int,
help="Keep the specified number of revisions (most recent) for each object.",
)
def handle(self, *app_labels, **options):
verbosity = options["verbosity"]
using = options["using"]
model_db = options["model_db"]
days = options["days"]
keep = options["keep"]
# Delete revisions.
using = using or router.db_for_write(Revision)
with transaction.atomic(using=using):
revision_query = models.Q()
keep_revision_ids = set()
# By default, delete nothing.
can_delete = False
# Get all revisions for the given revision manager and model.
for model in self.get_models(options):
if verbosity >= 1:
self.stdout.write("Finding stale revisions for {name}".format(
name=model._meta.verbose_name,
))
# Find all matching revision IDs.
model_query = Version.objects.using(using).get_for_model(
model,
model_db=model_db,
)
if keep:
overflow_object_ids = list(Version.objects.using(using).get_for_model(
model,
model_db=model_db,
).order_by().values_list("object_id").annotate(
count=models.Count("object_id"),
).filter(
count__gt=keep,
).values_list("object_id", flat=True).iterator())
# Only delete overflow revisions.
model_query = model_query.filter(object_id__in=overflow_object_ids)
for object_id in overflow_object_ids:
if verbosity >= 2:
self.stdout.write("- Finding stale revisions for {name} #{object_id}".format(
name=model._meta.verbose_name,
object_id=object_id,
))
# But keep the underflow revisions.
keep_revision_ids.update(Version.objects.using(using).get_for_object_reference(
model,
object_id,
model_db=model_db,
).values_list("revision_id", flat=True)[:keep].iterator())
# Add to revision query.
revision_query |= models.Q(
pk__in=model_query.order_by().values_list("revision_id", flat=True)
)
# If we have at least one model, then we can delete.
can_delete = True
if can_delete:
revisions_to_delete = Revision.objects.using(using).filter(
revision_query,
date_created__lt=timezone.now() - timedelta(days=days),
).exclude(
pk__in=keep_revision_ids
).order_by()
else:
revisions_to_delete = Revision.objects.using(using).none()
# Print out a message, if feeling verbose.
if verbosity >= 1:
self.stdout.write("Deleting {total} revisions...".format(
total=revisions_to_delete.count(),
))
revisions_to_delete.delete()

View File

@ -1,51 +0,0 @@
import sys
from reversion.revisions import create_revision as create_revision_base
from reversion.views import _request_creates_revision, _set_user_from_request, create_revision
class RevisionMiddleware(object):
"""Wraps the entire request in a revision."""
manage_manually = False
using = None
atomic = True
def __init__(self, get_response=None):
super(RevisionMiddleware, self).__init__()
# Support Django 1.10 middleware.
if get_response is not None:
self.get_response = create_revision(
manage_manually=self.manage_manually,
using=self.using,
atomic=self.atomic
)(get_response)
def process_request(self, request):
if _request_creates_revision(request):
context = create_revision_base(
manage_manually=self.manage_manually,
using=self.using,
atomic=self.atomic
)
context.__enter__()
if not hasattr(request, "_revision_middleware"):
setattr(request, "_revision_middleware", {})
request._revision_middleware[self] = context
def _close_revision(self, request, is_exception):
if self in getattr(request, "_revision_middleware", {}):
_set_user_from_request(request)
request._revision_middleware.pop(self).__exit__(*sys.exc_info() if is_exception else (None, None, None))
def process_response(self, request, response):
self._close_revision(request, False)
return response
def process_exception(self, request, exception):
self._close_revision(request, True)
def __call__(self, request):
return self.get_response(request)

View File

@ -1,54 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.7 on 2016-06-06 13:22
from __future__ import unicode_literals
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
replaces = [('reversion', '0001_initial'), ('reversion', '0002_auto_20141216_1509'), ('reversion', '0003_auto_20160601_1600'), ('reversion', '0004_auto_20160611_1202')]
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('contenttypes', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='Revision',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('date_created', models.DateTimeField(db_index=True, help_text='The date and time this revision was created.', verbose_name='date created')),
('comment', models.TextField(blank=True, help_text='A text comment on this revision.', verbose_name='comment')),
('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",)
},
),
migrations.CreateModel(
name='Version',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('object_id', models.CharField(help_text='Primary key of the model under version control.', max_length=191)),
('format', models.CharField(help_text='The serialization format used by this model.', max_length=255)),
('serialized_data', models.TextField(help_text='The serialized form of this version of the model.')),
('object_repr', models.TextField(help_text='A string representation of the object.')),
('content_type', models.ForeignKey(help_text='Content type of the model under version control.', on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')),
('revision', models.ForeignKey(help_text='The revision that contains this version.', on_delete=django.db.models.deletion.CASCADE, to='reversion.Revision')),
('db', models.CharField(help_text='The database the model under version control is stored in.', max_length=191)),
],
options={
"ordering": ("-pk",)
},
),
migrations.AlterUniqueTogether(
name='version',
unique_together=set([('db', 'content_type', 'object_id', 'revision')]),
),
]

View File

@ -1,108 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.6 on 2016-06-01 16:00
from __future__ import unicode_literals
from collections import defaultdict
from django.db import DEFAULT_DB_ALIAS, migrations, models, router
from django.apps import apps as live_apps
def de_dupe_version_table(apps, schema_editor):
"""
Removes some duplicate Version models that may have crept into the database and will prevent the
unique index being added by migration 0004.
"""
db_alias = schema_editor.connection.alias
Version = apps.get_model("reversion", "Version")
keep_version_ids = Version.objects.using(db_alias).order_by().values_list(
# Group by the unique constraint we intend to enforce.
"revision_id",
"content_type_id",
"object_id",
).annotate(
# Add in the most recent id for each duplicate row.
max_pk=models.Max("pk"),
).values_list("max_pk", flat=True)
# Do not do anything if we're keeping all ids anyway.
if keep_version_ids.count() == Version.objects.using(db_alias).all().count():
return
# Delete all duplicate versions. Can't do this as a delete with subquery because MySQL doesn't like running a
# subquery on the table being updated/deleted.
delete_version_ids = list(Version.objects.using(db_alias).exclude(
pk__in=keep_version_ids,
).values_list("pk", flat=True))
Version.objects.using(db_alias).filter(
pk__in=delete_version_ids,
).delete()
def set_version_db(apps, schema_editor):
"""
Updates the db field in all Version models to point to the correct write
db for the model.
"""
db_alias = schema_editor.connection.alias
Version = apps.get_model("reversion", "Version")
content_types = Version.objects.using(db_alias).order_by().values_list(
"content_type_id",
"content_type__app_label",
"content_type__model"
).distinct()
model_dbs = defaultdict(list)
for content_type_id, app_label, model_name in content_types:
# We need to be able to access all models in the project, and we can't
# specify them up-front in the migration dependencies. So we have to
# just get the live model. This should be fine, since we don't actually
# manipulate the live model in any way.
try:
model = live_apps.get_model(app_label, model_name)
except LookupError:
# If the model appears not to exist, play it safe and use the default db.
db = "default"
else:
db = router.db_for_write(model)
model_dbs[db].append(content_type_id)
# Update db field.
# speedup for case when there is only default db
if DEFAULT_DB_ALIAS in model_dbs and len(model_dbs) == 1:
Version.objects.using(db_alias).update(db=DEFAULT_DB_ALIAS)
else:
for db, content_type_ids in model_dbs.items():
Version.objects.using(db_alias).filter(
content_type__in=content_type_ids
).update(db=db)
class Migration(migrations.Migration):
dependencies = [
('reversion', '0002_auto_20141216_1509'),
]
operations = [
migrations.RemoveField(
model_name='revision',
name='manager_slug',
),
migrations.RemoveField(
model_name='version',
name='object_id_int',
),
migrations.AlterField(
model_name='version',
name='object_id',
field=models.CharField(help_text='Primary key of the model under version control.', max_length=191),
),
migrations.AlterField(
model_name='revision',
name='date_created',
field=models.DateTimeField(db_index=True, help_text='The date and time this revision was created.', verbose_name='date created'),
),
migrations.AddField(
model_name='version',
name='db',
field=models.CharField(null=True, help_text='The database the model under version control is stored in.', max_length=191),
),
migrations.RunPython(de_dupe_version_table),
migrations.RunPython(set_version_db),
]

View File

@ -1,24 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.7 on 2016-06-11 12:02
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('reversion', '0003_auto_20160601_1600'),
]
operations = [
migrations.AlterField(
model_name='version',
name='db',
field=models.CharField(help_text='The database the model under version control is stored in.', max_length=191),
),
migrations.AlterUniqueTogether(
name='version',
unique_together=set([('db', 'content_type', 'object_id', 'revision')]),
),
]

View File

@ -1,356 +0,0 @@
from __future__ import unicode_literals
from collections import defaultdict
from itertools import chain
from django.contrib.contenttypes.models import ContentType
try:
from django.contrib.contenttypes.fields import GenericForeignKey
except ImportError: # Django < 1.9 pragma: no cover
from django.contrib.contenttypes.generic import GenericForeignKey
from django.conf import settings
from django.core import serializers
from django.core.serializers.base import DeserializationError
from django.core.exceptions import ObjectDoesNotExist
from django.db import models, IntegrityError, transaction, router, connections
from django.db.models.deletion import Collector
from django.db.models.expressions import RawSQL
from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _, ugettext
from django.utils.encoding import force_text, python_2_unicode_compatible
from reversion.errors import RevertError
from reversion.revisions import _get_options, _get_content_type, _follow_relations_recursive
def _safe_revert(versions):
unreverted_versions = []
for version in versions:
try:
with transaction.atomic(using=version.db):
version.revert()
except (IntegrityError, ObjectDoesNotExist):
unreverted_versions.append(version)
if len(unreverted_versions) == len(versions):
raise RevertError(ugettext("Could not save %(object_repr)s version - missing dependency.") % {
"object_repr": unreverted_versions[0],
})
if unreverted_versions:
_safe_revert(unreverted_versions)
@python_2_unicode_compatible
class Revision(models.Model):
"""A group of related serialized versions."""
date_created = models.DateTimeField(
db_index=True,
verbose_name=_("date created"),
help_text="The date and time this revision was created.",
)
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
blank=True,
null=True,
on_delete=models.SET_NULL,
verbose_name=_("user"),
help_text="The user who created this revision.",
)
comment = models.TextField(
blank=True,
verbose_name=_("comment"),
help_text="A text comment on this revision.",
)
def revert(self, delete=False):
# Group the models by the database of the serialized model.
versions_by_db = defaultdict(list)
for version in self.version_set.iterator():
versions_by_db[version.db].append(version)
# For each db, perform a separate atomic revert.
for version_db, versions in versions_by_db.items():
with transaction.atomic(using=version_db):
# Optionally delete objects no longer in the current revision.
if delete:
# Get a set of all objects in this revision.
old_revision = set()
for version in versions:
model = version._model
try:
# Load the model instance from the same DB as it was saved under.
old_revision.add(model._default_manager.using(version.db).get(pk=version.object_id))
except model.DoesNotExist:
pass
# Calculate the set of all objects that are in the revision now.
current_revision = chain.from_iterable(
_follow_relations_recursive(obj)
for obj in old_revision
)
# Delete objects that are no longer in the current revision.
collector = Collector(using=version_db)
collector.collect([item for item in current_revision if item not in old_revision])
collector.delete()
# Attempt to revert all revisions.
_safe_revert(versions)
def __str__(self):
return ", ".join(force_text(version) for version in self.version_set.all())
class Meta:
app_label = "reversion"
ordering = ("-pk",)
class SubquerySQL(RawSQL):
def as_sql(self, compiler, connection):
return self.sql, self.params
class VersionQuerySet(models.QuerySet):
def get_for_model(self, model, model_db=None):
model_db = model_db or router.db_for_write(model)
content_type = _get_content_type(model, self.db)
return self.filter(
content_type=content_type,
db=model_db,
)
def get_for_object_reference(self, model, object_id, model_db=None):
return self.get_for_model(model, model_db=model_db).filter(
object_id=object_id,
)
def get_for_object(self, obj, model_db=None):
return self.get_for_object_reference(obj.__class__, obj.pk, model_db=model_db)
def get_deleted(self, model, model_db=None):
# Try to do a faster JOIN.
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"):
content_type = _get_content_type(model, self.db)
subquery = SubquerySQL(
"""
SELECT MAX(V.{id})
FROM {version} AS V
LEFT JOIN {model} ON V.{object_id} = CAST({model}.{model_id} as {str})
WHERE
V.{db} = %s AND
V.{content_type_id} = %s AND
{model}.{model_id} IS NULL
GROUP BY V.{object_id}
""".format(
id=connection.ops.quote_name("id"),
version=connection.ops.quote_name(Version._meta.db_table),
model=connection.ops.quote_name(model._meta.db_table),
model_id=connection.ops.quote_name(model._meta.pk.db_column or model._meta.pk.attname),
object_id=connection.ops.quote_name("object_id"),
str=Version._meta.get_field("object_id").db_type(connection),
db=connection.ops.quote_name("db"),
content_type_id=connection.ops.quote_name("content_type_id"),
),
(model_db, content_type.id),
output_field=Version._meta.pk,
)
else:
# We have to use a slow subquery.
subquery = self.get_for_model(model, model_db=model_db).exclude(
object_id__in=list(
model._default_manager.using(model_db).values_list("pk", flat=True).order_by().iterator()
),
).values_list("object_id").annotate(
latest_pk=models.Max("pk")
).order_by().values_list("latest_pk", flat=True)
# Perform the subquery.
return self.filter(
pk__in=subquery,
)
def get_unique(self):
last_key = None
for version in self.iterator():
key = (version.object_id, version.content_type_id, version.db, version._local_field_dict)
if last_key != key:
yield version
last_key = key
@python_2_unicode_compatible
class Version(models.Model):
"""A saved version of a database model."""
objects = VersionQuerySet.as_manager()
revision = models.ForeignKey(
Revision,
on_delete=models.CASCADE,
help_text="The revision that contains this version.",
)
object_id = models.CharField(
max_length=191,
help_text="Primary key of the model under version control.",
)
content_type = models.ForeignKey(
ContentType,
on_delete=models.CASCADE,
help_text="Content type of the model under version control.",
)
@property
def _content_type(self):
return ContentType.objects.db_manager(self._state.db).get_for_id(self.content_type_id)
@property
def _model(self):
return self._content_type.model_class()
# A link to the current instance, not the version stored in this Version!
object = GenericForeignKey(
ct_field="content_type",
fk_field="object_id",
)
db = models.CharField(
max_length=191,
help_text="The database the model under version control is stored in.",
)
format = models.CharField(
max_length=255,
help_text="The serialization format used by this model.",
)
serialized_data = models.TextField(
help_text="The serialized form of this version of the model.",
)
object_repr = models.TextField(
help_text="A string representation of the object.",
)
@cached_property
def _object_version(self):
data = self.serialized_data
data = force_text(data.encode("utf8"))
try:
return list(serializers.deserialize(self.format, data, ignorenonexistent=True))[0]
except DeserializationError:
raise RevertError(ugettext("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.") % {
"object_repr": self.object_repr,
"format": self.format,
})
@cached_property
def _local_field_dict(self):
"""
A dictionary mapping field names to field values in this version
of the model.
Parent links of inherited multi-table models will not be followed.
"""
version_options = _get_options(self._model)
object_version = self._object_version
obj = object_version.object
model = self._model
field_dict = {}
for field_name in version_options.fields:
field = model._meta.get_field(field_name)
if isinstance(field, models.ManyToManyField):
# M2M fields with a custom through are not stored in m2m_data, but as a separate model.
if field.attname in object_version.m2m_data:
field_dict[field.attname] = object_version.m2m_data[field.attname]
else:
field_dict[field.attname] = getattr(obj, field.attname)
return field_dict
@cached_property
def field_dict(self):
"""
A dictionary mapping field names to field values in this version
of the model.
This method will follow parent links, if present.
"""
field_dict = self._local_field_dict
# Add parent data.
for parent_model, field in self._model._meta.concrete_model._meta.parents.items():
content_type = _get_content_type(parent_model, self._state.db)
parent_id = field_dict[field.attname]
parent_version = self.revision.version_set.get(
content_type=content_type,
object_id=parent_id,
db=self.db,
)
field_dict.update(parent_version.field_dict)
return field_dict
def revert(self):
self._object_version.save(using=self.db)
def __str__(self):
return self.object_repr
class Meta:
app_label = 'reversion'
unique_together = (
("db", "content_type", "object_id", "revision"),
)
ordering = ("-pk",)
class _Str(models.Func):
"""Casts a value to the database's text type."""
function = "CAST"
template = "%(function)s(%(expressions)s as %(db_type)s)"
def __init__(self, expression):
super(_Str, self).__init__(expression, output_field=models.TextField())
def as_sql(self, compiler, connection):
self.extra["db_type"] = self.output_field.db_type(connection)
return super(_Str, self).as_sql(compiler, connection)
def _safe_subquery(method, left_query, left_field_name, right_subquery, right_field_name):
right_subquery = right_subquery.order_by().values_list(right_field_name, flat=True)
left_field = left_query.model._meta.get_field(left_field_name)
right_field = right_subquery.model._meta.get_field(right_field_name)
# If the databases don't match, we have to do it in-memory.
# If it's not a supported database, we also have to do it in-memory.
if (
left_query.db != right_subquery.db or not
(
left_field.get_internal_type() != right_field.get_internal_type() and
connections[left_query.db].vendor in ("sqlite", "postgresql")
)
):
right_subquery = 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_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_subquery = right_subquery.annotate(**{
right_field_name_str: _Str(right_field_name),
}).values_list(right_field_name_str, flat=True)
# All done!
return getattr(left_query, method)(**{
"{}__in".format(left_field_name): right_subquery,
})

View File

@ -1,433 +0,0 @@
from __future__ import unicode_literals
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.models.query import QuerySet
from django.db.models.signals import post_save, m2m_changed
from django.utils.encoding import force_text
from django.utils import timezone, six
from reversion.compat import remote_field
from reversion.errors import RevisionManagementError, RegistrationError
from reversion.signals import pre_revision_commit, post_revision_commit
_VersionOptions = namedtuple("VersionOptions", (
"fields",
"follow",
"format",
"for_concrete_model",
"ignore_duplicates",
))
_StackFrame = namedtuple("StackFrame", (
"manage_manually",
"user",
"comment",
"date_created",
"db_versions",
"meta",
))
class _Local(local):
def __init__(self):
self.stack = ()
_local = _Local()
def is_active():
return bool(_local.stack)
def _current_frame():
if not is_active():
raise RevisionManagementError("There is no active revision for this thread")
return _local.stack[-1]
def _copy_db_versions(db_versions):
return {
db: versions.copy()
for db, versions
in db_versions.items()
}
def _push_frame(manage_manually, using):
if is_active():
current_frame = _current_frame()
db_versions = _copy_db_versions(current_frame.db_versions)
db_versions.setdefault(using, {})
stack_frame = current_frame._replace(
manage_manually=manage_manually,
db_versions=db_versions,
)
else:
stack_frame = _StackFrame(
manage_manually=manage_manually,
user=None,
comment="",
date_created=timezone.now(),
db_versions={using: {}},
meta=(),
)
_local.stack += (stack_frame,)
def _update_frame(**kwargs):
_local.stack = _local.stack[:-1] + (_current_frame()._replace(**kwargs),)
def _pop_frame():
prev_frame = _current_frame()
_local.stack = _local.stack[:-1]
if is_active():
current_frame = _current_frame()
db_versions = {
db: prev_frame.db_versions[db]
for db
in current_frame.db_versions.keys()
}
_update_frame(
user=prev_frame.user,
comment=prev_frame.comment,
date_created=prev_frame.date_created,
db_versions=db_versions,
meta=prev_frame.meta,
)
def is_manage_manually():
return _current_frame().manage_manually
def set_user(user):
_update_frame(user=user)
def get_user():
return _current_frame().user
def set_comment(comment):
_update_frame(comment=comment)
def get_comment():
return _current_frame().comment
def set_date_created(date_created):
_update_frame(date_created=date_created)
def get_date_created():
return _current_frame().date_created
def add_meta(model, **values):
_update_frame(meta=_current_frame().meta + ((model, values),))
def _follow_relations(obj):
version_options = _get_options(obj.__class__)
for follow_name in version_options.follow:
try:
follow_obj = getattr(obj, follow_name)
except ObjectDoesNotExist:
continue
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
elif follow_obj is not None:
raise RegistrationError("{name}.{follow_name} should be a Model or QuerySet".format(
name=obj.__class__.__name__,
follow_name=follow_name,
))
def _follow_relations_recursive(obj):
def do_follow(obj):
if obj not in relations:
relations.add(obj)
for related in _follow_relations(obj):
do_follow(related)
relations = set()
do_follow(obj)
return relations
def _add_to_revision(obj, using, model_db, explicit):
from reversion.models import Version
# Exit early if the object is not fully-formed.
if obj.pk is None:
return
version_options = _get_options(obj.__class__)
content_type = _get_content_type(obj.__class__, using)
object_id = force_text(obj.pk)
version_key = (content_type, object_id)
# If the obj is already in the revision, stop now.
db_versions = _current_frame().db_versions
versions = db_versions[using]
if version_key in versions and not explicit:
return
# Get the version data.
version = Version(
content_type=content_type,
object_id=object_id,
db=model_db,
format=version_options.format,
serialized_data=serializers.serialize(
version_options.format,
(obj,),
fields=version_options.fields,
),
object_repr=force_text(obj),
)
# If the version is a duplicate, stop now.
if version_options.ignore_duplicates and explicit:
previous_version = Version.objects.using(using).get_for_object(obj, model_db=model_db).first()
if previous_version and previous_version._local_field_dict == version._local_field_dict:
return
# Store the version.
db_versions = _copy_db_versions(db_versions)
db_versions[using][version_key] = version
_update_frame(db_versions=db_versions)
# Follow relations.
for follow_obj in _follow_relations(obj):
_add_to_revision(follow_obj, using, model_db, False)
def add_to_revision(obj, model_db=None):
model_db = model_db or router.db_for_write(obj.__class__, instance=obj)
for db in _current_frame().db_versions.keys():
_add_to_revision(obj, db, model_db, True)
def _save_revision(versions, user=None, comment="", meta=(), date_created=None, using=None):
from reversion.models import Revision
# Only save versions that exist in the database.
model_db_pks = defaultdict(lambda: defaultdict(set))
for version in versions:
model_db_pks[version._model][version.db].add(version.object_id)
model_db_existing_pks = {
model: {
db: frozenset(map(
force_text,
model._default_manager.using(db).filter(pk__in=pks).values_list("pk", flat=True),
))
for db, pks in db_pks.items()
}
for model, db_pks in model_db_pks.items()
}
versions = [
version for version in versions
if version.object_id in model_db_existing_pks[version._model][version.db]
]
# Bail early if there are no objects to save.
if not versions:
return
# Save a new revision.
revision = Revision(
date_created=date_created,
user=user,
comment=comment,
)
# Send the pre_revision_commit signal.
pre_revision_commit.send(
sender=create_revision,
revision=revision,
versions=versions,
)
# Save the revision.
revision.save(using=using)
# Save version models.
for version in versions:
version.revision = revision
version.save(using=using)
# Save the meta information.
for meta_model, meta_fields in meta:
meta_model._default_manager.db_manager(using=using).create(
revision=revision,
**meta_fields
)
# Send the post_revision_commit signal.
post_revision_commit.send(
sender=create_revision,
revision=revision,
versions=versions,
)
@contextmanager
def _dummy_context():
yield
@contextmanager
def _create_revision_context(manage_manually, using, atomic):
_push_frame(manage_manually, using)
try:
context = transaction.atomic(using=using) if atomic else _dummy_context()
with context:
yield
# 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]):
current_frame = _current_frame()
_save_revision(
versions=current_frame.db_versions[using].values(),
user=current_frame.user,
comment=current_frame.comment,
meta=current_frame.meta,
date_created=current_frame.date_created,
using=using,
)
finally:
_pop_frame()
def create_revision(manage_manually=False, using=None, atomic=True):
from reversion.models import Revision
using = using or router.db_for_write(Revision)
return _ContextWrapper(_create_revision_context, (manage_manually, using, atomic))
class _ContextWrapper(object):
def __init__(self, func, args):
self._func = func
self._args = args
self._context = func(*args)
def __enter__(self):
return self._context.__enter__()
def __exit__(self, exc_type, exc_value, traceback):
return self._context.__exit__(exc_type, exc_value, traceback)
def __call__(self, func):
@wraps(func)
def do_revision_context(*args, **kwargs):
with self._func(*self._args):
return func(*args, **kwargs)
return do_revision_context
def _post_save_receiver(sender, instance, using, **kwargs):
if is_registered(sender) and is_active() and not is_manage_manually():
add_to_revision(instance, model_db=using)
def _m2m_changed_receiver(instance, using, action, model, reverse, **kwargs):
if action.startswith("post_") and not reverse:
if is_registered(instance) and is_active() and not is_manage_manually():
add_to_revision(instance, model_db=using)
def _get_registration_key(model):
return (model._meta.app_label, model._meta.model_name)
_registered_models = {}
def is_registered(model):
return _get_registration_key(model) in _registered_models
def get_registered_models():
return (apps.get_model(*key) for key in _registered_models.keys())
def _get_senders_and_signals(model):
yield model, post_save, _post_save_receiver
opts = model._meta.concrete_model._meta
for field in opts.local_many_to_many:
m2m_model = remote_field(field).through
if isinstance(m2m_model, six.string_types):
if "." not in m2m_model:
m2m_model = "{app_label}.{m2m_model}".format(
app_label=opts.app_label,
m2m_model=m2m_model
)
yield m2m_model, m2m_changed, _m2m_changed_receiver
def register(model=None, fields=None, exclude=(), follow=(), format="json",
for_concrete_model=True, ignore_duplicates=False):
def register(model):
# Prevent multiple registration.
if is_registered(model):
raise RegistrationError("{model} has already been registered with django-reversion".format(
model=model,
))
# Parse fields.
opts = model._meta.concrete_model._meta
version_options = _VersionOptions(
fields=tuple(
field_name
for field_name
in ([
field.name
for field
in opts.local_fields + opts.local_many_to_many
] if fields is None else fields)
if field_name not in exclude
),
follow=tuple(follow),
format=format,
for_concrete_model=for_concrete_model,
ignore_duplicates=ignore_duplicates,
)
# Register the model.
_registered_models[_get_registration_key(model)] = version_options
# Connect signals.
for sender, signal, signal_receiver in _get_senders_and_signals(model):
signal.connect(signal_receiver, sender=sender)
# All done!
return model
# Return a class decorator if model is not given
if model is None:
return register
# Register the model.
return register(model)
def _assert_registered(model):
if not is_registered(model):
raise RegistrationError("{model} has not been registered with django-reversion".format(
model=model,
))
def _get_options(model):
_assert_registered(model)
return _registered_models[_get_registration_key(model)]
def unregister(model):
_assert_registered(model)
del _registered_models[_get_registration_key(model)]
# Disconnect signals.
for sender, signal, signal_receiver in _get_senders_and_signals(model):
signal.disconnect(signal_receiver, sender=sender)
def _get_content_type(model, using):
from django.contrib.contenttypes.models import ContentType
version_options = _get_options(model)
return ContentType.objects.db_manager(using).get_for_model(
model,
for_concrete_model=version_options.for_concrete_model,
)

View File

@ -1,10 +0,0 @@
from django.dispatch.dispatcher import Signal
_signal_args = [
"revision",
"versions",
]
pre_revision_commit = Signal(providing_args=_signal_args)
post_revision_commit = Signal(providing_args=_signal_args)

View File

@ -1,10 +0,0 @@
{% 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 %}
<li><a href="{% url opts|admin_urlname:'recoverlist' %}" class="recoverlink">{% blocktrans with cl.opts.verbose_name_plural|escape as name %}Recover deleted {{name}}{% endblocktrans %}</a></li>
{% endif %}
{{block.super}}
{% endblock %}

View File

@ -1,25 +0,0 @@
{% extends "reversion/revision_form.html" %}
{% load i18n admin_urls %}
{% block breadcrumbs %}
<div class="breadcrumbs">
<a href="{% url 'admin:index' %}">{% trans "Home" %}</a> &rsaquo;
<a href="{% url 'admin:app_list' opts.app_label %}">{{opts.app_config.verbose_name}}</a> &rsaquo;
<a href="{% url opts|admin_urlname:'changelist' %}">{{opts.verbose_name_plural|capfirst}}</a> &rsaquo;
<a href="{% url opts|admin_urlname:'recoverlist' %}">{% blocktrans with opts.verbose_name_plural as name %}Recover deleted {{name}}{% endblocktrans %}</a> &rsaquo;
{{title}}
</div>
{% endblock %}
{% block object-tools %}{% endblock %}
{% block form_top %}
<p>{% blocktrans %}Press the save button below to recover this version of the object.{% endblocktrans %}</p>
{% endblock %}
{% block submit_buttons_top %}{% with is_popup=1 %}{{block.super}}{% endwith %}{% endblock %}
{% block submit_buttons_bottom %}{% with is_popup=1 %}{{block.super}}{% endwith %}{% endblock %}

View File

@ -1,26 +0,0 @@
{% extends "admin/change_form.html" %}
{% load i18n admin_urls %}
{% block breadcrumbs %}
<div class="breadcrumbs">
<a href="{% url 'admin:index' %}">{% trans "Home" %}</a> &rsaquo;
<a href="{% url 'admin:app_list' opts.app_label %}">{{opts.app_config.verbose_name}}</a> &rsaquo;
<a href="{% url opts|admin_urlname:'changelist' %}">{{opts.verbose_name_plural|capfirst}}</a> &rsaquo;
<a href="{% url opts|admin_urlname:'change' object_id %}">{{original|truncatewords:"18"}}</a> &rsaquo;
<a href="{% url opts|admin_urlname:'history' object_id %}">{% trans "History" %}</a> &rsaquo;
{% blocktrans with opts.verbose_name as verbose_name %}Revert {{verbose_name}}{% endblocktrans %}
</div>
{% endblock %}
{% block object-tools %}{% endblock %}
{% block form_top %}
<p>{% blocktrans %}Press the save button below to revert to this version of the object.{% endblocktrans %}</p>
{% endblock %}
{% block submit_buttons_top %}{% with is_popup=1 %}{{block.super}}{% endwith %}{% endblock %}
{% block submit_buttons_bottom %}{% with is_popup=1 %}{{block.super}}{% endwith %}{% endblock %}

View File

@ -1,68 +0,0 @@
from functools import wraps
from reversion.compat import is_authenticated
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")
def _set_user_from_request(request):
if getattr(request, "user", None) and is_authenticated(request.user) and get_user() is None:
set_user(request.user)
def create_revision(manage_manually=False, using=None, atomic=True):
"""
View decorator that wraps the request in a revision.
The revision will have it's user set from the request automatically.
"""
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
return func(request, *args, **kwargs)
return do_revision_view
return decorator
class RevisionMixin(object):
"""
A class-based view mixin that wraps the request in a revision.
The revision will have it's user set from the request automatically.
"""
revision_manage_manually = False
revision_using = None
revision_atomic = True
def __init__(self, *args, **kwargs):
super(RevisionMixin, self).__init__(*args, **kwargs)
self.dispatch = create_revision(
manage_manually=self.revision_manage_manually,
using=self.revision_using,
atomic=self.revision_atomic
)(self.dispatch)

View File

@ -1,2 +0,0 @@
[bdist_wheel]
universal = 1

View File

@ -1,45 +1,40 @@
from setuptools import setup, find_packages
from reversion import __version__
import sys
sys.path.insert(0, 'src/reversion')
from distutils.core import setup
from version 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,
"init_catalog": babel.init_catalog,
"update_catalog": babel.update_catalog,
}
cmdclass = {"compile_catalog": babel.compile_catalog,
"extract_messages": babel.extract_messages,
"init_catalog": babel.init_catalog,
"update_catalog": babel.update_catalog,}
except ImportError:
cmdclass = {}
setup(
name="django-reversion",
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.",
author="Dave Hall",
author_email="dave@etianen.com",
url="http://github.com/etianen/django-reversion",
zip_safe=False,
packages=find_packages(),
package_data={
"reversion": ["locale/*/LC_MESSAGES/django.*", "templates/reversion/*.html"]},
cmdclass=cmdclass,
install_requires=[
"django>=1.8",
],
classifiers=[
"Development Status :: 5 - Production/Stable",
"Environment :: Web Environment",
"Intended Audience :: Developers",
"License :: OSI Approved :: BSD License",
"Operating System :: OS Independent",
"Programming Language :: Python",
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
"Framework :: Django",
]
)
setup(name="django-reversion",
version='.'.join(str(x) for x in __version__),
license="BSD",
description="An extension to the Django web framework that provides comprehensive version control facilities",
author="Dave Hall",
author_email="dave@etianen.com",
url="http://github.com/etianen/django-reversion",
zip_safe=False,
packages=["reversion", "reversion.management", "reversion.management.commands", "reversion.migrations", "reversion.south_migrations"],
package_dir={"": "src"},
package_data = {"reversion": ["locale/*/LC_MESSAGES/django.*", "templates/reversion/*.html"]},
cmdclass = cmdclass,
classifiers=["Development Status :: 5 - Production/Stable",
"Environment :: Web Environment",
"Intended Audience :: Developers",
"License :: OSI Approved :: BSD License",
"Operating System :: OS Independent",
"Programming Language :: Python",
'Programming Language :: Python :: 2.6',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3.2',
'Programming Language :: Python :: 3.3',
'Programming Language :: Python :: 3.4',
"Framework :: Django",])

53
src/reversion/__init__.py Normal file
View File

@ -0,0 +1,53 @@
"""
Transactional version control for Django models.
Developed by Dave Hall.
<http://www.etianen.com/>
"""
from __future__ import unicode_literals
from reversion.revisions import default_revision_manager, revision_context_manager, VersionAdapter
from reversion.admin import VersionAdmin
from reversion.models import pre_revision_commit, post_revision_commit
from reversion.version import __version__
VERSION = __version__
# Legacy revision reference.
revision = default_revision_manager # TODO: Deprecate eventually.
# Easy registration methods.
register = default_revision_manager.register
is_registered = default_revision_manager.is_registered
unregister = default_revision_manager.unregister
get_adapter = default_revision_manager.get_adapter
get_registered_models = default_revision_manager.get_registered_models
# Context management.
create_revision = revision_context_manager.create_revision
# Revision meta data.
get_db = revision_context_manager.get_db
set_db = revision_context_manager.set_db
get_user = revision_context_manager.get_user
set_user = revision_context_manager.set_user
get_comment = revision_context_manager.get_comment
set_comment = revision_context_manager.set_comment
add_meta = revision_context_manager.add_meta
get_ignore_duplicates = revision_context_manager.get_ignore_duplicates
set_ignore_duplicates = revision_context_manager.set_ignore_duplicates
# Low level API.
get_for_object_reference = default_revision_manager.get_for_object_reference
get_for_object = default_revision_manager.get_for_object
get_unique_for_object = default_revision_manager.get_unique_for_object
get_for_date = default_revision_manager.get_for_date
get_deleted = default_revision_manager.get_deleted

485
src/reversion/admin.py Normal file
View File

@ -0,0 +1,485 @@
"""Admin extensions for django-reversion."""
from __future__ import unicode_literals
from functools import partial
from django import template
from django.db import models, transaction, connection
from django.conf.urls import patterns, url
from django.contrib import admin
from django.contrib.admin import helpers, options
try:
from django.contrib.admin.utils import unquote, quote
except ImportError: # Django < 1.7
from django.contrib.admin.util import unquote, quote
try:
from django.contrib.contenttypes.admin import GenericInlineModelAdmin
from django.contrib.contenttypes.fields import GenericRelation
except ImportError: # Django < 1.9
from django.contrib.contenttypes.generic import GenericInlineModelAdmin, GenericRelation
from django.contrib.contenttypes.models import ContentType
from django.core.urlresolvers import reverse
from django.forms.formsets import all_valid
from django.forms.models import model_to_dict
from django.http import HttpResponseRedirect
from django.core.exceptions import PermissionDenied
from django.shortcuts import get_object_or_404, render_to_response
from django.utils.html import mark_safe
from django.utils.text import capfirst
from django.utils.translation import ugettext as _
from django.utils.encoding import force_text
from django.utils.formats import localize
from reversion.models import Revision, Version, has_int_pk
from reversion.revisions import default_revision_manager, RegistrationError
class VersionAdmin(admin.ModelAdmin):
"""Abstract admin class for handling version controlled models."""
object_history_template = "reversion/object_history.html"
change_list_template = "reversion/change_list.html"
revision_form_template = None
recover_list_template = None
recover_form_template = None
# The revision manager instance used to manage revisions.
revision_manager = default_revision_manager
# The serialization format to use when registering models with reversion.
reversion_format = "json"
# Whether to ignore duplicate revision data.
ignore_duplicate_revisions = False
# If True, then the default ordering of object_history and recover lists will be reversed.
history_latest_first = False
def _autoregister(self, model, follow=None):
"""Registers a model with reversion, if required."""
if not self.revision_manager.is_registered(model):
follow = follow or []
# Use model_meta.concrete_model to catch proxy models
for parent_cls, field in model._meta.concrete_model._meta.parents.items():
follow.append(field.name)
self._autoregister(parent_cls)
self.revision_manager.register(model, follow=follow, format=self.reversion_format)
@property
def revision_context_manager(self):
"""The revision context manager for this VersionAdmin."""
return self.revision_manager._revision_context_manager
def _introspect_inline_admin(self, inline):
"""Introspects the given inline admin, returning a tuple of (inline_model, follow_field)."""
inline_model = None
follow_field = None
if issubclass(inline, GenericInlineModelAdmin):
inline_model = inline.model
ct_field = inline.ct_field
ct_fk_field = inline.ct_fk_field
for field in self.model._meta.virtual_fields:
if isinstance(field, GenericRelation) and field.rel.to == inline_model and field.object_id_field_name == ct_fk_field and field.content_type_field_name == ct_field:
follow_field = field.name
break
elif issubclass(inline, options.InlineModelAdmin):
inline_model = inline.model
fk_name = inline.fk_name
if not fk_name:
for field in inline_model._meta.fields:
if isinstance(field, (models.ForeignKey, models.OneToOneField)) and issubclass(self.model, field.rel.to):
fk_name = field.name
break
if fk_name and not inline_model._meta.get_field(fk_name).rel.is_hidden():
accessor = inline_model._meta.get_field(fk_name).related.get_accessor_name()
follow_field = accessor
return inline_model, follow_field
def __init__(self, *args, **kwargs):
"""Initializes the VersionAdmin"""
super(VersionAdmin, self).__init__(*args, **kwargs)
# Automatically register models if required.
if not self.revision_manager.is_registered(self.model):
inline_fields = []
for inline in self.inlines:
inline_model, follow_field = self._introspect_inline_admin(inline)
if inline_model:
self._autoregister(inline_model)
if follow_field:
inline_fields.append(follow_field)
self._autoregister(self.model, inline_fields)
# Wrap own methods in manual revision management.
self.add_view = self.revision_context_manager.create_revision(manage_manually=True)(self.add_view)
self.change_view = self.revision_context_manager.create_revision(manage_manually=True)(self.change_view)
self.recover_view = self.revision_context_manager.create_revision(manage_manually=True)(self.recover_view)
self.revision_view = self.revision_context_manager.create_revision(manage_manually=True)(self.revision_view)
self.changelist_view = self.revision_context_manager.create_revision(manage_manually=True)(self.changelist_view)
def _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),
"reversion/%s" % template_name,
)
def get_urls(self):
"""Returns the additional urls used by the Reversion admin."""
urls = super(VersionAdmin, self).get_urls()
admin_site = self.admin_site
opts = self.model._meta
info = opts.app_label, opts.model_name,
reversion_urls = patterns("",
url("^recover/$", admin_site.admin_view(self.recoverlist_view), name='%s_%s_recoverlist' % info),
url("^recover/([^/]+)/$", admin_site.admin_view(self.recover_view), name='%s_%s_recover' % info),
url("^([^/]+)/history/([^/]+)/$", admin_site.admin_view(self.revision_view), name='%s_%s_revision' % info),)
return reversion_urls + urls
def get_revision_instances(self, request, object):
"""Returns all the instances to be used in the object's revision."""
return [object]
def get_revision_data(self, request, object):
"""Returns all the revision data to be used in the object's revision."""
return dict(
(o, self.revision_manager.get_adapter(o.__class__).get_version_data(o))
for o in self.get_revision_instances(request, object)
)
def log_addition(self, request, object):
"""Sets the version meta information."""
super(VersionAdmin, self).log_addition(request, object)
self.revision_manager.save_revision(
self.get_revision_data(request, object),
user = request.user,
comment = _("Initial version."),
ignore_duplicates = self.ignore_duplicate_revisions,
db = self.revision_context_manager.get_db(),
)
def log_change(self, request, object, message):
"""Sets the version meta information."""
super(VersionAdmin, self).log_change(request, object, message)
self.revision_manager.save_revision(
self.get_revision_data(request, object),
user = request.user,
comment = message,
ignore_duplicates = self.ignore_duplicate_revisions,
db = self.revision_context_manager.get_db(),
)
def _order_version_queryset(self, queryset):
"""Applies the correct ordering to the given version queryset."""
if self.history_latest_first:
return queryset.order_by("-pk")
return queryset.order_by("pk")
def recoverlist_view(self, request, extra_context=None):
"""Displays a deleted model to allow recovery."""
# check if user has change or add permissions for model
if not self.has_change_permission(request) and not self.has_add_permission(request):
raise PermissionDenied
model = self.model
opts = model._meta
deleted = self._order_version_queryset(self.revision_manager.get_deleted(self.model))
context = {
"opts": opts,
"app_label": opts.app_label,
"module_name": capfirst(opts.verbose_name),
"title": _("Recover deleted %(name)s") % {"name": force_text(opts.verbose_name_plural)},
"deleted": deleted,
"changelist_url": reverse("%s:%s_%s_changelist" % (self.admin_site.name, opts.app_label, opts.model_name)),
}
extra_context = extra_context or {}
context.update(extra_context)
return render_to_response(self.recover_list_template or self._get_template_list("recover_list.html"),
context, template.RequestContext(request))
def get_revision_form_data(self, request, obj, version):
"""
Returns a dictionary of data to set in the admin form in order to revert
to the given revision.
"""
return version.field_dict
def get_related_versions(self, obj, version, FormSet):
"""Retreives all the related Version objects for the given FormSet."""
object_id = obj.pk
# Get the fk name.
try:
fk_name = FormSet.fk.name
except AttributeError:
# This is a GenericInlineFormset, or similar.
fk_name = FormSet.ct_fk_field.name
# Look up the revision data.
revision_versions = version.revision.version_set.all()
related_versions = dict([(related_version.object_id, related_version)
for related_version in revision_versions
if ContentType.objects.get_for_id(related_version.content_type_id).model_class() == FormSet.model
and force_text(related_version.field_dict[fk_name]) == force_text(object_id)])
return related_versions
def _hack_inline_formset_initial(self, inline, FormSet, formset, obj, version, revert, recover):
"""Hacks the given formset to contain the correct initial data."""
# if the FK this inline formset represents is not being followed, don't process data for it.
# see https://github.com/etianen/django-reversion/issues/222
_, follow_field = self._introspect_inline_admin(inline.__class__)
if follow_field not in self.revision_manager.get_adapter(self.model).follow:
return
# Now we hack it to push in the data from the revision!
initial = []
related_versions = self.get_related_versions(obj, version, FormSet)
formset.related_versions = related_versions
for related_obj in formset.queryset:
if force_text(related_obj.pk) in related_versions:
initial.append(related_versions.pop(force_text(related_obj.pk)).field_dict)
else:
initial_data = model_to_dict(related_obj)
initial_data["DELETE"] = True
initial.append(initial_data)
for related_version in related_versions.values():
initial_row = related_version.field_dict
pk_name = ContentType.objects.get_for_id(related_version.content_type_id).model_class()._meta.pk.name
del initial_row[pk_name]
initial.append(initial_row)
# Reconstruct the forms with the new revision data.
formset.initial = initial
formset.forms = [formset._construct_form(n) for n in range(len(initial))]
# Hack the formset to force a save of everything.
def get_changed_data(form):
return [field.name for field in form.fields]
for form in formset.forms:
form.has_changed = lambda: True
form._get_changed_data = partial(get_changed_data, form=form)
def total_form_count_hack(count):
return lambda: count
formset.total_form_count = total_form_count_hack(len(initial))
def render_revision_form(self, request, obj, version, context, revert=False, recover=False):
"""Renders the object revision form."""
model = self.model
opts = model._meta
object_id = obj.pk
# Generate the model form.
ModelForm = self.get_form(request, obj)
formsets = []
if request.method == "POST":
# This section is copied directly from the model admin change view
# method. Maybe one day there will be a hook for doing this better.
form = ModelForm(request.POST, request.FILES, instance=obj, initial=self.get_revision_form_data(request, obj, version))
if form.is_valid():
form_validated = True
new_object = self.save_form(request, form, change=True)
# HACK: If the value of a file field is None, remove the file from the model.
for field in new_object._meta.fields:
if isinstance(field, models.FileField) and field.name in form.cleaned_data and form.cleaned_data[field.name] is None:
setattr(new_object, field.name, None)
else:
form_validated = False
new_object = obj
prefixes = {}
for FormSet, inline in zip(self.get_formsets(request, new_object),
self.get_inline_instances(request)):
prefix = FormSet.get_default_prefix()
prefixes[prefix] = prefixes.get(prefix, 0) + 1
if prefixes[prefix] != 1:
prefix = "%s-%s" % (prefix, prefixes[prefix])
formset = FormSet(request.POST, request.FILES,
instance=new_object, prefix=prefix,
queryset=inline.get_queryset(request))
self._hack_inline_formset_initial(inline, FormSet, formset, obj, version, revert, recover)
# Add this hacked formset to the form.
formsets.append(formset)
if all_valid(formsets) and form_validated:
self.save_model(request, new_object, form, change=True)
form.save_m2m()
for formset in formsets:
# HACK: If the value of a file field is None, remove the file from the model.
related_objects = formset.save(commit=False)
for related_obj, related_form in zip(related_objects, formset.saved_forms):
for field in related_obj._meta.fields:
if isinstance(field, models.FileField) and field.name in related_form.cleaned_data and related_form.cleaned_data[field.name] is None:
setattr(related_obj, field.name, None)
related_obj.save()
formset.save_m2m()
change_message = _("Reverted to previous version, saved on %(datetime)s") % {"datetime": localize(version.revision.date_created)}
self.log_change(request, new_object, change_message)
self.message_user(request, _('The %(model)s "%(name)s" was reverted successfully. You may edit it again below.') % {"model": force_text(opts.verbose_name), "name": force_text(obj)})
# Redirect to the model change form.
if revert:
return HttpResponseRedirect("../../")
elif recover:
return HttpResponseRedirect("../../%s/" % quote(object_id))
else:
assert False
else:
# This is a mutated version of the code in the standard model admin
# change_view. Once again, a hook for this kind of functionality
# would be nice. Unfortunately, it results in doubling the number
# of queries required to construct the formets.
form = ModelForm(instance=obj, initial=self.get_revision_form_data(request, obj, version))
prefixes = {}
for FormSet, inline in zip(self.get_formsets(request, obj), self.get_inline_instances(request)):
# This code is standard for creating the formset.
prefix = FormSet.get_default_prefix()
prefixes[prefix] = prefixes.get(prefix, 0) + 1
if prefixes[prefix] != 1:
prefix = "%s-%s" % (prefix, prefixes[prefix])
formset = FormSet(instance=obj, prefix=prefix,
queryset=inline.get_queryset(request))
self._hack_inline_formset_initial(inline, FormSet, formset, obj, version, revert, recover)
# Add this hacked formset to the form.
formsets.append(formset)
# Generate admin form helper.
adminForm = helpers.AdminForm(form, self.get_fieldsets(request, obj),
self.prepopulated_fields, self.get_readonly_fields(request, obj),
model_admin=self)
media = self.media + adminForm.media
# Generate formset helpers.
inline_admin_formsets = []
for inline, formset in zip(self.get_inline_instances(request), formsets):
fieldsets = list(inline.get_fieldsets(request, obj))
readonly = list(inline.get_readonly_fields(request, obj))
prepopulated = inline.get_prepopulated_fields(request, obj)
inline_admin_formset = helpers.InlineAdminFormSet(inline, formset,
fieldsets, prepopulated, readonly, model_admin=self)
inline_admin_formsets.append(inline_admin_formset)
media = media + inline_admin_formset.media
# Generate the context.
context.update({"adminform": adminForm,
"object_id": object_id,
"original": obj,
"is_popup": False,
"media": mark_safe(media),
"inline_admin_formsets": inline_admin_formsets,
"errors": helpers.AdminErrorList(form, formsets),
"app_label": opts.app_label,
"add": False,
"change": True,
"revert": revert,
"recover": recover,
"has_add_permission": self.has_add_permission(request),
"has_change_permission": self.has_change_permission(request, obj),
"has_delete_permission": self.has_delete_permission(request, obj),
"has_file_field": True,
"has_absolute_url": False,
"form_url": mark_safe(request.path),
"opts": opts,
"content_type_id": ContentType.objects.get_for_model(self.model).id,
"save_as": False,
"save_on_top": self.save_on_top,
"changelist_url": reverse("%s:%s_%s_changelist" % (self.admin_site.name, opts.app_label, opts.model_name)),
"change_url": reverse("%s:%s_%s_change" % (self.admin_site.name, opts.app_label, opts.model_name), args=(quote(obj.pk),)),
"history_url": reverse("%s:%s_%s_history" % (self.admin_site.name, opts.app_label, opts.model_name), args=(quote(obj.pk),)),
"recoverlist_url": reverse("%s:%s_%s_recoverlist" % (self.admin_site.name, opts.app_label, opts.model_name))})
# Render the form.
if revert:
form_template = self.revision_form_template or self._get_template_list("revision_form.html")
elif recover:
form_template = self.recover_form_template or self._get_template_list("recover_form.html")
else:
assert False
return render_to_response(form_template, context, template.RequestContext(request))
@transaction.atomic
def recover_view(self, request, version_id, extra_context=None):
"""Displays a form that can recover a deleted model."""
# check if user has change or add permissions for model
if not self.has_change_permission(request) and not self.has_add_permission(request):
raise PermissionDenied
version = get_object_or_404(Version, pk=version_id)
obj = version.object_version.object
context = {"title": _("Recover %(name)s") % {"name": version.object_repr},}
context.update(extra_context or {})
return self.render_revision_form(request, obj, version, context, recover=True)
@transaction.atomic
def revision_view(self, request, object_id, version_id, extra_context=None):
"""Displays the contents of the given revision."""
# check if user has change or add permissions for model
if not self.has_change_permission(request):
raise PermissionDenied
object_id = unquote(object_id) # Underscores in primary key get quoted to "_5F"
obj = get_object_or_404(self.model, pk=object_id)
version = get_object_or_404(Version, pk=version_id, object_id=force_text(obj.pk))
# Generate the context.
context = {"title": _("Revert %(name)s") % {"name": force_text(self.model._meta.verbose_name)},}
context.update(extra_context or {})
return self.render_revision_form(request, obj, version, context, revert=True)
def changelist_view(self, request, extra_context=None):
"""Renders the change view."""
opts = self.model._meta
context = {"recoverlist_url": reverse("%s:%s_%s_recoverlist" % (self.admin_site.name, opts.app_label, opts.model_name)),
"add_url": reverse("%s:%s_%s_add" % (self.admin_site.name, opts.app_label, opts.model_name)),}
context.update(extra_context or {})
return super(VersionAdmin, self).changelist_view(request, context)
def history_view(self, request, object_id, extra_context=None):
"""Renders the history view."""
# check if user has change or add permissions for model
if not self.has_change_permission(request):
raise PermissionDenied
object_id = unquote(object_id) # Underscores in primary key get quoted to "_5F"
opts = self.model._meta
action_list = [
{
"revision": version.revision,
"url": reverse("%s:%s_%s_revision" % (self.admin_site.name, opts.app_label, opts.model_name), args=(quote(version.object_id), version.id)),
}
for version
in self._order_version_queryset(self.revision_manager.get_for_object_reference(
self.model,
object_id,
).select_related("revision__user"))
]
# Compile the context.
context = {"action_list": action_list}
context.update(extra_context or {})
return super(VersionAdmin, self).history_view(request, object_id, context)
class VersionMetaAdmin(VersionAdmin):
"""
An enhanced VersionAdmin that annotates the given object with information about
the last version that was saved.
"""
def get_queryset(self, request):
"""Returns the annotated queryset."""
content_type = ContentType.objects.get_for_model(self.model)
pk = self.model._meta.pk
if has_int_pk(self.model):
version_table_field = "object_id_int"
else:
version_table_field = "object_id"
return super(VersionMetaAdmin, self).get_queryset(request).extra(
select = {
"date_modified": """
SELECT MAX(%(revision_table)s.date_created)
FROM %(version_table)s
JOIN %(revision_table)s ON %(revision_table)s.id = %(version_table)s.revision_id
WHERE %(version_table)s.content_type_id = %%s AND %(version_table)s.%(version_table_field)s = %(table)s.%(pk)s
""" % {
"revision_table": connection.ops.quote_name(Revision._meta.db_table),
"version_table": connection.ops.quote_name(Version._meta.db_table),
"table": connection.ops.quote_name(self.model._meta.db_table),
"pk": connection.ops.quote_name(pk.db_column or pk.attname),
"version_table_field": connection.ops.quote_name(version_table_field),
}
},
select_params = (content_type.id,),
)
def get_date_modified(self, obj):
"""Displays the last modified date of the given object, typically for use in a change list."""
return localize(obj.date_modified)
get_date_modified.short_description = "date modified"

86
src/reversion/helpers.py Normal file
View File

@ -0,0 +1,86 @@
"""A number of useful helper functions to automate common tasks."""
from __future__ import unicode_literals
from django.contrib import admin
from django.contrib.admin.sites import NotRegistered
from django.utils.encoding import force_text
from reversion.admin import VersionAdmin
def patch_admin(model, admin_site=None):
"""
Enables version control with full admin integration for a model that has
already been registered with the django admin site.
This is excellent for adding version control to existing Django contrib
applications.
"""
admin_site = admin_site or admin.site
try:
ModelAdmin = admin_site._registry[model].__class__
except KeyError:
raise NotRegistered("The model {model} has not been registered with the admin site.".format(
model = model,
))
# Unregister existing admin class.
admin_site.unregister(model)
# Register patched admin class.
class PatchedModelAdmin(VersionAdmin, ModelAdmin):
pass
admin_site.register(model, PatchedModelAdmin)
# Patch generation methods, only available if the google-diff-match-patch
# library is installed.
#
# http://code.google.com/p/google-diff-match-patch/
try:
from diff_match_patch import diff_match_patch
except ImportError:
pass
else:
dmp = diff_match_patch()
def generate_diffs(old_version, new_version, field_name, cleanup):
"""Generates a diff array for the named field between the two versions."""
# Extract the text from the versions.
old_text = old_version.field_dict[field_name] or ""
new_text = new_version.field_dict[field_name] or ""
# Generate the patch.
diffs = dmp.diff_main(force_text(old_text), force_text(new_text))
if cleanup == "semantic":
dmp.diff_cleanupSemantic(diffs)
elif cleanup == "efficiency":
dmp.diff_cleanupEfficiency(diffs)
elif cleanup is None:
pass
else:
raise ValueError("cleanup parameter should be one of 'semantic', 'efficiency' or None.")
return diffs
def generate_patch(old_version, new_version, field_name, cleanup=None):
"""
Generates a text patch of the named field between the two versions.
The cleanup parameter can be None, "semantic" or "efficiency" to clean up the diff
for greater human readibility.
"""
diffs = generate_diffs(old_version, new_version, field_name, cleanup)
patch = dmp.patch_make(diffs)
return dmp.patch_toText(patch)
def generate_patch_html(old_version, new_version, field_name, cleanup=None):
"""
Generates a pretty html version of the differences between the named
field in two versions.
The cleanup parameter can be None, "semantic" or "efficiency" to clean up the diff
for greater human readibility.
"""
diffs = generate_diffs(old_version, new_version, field_name, cleanup)
return dmp.diff_prettyHtml(diffs)

Binary file not shown.

View File

@ -3,6 +3,7 @@
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
@ -11,7 +12,6 @@ msgstr ""
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: cs\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
@ -26,7 +26,7 @@ msgstr "Obnovit smazané %(name)s"
#: admin.py:165
#, python-format
msgid "Reverted to previous version, saved on %(datetime)s"
msgstr "Vráceno do předchozí verze uložené v %(datetime)s"
msgstr "Vrátit se k předchozí verzi uložené v %(datetime)s"
#: admin.py:167
#, python-format
@ -49,7 +49,7 @@ msgstr "Navrátit se k předchozí verzi %(name)s"
#: management/commands/createinitialrevisions.py:76
msgid "Initial version."
msgstr "První verze"
msgstr ""
#: templates/reversion/change_list.html:11
#, python-format
@ -125,4 +125,4 @@ msgstr "Navrátit %(verbose_name)s k předchozí verzi"
#: templates/reversion/revision_form.html:28
msgid "Press the save button below to revert to this version of the object."
msgstr "Klikněte na tlačítko uložit pro návrat k této verzi objektu."
msgstr "Klikněte na tlačítko uložit pro návrat k této verze objektu."

View File

@ -12,7 +12,7 @@ msgstr ""
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: da\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"

Binary file not shown.

View File

@ -11,7 +11,6 @@ msgstr ""
"PO-Revision-Date: 2009-02-03 08:41+0100\n"
"Last-Translator: Jannis Leidel <jannis@leidel.info>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: de\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
@ -46,10 +45,6 @@ msgstr "%s wiederherstellen"
msgid "Revert %(name)s"
msgstr "%(name)s zurücksetzen"
#: management/commands/createinitialrevisions.py:76
msgid "Initial version."
msgstr "Ursprüngliche Version."
#: templates/reversion/change_list.html:11
#, python-format
msgid "Add %(name)s"

View File

@ -11,7 +11,6 @@ msgstr ""
"PO-Revision-Date: 2009-12-10 10:45+0200\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: he\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"

View File

@ -11,7 +11,6 @@ msgstr ""
"PO-Revision-Date: 2009-08-29 13:44+0100\n"
"Last-Translator: Marco Beri <marcoberi@gmail.com>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: it\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"

View File

@ -10,7 +10,7 @@ msgstr ""
"PO-Revision-Date: 2011-10-17 10:17+0100\n"
"Last-Translator: Sindre Sorhus <sindresorhus@gmail.com>\n"
"Language-Team: \n"
"Language: nb\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"

Some files were not shown because too many files have changed in this diff Show More