django-reversion (1.8.7-1) unstable; urgency=medium
* New upstream release. # imported from the archive
This commit is contained in:
commit
41a17b0933
|
@ -0,0 +1,12 @@
|
|||
.DS_Store
|
||||
*.db
|
||||
.project
|
||||
.pydevproject
|
||||
.settings
|
||||
*.pyc
|
||||
*.pyo
|
||||
dist
|
||||
build
|
||||
MANIFEST
|
||||
src/django_reversion.egg-info/
|
||||
docs/_build
|
|
@ -0,0 +1,24 @@
|
|||
language: python
|
||||
python:
|
||||
- 2.6
|
||||
- 2.7
|
||||
- 3.2
|
||||
- 3.3
|
||||
- 3.4
|
||||
env:
|
||||
- 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
|
||||
install:
|
||||
- travis_retry pip install $DJANGO
|
||||
- pip install -e .
|
||||
script: python src/tests/runtests.py
|
||||
notifications:
|
||||
email: false
|
|
@ -0,0 +1,252 @@
|
|||
django-reversion changelog
|
||||
==========================
|
||||
|
||||
|
||||
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).
|
||||
|
||||
|
||||
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).
|
||||
|
||||
|
||||
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).
|
||||
|
||||
|
||||
1.8.4 - 07/09/2014
|
||||
------------------
|
||||
|
||||
- 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).
|
||||
|
||||
|
||||
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).
|
||||
|
||||
|
||||
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.
|
||||
|
||||
|
||||
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).
|
||||
|
||||
|
||||
1.7.1 - 26/06/2013
|
||||
------------------
|
||||
|
||||
- Bugfixes when using a custom User model.
|
||||
- Minor bugfixes.
|
||||
|
||||
|
||||
1.7 - 27/02/2013
|
||||
----------------
|
||||
|
||||
- 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.
|
||||
|
||||
|
||||
1.6.5 - 12/12/2012
|
||||
------------------
|
||||
|
||||
- Support for Django 1.4.3.
|
||||
|
||||
|
||||
1.6.4 - 28/10/2012
|
||||
------------------
|
||||
|
||||
- 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.
|
||||
|
||||
|
||||
1.6.2 - 31/07/2012
|
||||
------------------
|
||||
|
||||
- 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.
|
||||
|
||||
|
||||
1.6 - 27/03/2012
|
||||
----------------
|
||||
|
||||
- Django 1.4 compatibility.
|
||||
|
||||
|
||||
1.5.2 - 27/03/2012
|
||||
------------------
|
||||
|
||||
- Multi-db support.
|
||||
- Brazillian Portuguese translation.
|
||||
- New manage_manually revision mode.
|
||||
|
||||
|
||||
1.5.1 - 20/10/2011
|
||||
------------------
|
||||
|
||||
- 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.
|
||||
|
||||
|
||||
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.
|
||||
|
||||
|
||||
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.
|
||||
|
||||
|
||||
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.
|
||||
|
||||
|
||||
1.3.1 - 31/05/2010
|
||||
------------------
|
||||
|
||||
This release is compatible with Django 1.2.1.
|
||||
|
||||
- Django 1.2.1 admin compatibility.
|
||||
|
||||
|
||||
1.2.1 - 03/03/2010
|
||||
------------------
|
||||
|
||||
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.
|
||||
|
||||
|
||||
1.2 - 12/10/2009
|
||||
----------------
|
||||
|
||||
This release is compatible with Django 1.1.
|
||||
|
||||
- Django 1.1 admin compatibility.
|
||||
|
||||
|
||||
1.1.2 - 23/07/2009
|
||||
------------------
|
||||
|
||||
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.
|
||||
|
||||
|
||||
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.
|
|
@ -0,0 +1,27 @@
|
|||
Copyright (c) 2009, David Hall.
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice,
|
||||
this list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of David Hall nor the names of its contributors may be
|
||||
used to endorse or promote products derived from this software without
|
||||
specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
||||
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@ -0,0 +1,8 @@
|
|||
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 *
|
||||
prune docs/_build
|
|
@ -0,0 +1,82 @@
|
|||
django-reversion
|
||||
================
|
||||
|
||||
**django-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.
|
||||
|
||||
**django-reversion** can be easily added to your existing Django project with an
|
||||
absolute minimum of code changes.
|
||||
|
||||
|
||||
Documentation
|
||||
-------------
|
||||
|
||||
Please read the `Getting Started <http://django-reversion.readthedocs.org/en/latest/>`_
|
||||
guide for more information.
|
||||
|
||||
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>`_.
|
||||
|
||||
|
||||
Upgrading
|
||||
---------
|
||||
|
||||
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
|
||||
------------
|
||||
|
||||
Bug reports, bug fixes, and new features are always welcome. Please raise issues on the
|
||||
`django-reversion project site <http://github.com/etianen/django-reversion>`_, and submit
|
||||
pull requests for any new code.
|
||||
|
||||
You can run the test suite yourself from within a virtual environment with the following
|
||||
commands:
|
||||
|
||||
::
|
||||
|
||||
$ 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
|
||||
----------------
|
||||
|
||||
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:
|
||||
|
||||
- `Website <http://www.etianen.com/>`_
|
||||
- `Twitter <http://twitter.com/etianen>`_
|
||||
- `Google Profile <http://www.google.com/profiles/david.etianen>`_
|
|
@ -0,0 +1,134 @@
|
|||
django-reversion (1.8.7-1) unstable; urgency=medium
|
||||
|
||||
* New upstream release.
|
||||
|
||||
-- Michael Fladischer <fladi@debian.org> Tue, 02 Jun 2015 08:38:51 +0200
|
||||
|
||||
django-reversion (1.8.6-1) unstable; urgency=medium
|
||||
|
||||
* New upstream release.
|
||||
* Add intersphinx.patch to use local Python objects.inv for
|
||||
documentation.
|
||||
* Drop unnecessary versioned dependency on python3-django.
|
||||
* Change my email address to fladi@debian.org.
|
||||
|
||||
-- Michael Fladischer <fladi@debian.org> Tue, 19 May 2015 10:32:11 +0200
|
||||
|
||||
django-reversion (1.8.4-1) unstable; urgency=medium
|
||||
|
||||
* New upstream release (Closes: #755612).
|
||||
* Add dh-python to Build-Depends.
|
||||
* Switch buildsystem to pybuild.
|
||||
* Remove superfluous install file.
|
||||
* Add Python3 support.
|
||||
* Bump Build-Depends and Depends for python(3)-django to (>= 1.7).
|
||||
* Bump Standards-Version to 3.9.6.
|
||||
* Fix the way upstream tests are run during build.
|
||||
* Add lintian override for missing PGP signature on upstream tarballs.
|
||||
* Remove PHONY target declarations in d/rules.
|
||||
|
||||
-- Michael Fladischer <FladischerMichael@fladi.at> Mon, 22 Sep 2014 08:32:54 +0200
|
||||
|
||||
django-reversion (1.8-2) unstable; urgency=low
|
||||
|
||||
* Use correct BSD-3-clause license in d/copyright.
|
||||
|
||||
-- Michael Fladischer <FladischerMichael@fladi.at> Fri, 22 Nov 2013 07:27:22 +0100
|
||||
|
||||
django-reversion (1.8-1) unstable; urgency=low
|
||||
|
||||
* New upstream release.
|
||||
* Bump django to >= 1.6 in Depends and Build-Depends.
|
||||
* Bump Standards version to 3.9.5.
|
||||
* Add a spearate package for the upstream documentation.
|
||||
+ Add python-sphinx (>= 1.0.7+dfsg) to Build-Depends.
|
||||
+ Load sphinxdoc addon for debhelper.
|
||||
+ Build documentation with sphinx.
|
||||
+ Clean up documentation after build.
|
||||
+ Suggest python-django-reversion-doc from python-django-reversion
|
||||
package.
|
||||
* Change upstream changelog name to CHANGELOG.rst.
|
||||
* Change upstream readme name to README.rst.
|
||||
|
||||
-- Michael Fladischer <FladischerMichael@fladi.at> Thu, 07 Nov 2013 13:07:55 +0100
|
||||
|
||||
django-reversion (1.7.1-1) unstable; urgency=low
|
||||
|
||||
* New upstream release (Closes: #702080).
|
||||
* Bump Depends and Build-Depends on python-django to >= 1.5.
|
||||
* Bump Standards version to 3.9.4.
|
||||
* Bump Build-Depends on debhelper to >= 8.1.0 to satisfy build-arch
|
||||
and build-indep targets.
|
||||
* Change python-pybabel to python-babel in Build-Depends.
|
||||
* Drop versioned Build-Depends on python-all as no prior versions are
|
||||
available in Wheezy.
|
||||
* Update homepage URL in d/control (Closes: #702078).
|
||||
* Remove testproject directory to allow two builds in a row.
|
||||
|
||||
-- Michael Fladischer <FladischerMichael@fladi.at> Mon, 04 Nov 2013 14:34:46 +0100
|
||||
|
||||
django-reversion (1.6.6-1) unstable; urgency=low
|
||||
|
||||
[ Michael Fladischer ]
|
||||
* New upstream release.
|
||||
* Use github tags directly in d/watch.
|
||||
* Run tests with django-admin and manage.py.
|
||||
* Bump dependency on django to >= 1.4.4.
|
||||
* Drop temporary fix for PKG-INFO.
|
||||
* Update years in d/copyright.
|
||||
|
||||
[ Jakub Wilk ]
|
||||
* Use canonical URIs for Vcs-* fields.
|
||||
|
||||
-- Michael Fladischer <FladischerMichael@fladi.at> Thu, 24 Jan 2013 07:54:56 +0100
|
||||
|
||||
django-reversion (1.6-1) unstable; urgency=low
|
||||
|
||||
* New upstream release.
|
||||
* Bump Standards-Version to 3.9.3.
|
||||
* Update years in d/copyright.
|
||||
* Update DEP-5 URL to 1.0.
|
||||
* Increase dependency on python-django to >= 1.4.
|
||||
* Add python-django-south to B-D and Recommends.
|
||||
* Drop our own test project and use the one shipped by upstream.
|
||||
* Add 'set -e' to for loop in override_dh_auto_build.
|
||||
* Fix indent in d/copyright.
|
||||
* Remove d/s/local-options.
|
||||
|
||||
-- Michael Fladischer <FladischerMichael@fladi.at> Thu, 05 Apr 2012 07:30:17 +0200
|
||||
|
||||
django-reversion (1.5.1-1) unstable; urgency=low
|
||||
|
||||
* New upstream release.
|
||||
* Set PMPT as maintainer and myself as uploader.
|
||||
* Build .mo i18n files during build.
|
||||
* Use DEP5 for copyright file.
|
||||
* Drop debian/pyversions in favour of XS-Python-Version.
|
||||
* Update debian/watch to use http://githubredir.debian.net/.
|
||||
* Bumped Standards-Version to 3.9.2 (no change necessary).
|
||||
* Include upstream changelog.
|
||||
* Simplify debian/rules by using a static settings.py.
|
||||
* Update settings.py to Django-1.3 DATABASES format.
|
||||
* Bumped python-django dependency to (>= 1.3).
|
||||
* Add local-options to debian/source.
|
||||
* Streamlined files in debian/ with `wrap-and-sort`.
|
||||
* README has been renamed to README.markdown.
|
||||
* Update format URL in d/copyright.
|
||||
* Change order of my name.
|
||||
* Switch to dh_python2.
|
||||
* Temporary fix for Metadata-Version in django_reversion-1.5.1.egg-
|
||||
info.
|
||||
|
||||
-- Michael Fladischer <FladischerMichael@fladi.at> Wed, 28 Dec 2011 22:01:29 +0100
|
||||
|
||||
django-reversion (1.3.1-2) unstable; urgency=low
|
||||
|
||||
* Update to Debian policy 3.9.0.
|
||||
|
||||
-- Fladischer Michael <FladischerMichael@fladi.at> Mon, 05 Jul 2010 12:54:45 +0200
|
||||
|
||||
django-reversion (1.3.1-1) unstable; urgency=low
|
||||
|
||||
* Initial release (Closes: #581926)
|
||||
|
||||
-- Fladischer Michael <FladischerMichael@fladi.at> Mon, 17 May 2010 10:53:38 +0200
|
|
@ -0,0 +1 @@
|
|||
src/reversion/locale/*/LC_MESSAGES/django.mo
|
|
@ -0,0 +1 @@
|
|||
7
|
|
@ -0,0 +1,82 @@
|
|||
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 (>= 8.1.0~),
|
||||
dh-python,
|
||||
python-all,
|
||||
python-babel,
|
||||
python-django (>= 1.7),
|
||||
python-sphinx (>= 1.0.7+dfsg),
|
||||
python3-all,
|
||||
python3-babel,
|
||||
python3-django
|
||||
Standards-Version: 3.9.6
|
||||
X-Python-Version: >= 2.5
|
||||
X-Python3-Version: >= 3.2
|
||||
Homepage: https://github.com/etianen/django-reversion
|
||||
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
|
||||
Description: Provides comprehensive version control facilities for Django
|
||||
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.
|
||||
|
||||
Package: python3-django-reversion
|
||||
Architecture: all
|
||||
Depends: python3-django,
|
||||
${misc:Depends},
|
||||
${python3:Depends}
|
||||
Suggests: python-django-reversion-doc
|
||||
Description: Provides comprehensive version control facilities for Django (Python3 version)
|
||||
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 Python 3 version of the library.
|
||||
|
||||
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.
|
|
@ -0,0 +1,38 @@
|
|||
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://code.google.com/p/django-reversion/downloads/list
|
||||
|
||||
Files: *
|
||||
Copyright: 2009-2013, David Hall <david@etianen.com>
|
||||
License: BSD-3-clause
|
||||
|
||||
Files: debian/*
|
||||
Copyright: 2010-2015, Michael Fladischer <fladi@debian.org>
|
||||
License: BSD-3-clause
|
||||
|
||||
License: BSD-3-clause
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
.
|
||||
1. Redistributions of source code must retain the above copyright notice,
|
||||
this list of conditions and the following disclaimer.
|
||||
.
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
.
|
||||
3. Neither the name of David Hall nor the names of its contributors may be
|
||||
used to endorse or promote products derived from this software without
|
||||
specific prior written permission.
|
||||
.
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
||||
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@ -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'))
|
||||
+
|
|
@ -0,0 +1 @@
|
|||
intersphinx.patch
|
|
@ -0,0 +1,8 @@
|
|||
Document: python-django-reversion-doc
|
||||
Title: django-reversion documentation
|
||||
Author: Dave Hall <dave@etianen.com>
|
||||
Section: Programming/Python
|
||||
|
||||
Format: HTML
|
||||
Index: /usr/share/doc/python-django-reversion-doc/html/index.html
|
||||
Files: /usr/share/doc/python-django-reversion-doc/html/*.html
|
|
@ -0,0 +1 @@
|
|||
docs/.build/html
|
|
@ -0,0 +1 @@
|
|||
usr/share/doc/python-django-reversion-doc/html/_sources usr/share/doc/python-django-reversion-doc/rst
|
|
@ -0,0 +1 @@
|
|||
README.rst
|
|
@ -0,0 +1,32 @@
|
|||
#!/usr/bin/make -f
|
||||
#
|
||||
# Uncomment this to turn on verbose mode.
|
||||
#export DH_VERBOSE=1
|
||||
|
||||
export PYBUILD_NAME=django-reversion
|
||||
|
||||
%:
|
||||
dh $@ --with python2,python3,sphinxdoc --buildsystem=pybuild
|
||||
|
||||
override_dh_auto_test:
|
||||
ifeq (,$(filter nocheck,$(DEB_BUILD_OPTIONS)))
|
||||
set -e; \
|
||||
for python in $(shell pyversions -r) $(shell py3versions -r); do \
|
||||
PYTHONPATH="src" $$python src/tests/runtests.py ; \
|
||||
done
|
||||
endif
|
||||
|
||||
override_dh_auto_build:
|
||||
set -e; \
|
||||
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
|
||||
|
||||
override_dh_clean:
|
||||
dh_clean
|
||||
rm -rf docs/.build
|
|
@ -0,0 +1,16 @@
|
|||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': 'testproject/test.db3'
|
||||
}
|
||||
}
|
||||
INSTALLED_APPS = ["reversion",
|
||||
"django.contrib.auth",
|
||||
"django.contrib.contenttypes",
|
||||
"django.contrib.admin",
|
||||
"django.contrib.sites",
|
||||
"django.contrib.sessions",
|
||||
]
|
||||
SITE_ID = 1
|
||||
ROOT_URLCONF = "testproject.urls"
|
||||
SECRET_KEY = "1"
|
|
@ -0,0 +1 @@
|
|||
3.0 (quilt)
|
|
@ -0,0 +1,2 @@
|
|||
# Upstream does not provide detached PGP signatures for their source tarballs.
|
||||
django-reversion source: debian-watch-may-check-gpg-signature
|
|
@ -0,0 +1,3 @@
|
|||
version=3
|
||||
https://github.com/etianen/django-reversion/tags \
|
||||
/etianen/django-reversion/archive/release-([\d\.]+)\.tar\.gz
|
|
@ -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."
|
|
@ -0,0 +1,65 @@
|
|||
.. _admin:
|
||||
|
||||
Admin integration
|
||||
=================
|
||||
|
||||
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``.
|
||||
|
||||
::
|
||||
|
||||
import reversion
|
||||
|
||||
class YourModelAdmin(reversion.VersionAdmin):
|
||||
|
||||
pass
|
||||
|
||||
admin.site.register(YourModel, YourModelAdmin)
|
||||
|
||||
You can also use ``reversion.VersionAdmin`` as a mixin with another specialized admin class.
|
||||
|
||||
::
|
||||
|
||||
class YourModelAdmin(reversion.VersionAdmin, YourBaseModelAdmin):
|
||||
|
||||
pass
|
||||
|
||||
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:
|
||||
|
||||
::
|
||||
|
||||
from reversion.helpers import patch_admin
|
||||
|
||||
patch_admin(User)
|
||||
|
||||
|
||||
Admin customizations
|
||||
--------------------
|
||||
|
||||
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:
|
||||
|
||||
::
|
||||
|
||||
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'``)
|
||||
|
||||
|
||||
Customizing admin templates
|
||||
---------------------------
|
||||
|
||||
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.
|
||||
|
||||
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/auth/user/recover_list.html'``
|
||||
* ``'reversion/auth/recover_list.html'``
|
||||
* ``'reversion/recover_list.html'``
|
|
@ -0,0 +1,302 @@
|
|||
.. _api:
|
||||
|
||||
Low-level API
|
||||
=============
|
||||
|
||||
You can use django-reversion's API to build powerful version-controlled views outside of the built-in admin site.
|
||||
|
||||
|
||||
Registering models with django-reversion
|
||||
----------------------------------------
|
||||
|
||||
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.
|
||||
|
||||
::
|
||||
|
||||
import reversion
|
||||
|
||||
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):
|
||||
...
|
||||
|
||||
@reversion.register(format='yaml')
|
||||
class YourOtherModel(models.Model):
|
||||
...
|
||||
|
||||
**Warning:** If you’re using django-reversion in an management command, and are using the automatic ``VersionAdmin`` registration method, then you’ll need to import the relevant ``admin.py`` file at the top of your management command file.
|
||||
|
||||
**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
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
|
||||
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.
|
||||
|
||||
To enable the revision middleware, simply add it to your ``MIDDLEWARE_CLASSES`` setting as follows::
|
||||
|
||||
MIDDLEWARE_CLASSES = (
|
||||
'reversion.middleware.RevisionMiddleware',
|
||||
# Other middleware goes here...
|
||||
)
|
||||
|
||||
**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()``.
|
||||
|
||||
|
||||
Version meta data
|
||||
-----------------
|
||||
|
||||
It is possible to attach a comment and a user reference to an active revision using the following method::
|
||||
|
||||
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.
|
||||
|
||||
Custom meta data
|
||||
^^^^^^^^^^^^^^^^
|
||||
|
||||
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
|
||||
|
||||
class VersionRating(models.Model):
|
||||
revision = models.ForeignKey(Revision) # There must be a relationship with Revision
|
||||
rating = models.PositiveIntegerField()
|
||||
|
||||
You can then attach this meta class to a revision using the following method::
|
||||
|
||||
reversion.add_meta(VersionRating, rating=5)
|
||||
|
||||
|
||||
Reverting to previous revisions
|
||||
-------------------------------
|
||||
|
||||
To revert a model to a previous version, use the following method::
|
||||
|
||||
your_model = YourModel.objects.get(pk=1)
|
||||
|
||||
# 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)
|
||||
|
||||
# Find the most recent version for a given date:
|
||||
version = reversion.get_for_date(your_model, datetime.datetime(2008, 7, 10))
|
||||
|
||||
# Access the model data stored within the version:
|
||||
version_data = version.field_dict
|
||||
|
||||
# Revert all objects in this revision:
|
||||
version.revision.revert()
|
||||
|
||||
# Revert all objects in this revision, deleting related objects that have been created since the revision:
|
||||
version.revision.revert(delete=True)
|
||||
|
||||
# Just revert this object, leaving the rest of the revision unchanged:
|
||||
version.revert()
|
||||
|
||||
|
||||
Recovering Deleted Objects
|
||||
--------------------------
|
||||
|
||||
To recover a deleted object, use the following method::
|
||||
|
||||
# Built a list of all deleted objects, latest deletions first.
|
||||
deleted_list = reversion.get_deleted(YourModel)
|
||||
|
||||
# Access a specific deleted object.
|
||||
delete_version = deleted_list.get(id=5)
|
||||
|
||||
# Recover all objects in this revision:
|
||||
deleted_version.revision.revert()
|
||||
|
||||
# Just recover this object, leaving the rest of the revision unchanged:
|
||||
deleted_version.revert()
|
||||
|
||||
|
||||
Advanced model registration
|
||||
---------------------------
|
||||
|
||||
Following foreign key relationships
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
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.
|
||||
|
||||
::
|
||||
|
||||
reversion.register(YourModel, follow=["your_foreign_key_field"])
|
||||
|
||||
**Please note:** If you use the follow parameter, you must also ensure that the related model has been registered with django-reversion.
|
||||
|
||||
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
|
||||
|
||||
class Pet(models.Model):
|
||||
person = models.ForeignKey(Person)
|
||||
|
||||
reversion.register(Person, follow=["pet_set"])
|
||||
reversion.register(Pet)
|
||||
|
||||
Now whenever you save a revision containing a ``Person``, all related ``Pet`` instances will be automatically saved to the same revision.
|
||||
|
||||
Multi-table inheritance
|
||||
^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
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.
|
||||
|
||||
For example::
|
||||
|
||||
class Place(models.Model):
|
||||
pass
|
||||
|
||||
class Restaurant(Place):
|
||||
pass
|
||||
|
||||
reversion.register(Place)
|
||||
reversion.register(Restaurant, follow=["place_ptr"])
|
||||
|
||||
|
||||
Saving a subset of fields
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
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.
|
||||
|
||||
::
|
||||
|
||||
reversion.register(YourModel, fields=["pk", "foo", "bar"])
|
||||
reversion.register(YourModel, exclude=["foo"])
|
||||
|
||||
**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.
|
||||
|
||||
|
||||
Custom serialization format
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
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.
|
||||
|
||||
::
|
||||
|
||||
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.
|
||||
|
||||
|
||||
Registering with custom signals
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
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.
|
||||
|
||||
::
|
||||
|
||||
from django.db.models.signals import post_save
|
||||
from your_app.signals import custom_signal
|
||||
|
||||
reversion.register(YourModel, signals=[post_save, custom_signal])
|
||||
|
||||
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.
|
||||
|
||||
::
|
||||
|
||||
from django.db.models.signals import post_save, pre_delete
|
||||
|
||||
reversion.register(YourModel, signals=[post_save], eager_signals=[pre_delete])
|
||||
|
||||
**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.
|
||||
|
||||
|
||||
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.
|
||||
|
||||
::
|
||||
|
||||
class MyVersionAdapter(reversion.VersionAdapter):
|
||||
pass # Please see the reversion source code for available methods to override.
|
||||
|
||||
reversion.register(MyModel, adapter_cls=MyVersionAdapter)
|
||||
|
||||
|
||||
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.
|
||||
|
||||
For example::
|
||||
|
||||
# models.py
|
||||
|
||||
class Place(models.Model):
|
||||
pass
|
||||
|
||||
class Restaurant(Place):
|
||||
pass
|
||||
|
||||
class Meal(models.Model):
|
||||
restaurant = models.ForeignKey(Restaurant)
|
||||
|
||||
# admin.py
|
||||
|
||||
class MealInlineAdmin(admin.StackedInline):
|
||||
model = Meal
|
||||
|
||||
class RestaurantAdmin(VersionAdmin):
|
||||
inlines = MealInlineAdmin,
|
||||
|
||||
admin.site.register(Restaurant, RestaurantAdmin)
|
||||
|
||||
Since ``Restaurant`` has been registered with a subclass of ``VersionAdmin``, the following registration calls will be made automatically::
|
||||
|
||||
reversion.register(Place)
|
||||
reversion.register(Restaurant, follow=("place_ptr", "meal_set"))
|
||||
reversion.register(Meal)
|
||||
|
||||
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.
|
|
@ -0,0 +1,17 @@
|
|||
.. _commands:
|
||||
|
||||
Management commands
|
||||
===================
|
||||
|
||||
django-reversion comes with a number of additional django-admin.py management commands, detailed below.
|
||||
|
||||
createinitialrevisions
|
||||
----------------------
|
||||
|
||||
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.
|
||||
|
||||
::
|
||||
|
||||
django-admin.py createinitialrevisions
|
||||
django-admin.py createinitialrevisions someapp
|
||||
django-admin.py createinitialrevisions someapp.SomeModel
|
|
@ -0,0 +1,258 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# django-reversion documentation build configuration file, created by
|
||||
# sphinx-quickstart on Thu Aug 29 09:17:37 2013.
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
# 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.
|
||||
#sys.path.insert(0, os.path.abspath('.'))
|
||||
|
||||
# -- General configuration -----------------------------------------------------
|
||||
|
||||
# If your documentation needs a minimal Sphinx version, state it here.
|
||||
#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 = ['sphinx.ext.intersphinx']
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ['_templates']
|
||||
|
||||
# The suffix of source filenames.
|
||||
source_suffix = '.rst'
|
||||
|
||||
# The encoding of source files.
|
||||
#source_encoding = 'utf-8-sig'
|
||||
|
||||
# The master toctree document.
|
||||
master_doc = 'index'
|
||||
|
||||
# General information about the project.
|
||||
project = 'django-reversion'
|
||||
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 = '1.8'
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = '1.8.7'
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
#language = None
|
||||
|
||||
# There are two options for replacing |today|: either, you set today to some
|
||||
# non-false value, then it is used:
|
||||
#today = ''
|
||||
# Else, today_fmt is used as the format for a strftime call.
|
||||
#today_fmt = '%B %d, %Y'
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
exclude_patterns = ['_build']
|
||||
|
||||
# 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
|
||||
|
||||
# If true, the current module name will be prepended to all description
|
||||
# unit titles (such as .. function::).
|
||||
#add_module_names = True
|
||||
|
||||
# If true, sectionauthor and moduleauthor directives will be shown in the
|
||||
# output. They are ignored by default.
|
||||
#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 = []
|
||||
|
||||
|
||||
# -- 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 = '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 = {}
|
||||
|
||||
# Add any paths that contain custom themes here, relative to this directory.
|
||||
#html_theme_path = []
|
||||
|
||||
# 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
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top
|
||||
# of the sidebar.
|
||||
#html_logo = None
|
||||
|
||||
# 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
|
||||
|
||||
# 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 = []
|
||||
|
||||
# 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
|
||||
|
||||
# Custom sidebar templates, maps document names to template names.
|
||||
#html_sidebars = {}
|
||||
|
||||
# Additional templates that should be rendered to pages, maps page names to
|
||||
# template names.
|
||||
#html_additional_pages = {}
|
||||
|
||||
# If false, no module index is generated.
|
||||
#html_domain_indices = True
|
||||
|
||||
# If false, no index is generated.
|
||||
#html_use_index = True
|
||||
|
||||
# If true, the index is split into individual pages for each letter.
|
||||
#html_split_index = False
|
||||
|
||||
# If true, links to the reST sources are added to the pages.
|
||||
#html_show_sourcelink = True
|
||||
|
||||
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
|
||||
#html_show_sphinx = True
|
||||
|
||||
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is 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 = ''
|
||||
|
||||
# This is the file name suffix for HTML files (e.g. ".xhtml").
|
||||
#html_file_suffix = None
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = 'django-reversiondoc'
|
||||
|
||||
|
||||
# -- Options for LaTeX output --------------------------------------------------
|
||||
|
||||
latex_elements = {
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
#'papersize': 'letterpaper',
|
||||
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
#'pointsize': '10pt',
|
||||
|
||||
# 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]).
|
||||
latex_documents = [
|
||||
('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
|
||||
|
||||
# For "manual" documents, if this is true, then toplevel headings are parts,
|
||||
# not chapters.
|
||||
#latex_use_parts = False
|
||||
|
||||
# If true, show page references after internal links.
|
||||
#latex_show_pagerefs = False
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
#latex_show_urls = False
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
#latex_appendices = []
|
||||
|
||||
# If false, no module index is generated.
|
||||
#latex_domain_indices = True
|
||||
|
||||
|
||||
# -- Options for manual page output --------------------------------------------
|
||||
|
||||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [
|
||||
('index', 'django-reversion', 'django-reversion Documentation',
|
||||
['Dave Hall'], 1)
|
||||
]
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
#man_show_urls = False
|
||||
|
||||
|
||||
# -- 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 = [
|
||||
('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 = []
|
||||
|
||||
# If false, no module index is generated.
|
||||
#texinfo_domain_indices = True
|
||||
|
||||
# How to display URL addresses: 'footnote', 'no', or 'inline'.
|
||||
#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'))
|
||||
|
|
@ -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.
|
|
@ -0,0 +1,46 @@
|
|||
.. _django-versions:
|
||||
|
||||
Compatible Django Versions
|
||||
==========================
|
||||
|
||||
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.
|
||||
|
||||
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.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
|
||||
============== =================
|
||||
|
||||
|
||||
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.
|
|
@ -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.
|
|
@ -0,0 +1,68 @@
|
|||
.. _index:
|
||||
|
||||
django-reversion documentation
|
||||
==============================
|
||||
|
||||
Getting started with 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 syncdb``.
|
||||
|
||||
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 a powerful rollback and recovery facility to your admin site. To enable this, simply register your models with a subclass of ``reversion.VersionAdmin``::
|
||||
|
||||
import reversion
|
||||
|
||||
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
|
||||
-------------
|
||||
|
||||
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
|
||||
----------------
|
||||
|
||||
Installation
|
||||
^^^^^^^^^^^^
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
installation
|
||||
django-versions
|
||||
migrations
|
||||
admin
|
||||
|
||||
Further reading
|
||||
^^^^^^^^^^^^^^^
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
api
|
||||
commands
|
||||
signals
|
||||
how-it-works
|
||||
diffs
|
|
@ -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
|
|
@ -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
|
|
@ -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.
|
|
@ -0,0 +1,45 @@
|
|||
.. _signals:
|
||||
|
||||
Signals sent by django-reversion
|
||||
================================
|
||||
|
||||
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.post_revision_commit
|
||||
------------------------------
|
||||
|
||||
This signal is triggered just after 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 saved Revision model.
|
||||
* **versions** - The saved Version models in the revision.
|
||||
|
||||
|
||||
Connecting to signals
|
||||
---------------------
|
||||
|
||||
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::
|
||||
|
||||
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)
|
|
@ -0,0 +1,40 @@
|
|||
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,}
|
||||
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 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",])
|
|
@ -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
|
|
@ -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"
|
|
@ -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.
|
@ -0,0 +1,128 @@
|
|||
# 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: 2011-01-12 11:13+0100\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"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=3; plural=n==1 ? 0 : n>1 && n<5 ? 1 : 2;\n"
|
||||
|
||||
#: admin.py:112 templates/reversion/change_list.html:8
|
||||
#: templates/reversion/recover_list.html:10
|
||||
#, python-format
|
||||
msgid "Recover deleted %(name)s"
|
||||
msgstr "Obnovit smazané %(name)s"
|
||||
|
||||
#: admin.py:165
|
||||
#, python-format
|
||||
msgid "Reverted to previous version, saved on %(datetime)s"
|
||||
msgstr "Vrátit se k předchozí verzi uložené v %(datetime)s"
|
||||
|
||||
#: admin.py:167
|
||||
#, python-format
|
||||
msgid ""
|
||||
"The %(model)s \"%(name)s\" was reverted successfully. You may edit it again "
|
||||
"below."
|
||||
msgstr ""
|
||||
"Objekt %(model)s \"%(name)s\" byl úspěšně obnoven. Můžete ho znovu začít upravovat "
|
||||
"níže."
|
||||
|
||||
#: admin.py:273
|
||||
#, python-format
|
||||
msgid "Recover %(name)s"
|
||||
msgstr "Obnovit %(name)s"
|
||||
|
||||
#: admin.py:284
|
||||
#, python-format
|
||||
msgid "Revert %(name)s"
|
||||
msgstr "Navrátit se k předchozí verzi %(name)s"
|
||||
|
||||
#: management/commands/createinitialrevisions.py:76
|
||||
msgid "Initial version."
|
||||
msgstr ""
|
||||
|
||||
#: templates/reversion/change_list.html:11
|
||||
#, python-format
|
||||
msgid "Add %(name)s"
|
||||
msgstr "Přidat %(name)s"
|
||||
|
||||
#: templates/reversion/object_history.html:8
|
||||
msgid ""
|
||||
"Choose a date from the list below to revert to a previous version of this "
|
||||
"object."
|
||||
msgstr ""
|
||||
"Zvolte datum ze seznamu níže pro návrat k předchozí verzi tohoto objektu."
|
||||
|
||||
#: templates/reversion/object_history.html:15
|
||||
#: templates/reversion/recover_list.html:23
|
||||
msgid "Date/time"
|
||||
msgstr "Datum/čas"
|
||||
|
||||
#: templates/reversion/object_history.html:16
|
||||
msgid "User"
|
||||
msgstr "Uživatel"
|
||||
|
||||
#: templates/reversion/object_history.html:17
|
||||
msgid "Comment"
|
||||
msgstr "Komentář"
|
||||
|
||||
#: templates/reversion/object_history.html:23
|
||||
#: templates/reversion/recover_list.html:30
|
||||
msgid "DATETIME_FORMAT"
|
||||
msgstr "DATETIME_FORMAT"
|
||||
|
||||
#: templates/reversion/object_history.html:31
|
||||
msgid ""
|
||||
"This object doesn't have a change history. It probably wasn't added via this "
|
||||
"admin site."
|
||||
msgstr ""
|
||||
"Tento objekt nemá uloženou žádnou historii změn. Zřejmě nebyl přidát přes toto "
|
||||
"administrační rozhraní."
|
||||
|
||||
#: templates/reversion/recover_form.html:7
|
||||
#: templates/reversion/recover_list.html:7
|
||||
#: templates/reversion/revision_form.html:10
|
||||
msgid "Home"
|
||||
msgstr "Domů"
|
||||
|
||||
#: templates/reversion/recover_form.html:10
|
||||
#, python-format
|
||||
msgid "Recover deleted %(verbose_name)s"
|
||||
msgstr "Obnovit smazané %(verbose_name)s"
|
||||
|
||||
#: templates/reversion/recover_form.html:17
|
||||
msgid "Press the save button below to recover this version of the object."
|
||||
msgstr "Klikněte na tlačítko uložit pro obnovení této verze objektu."
|
||||
|
||||
#: templates/reversion/recover_list.html:17
|
||||
msgid ""
|
||||
"Choose a date from the list below to recover a deleted version of an object."
|
||||
msgstr ""
|
||||
"Zvolte datum ze seznamu níže pro obnovení smazané verze objektu."
|
||||
|
||||
#: templates/reversion/recover_list.html:37
|
||||
msgid "There are no deleted objects to recover."
|
||||
msgstr "Žádné smazané objekty k obnovení."
|
||||
|
||||
#: templates/reversion/revision_form.html:14
|
||||
msgid "History"
|
||||
msgstr "Historie"
|
||||
|
||||
#: templates/reversion/revision_form.html:15
|
||||
#, python-format
|
||||
msgid "Revert %(verbose_name)s"
|
||||
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 verze objektu."
|
Binary file not shown.
|
@ -0,0 +1,122 @@
|
|||
# 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-07-30 11:17+0200\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: \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"
|
||||
|
||||
#: admin.py:160
|
||||
msgid "Initial version."
|
||||
msgstr "Første version."
|
||||
|
||||
#: 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 "Gendan slettede %(name)s"
|
||||
|
||||
#: admin.py:311
|
||||
#, python-format
|
||||
msgid "Reverted to previous version, saved on %(datetime)s"
|
||||
msgstr "Gendannet til tidligere version, gemt den %(datetime)s"
|
||||
|
||||
#: admin.py:313
|
||||
#, python-format
|
||||
msgid ""
|
||||
"The %(model)s \"%(name)s\" was reverted successfully. You may edit it again "
|
||||
"below."
|
||||
msgstr ""
|
||||
"Gendannelsen af %(model)s \"%(name)s\" var succesfuld. Du kan redigere den igen"
|
||||
"her under"
|
||||
|
||||
#: admin.py:398
|
||||
#, python-format
|
||||
msgid "Recover %(name)s"
|
||||
msgstr "Genskab %(name)s"
|
||||
|
||||
#: admin.py:412
|
||||
#, python-format
|
||||
msgid "Revert %(name)s"
|
||||
msgstr "Revertere %(name)s"
|
||||
|
||||
#: models.py:55
|
||||
msgid "date created"
|
||||
msgstr "oprettelsesdato"
|
||||
|
||||
#: models.py:62
|
||||
msgid "user"
|
||||
msgstr "bruger"
|
||||
|
||||
#: models.py:66
|
||||
msgid "comment"
|
||||
msgstr "kommentar"
|
||||
|
||||
#: templates/reversion/object_history.html:8
|
||||
msgid "Choose a date from the list below to revert to a previous version of this object."
|
||||
msgstr "Vælg en dato fra listen her under for at Revertere til en tidligere version af det her objekt."
|
||||
|
||||
#: templates/reversion/object_history.html:15
|
||||
#: templates/reversion/recover_list.html:24
|
||||
msgid "Date/time"
|
||||
msgstr "Dato/tid"
|
||||
|
||||
#: templates/reversion/object_history.html:16
|
||||
msgid "User"
|
||||
msgstr "Bruger"
|
||||
|
||||
#: templates/reversion/object_history.html:17
|
||||
msgid "Comment"
|
||||
msgstr "Kommentar"
|
||||
|
||||
#: 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 ""
|
||||
"Det her objekt har ingen ændringshistorik. Det er sandsynligvis ikke tilføjet via"
|
||||
"dette admin-side."
|
||||
|
||||
#: templates/reversion/recover_form.html:8
|
||||
#: templates/reversion/recover_list.html:8
|
||||
#: templates/reversion/revision_form.html:8
|
||||
msgid "Home"
|
||||
msgstr "Hjem"
|
||||
|
||||
#: templates/reversion/recover_form.html:18
|
||||
msgid "Press the save button below to recover this version of the object."
|
||||
msgstr "Tryk på gem knappen nedenunder for at genskab denne version af objektet."
|
||||
|
||||
#: templates/reversion/recover_list.html:18
|
||||
msgid "Choose a date from the list below to recover a deleted version of an object."
|
||||
msgstr "Vælg en dato fra listen her under for at gendanne til en tidligere version af objektet."
|
||||
|
||||
#: templates/reversion/recover_list.html:38
|
||||
msgid "There are no deleted objects to recover."
|
||||
msgstr "Der findes inden slettede objekter at gendanne."
|
||||
|
||||
#: templates/reversion/revision_form.html:12
|
||||
msgid "History"
|
||||
msgstr "Historik"
|
||||
|
||||
#: templates/reversion/revision_form.html:13
|
||||
#, python-format
|
||||
msgid "Revert %(verbose_name)s"
|
||||
msgstr "Revertere %(verbose_name)s"
|
||||
|
||||
#: templates/reversion/revision_form.html:26
|
||||
msgid "Press the save button below to revert to this version of the object."
|
||||
msgstr "Tryk på gem her nedenunder for at revertere til denne version af objektet."
|
Binary file not shown.
|
@ -0,0 +1,129 @@
|
|||
# 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: reversion\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2009-02-03 08:31+0100\n"
|
||||
"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"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: admin.py:122 templates/reversion/change_list.html:8
|
||||
#: templates/reversion/recover_list.html:9
|
||||
#, python-format
|
||||
msgid "Recover deleted %(name)s"
|
||||
msgstr "Gelöschte %(name)s wiederherstellen"
|
||||
|
||||
#: admin.py:155
|
||||
#, python-format
|
||||
msgid "Reverted to previous version, saved on %(datetime)s"
|
||||
msgstr "Zu vorheriger Version zurückgesetzt, %(datetime)s gespeichert"
|
||||
|
||||
#: admin.py:157
|
||||
#, python-format
|
||||
msgid ""
|
||||
"The %(model)s \"%(name)s\" was reverted successfully. You may edit it again "
|
||||
"below."
|
||||
msgstr ""
|
||||
"%(model)s \"%(name)s\" wurde erfolgreich zurückgesetzt. Sie können mit der "
|
||||
"Bearbeitung forfahren."
|
||||
|
||||
#: admin.py:227
|
||||
#, python-format
|
||||
msgid "Recover %s"
|
||||
msgstr "%s wiederherstellen"
|
||||
|
||||
#: admin.py:243
|
||||
#, python-format
|
||||
msgid "Revert %(name)s"
|
||||
msgstr "%(name)s zurücksetzen"
|
||||
|
||||
#: templates/reversion/change_list.html:11
|
||||
#, python-format
|
||||
msgid "Add %(name)s"
|
||||
msgstr "%(name)s hinzufügen"
|
||||
|
||||
#: templates/reversion/object_history.html:8
|
||||
msgid ""
|
||||
"Choose a date from the list below to revert to a previous version of this "
|
||||
"object."
|
||||
msgstr ""
|
||||
"Wählen Sie einen Zeitpunkt aus der untenstehenden Liste aus, um zu einer "
|
||||
"vorherigen Version dieses Objektes zurückzukehren."
|
||||
|
||||
#: templates/reversion/object_history.html:15
|
||||
#: templates/reversion/recover_list.html:21
|
||||
msgid "Date/time"
|
||||
msgstr "Datum/Zeit"
|
||||
|
||||
#: templates/reversion/object_history.html:16
|
||||
msgid "User"
|
||||
msgstr "Benutzer"
|
||||
|
||||
#: templates/reversion/object_history.html:17
|
||||
msgid "Comment"
|
||||
msgstr "Kommentar"
|
||||
|
||||
#: templates/reversion/object_history.html:23
|
||||
#: templates/reversion/recover_list.html:28
|
||||
msgid "DATETIME_FORMAT"
|
||||
msgstr "j. N Y, H:i"
|
||||
|
||||
#: templates/reversion/object_history.html:31
|
||||
msgid ""
|
||||
"This object doesn't have a change history. It probably wasn't added via this "
|
||||
"admin site."
|
||||
msgstr ""
|
||||
"Dieses Objekt hat keine Änderungsgeschichte. Es wurde möglicherweise nicht "
|
||||
"über diese Verwaltungsseiten angelegt."
|
||||
|
||||
#: templates/reversion/recover_form.html:14
|
||||
#: templates/reversion/recover_list.html:6
|
||||
#: templates/reversion/revision_form.html:14
|
||||
msgid "Home"
|
||||
msgstr "Start"
|
||||
|
||||
#: templates/reversion/recover_form.html:17
|
||||
#, python-format
|
||||
msgid "Recover deleted %(verbose_name)s"
|
||||
msgstr "Gelöschte %(verbose_name)s wiederherstellen"
|
||||
|
||||
#: templates/reversion/recover_form.html:18
|
||||
#, python-format
|
||||
msgid "Recover %(name)s"
|
||||
msgstr "%(name)s wiederherstellen"
|
||||
|
||||
#: templates/reversion/recover_form.html:24
|
||||
msgid "Press the save button below to recover this version of the object."
|
||||
msgstr "Sichern Sie, um diese Version des Objektes wiederherzustellen."
|
||||
|
||||
#: templates/reversion/recover_list.html:15
|
||||
msgid ""
|
||||
"Choose a date from the list below to recover a deleted version of an object."
|
||||
msgstr ""
|
||||
"Wählen Sie einen Zeitpunk aus der untenstehenden Liste, um eine gelöschte "
|
||||
"Version des Objektes wiederherzustellen."
|
||||
|
||||
#: templates/reversion/recover_list.html:35
|
||||
msgid "There are no deleted objects to recover."
|
||||
msgstr "Es sind keine gelöschten Objekte zur Wiederherstellung vorhanden."
|
||||
|
||||
#: templates/reversion/revision_form.html:18
|
||||
msgid "History"
|
||||
msgstr "Geschichte"
|
||||
|
||||
#: templates/reversion/revision_form.html:19
|
||||
#, python-format
|
||||
msgid "Revert %(verbose_name)s"
|
||||
msgstr "%(verbose_name)s zurücksetzen"
|
||||
|
||||
#: templates/reversion/revision_form.html:32
|
||||
msgid "Press the save button below to revert to this version of the object."
|
||||
msgstr "Sichern Sie, um das Objekt zu dieser Version zurückzusetzen."
|
Binary file not shown.
|
@ -0,0 +1,126 @@
|
|||
# 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 <alexander.ayasca.esquives@gmail.com>, 2013.
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2013-08-31 15:49-0500\n"
|
||||
"PO-Revision-Date: 2013-08-31 16:22-0500\n"
|
||||
"Last-Translator: Alexander Ayasca Esquives <alexander.ayasca.esquives@gmail.com>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: es\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"
|
||||
|
||||
#: 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 "Recuperar %(name)s eliminados"
|
||||
|
||||
#: admin.py:304
|
||||
#, python-format
|
||||
msgid "Reverted to previous version, saved on %(datetime)s"
|
||||
msgstr "Revertido a una versión anterior, grabada 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 satisfactoriamente. Puede editarlo nuevamente "
|
||||
|
||||
#: admin.py:392
|
||||
#, python-format
|
||||
msgid "Recover %(name)s"
|
||||
msgstr "Recuperar %(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 "Escoja una fecha de la lista siguiente 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. Probablemente no fue añadido por medio 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 recuperar 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 "Escoja una fecha de la lista siguiente para recuperar 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 a recuperar"
|
||||
|
||||
#: 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"
|
Binary file not shown.
|
@ -0,0 +1,127 @@
|
|||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# Simon Charette <charette.s@gmail.com>, 2010.
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2010-10-01 20:56-0400\n"
|
||||
"PO-Revision-Date: 2011-09-21 16:31-0400\n"
|
||||
"Last-Translator: Etienne Desautels <etienne.desautels@gmail.com>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: fr\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"
|
||||
|
||||
#: admin.py:143 templates/reversion/change_list.html:7
|
||||
#: templates/reversion/recover_form.html:10
|
||||
#: templates/reversion/recover_list.html:10
|
||||
#, python-format
|
||||
msgid "Recover deleted %(name)s"
|
||||
msgstr "Récupérer %(name)s supprimés"
|
||||
|
||||
#: admin.py:123
|
||||
#, python-format
|
||||
msgid "Deleted %(verbose_name)s."
|
||||
msgstr "Supprimé %(verbose_name)s."
|
||||
|
||||
#: admin.py:252
|
||||
#, python-format
|
||||
msgid "Reverted to previous version, saved on %(datetime)s"
|
||||
msgstr "Restauré depuis une version précédente, sauvée le %(datetime)s"
|
||||
|
||||
#: admin.py:254
|
||||
#, python-format
|
||||
msgid ""
|
||||
"The %(model)s \"%(name)s\" was reverted successfully. You may edit it again "
|
||||
"below."
|
||||
msgstr ""
|
||||
"L’élément %(model)s \"%(name)s\" a été restauré avec succès. Vous pouvez "
|
||||
"l’éditer à nouveau."
|
||||
|
||||
#: admin.py:337
|
||||
#, python-format
|
||||
msgid "Recover %(name)s"
|
||||
msgstr "Récupérer %(name)s"
|
||||
|
||||
#: admin.py:349
|
||||
#, python-format
|
||||
msgid "Revert %(name)s"
|
||||
msgstr "Restaurer %(name)s"
|
||||
|
||||
#: admin.py:111
|
||||
msgid "Initial version."
|
||||
msgstr "Version initiale."
|
||||
|
||||
#: templates/reversion/object_history.html:8
|
||||
msgid ""
|
||||
"Choose a date from the list below to revert to a previous version of this "
|
||||
"object."
|
||||
msgstr "Choisissez une date dans la liste ci-dessous afin de restaurer "
|
||||
"une version précédente de cet élément."
|
||||
|
||||
#: templates/reversion/object_history.html:15
|
||||
#: templates/reversion/recover_list.html:23
|
||||
msgid "Date/time"
|
||||
msgstr "Date/heure"
|
||||
|
||||
#: templates/reversion/object_history.html:16
|
||||
msgid "User"
|
||||
msgstr "Utilisateur"
|
||||
|
||||
#: templates/reversion/object_history.html:17
|
||||
msgid "Comment"
|
||||
msgstr "Commentaire"
|
||||
|
||||
#: admin.py:252 templates/reversion/object_history.html:23
|
||||
#: templates/reversion/recover_list.html:30
|
||||
msgid "DATETIME_FORMAT"
|
||||
msgstr "j F Y H:i:s"
|
||||
|
||||
#: 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 ""
|
||||
"Cet élément ne possède pas d’historique de modifications. Il n’a "
|
||||
"probablement pas été ajouté à partir de ce site d’administration."
|
||||
|
||||
#: templates/reversion/recover_form.html:7
|
||||
#: templates/reversion/recover_list.html:7
|
||||
#: templates/reversion/revision_form.html:7
|
||||
msgid "Home"
|
||||
msgstr "Accueil"
|
||||
|
||||
#: templates/reversion/recover_form.html:17
|
||||
msgid "Press the save button below to recover this version of the object."
|
||||
msgstr "Cliquez sur le bouton <strong>Enregistrer</strong> ci-dessous afin de "
|
||||
"récupérer cet élément."
|
||||
|
||||
#: templates/reversion/recover_list.html:17
|
||||
msgid ""
|
||||
"Choose a date from the list below to recover a deleted version of an object."
|
||||
msgstr "Choisissez une date dans la liste ci-dessous afin de récupérer un "
|
||||
"élément supprimé."
|
||||
|
||||
#: templates/reversion/recover_list.html:37
|
||||
msgid "There are no deleted objects to recover."
|
||||
msgstr "Il n’y a pas d’éléments supprimés à récupérer."
|
||||
|
||||
#: templates/reversion/revision_form.html:11
|
||||
msgid "History"
|
||||
msgstr "Historique"
|
||||
|
||||
#: templates/reversion/revision_form.html:12
|
||||
#, python-format
|
||||
msgid "Revert %(verbose_name)s"
|
||||
msgstr "Restaurer %(verbose_name)s"
|
||||
|
||||
#: templates/reversion/revision_form.html:25
|
||||
msgid "Press the save button below to revert to this version of the object."
|
||||
msgstr "Cliquez sur le bouton <strong>Enregistrer</strong> ci-dessous pour "
|
||||
"restaurer cette version de l’élément."
|
Binary file not shown.
|
@ -0,0 +1,123 @@
|
|||
# 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: 2009-12-10 10:27+0200\n"
|
||||
"PO-Revision-Date: 2009-12-10 10:45+0200\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: admin.py:112 templates/reversion/change_list.html:7
|
||||
#: templates/reversion/recover_list.html:10
|
||||
#, python-format
|
||||
msgid "Recover deleted %(name)s"
|
||||
msgstr "שחזור %(name)s שנמחקו"
|
||||
|
||||
#: admin.py:158
|
||||
#, python-format
|
||||
msgid "Reverted to previous version, saved on %(datetime)s"
|
||||
msgstr "שוחזר לגרסה קודמת, נשמרה ב-%(datetime)s"
|
||||
|
||||
#: admin.py:160
|
||||
#, python-format
|
||||
msgid ""
|
||||
"The %(model)s \"%(name)s\" was reverted successfully. You may edit it again "
|
||||
"below."
|
||||
msgstr ""
|
||||
"שחזור %(model)s \"%(name)s\" לגרסה קודמת הצליח. ניתן לערוך שוב "
|
||||
"מתחת."
|
||||
|
||||
#: admin.py:259
|
||||
#, python-format
|
||||
msgid "Recover %(name)s"
|
||||
msgstr "אחזור %(name)s"
|
||||
|
||||
#: admin.py:269
|
||||
#, python-format
|
||||
msgid "Revert %(name)s"
|
||||
msgstr "שחזור %(name)s"
|
||||
|
||||
#: templates/reversion/change_list.html:9
|
||||
#, python-format
|
||||
msgid "Add %(name)s"
|
||||
msgstr "הוספת %(name)s"
|
||||
|
||||
#: 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:23
|
||||
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:23
|
||||
#: templates/reversion/recover_list.html:30
|
||||
msgid "DATETIME_FORMAT"
|
||||
msgstr "d.m.Y H:i:s"
|
||||
|
||||
#: templates/reversion/object_history.html:31
|
||||
msgid ""
|
||||
"This object doesn't have a change history. It probably wasn't added via this "
|
||||
"admin site."
|
||||
msgstr ""
|
||||
"לאובייקט זה אין היסטוריית שינוי. כנראה לא התווסף דרך "
|
||||
"ממשק הניהול."
|
||||
|
||||
#: templates/reversion/recover_form.html:14
|
||||
#: templates/reversion/recover_list.html:7
|
||||
#: templates/reversion/revision_form.html:14
|
||||
msgid "Home"
|
||||
msgstr "ראשי"
|
||||
|
||||
#: templates/reversion/recover_form.html:17
|
||||
#, python-format
|
||||
msgid "Recover deleted %(verbose_name)s"
|
||||
msgstr "אחזור %(verbose_name)s שנמחק"
|
||||
|
||||
#: templates/reversion/recover_form.html:24
|
||||
msgid "Press the save button below to recover this version of the object."
|
||||
msgstr "נא ללחוץ על לחצן השמירה מתחת כדי לאחזר לגרסה זו של האובייקט"
|
||||
|
||||
#: templates/reversion/recover_list.html:17
|
||||
msgid ""
|
||||
"Choose a date from the list below to recover a deleted version of an object."
|
||||
msgstr ""
|
||||
"נא לבחור תאריך מתחת כדי לאחזר גרסה מחוקה של אובייקט"
|
||||
|
||||
#: templates/reversion/recover_list.html:37
|
||||
msgid "There are no deleted objects to recover."
|
||||
msgstr "אין אובייקטים מחוקים לאחזור"
|
||||
|
||||
#: templates/reversion/revision_form.html:18
|
||||
msgid "History"
|
||||
msgstr "היסטוריה"
|
||||
|
||||
#: templates/reversion/revision_form.html:19
|
||||
#, python-format
|
||||
msgid "Revert %(verbose_name)s"
|
||||
msgstr "שחזור %(verbose_name)s"
|
||||
|
||||
#: templates/reversion/revision_form.html:32
|
||||
msgid "Press the save button below to revert to this version of the object."
|
||||
msgstr "נא ללחוץ על לחצן השמירה מתחת כדי לשחזר לגרסה זו של האובייקט"
|
Binary file not shown.
|
@ -0,0 +1,113 @@
|
|||
# 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: 2009-08-29 13:04+0200\n"
|
||||
"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"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: .\admin.py:128
|
||||
#: .\templates\reversion\change_list.html.py:7
|
||||
#: .\templates\reversion\recover_list.html.py:10
|
||||
msgid "Recover deleted %(name)s"
|
||||
msgstr "Recupera %(name)s cancellati"
|
||||
|
||||
#: .\admin.py:173
|
||||
#, python-format
|
||||
msgid "Reverted to previous version, saved on %(datetime)s"
|
||||
msgstr "Ritorna alla precedente versione, salvata il %(datetime)s"
|
||||
|
||||
#: .\admin.py:175
|
||||
#, python-format
|
||||
msgid "The %(model)s \"%(name)s\" was reverted successfully. You may edit it again below."
|
||||
msgstr "%(model)s \"%(name)s\" è alla versione precedente. Puoi effettuare nuove modifiche se lo desideri."
|
||||
|
||||
#: .\admin.py:271
|
||||
#, python-format
|
||||
msgid "Recover %(name)s"
|
||||
msgstr "Recupera %(name)s"
|
||||
|
||||
#: .\admin.py:281
|
||||
#, python-format
|
||||
msgid "Revert %(name)s"
|
||||
msgstr "Ritorna %(name)s"
|
||||
|
||||
#: .\templates\reversion\change_list.html.py:9
|
||||
#, python-format
|
||||
msgid "Add %(name)s"
|
||||
msgstr "Aggiungi %(name)s"
|
||||
|
||||
#: .\templates\reversion\object_history.html.py:8
|
||||
msgid "Choose a date from the list below to revert to a previous version of this object."
|
||||
msgstr "Scegli una data dall'elenco qui sotto per ritornare a una precedente versione di questo oggetto."
|
||||
|
||||
#: .\templates\reversion\object_history.html.py:15
|
||||
#: .\templates\reversion\recover_list.html.py:23
|
||||
msgid "Date/time"
|
||||
msgstr "Data/ora"
|
||||
|
||||
#: .\templates\reversion\object_history.html.py:16
|
||||
msgid "User"
|
||||
msgstr "Utente"
|
||||
|
||||
#: .\templates\reversion\object_history.html.py:17
|
||||
msgid "Comment"
|
||||
msgstr "Commento"
|
||||
|
||||
#: .\templates\reversion\object_history.html.py:23
|
||||
#: .\templates\reversion\recover_list.html.py:30
|
||||
msgid "DATETIME_FORMAT"
|
||||
msgstr "d/m/Y, G:i"
|
||||
|
||||
#: .\templates\reversion\object_history.html.py:31
|
||||
msgid "This object doesn't have a change history. It probably wasn't added via this admin site."
|
||||
msgstr "Questo oggetto non ha una storia di modifiche. Probabilmente non è stato aggiunto attraverso questo sito di Admin."
|
||||
|
||||
#: .\templates\reversion\recover_form.html.py:14
|
||||
#: .\templates\reversion\recover_list.html.py:7
|
||||
#: .\templates\reversion\revision_form.html.py:14
|
||||
msgid "Home"
|
||||
msgstr "Home"
|
||||
|
||||
#: .\templates\reversion\recover_form.html.py:17
|
||||
#, python-format
|
||||
msgid "Recover deleted %(verbose_name)s"
|
||||
msgstr "Recupera %(verbose_name)s cancellato"
|
||||
|
||||
#: .\templates\reversion\recover_form.html.py:24
|
||||
msgid "Press the save button below to recover this version of the object."
|
||||
msgstr "Premi il pulsante Salva in basso per recuperare questa versione dell'oggetto."
|
||||
|
||||
#: .\templates\reversion\recover_list.html.py:17
|
||||
msgid "Choose a date from the list below to recover a deleted version of an object."
|
||||
msgstr "Scegli una data dall'elenco qui sotto per recuperare una versione cancellata di questo oggetto."
|
||||
|
||||
#: .\templates\reversion\recover_list.html.py:37
|
||||
msgid "There are no deleted objects to recover."
|
||||
msgstr "Non ci sono oggetti cancellati da recuperare."
|
||||
|
||||
#: .\templates\reversion\revision_form.html.py:18
|
||||
msgid "History"
|
||||
msgstr "Storia"
|
||||
|
||||
#: .\templates\reversion\revision_form.html.py:19
|
||||
#, python-format
|
||||
msgid "Revert %(verbose_name)s"
|
||||
msgstr "Ritorna %(verbose_name)s"
|
||||
|
||||
#: .\templates\reversion\revision_form.html.py:32
|
||||
msgid "Press the save button below to revert to this version of the object."
|
||||
msgstr "Premi il pulsante Salva in basso per ritornare a questa versione dell'oggetto"
|
||||
|
||||
#~ msgid "Recover %s"
|
||||
#~ msgstr "Recupera %s"
|
||||
|
Binary file not shown.
|
@ -0,0 +1,116 @@
|
|||
# Norwegian translation for django-reversion
|
||||
# This file is distributed under the same license as the django-reversion package.
|
||||
# Sindre Sorhus <sindresorhus@gmail.com>, 2011.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: django-reversion\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2011-10-17 09:34+0200\n"
|
||||
"PO-Revision-Date: 2011-10-17 10:17+0100\n"
|
||||
"Last-Translator: Sindre Sorhus <sindresorhus@gmail.com>\n"
|
||||
"Language-Team: \n"
|
||||
"Language: \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-Poedit-Language: Norwegian Bokmal\n"
|
||||
"X-Poedit-Country: NORWAY\n"
|
||||
"X-Poedit-SourceCharset: utf-8\n"
|
||||
|
||||
#: admin.py:111
|
||||
msgid "Initial version."
|
||||
msgstr "Initial versjon"
|
||||
|
||||
#: admin.py:125
|
||||
#, python-format
|
||||
msgid "Deleted %(verbose_name)s."
|
||||
msgstr "Slettet %(verbose_name)s."
|
||||
|
||||
#: admin.py:143
|
||||
#: templates/reversion/change_list.html:7
|
||||
#: templates/reversion/recover_form.html:10
|
||||
#: templates/reversion/recover_list.html:10
|
||||
#, python-format
|
||||
msgid "Recover deleted %(name)s"
|
||||
msgstr "Gjenopprett slettede %(name)s"
|
||||
|
||||
#: admin.py:252
|
||||
#, python-format
|
||||
msgid "Reverted to previous version, saved on %(datetime)s"
|
||||
msgstr "Gjenopprettet til forrige versjon, lagret den %(datetime)s"
|
||||
|
||||
#: admin.py:252
|
||||
#: templates/reversion/object_history.html:23
|
||||
#: templates/reversion/recover_list.html:30
|
||||
msgid "DATETIME_FORMAT"
|
||||
msgstr "j. F Y H:i"
|
||||
|
||||
#: admin.py:254
|
||||
#, python-format
|
||||
msgid "The %(model)s \"%(name)s\" was reverted successfully. You may edit it again below."
|
||||
msgstr "%(model)s \"%(name)s\" ble gjenopprettet. Du kan redigere den igjen nedenfor."
|
||||
|
||||
#: admin.py:337
|
||||
#, python-format
|
||||
msgid "Recover %(name)s"
|
||||
msgstr "Gjenopprett %(name)s"
|
||||
|
||||
#: admin.py:349
|
||||
#, python-format
|
||||
msgid "Revert %(name)s"
|
||||
msgstr "Tilbakestill %(name)s"
|
||||
|
||||
#: templates/reversion/object_history.html:8
|
||||
msgid "Choose a date from the list below to revert to a previous version of this object."
|
||||
msgstr "Velg en dato fra listen nedenfor for å gå tilbake til en tidligere versjon av dette objektet."
|
||||
|
||||
#: templates/reversion/object_history.html:15
|
||||
#: templates/reversion/recover_list.html:23
|
||||
msgid "Date/time"
|
||||
msgstr "Dato/tid"
|
||||
|
||||
#: templates/reversion/object_history.html:16
|
||||
msgid "User"
|
||||
msgstr "Bruker"
|
||||
|
||||
#: templates/reversion/object_history.html:17
|
||||
msgid "Comment"
|
||||
msgstr "Kommentar"
|
||||
|
||||
#: 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 "Dette objektet har ingen endringshistorie. Objektet er sannsynligvis ikke blitt lagt inn via dette admin nettstedet."
|
||||
|
||||
#: templates/reversion/recover_form.html:7
|
||||
#: templates/reversion/recover_list.html:7
|
||||
#: templates/reversion/revision_form.html:7
|
||||
msgid "Home"
|
||||
msgstr "Hjem"
|
||||
|
||||
#: templates/reversion/recover_form.html:17
|
||||
msgid "Press the save button below to recover this version of the object."
|
||||
msgstr "Trykk på lagre-knappen nedenfor for å gjenopprette denne versjonen av objektet."
|
||||
|
||||
#: templates/reversion/recover_list.html:17
|
||||
msgid "Choose a date from the list below to recover a deleted version of an object."
|
||||
msgstr "Velg en dato fra listen nedenfor for å gjenopprette en slettet versjon av et objekt."
|
||||
|
||||
#: templates/reversion/recover_list.html:37
|
||||
msgid "There are no deleted objects to recover."
|
||||
msgstr "Finner ingen slettede objekter å gjenopprette."
|
||||
|
||||
#: templates/reversion/revision_form.html:11
|
||||
msgid "History"
|
||||
msgstr "Historie"
|
||||
|
||||
#: templates/reversion/revision_form.html:12
|
||||
#, python-format
|
||||
msgid "Revert %(verbose_name)s"
|
||||
msgstr "Tilbakestill %(verbose_name)s"
|
||||
|
||||
#: templates/reversion/revision_form.html:25
|
||||
msgid "Press the save button below to revert to this version of the object."
|
||||
msgstr "Trykk på lagre-knappen under for å gå tilbake til denne versjonen av objektet."
|
||||
|
Binary file not shown.
|
@ -0,0 +1,135 @@
|
|||
# Dutch translations for django-reversion extension
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# Alexander Schoemaker <alexander.schoemaker@gmail.com>, 2012.
|
||||
# Bouke Haarsma <bouke@webatoom.nl>, 2013.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2012-12-12 10:41+0100\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: Alexander Schoemaker <alexander.schoemaker@gmail.com>\n"
|
||||
"Language-Team: Dutch\n"
|
||||
"Language: nl\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"
|
||||
|
||||
#: admin.py:141
|
||||
msgid "Initial version."
|
||||
msgstr "Eerste versie."
|
||||
|
||||
#: admin.py:163
|
||||
#, python-format
|
||||
msgid "Deleted %(verbose_name)s."
|
||||
msgstr "%(verbose_name)s is verwijderd."
|
||||
|
||||
#: admin.py:186 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 "Herstel verwijderde %(name)s"
|
||||
|
||||
#: admin.py:297
|
||||
#, python-format
|
||||
msgid "Reverted to previous version, saved on %(datetime)s"
|
||||
msgstr "Vorige versie van %(datetime)s is teruggeplaatst"
|
||||
|
||||
#: admin.py:299
|
||||
#, python-format
|
||||
msgid ""
|
||||
"The %(model)s \"%(name)s\" was reverted successfully. You may edit it again "
|
||||
"below."
|
||||
msgstr ""
|
||||
"%(model)s \"%(name)s\" is succesvol teruggeplaatst. Je kunt het nu opnieuw "
|
||||
"wijzigen."
|
||||
|
||||
#: admin.py:385
|
||||
#, python-format
|
||||
msgid "Recover %(name)s"
|
||||
msgstr "Herstel %(name)s"
|
||||
|
||||
#: admin.py:399
|
||||
#, python-format
|
||||
msgid "Revert %(name)s"
|
||||
msgstr "%(name)s terugplaatsen"
|
||||
|
||||
#: models.py:68
|
||||
msgid "date created"
|
||||
msgstr "datum aangemaakt"
|
||||
|
||||
#: models.py:74
|
||||
msgid "user"
|
||||
msgstr "gebruiker"
|
||||
|
||||
#: models.py:78
|
||||
msgid "comment"
|
||||
msgstr "toelichting"
|
||||
|
||||
#: templates/reversion/object_history.html:8
|
||||
msgid ""
|
||||
"Choose a date from the list below to revert to a previous version of this "
|
||||
"object."
|
||||
msgstr ""
|
||||
"Selecteer een datum uit de lijst om een vorige versie terug te plaatsen."
|
||||
|
||||
#: templates/reversion/object_history.html:15
|
||||
#: templates/reversion/recover_list.html:24
|
||||
msgid "Date/time"
|
||||
msgstr "Datum/tijdstip"
|
||||
|
||||
#: templates/reversion/object_history.html:16
|
||||
msgid "User"
|
||||
msgstr "Gebruiker"
|
||||
|
||||
#: templates/reversion/object_history.html:17
|
||||
msgid "Comment"
|
||||
msgstr "Toelichting"
|
||||
|
||||
#: 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 ""
|
||||
"Dit object bevat geen wijzigingshistorie. Vermoedelijk is het niet "
|
||||
"vanuit sitebeheer toegevoegd."
|
||||
|
||||
#: 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 ""
|
||||
"Klik op onderstaande knop om deze versie van het object te herstellen."
|
||||
|
||||
#: templates/reversion/recover_list.html:18
|
||||
msgid ""
|
||||
"Choose a date from the list below to recover a deleted version of an object."
|
||||
msgstr ""
|
||||
"Selecteer een datum uit de lijst om een verwijderde versie van het object "
|
||||
"te herstellen."
|
||||
|
||||
#: templates/reversion/recover_list.html:38
|
||||
msgid "There are no deleted objects to recover."
|
||||
msgstr "Er zijn geen verwijderde objecten die hersteld kunnen worden."
|
||||
|
||||
#: templates/reversion/revision_form.html:12
|
||||
msgid "History"
|
||||
msgstr "Geschiedenis"
|
||||
|
||||
#: templates/reversion/revision_form.html:13
|
||||
#, python-format
|
||||
msgid "Revert %(verbose_name)s"
|
||||
msgstr "%(verbose_name)s terugplaatsen"
|
||||
|
||||
#: templates/reversion/revision_form.html:26
|
||||
msgid "Press the save button below to revert to this version of the object."
|
||||
msgstr ""
|
||||
"Klik op onderstaande knop om deze versie van het object terug te plaatsen."
|
Binary file not shown.
|
@ -0,0 +1,124 @@
|
|||
# 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: reversion\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2011-03-21 20:05+0100\n"
|
||||
"PO-Revision-Date: 2011-03-21 20:12+0100\n"
|
||||
"Last-Translator: Zbigniew Siciarz <zbigniew@siciarz.net>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: pl\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: admin.py:100
|
||||
msgid "Initial version."
|
||||
msgstr "Pierwsza wersja."
|
||||
|
||||
#: admin.py:115
|
||||
#, python-format
|
||||
msgid "Deleted %(verbose_name)s."
|
||||
msgstr "Usunięto %(verbose_name)s"
|
||||
|
||||
#: admin.py:127
|
||||
#: templates/reversion/change_list.html:8
|
||||
#: templates/reversion/recover_list.html:10
|
||||
#, python-format
|
||||
msgid "Recover deleted %(name)s"
|
||||
msgstr "Odzyskaj usunięte %(name)s"
|
||||
|
||||
#: admin.py:218
|
||||
#, python-format
|
||||
msgid "Reverted to previous version, saved on %(datetime)s"
|
||||
msgstr "Przywrócono poprzednią wersję, zapisaną %(datetime)s"
|
||||
|
||||
#: admin.py:220
|
||||
#, python-format
|
||||
msgid "The %(model)s \"%(name)s\" was reverted successfully. You may edit it again below."
|
||||
msgstr "%(model)s \"%(name)s\" został pomyślnie przywrócony. Możesz go ponownie edytować poniżej."
|
||||
|
||||
#: admin.py:321
|
||||
#, python-format
|
||||
msgid "Recover %(name)s"
|
||||
msgstr "Przywróć %(name)s"
|
||||
|
||||
#: admin.py:332
|
||||
#, python-format
|
||||
msgid "Revert %(name)s"
|
||||
msgstr "Przywróć %(name)s"
|
||||
|
||||
#: templates/reversion/change_list.html:11
|
||||
#, python-format
|
||||
msgid "Add %(name)s"
|
||||
msgstr "Dodaj %(name)s"
|
||||
|
||||
#: templates/reversion/object_history.html:8
|
||||
msgid "Choose a date from the list below to revert to a previous version of this object."
|
||||
msgstr "Wybierz datę z poniższej listy, by przywrócić ten obiekt do poprzedniej wersji."
|
||||
|
||||
#: templates/reversion/object_history.html:15
|
||||
#: templates/reversion/recover_list.html:23
|
||||
msgid "Date/time"
|
||||
msgstr "Data/czas"
|
||||
|
||||
#: templates/reversion/object_history.html:16
|
||||
msgid "User"
|
||||
msgstr "Użytkownik"
|
||||
|
||||
#: templates/reversion/object_history.html:17
|
||||
msgid "Comment"
|
||||
msgstr "Komentarz"
|
||||
|
||||
#: templates/reversion/object_history.html:23
|
||||
#: templates/reversion/recover_list.html:30
|
||||
msgid "DATETIME_FORMAT"
|
||||
msgstr "j. N Y, H:i"
|
||||
|
||||
#: templates/reversion/object_history.html:31
|
||||
msgid "This object doesn't have a change history. It probably wasn't added via this admin site."
|
||||
msgstr "Ten obiekt nie posiada historii zmian. Prawdopodobnie nie został dodany za pomocą tego panelu administracyjnego."
|
||||
|
||||
#: templates/reversion/recover_form.html:7
|
||||
#: templates/reversion/recover_list.html:7
|
||||
#: templates/reversion/revision_form.html:10
|
||||
msgid "Home"
|
||||
msgstr "Strona główna"
|
||||
|
||||
#: templates/reversion/recover_form.html:10
|
||||
#, python-format
|
||||
msgid "Recover deleted %(verbose_name)s"
|
||||
msgstr "Przywróć usunięte %(verbose_name)s"
|
||||
|
||||
#: templates/reversion/recover_form.html:17
|
||||
msgid "Press the save button below to recover this version of the object."
|
||||
msgstr "Naciśnij przycisk Zapisz poniżej, by przywrócić tę wersję obiektu."
|
||||
|
||||
#: templates/reversion/recover_list.html:17
|
||||
msgid "Choose a date from the list below to recover a deleted version of an object."
|
||||
msgstr "Wybierz datę z poniższej listy, by przywrócić usuniętą wersję obiektu. "
|
||||
|
||||
#: templates/reversion/recover_list.html:37
|
||||
msgid "There are no deleted objects to recover."
|
||||
msgstr "Nie ma żadnych usuniętych obiektów do przywrócenia."
|
||||
|
||||
#: templates/reversion/revision_form.html:14
|
||||
msgid "History"
|
||||
msgstr "Historia"
|
||||
|
||||
#: templates/reversion/revision_form.html:15
|
||||
#, python-format
|
||||
msgid "Revert %(verbose_name)s"
|
||||
msgstr "Przywróć %(verbose_name)s"
|
||||
|
||||
#: templates/reversion/revision_form.html:28
|
||||
msgid "Press the save button below to revert to this version of the object."
|
||||
msgstr "Naciśnij przycisk Zapisz poniżej, by przywrócić tę wersję obiektu."
|
||||
|
||||
#~ msgid "Recover %s"
|
||||
#~ msgstr "Przywróć %s"
|
||||
|
Binary file not shown.
|
@ -0,0 +1,113 @@
|
|||
# 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: 2009-08-29 13:04+0200\n"
|
||||
"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"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: .\admin.py:128
|
||||
#: .\templates\reversion\change_list.html.py:7
|
||||
#: .\templates\reversion\recover_list.html.py:10
|
||||
msgid "Recover deleted %(name)s"
|
||||
msgstr "Recuperar %(name)s excluído"
|
||||
|
||||
#: .\admin.py:173
|
||||
#, python-format
|
||||
msgid "Reverted to previous version, saved on %(datetime)s"
|
||||
msgstr "Revertido para versão anterior, salva em %(datetime)s"
|
||||
|
||||
#: .\admin.py:175
|
||||
#, python-format
|
||||
msgid "The %(model)s \"%(name)s\" was reverted successfully. You may edit it again below."
|
||||
msgstr "%(model)s \"%(name)s\" foi revertido com sucesso. Você pode editar novamente abaixo."
|
||||
|
||||
#: .\admin.py:271
|
||||
#, python-format
|
||||
msgid "Recover %(name)s"
|
||||
msgstr "Recuperar %(name)s"
|
||||
|
||||
#: .\admin.py:281
|
||||
#, python-format
|
||||
msgid "Revert %(name)s"
|
||||
msgstr "Reverter %(name)s"
|
||||
|
||||
#: .\templates\reversion\change_list.html.py:9
|
||||
#, python-format
|
||||
msgid "Add %(name)s"
|
||||
msgstr "Adicionar %(name)s"
|
||||
|
||||
#: .\templates\reversion\object_history.html.py:8
|
||||
msgid "Choose a date from the list below to revert to a previous version of this object."
|
||||
msgstr "Escolha uma data da lista abaixo para reverter para uma versão anterior deste objeto."
|
||||
|
||||
#: .\templates\reversion\object_history.html.py:15
|
||||
#: .\templates\reversion\recover_list.html.py:23
|
||||
msgid "Date/time"
|
||||
msgstr "Data/hora"
|
||||
|
||||
#: .\templates\reversion\object_history.html.py:16
|
||||
msgid "User"
|
||||
msgstr "Usuário"
|
||||
|
||||
#: .\templates\reversion\object_history.html.py:17
|
||||
msgid "Comment"
|
||||
msgstr "Comentário"
|
||||
|
||||
#: .\templates\reversion\object_history.html.py:23
|
||||
#: .\templates\reversion\recover_list.html.py:30
|
||||
msgid "DATETIME_FORMAT"
|
||||
msgstr "d/m/Y, G:i"
|
||||
|
||||
#: .\templates\reversion\object_history.html.py:31
|
||||
msgid "This object doesn't have a change history. It probably wasn't added via this admin site."
|
||||
msgstr "Este objeto não possui um histórico de mudanças. Ele provavelmente não foi adicionado por este site de admin."
|
||||
|
||||
#: .\templates\reversion\recover_form.html.py:14
|
||||
#: .\templates\reversion\recover_list.html.py:7
|
||||
#: .\templates\reversion\revision_form.html.py:14
|
||||
msgid "Home"
|
||||
msgstr "Home"
|
||||
|
||||
#: .\templates\reversion\recover_form.html.py:17
|
||||
#, python-format
|
||||
msgid "Recover deleted %(verbose_name)s"
|
||||
msgstr "Recuperar %(verbose_name)s excluído"
|
||||
|
||||
#: .\templates\reversion\recover_form.html.py:24
|
||||
msgid "Press the save button below to recover this version of the object."
|
||||
msgstr "Pressione o botão salvar, abaixo, para recuperar essa versão do objeto."
|
||||
|
||||
#: .\templates\reversion\recover_list.html.py:17
|
||||
msgid "Choose a date from the list below to recover a deleted version of an object."
|
||||
msgstr "Escolha uma data da lista abaixo para recuperar uma versão excluída de um objeto."
|
||||
|
||||
#: .\templates\reversion\recover_list.html.py:37
|
||||
msgid "There are no deleted objects to recover."
|
||||
msgstr "Não há objetos excluídos para recuperar."
|
||||
|
||||
#: .\templates\reversion\revision_form.html.py:18
|
||||
msgid "History"
|
||||
msgstr "Histórico"
|
||||
|
||||
#: .\templates\reversion\revision_form.html.py:19
|
||||
#, python-format
|
||||
msgid "Revert %(verbose_name)s"
|
||||
msgstr "Reverter %(verbose_name)s"
|
||||
|
||||
#: .\templates\reversion\revision_form.html.py:32
|
||||
msgid "Press the save button below to revert to this version of the object."
|
||||
msgstr "Pressione o botão salvar, abaixo, para reverter para essa versão do objeto."
|
||||
|
||||
#~ msgid "Recover %s"
|
||||
#~ msgstr "Recuperar %s"
|
||||
|
Binary file not shown.
|
@ -0,0 +1,118 @@
|
|||
# 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: reversion\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2009-02-03 08:31+0100\n"
|
||||
"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"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Poedit-Language: Russian\n"
|
||||
"X-Poedit-Country: RUSSIAN FEDERATION\n"
|
||||
|
||||
#: admin.py:122
|
||||
#: templates/reversion/change_list.html:8
|
||||
#: templates/reversion/recover_list.html:9
|
||||
#, python-format
|
||||
msgid "Recover deleted %(name)s"
|
||||
msgstr "Восстановить удаленный %(name)s"
|
||||
|
||||
#: admin.py:155
|
||||
#, python-format
|
||||
msgid "Reverted to previous version, saved on %(datetime)s"
|
||||
msgstr "Возвращено к предыдущей версии, сохраненной %(datetime)s"
|
||||
|
||||
#: admin.py:157
|
||||
#, python-format
|
||||
msgid "The %(model)s \"%(name)s\" was reverted successfully. You may edit it again below."
|
||||
msgstr "%(model)s \"%(name)s\" возвращен. Можете продолжить его редактирование."
|
||||
|
||||
#: admin.py:227
|
||||
#, python-format
|
||||
msgid "Recover %s"
|
||||
msgstr "Восстановить %s"
|
||||
|
||||
#: admin.py:243
|
||||
#, python-format
|
||||
msgid "Revert %(name)s"
|
||||
msgstr "Вернуть %(name)s"
|
||||
|
||||
#: templates/reversion/change_list.html:11
|
||||
#, python-format
|
||||
msgid "Add %(name)s"
|
||||
msgstr "Добавить %(name)s"
|
||||
|
||||
#: 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:21
|
||||
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:23
|
||||
#: templates/reversion/recover_list.html:28
|
||||
msgid "DATETIME_FORMAT"
|
||||
msgstr "d.m.Y H:i"
|
||||
|
||||
#: templates/reversion/object_history.html:31
|
||||
msgid "This object doesn't have a change history. It probably wasn't added via this admin site."
|
||||
msgstr "У этого объекта нет истории изменений. Возможно, он был добавлен не через администраторский сайт."
|
||||
|
||||
#: templates/reversion/recover_form.html:14
|
||||
#: templates/reversion/recover_list.html:6
|
||||
#: templates/reversion/revision_form.html:14
|
||||
msgid "Home"
|
||||
msgstr "Начало"
|
||||
|
||||
#: templates/reversion/recover_form.html:17
|
||||
#, python-format
|
||||
msgid "Recover deleted %(verbose_name)s"
|
||||
msgstr "Восстановить удаленный %(verbose_name)s"
|
||||
|
||||
#: templates/reversion/recover_form.html:18
|
||||
#, python-format
|
||||
msgid "Recover %(name)s"
|
||||
msgstr "Восстановить %(name)s"
|
||||
|
||||
#: templates/reversion/recover_form.html:24
|
||||
msgid "Press the save button below to recover this version of the object."
|
||||
msgstr "Нажмите кнопку \"Сохранить\" далее, чтобы восстановить эту версию объекта."
|
||||
|
||||
#: templates/reversion/recover_list.html:15
|
||||
msgid "Choose a date from the list below to recover a deleted version of an object."
|
||||
msgstr "Выберите дату из списка, чтобы восстановить удаленную версию объекта."
|
||||
|
||||
#: templates/reversion/recover_list.html:35
|
||||
msgid "There are no deleted objects to recover."
|
||||
msgstr "Не найдено удаленных объектов для восстановления."
|
||||
|
||||
#: templates/reversion/revision_form.html:18
|
||||
msgid "History"
|
||||
msgstr "История"
|
||||
|
||||
#: templates/reversion/revision_form.html:19
|
||||
#, python-format
|
||||
msgid "Revert %(verbose_name)s"
|
||||
msgstr "Вернуть %(verbose_name)s"
|
||||
|
||||
#: templates/reversion/revision_form.html:32
|
||||
msgid "Press the save button below to revert to this version of the object."
|
||||
msgstr "Нажмите кнопку \"Сохранить\" далее, чтобы вернуть эту версию объекта."
|
||||
|
Binary file not shown.
|
@ -0,0 +1,128 @@
|
|||
# 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-01-14 19:05+0100\n"
|
||||
"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"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n"
|
||||
|
||||
#: admin.py:153
|
||||
msgid "Initial version."
|
||||
msgstr "Počiatočná verzia."
|
||||
|
||||
#: admin.py:187 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 "Obnoviť vymazaný %(name)s"
|
||||
|
||||
#: admin.py:304
|
||||
#, python-format
|
||||
msgid "Reverted to previous version, saved on %(datetime)s"
|
||||
msgstr "Obnovená predošlá verzia, uložená %(datetime)s"
|
||||
|
||||
#: admin.py:306
|
||||
#, python-format
|
||||
msgid ""
|
||||
"The %(model)s \"%(name)s\" was reverted successfully. You may edit it again "
|
||||
"below."
|
||||
msgstr ""
|
||||
"Objekt %(model)s \"%(name)s\" bol úspešne obnovený. Môžete ho znovu upraviť "
|
||||
"nižšie."
|
||||
|
||||
#: admin.py:391
|
||||
#, python-format
|
||||
msgid "Recover %(name)s"
|
||||
msgstr "Obnoviť %(name)s"
|
||||
|
||||
#: admin.py:405
|
||||
#, python-format
|
||||
msgid "Revert %(name)s"
|
||||
msgstr "Vrátiť sa k predošlej verzii %(name)s"
|
||||
|
||||
#: models.py:55
|
||||
msgid "date created"
|
||||
msgstr "dátum vytvorenia"
|
||||
|
||||
#: models.py:61
|
||||
msgid "user"
|
||||
msgstr "používateľ"
|
||||
|
||||
#: models.py:65
|
||||
msgid "comment"
|
||||
msgstr "komentár"
|
||||
|
||||
#: templates/reversion/object_history.html:8
|
||||
msgid ""
|
||||
"Choose a date from the list below to revert to a previous version of this "
|
||||
"object."
|
||||
msgstr ""
|
||||
"Vyberte dátum z nižšie uvedeného zoznamu pre návrat k predošlej verzii tohto "
|
||||
"objektu."
|
||||
|
||||
#: templates/reversion/object_history.html:15
|
||||
#: templates/reversion/recover_list.html:24
|
||||
msgid "Date/time"
|
||||
msgstr "Dátum/čas"
|
||||
|
||||
#: templates/reversion/object_history.html:16
|
||||
msgid "User"
|
||||
msgstr "Používateľ"
|
||||
|
||||
#: templates/reversion/object_history.html:17
|
||||
msgid "Comment"
|
||||
msgstr "Komentár"
|
||||
|
||||
#: 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 ""
|
||||
"Tento objekt nemá históriu zmien. Pravdepodobne nebol pridaný cez túto "
|
||||
"admin stránku."
|
||||
|
||||
#: templates/reversion/recover_form.html:8
|
||||
#: templates/reversion/recover_list.html:8
|
||||
#: templates/reversion/revision_form.html:8
|
||||
msgid "Home"
|
||||
msgstr "Domov"
|
||||
|
||||
#: templates/reversion/recover_form.html:18
|
||||
msgid "Press the save button below to recover this version of the object."
|
||||
msgstr "Pre obnovenie tejto verzie objektu kliknite na tlačidlo uložiť nižšie."
|
||||
|
||||
#: templates/reversion/recover_list.html:18
|
||||
msgid ""
|
||||
"Choose a date from the list below to recover a deleted version of an object."
|
||||
msgstr ""
|
||||
"Pre obnovenie vymazanej verzie objektu vyberte dátum z nižšie uvedeného zoznamu."
|
||||
|
||||
#: templates/reversion/recover_list.html:38
|
||||
msgid "There are no deleted objects to recover."
|
||||
msgstr "Niesú dostupné žiadne vymazané objekty pre obnovenie."
|
||||
|
||||
#: templates/reversion/revision_form.html:12
|
||||
msgid "History"
|
||||
msgstr "História"
|
||||
|
||||
#: templates/reversion/revision_form.html:13
|
||||
#, python-format
|
||||
msgid "Revert %(verbose_name)s"
|
||||
msgstr "Vrátiť sa k predošlej verzii %(verbose_name)s"
|
||||
|
||||
#: templates/reversion/revision_form.html:26
|
||||
msgid "Press the save button below to revert to this version of the object."
|
||||
msgstr "Pre návrat na túto verziu objektu kliknite na tlačidlo uložiť nižšie."
|
Binary file not shown.
|
@ -0,0 +1,138 @@
|
|||
# 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: 2012-06-13 09:56+0200\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: \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"
|
||||
|
||||
#: admin.py:139
|
||||
msgid "Initial version."
|
||||
msgstr "Första versionen."
|
||||
|
||||
#: admin.py:161
|
||||
#, python-format
|
||||
msgid "Deleted %(verbose_name)s."
|
||||
msgstr "Tog bort %(verbose_name)s"
|
||||
|
||||
#: admin.py:181 templates/reversion/change_list.html:7
|
||||
#: templates/reversion/recover_form.html:10
|
||||
#: templates/reversion/recover_list.html:10
|
||||
#, python-format
|
||||
msgid "Recover deleted %(name)s"
|
||||
msgstr "Återskapa bortagna %(name)s"
|
||||
|
||||
#: admin.py:292
|
||||
#, python-format
|
||||
msgid "Reverted to previous version, saved on %(datetime)s"
|
||||
msgstr "Återgick till föregående version, sparad %(datetime)s"
|
||||
|
||||
#: admin.py:292 admin.py:456 templates/reversion/object_history.html:23
|
||||
#: templates/reversion/recover_list.html:30
|
||||
msgid "DATETIME_FORMAT"
|
||||
msgstr "Y-m-d H:i"
|
||||
|
||||
#: admin.py:294
|
||||
#, python-format
|
||||
msgid ""
|
||||
"The %(model)s \"%(name)s\" was reverted successfully. You may edit it again "
|
||||
"below."
|
||||
msgstr ""
|
||||
"Återställandet av %(model)s \"%(name)s\" lyckades. Du kan redigera den igen "
|
||||
"här nedan."
|
||||
|
||||
#: admin.py:377
|
||||
#, python-format
|
||||
msgid "Recover %(name)s"
|
||||
msgstr "Återskapa %(name)s"
|
||||
|
||||
#: admin.py:388
|
||||
#, python-format
|
||||
msgid "Revert %(name)s"
|
||||
msgstr "Återställ %(name)s"
|
||||
|
||||
#: models.py:68
|
||||
msgid "date created"
|
||||
msgstr "datum skapad"
|
||||
|
||||
#: models.py:74
|
||||
msgid "user"
|
||||
msgstr "användare"
|
||||
|
||||
#: models.py:78
|
||||
msgid "comment"
|
||||
msgstr "kommentar"
|
||||
|
||||
#: templates/reversion/object_history.html:8
|
||||
msgid ""
|
||||
"Choose a date from the list below to revert to a previous version of this "
|
||||
"object."
|
||||
msgstr ""
|
||||
"Välj ett datum från listan här under för att återställa till en tidigare "
|
||||
"version av det här objektet."
|
||||
|
||||
#: templates/reversion/object_history.html:15
|
||||
#: templates/reversion/recover_list.html:23
|
||||
msgid "Date/time"
|
||||
msgstr "Datum/tid"
|
||||
|
||||
#: templates/reversion/object_history.html:16
|
||||
msgid "User"
|
||||
msgstr "Användare"
|
||||
|
||||
#: templates/reversion/object_history.html:17
|
||||
msgid "Comment"
|
||||
msgstr "Kommentar"
|
||||
|
||||
#: 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 ""
|
||||
"Det här objektet saknar ändringshistorik. Det skapades förmodligen inte via "
|
||||
"den här admin-sajten."
|
||||
|
||||
#: templates/reversion/recover_form.html:7
|
||||
#: templates/reversion/recover_list.html:7
|
||||
#: templates/reversion/revision_form.html:7
|
||||
msgid "Home"
|
||||
msgstr "Hem"
|
||||
|
||||
#: templates/reversion/recover_form.html:17
|
||||
msgid "Press the save button below to recover this version of the object."
|
||||
msgstr "Tryck på spara här nedan fär att återskapa den här versionen."
|
||||
|
||||
#: templates/reversion/recover_list.html:17
|
||||
msgid ""
|
||||
"Choose a date from the list below to recover a deleted version of an object."
|
||||
msgstr ""
|
||||
"Välj ett datum i listan här nedan för att återskapa en borttagen version."
|
||||
|
||||
#: templates/reversion/recover_list.html:37
|
||||
msgid "There are no deleted objects to recover."
|
||||
msgstr "Det finns inga borttagna objekt att återskapa."
|
||||
|
||||
#: templates/reversion/revision_form.html:11
|
||||
msgid "History"
|
||||
msgstr "Historik"
|
||||
|
||||
#: templates/reversion/revision_form.html:12
|
||||
#, python-format
|
||||
msgid "Revert %(verbose_name)s"
|
||||
msgstr "Återställ %(verbose_name)s"
|
||||
|
||||
#: templates/reversion/revision_form.html:25
|
||||
msgid "Press the save button below to revert to this version of the object."
|
||||
msgstr "Tryck på spara här nedan för att återställa den här versionen."
|
Binary file not shown.
|
@ -0,0 +1,121 @@
|
|||
# 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: \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 "单击保存按钮将此对象恢复到此版本。"
|
|
@ -0,0 +1,3 @@
|
|||
"""Reversion management utilities."""
|
||||
|
||||
from __future__ import unicode_literals
|
|
@ -0,0 +1,140 @@
|
|||
from __future__ import unicode_literals, print_function
|
||||
|
||||
from optparse import make_option
|
||||
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.core.management.base import CommandError
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db import models, reset_queries
|
||||
from django.utils.importlib import import_module
|
||||
from django.utils.datastructures import SortedDict
|
||||
from django.utils.encoding import force_text
|
||||
|
||||
from reversion import default_revision_manager
|
||||
from reversion.models import Version, has_int_pk
|
||||
from django.utils import translation
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
option_list = BaseCommand.option_list + (
|
||||
make_option("--comment",
|
||||
action="store",
|
||||
dest="comment",
|
||||
default="Initial version.",
|
||||
help='Specify the comment to add to the revisions. Defaults to "Initial version.".'),
|
||||
make_option("--batch-size",
|
||||
action="store",
|
||||
dest="batch_size",
|
||||
type=int,
|
||||
default=500,
|
||||
help="For large sets of data, revisions will be populated in batches. Defaults to 500"),
|
||||
make_option('--database', action='store', dest='database',
|
||||
help='Nominates a database to create revisions in.'),
|
||||
)
|
||||
args = '[appname, appname.ModelName, ...] [--comment="Initial version."]'
|
||||
help = "Creates initial revisions for a given app [and model]."
|
||||
|
||||
def handle(self, *app_labels, **options):
|
||||
|
||||
# Activate project's default language
|
||||
translation.activate(settings.LANGUAGE_CODE)
|
||||
|
||||
comment = options["comment"]
|
||||
batch_size = options["batch_size"]
|
||||
database = options.get('database')
|
||||
|
||||
verbosity = int(options.get("verbosity", 1))
|
||||
app_list = SortedDict()
|
||||
# if no apps given, use all installed.
|
||||
if len(app_labels) == 0:
|
||||
for app in models.get_apps ():
|
||||
if not app in app_list:
|
||||
app_list[app] = []
|
||||
for model_class in models.get_models(app):
|
||||
if not model_class in app_list[app]:
|
||||
app_list[app].append(model_class)
|
||||
else:
|
||||
for label in app_labels:
|
||||
try:
|
||||
app_label, model_label = label.split(".")
|
||||
try:
|
||||
app = models.get_app(app_label)
|
||||
except ImproperlyConfigured:
|
||||
raise CommandError("Unknown application: %s" % app_label)
|
||||
|
||||
model_class = models.get_model(app_label, model_label)
|
||||
if model_class is None:
|
||||
raise CommandError("Unknown model: %s.%s" % (app_label, model_label))
|
||||
if app in app_list:
|
||||
if app_list[app] and model_class not in app_list[app]:
|
||||
app_list[app].append(model_class)
|
||||
else:
|
||||
app_list[app] = [model_class]
|
||||
except ValueError:
|
||||
# This is just an app - no model qualifier.
|
||||
app_label = label
|
||||
try:
|
||||
app = models.get_app(app_label)
|
||||
if not app in app_list:
|
||||
app_list[app] = []
|
||||
for model_class in models.get_models(app):
|
||||
if not model_class in app_list[app]:
|
||||
app_list[app].append(model_class)
|
||||
except ImproperlyConfigured:
|
||||
raise CommandError("Unknown application: %s" % app_label)
|
||||
# Create revisions.
|
||||
for app, model_classes in app_list.items():
|
||||
for model_class in model_classes:
|
||||
self.create_initial_revisions(app, model_class, comment, batch_size, verbosity, database=database)
|
||||
|
||||
# Go back to default language
|
||||
translation.deactivate()
|
||||
|
||||
def create_initial_revisions(self, app, model_class, comment, batch_size, verbosity=2, database=None, **kwargs):
|
||||
"""Creates the set of initial revisions for the given model."""
|
||||
# Import the relevant admin module.
|
||||
try:
|
||||
import_module("%s.admin" % app.__name__.rsplit(".", 1)[0])
|
||||
except ImportError:
|
||||
pass
|
||||
# Check all models for empty revisions.
|
||||
if default_revision_manager.is_registered(model_class):
|
||||
created_count = 0
|
||||
content_type = ContentType.objects.db_manager(database).get_for_model(model_class)
|
||||
versioned_pk_queryset = Version.objects.using(database).filter(content_type=content_type).all()
|
||||
live_objs = model_class._default_manager.using(database).all()
|
||||
if has_int_pk(model_class):
|
||||
# We can do this as a fast database join!
|
||||
live_objs = live_objs.exclude(
|
||||
pk__in = versioned_pk_queryset.values_list("object_id_int", flat=True)
|
||||
)
|
||||
else:
|
||||
# This join has to be done as two separate queries.
|
||||
live_objs = live_objs.exclude(
|
||||
pk__in = list(versioned_pk_queryset.values_list("object_id", flat=True).iterator())
|
||||
)
|
||||
# Save all the versions.
|
||||
ids = list(live_objs.values_list(model_class._meta.pk.name, flat=True))
|
||||
total = len(ids)
|
||||
for i in range(0, total, batch_size):
|
||||
chunked_ids = ids[i:i+batch_size]
|
||||
objects = live_objs.in_bulk(chunked_ids)
|
||||
for id, obj in objects.items():
|
||||
try:
|
||||
default_revision_manager.save_revision((obj,), comment=comment, db=database)
|
||||
except:
|
||||
print("ERROR: Could not save initial version for %s %s." % (model_class.__name__, obj.pk))
|
||||
raise
|
||||
created_count += 1
|
||||
reset_queries()
|
||||
if verbosity >= 2:
|
||||
print("Created %s of %s." % (created_count, total))
|
||||
|
||||
# Print out a message, if feeling verbose.
|
||||
if verbosity >= 2:
|
||||
print("Created %s initial revision(s) for model %s." % (created_count, force_text(model_class._meta.verbose_name)))
|
||||
else:
|
||||
if verbosity >= 2:
|
||||
print("Model %s is not registered." % (force_text(model_class._meta.verbose_name)))
|
|
@ -0,0 +1,219 @@
|
|||
from __future__ import unicode_literals, print_function
|
||||
|
||||
import datetime, operator, sys
|
||||
from optparse import make_option
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.core.management.base import CommandError
|
||||
from django.db.models import Q, Count
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
from reversion.models import Revision, Version
|
||||
from django.db.utils import DatabaseError
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
option_list = BaseCommand.option_list + (
|
||||
make_option("--date", "-t",
|
||||
dest="date",
|
||||
help="Delete only revisions older then the specify date. The date should be a valid date given in the ISO format (YYYY-MM-DD)"),
|
||||
make_option("--days", "-d",
|
||||
dest="days",
|
||||
default=0,
|
||||
type="int",
|
||||
help="Delete only revisions older then the specify number of days."),
|
||||
make_option("--keep-revision", "-k",
|
||||
dest="keep",
|
||||
default=0,
|
||||
type="int",
|
||||
help="Keep the specified number of revisions (most recent) for each object."),
|
||||
make_option("--force", "-f",
|
||||
action="store_true",
|
||||
dest="force",
|
||||
default=False,
|
||||
help="Force the deletion of revisions even if an other app/model is involved"),
|
||||
make_option("--no-confirmation", "-c",
|
||||
action="store_false",
|
||||
dest="confirmation",
|
||||
default=True,
|
||||
help="Disable the confirmation before deleting revisions"),
|
||||
make_option('--manager', '-m', dest='manager',
|
||||
help='Delete revisions only from specified manager. Defaults from all managers.'),
|
||||
make_option('--database', action='store', dest='database',
|
||||
help='Nominates a database to delete revisions from.'),
|
||||
)
|
||||
args = "[appname, appname.ModelName, ...] [--date=YYYY-MM-DD | days=0] [--keep=0] [--force] [--no-confirmation]"
|
||||
help = """Deletes revisions for a given app [and model] and/or before a specified delay or date.
|
||||
|
||||
If an app or a model is specified, revisions that have an other app/model involved will not be deleted. Use --force to avoid that.
|
||||
|
||||
You can specify only apps/models or only delay or date or only a nuber of revision to keep or use all possible combinations of these options.
|
||||
|
||||
Examples:
|
||||
|
||||
deleterevisions myapp
|
||||
|
||||
That will delete every revisions of myapp (except if there's an other app involved in the revision)
|
||||
|
||||
deleterevisions --date=2010-11-01
|
||||
|
||||
That will delete every revision created before November 1, 2010 for all apps.
|
||||
|
||||
deleterevisions myapp.mymodel --days=365 --force
|
||||
|
||||
That will delete every revision of myapp.model that are older then 365 days, even if there's revisions involving other apps and/or models.
|
||||
|
||||
deleterevisions myapp.mymodel --keep=10
|
||||
|
||||
That will delete only revisions of myapp.model if there's more then 10 revisions for an object, keeping the 10 most recent revisons.
|
||||
"""
|
||||
|
||||
def handle(self, *app_labels, **options):
|
||||
days = options["days"]
|
||||
keep = options["keep"]
|
||||
force = options["force"]
|
||||
confirmation = options["confirmation"]
|
||||
manager = options.get('manager')
|
||||
database = options.get('database')
|
||||
# I don't know why verbosity is not already an int in Django?
|
||||
try:
|
||||
verbosity = int(options["verbosity"])
|
||||
except ValueError:
|
||||
raise CommandError("option -v: invalid choice: '%s' (choose from '0', '1', '2')" % options["verbosity"])
|
||||
|
||||
date = None
|
||||
|
||||
# Validating arguments
|
||||
if options["date"]:
|
||||
if days:
|
||||
raise CommandError("You cannot use --date and --days at the same time. They are exclusive.")
|
||||
|
||||
try:
|
||||
date = datetime.datetime.strptime(options["date"], "%Y-%m-%d").date()
|
||||
except ValueError:
|
||||
raise CommandError("The date you give (%s) is not a valid date. The date should be in the ISO format (YYYY-MM-DD)." % options["date"])
|
||||
|
||||
# Find the date from the days arguments.
|
||||
elif days:
|
||||
date = datetime.datetime.now() - datetime.timedelta(days)
|
||||
|
||||
# Build the queries
|
||||
revision_query = Revision.objects.using(database).all()
|
||||
|
||||
if manager:
|
||||
revision_query = revision_query.filter(manager_slug=manager)
|
||||
|
||||
if date:
|
||||
revision_query = revision_query.filter(date_created__lt=date)
|
||||
|
||||
if app_labels:
|
||||
app_list = set()
|
||||
mod_list = set()
|
||||
for label in app_labels:
|
||||
try:
|
||||
app_label, model_label = label.split(".")
|
||||
mod_list.add((app_label, model_label))
|
||||
except ValueError:
|
||||
# This is just an app, no model qualifier.
|
||||
app_list.add(label)
|
||||
|
||||
# Remove models that their app is already in app_list
|
||||
for app, model in mod_list.copy():
|
||||
if app in app_list:
|
||||
mod_list.remove((app, model))
|
||||
|
||||
# Build apps/models subqueries
|
||||
subqueries = []
|
||||
if app_list:
|
||||
subqueries.append(Q(app_label__in=app_list))
|
||||
if mod_list:
|
||||
subqueries.extend([Q(app_label=app, model=model) for app, model in mod_list])
|
||||
subqueries = reduce(operator.or_, subqueries)
|
||||
|
||||
if force:
|
||||
models = ContentType.objects.using(database).filter(subqueries)
|
||||
revision_query = revision_query.filter(version__content_type__in=models)
|
||||
else:
|
||||
models = ContentType.objects.using(database).exclude(subqueries)
|
||||
revision_query = revision_query.exclude(version__content_type__in=models)
|
||||
|
||||
if keep:
|
||||
objs = Version.objects.using(database).all()
|
||||
|
||||
# If app is specified, to speed up the loop on theses objects,
|
||||
# get only the specified subset.
|
||||
if app_labels:
|
||||
if force:
|
||||
objs = objs.filter(content_type__in=models)
|
||||
else:
|
||||
objs = objs.exclude(content_type__in=models)
|
||||
|
||||
# Get all the objects that have more then the maximum revisions
|
||||
objs = objs.values("object_id", "content_type_id").annotate(total_ver=Count("object_id")).filter(total_ver__gt=keep)
|
||||
|
||||
revisions_not_keeped = set()
|
||||
|
||||
# Get all ids of the oldest revisions minus the max allowed
|
||||
# revisions for all objects.
|
||||
# Was not able to avoid this loop...
|
||||
for obj in objs:
|
||||
revisions_not_keeped.update(list(Version.objects.using(database).filter(content_type__id=obj["content_type_id"], object_id=obj["object_id"]).order_by("-revision__date_created").values_list("revision_id", flat=True)[keep:]))
|
||||
|
||||
revision_query = revision_query.filter(pk__in=revisions_not_keeped)
|
||||
|
||||
|
||||
# Prepare message if verbose
|
||||
if verbosity > 0:
|
||||
if not date and not app_labels and not keep:
|
||||
print("All revisions will be deleted for all models.")
|
||||
else:
|
||||
date_msg = ""
|
||||
if date:
|
||||
date_msg = " older than %s" % date.isoformat()
|
||||
models_msg = " "
|
||||
if app_labels:
|
||||
force_msg = ""
|
||||
if not force:
|
||||
force_msg = " only"
|
||||
models_msg = " having%s theses apps and models:\n- %s\n" % (force_msg, "\n- ".join(sorted(app_list.union(["%s.%s" % (app, model) for app, model in mod_list])),))
|
||||
if date:
|
||||
models_msg = " and" + models_msg
|
||||
keep_msg = ""
|
||||
if keep:
|
||||
keep_msg = " keeping the %s most recent revisions of each object" % keep
|
||||
|
||||
revision_count = revision_query.count()
|
||||
if revision_count:
|
||||
version_query = Version.objects.using(database).all()
|
||||
if date or app_labels or keep:
|
||||
version_query = version_query.filter(revision__in=revision_query)
|
||||
print("%s revision(s)%s%swill be deleted%s.\n%s model version(s) will be deleted." % (revision_count, date_msg, models_msg, keep_msg, version_query.count()))
|
||||
else:
|
||||
print("No revision%s%sto delete%s.\nDone" % (date_msg, models_msg, keep_msg))
|
||||
return
|
||||
|
||||
|
||||
# Ask confirmation
|
||||
if confirmation:
|
||||
try:
|
||||
raw_input = raw_input
|
||||
except NameError:
|
||||
raw_input = input
|
||||
choice = raw_input("Are you sure you want to delete theses revisions? [y|N] ")
|
||||
if choice.lower() != "y":
|
||||
print("Aborting revision deletion.")
|
||||
return
|
||||
|
||||
|
||||
# Delete versions and revisions
|
||||
print("Deleting revisions...")
|
||||
|
||||
try:
|
||||
revision_query.delete()
|
||||
except DatabaseError:
|
||||
# may fail on sqlite if the query is too long
|
||||
print("Delete failed. Trying again with slower method.")
|
||||
for item in revision_query:
|
||||
item.delete()
|
||||
|
||||
print("Done")
|
|
@ -0,0 +1,43 @@
|
|||
"""Middleware used by Reversion."""
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
|
||||
from reversion.revisions import revision_context_manager
|
||||
|
||||
|
||||
REVISION_MIDDLEWARE_FLAG = "reversion.revision_middleware_active"
|
||||
|
||||
|
||||
class RevisionMiddleware(object):
|
||||
|
||||
"""Wraps the entire request in a revision."""
|
||||
|
||||
def process_request(self, request):
|
||||
"""Starts a new revision."""
|
||||
if request.META.get(REVISION_MIDDLEWARE_FLAG, False):
|
||||
raise ImproperlyConfigured("RevisionMiddleware can only be included in MIDDLEWARE_CLASSES once.")
|
||||
request.META[REVISION_MIDDLEWARE_FLAG] = True
|
||||
revision_context_manager.start()
|
||||
|
||||
def _close_revision(self, request):
|
||||
"""Closes the revision."""
|
||||
if request.META.get(REVISION_MIDDLEWARE_FLAG, False):
|
||||
del request.META[REVISION_MIDDLEWARE_FLAG]
|
||||
revision_context_manager.end()
|
||||
|
||||
def process_response(self, request, response):
|
||||
"""Closes the revision."""
|
||||
# look to see if the session has been accessed before looking for user to stop Vary: Cookie
|
||||
if hasattr(request, 'session') and request.session.accessed \
|
||||
and hasattr(request, "user") and request.user is not None and request.user.is_authenticated() \
|
||||
and revision_context_manager.is_active():
|
||||
revision_context_manager.set_user(request.user)
|
||||
self._close_revision(request)
|
||||
return response
|
||||
|
||||
def process_exception(self, request, exception):
|
||||
"""Closes the revision."""
|
||||
revision_context_manager.invalidate()
|
||||
self._close_revision(request)
|
|
@ -0,0 +1,46 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('contenttypes', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Revision',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('manager_slug', models.CharField(default='default', max_length=200, db_index=True)),
|
||||
('date_created', models.DateTimeField(auto_now_add=True, help_text='The date and time this revision was created.', verbose_name='date created', db_index=True)),
|
||||
('comment', models.TextField(help_text='A text comment on this revision.', verbose_name='comment', blank=True)),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.SET_NULL, blank=True, to=settings.AUTH_USER_MODEL, help_text='The user who created this revision.', null=True, verbose_name='user')),
|
||||
],
|
||||
options={
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Version',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('object_id', models.TextField(help_text='Primary key of the model under version control.')),
|
||||
('object_id_int', models.IntegerField(help_text="An indexed, integer version of the stored model's primary key, used for faster lookups.", null=True, db_index=True, blank=True)),
|
||||
('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.', to='contenttypes.ContentType')),
|
||||
('revision', models.ForeignKey(help_text='The revision that contains this version.', to='reversion.Revision')),
|
||||
],
|
||||
options={
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,19 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('reversion', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='revision',
|
||||
name='manager_slug',
|
||||
field=models.CharField(default='default', max_length=191, db_index=True),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,206 @@
|
|||
"""Database models used by django-reversion."""
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
try:
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||
except ImportError: # Django < 1.9
|
||||
from django.contrib.contenttypes.generic import GenericForeignKey
|
||||
from django.conf import settings
|
||||
from django.core import serializers
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.db import models, IntegrityError
|
||||
from django.dispatch.dispatcher import Signal
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.encoding import force_text, python_2_unicode_compatible
|
||||
|
||||
|
||||
def safe_revert(versions):
|
||||
"""
|
||||
Attempts to revert the given models contained in the give versions.
|
||||
|
||||
This method will attempt to resolve dependencies between the versions to revert
|
||||
them in the correct order to avoid database integrity errors.
|
||||
"""
|
||||
unreverted_versions = []
|
||||
for version in versions:
|
||||
try:
|
||||
version.revert()
|
||||
except (IntegrityError, ObjectDoesNotExist):
|
||||
unreverted_versions.append(version)
|
||||
if len(unreverted_versions) == len(versions):
|
||||
raise RevertError("Could not revert revision, due to database integrity errors.")
|
||||
if unreverted_versions:
|
||||
safe_revert(unreverted_versions)
|
||||
|
||||
|
||||
class RevertError(Exception):
|
||||
|
||||
"""Exception thrown when something goes wrong with reverting a model."""
|
||||
|
||||
|
||||
UserModel = getattr(settings, 'AUTH_USER_MODEL', 'auth.User')
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class Revision(models.Model):
|
||||
|
||||
"""A group of related object versions."""
|
||||
|
||||
manager_slug = models.CharField(
|
||||
max_length = 191,
|
||||
db_index = True,
|
||||
default = "default",
|
||||
)
|
||||
|
||||
date_created = models.DateTimeField(auto_now_add=True,
|
||||
db_index=True,
|
||||
verbose_name=_("date created"),
|
||||
help_text="The date and time this revision was created.")
|
||||
|
||||
user = models.ForeignKey(UserModel,
|
||||
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):
|
||||
"""Reverts all objects in this revision."""
|
||||
version_set = self.version_set.all()
|
||||
# Optionally delete objects no longer in the current revision.
|
||||
if delete:
|
||||
# Get a dict of all objects in this revision.
|
||||
old_revision = {}
|
||||
for version in version_set:
|
||||
try:
|
||||
obj = version.object
|
||||
except ContentType.objects.get_for_id(version.content_type_id).model_class().DoesNotExist:
|
||||
pass
|
||||
else:
|
||||
old_revision[obj] = version
|
||||
# Calculate the set of all objects that are in the revision now.
|
||||
from reversion.revisions import RevisionManager
|
||||
current_revision = RevisionManager.get_manager(self.manager_slug)._follow_relationships(obj for obj in old_revision.keys() if obj is not None)
|
||||
# Delete objects that are no longer in the current revision.
|
||||
for item in current_revision:
|
||||
if item not in old_revision:
|
||||
item.delete()
|
||||
# Attempt to revert all revisions.
|
||||
safe_revert(version_set)
|
||||
|
||||
def __str__(self):
|
||||
"""Returns a unicode representation."""
|
||||
return ", ".join(force_text(version) for version in self.version_set.all())
|
||||
|
||||
#Meta
|
||||
class Meta:
|
||||
app_label = 'reversion'
|
||||
|
||||
|
||||
def has_int_pk(model):
|
||||
"""Tests whether the given model has an integer primary key."""
|
||||
pk = model._meta.pk
|
||||
return (
|
||||
(
|
||||
isinstance(pk, (models.IntegerField, models.AutoField)) and
|
||||
not isinstance(pk, models.BigIntegerField)
|
||||
) or (
|
||||
isinstance(pk, models.ForeignKey) and has_int_pk(pk.rel.to)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class Version(models.Model):
|
||||
|
||||
"""A saved version of a database model."""
|
||||
|
||||
revision = models.ForeignKey(Revision,
|
||||
help_text="The revision that contains this version.")
|
||||
|
||||
object_id = models.TextField(help_text="Primary key of the model under version control.")
|
||||
|
||||
object_id_int = models.IntegerField(
|
||||
blank = True,
|
||||
null = True,
|
||||
db_index = True,
|
||||
help_text = "An indexed, integer version of the stored model's primary key, used for faster lookups.",
|
||||
)
|
||||
|
||||
content_type = models.ForeignKey(ContentType,
|
||||
help_text="Content type of the model under version control.")
|
||||
|
||||
# A link to the current instance, not the version stored in this Version!
|
||||
object = GenericForeignKey()
|
||||
|
||||
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.")
|
||||
|
||||
@property
|
||||
def object_version(self):
|
||||
"""The stored version of the model."""
|
||||
data = self.serialized_data
|
||||
data = force_text(data.encode("utf8"))
|
||||
return list(serializers.deserialize(self.format, data, ignorenonexistent=True))[0]
|
||||
|
||||
@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.
|
||||
"""
|
||||
if not hasattr(self, "_field_dict_cache"):
|
||||
object_version = self.object_version
|
||||
obj = object_version.object
|
||||
result = {}
|
||||
for field in obj._meta.fields:
|
||||
result[field.name] = field.value_from_object(obj)
|
||||
result.update(object_version.m2m_data)
|
||||
# Add parent data.
|
||||
for parent_class, field in obj._meta.concrete_model._meta.parents.items():
|
||||
if obj._meta.proxy and parent_class == obj._meta.concrete_model:
|
||||
continue
|
||||
content_type = ContentType.objects.get_for_model(parent_class)
|
||||
if field:
|
||||
parent_id = force_text(getattr(obj, field.attname))
|
||||
else:
|
||||
parent_id = obj.pk
|
||||
try:
|
||||
parent_version = Version.objects.get(revision__id=self.revision_id,
|
||||
content_type=content_type,
|
||||
object_id=parent_id)
|
||||
except Version.DoesNotExist:
|
||||
pass
|
||||
else:
|
||||
result.update(parent_version.field_dict)
|
||||
setattr(self, "_field_dict_cache", result)
|
||||
return getattr(self, "_field_dict_cache")
|
||||
|
||||
def revert(self):
|
||||
"""Recovers the model in this version."""
|
||||
self.object_version.save()
|
||||
|
||||
def __str__(self):
|
||||
"""Returns a unicode representation."""
|
||||
return self.object_repr
|
||||
|
||||
#Meta
|
||||
class Meta:
|
||||
app_label = 'reversion'
|
||||
|
||||
|
||||
# Version management signals.
|
||||
pre_revision_commit = Signal(providing_args=["instances", "revision", "versions"])
|
||||
post_revision_commit = Signal(providing_args=["instances", "revision", "versions"])
|
|
@ -0,0 +1,621 @@
|
|||
"""Revision management for django-reversion."""
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import operator, sys
|
||||
from functools import wraps, reduce, partial
|
||||
from threading import local
|
||||
from weakref import WeakValueDictionary
|
||||
import copy
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core import serializers
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.core.signals import request_finished
|
||||
from django.db import models, connection, transaction
|
||||
from django.db.models import Q, Max, get_model
|
||||
from django.db.models.query import QuerySet
|
||||
from django.db.models.signals import post_save
|
||||
from django.utils.encoding import force_text
|
||||
|
||||
from reversion.models import Revision, Version, has_int_pk, pre_revision_commit, post_revision_commit
|
||||
|
||||
|
||||
class VersionAdapter(object):
|
||||
|
||||
"""Adapter class for serializing a registered model."""
|
||||
|
||||
# Fields to include in the serialized data.
|
||||
fields = ()
|
||||
|
||||
# Fields to exclude from the serialized data.
|
||||
exclude = ()
|
||||
|
||||
# Foreign key relationships to follow when saving a version of this model.
|
||||
follow = ()
|
||||
|
||||
# The serialization format to use.
|
||||
format = "json"
|
||||
|
||||
def __init__(self, model):
|
||||
"""Initializes the version adapter."""
|
||||
self.model = model
|
||||
|
||||
def get_fields_to_serialize(self):
|
||||
"""Returns an iterable of field names to serialize in the version data."""
|
||||
opts = self.model._meta.concrete_model._meta
|
||||
fields = self.fields or (field.name for field in opts.local_fields + opts.local_many_to_many)
|
||||
fields = (opts.get_field(field) for field in fields if not field in self.exclude)
|
||||
for field in fields:
|
||||
if field.rel:
|
||||
yield field.name
|
||||
else:
|
||||
yield field.attname
|
||||
|
||||
def get_followed_relations(self, obj):
|
||||
"""Returns an iterable of related models that should be included in the revision data."""
|
||||
for relationship in self.follow:
|
||||
# Clear foreign key cache.
|
||||
try:
|
||||
related_field = obj._meta.get_field(relationship)
|
||||
except models.FieldDoesNotExist:
|
||||
pass
|
||||
else:
|
||||
if isinstance(related_field, models.ForeignKey):
|
||||
if hasattr(obj, related_field.get_cache_name()):
|
||||
delattr(obj, related_field.get_cache_name())
|
||||
# Get the referenced obj(s).
|
||||
try:
|
||||
related = getattr(obj, relationship)
|
||||
except ObjectDoesNotExist:
|
||||
continue
|
||||
if isinstance(related, models.Model):
|
||||
yield related
|
||||
elif isinstance(related, (models.Manager, QuerySet)):
|
||||
for related_obj in related.all():
|
||||
yield related_obj
|
||||
elif related is not None:
|
||||
raise TypeError("Cannot follow the relationship {relationship}. Expected a model or QuerySet, found {related}".format(
|
||||
relationship = relationship,
|
||||
related = related,
|
||||
))
|
||||
|
||||
def get_serialization_format(self):
|
||||
"""Returns the serialization format to use."""
|
||||
return self.format
|
||||
|
||||
def get_serialized_data(self, obj):
|
||||
"""Returns a string of serialized data for the given obj."""
|
||||
return serializers.serialize(
|
||||
self.get_serialization_format(),
|
||||
(obj,),
|
||||
fields = list(self.get_fields_to_serialize()),
|
||||
)
|
||||
|
||||
def get_version_data(self, obj, db=None):
|
||||
"""Creates the version data to be saved to the version model."""
|
||||
object_id = force_text(obj.pk)
|
||||
content_type = ContentType.objects.db_manager(db).get_for_model(obj)
|
||||
if has_int_pk(obj.__class__):
|
||||
object_id_int = int(obj.pk)
|
||||
else:
|
||||
object_id_int = None
|
||||
return {
|
||||
"object_id": object_id,
|
||||
"object_id_int": object_id_int,
|
||||
"content_type": content_type,
|
||||
"format": self.get_serialization_format(),
|
||||
"serialized_data": self.get_serialized_data(obj),
|
||||
"object_repr": force_text(obj),
|
||||
}
|
||||
|
||||
|
||||
class RevisionManagementError(Exception):
|
||||
|
||||
"""Exception that is thrown when something goes wrong with revision managment."""
|
||||
|
||||
|
||||
class RevisionContextManager(local):
|
||||
|
||||
"""Manages the state of the current revision."""
|
||||
|
||||
def __init__(self):
|
||||
"""Initializes the revision state."""
|
||||
self.clear()
|
||||
# Connect to the request finished signal.
|
||||
request_finished.connect(self._request_finished_receiver)
|
||||
|
||||
def clear(self):
|
||||
"""Puts the revision manager back into its default state."""
|
||||
self._objects = {}
|
||||
self._user = None
|
||||
self._comment = ""
|
||||
self._stack = []
|
||||
self._is_invalid = False
|
||||
self._meta = []
|
||||
self._ignore_duplicates = False
|
||||
self._db = None
|
||||
|
||||
def is_active(self):
|
||||
"""Returns whether there is an active revision for this thread."""
|
||||
return bool(self._stack)
|
||||
|
||||
def is_managing_manually(self):
|
||||
"""Returns whether this revision context has manual management enabled."""
|
||||
self._assert_active()
|
||||
return self._stack[-1]
|
||||
|
||||
def _assert_active(self):
|
||||
"""Checks for an active revision, throwning an exception if none."""
|
||||
if not self.is_active():
|
||||
raise RevisionManagementError("There is no active revision for this thread")
|
||||
|
||||
def start(self, manage_manually=False):
|
||||
"""
|
||||
Begins a revision for this thread.
|
||||
|
||||
This MUST be balanced by a call to `end`. It is recommended that you
|
||||
leave these methods alone and instead use the revision context manager
|
||||
or the `create_revision` decorator.
|
||||
"""
|
||||
self._stack.append(manage_manually)
|
||||
|
||||
def end(self):
|
||||
"""Ends a revision for this thread."""
|
||||
self._assert_active()
|
||||
self._stack.pop()
|
||||
if not self._stack:
|
||||
try:
|
||||
if not self.is_invalid():
|
||||
# Save the revision data.
|
||||
for manager, manager_context in self._objects.items():
|
||||
manager.save_revision(
|
||||
dict(
|
||||
(obj, callable(data) and data() or data)
|
||||
for obj, data
|
||||
in manager_context.items()
|
||||
),
|
||||
user = self._user,
|
||||
comment = self._comment,
|
||||
meta = self._meta,
|
||||
ignore_duplicates = self._ignore_duplicates,
|
||||
db = self._db,
|
||||
)
|
||||
finally:
|
||||
self.clear()
|
||||
|
||||
def invalidate(self):
|
||||
"""Marks this revision as broken, so should not be commited."""
|
||||
self._assert_active()
|
||||
self._is_invalid = True
|
||||
|
||||
def is_invalid(self):
|
||||
"""Checks whether this revision is invalid."""
|
||||
return self._is_invalid
|
||||
|
||||
def add_to_context(self, manager, obj, version_data):
|
||||
"""Adds an object to the current revision."""
|
||||
self._assert_active()
|
||||
try:
|
||||
manager_context = self._objects[manager]
|
||||
except KeyError:
|
||||
manager_context = {}
|
||||
self._objects[manager] = manager_context
|
||||
manager_context[obj] = version_data
|
||||
|
||||
def get_db(self):
|
||||
"""Returns the current DB alias being used."""
|
||||
return self._db
|
||||
|
||||
def set_db(self, db):
|
||||
"""Sets the DB alias to use."""
|
||||
self._db = db
|
||||
|
||||
def set_user(self, user):
|
||||
"""Sets the current user for the revision."""
|
||||
self._assert_active()
|
||||
self._user = user
|
||||
|
||||
def get_user(self):
|
||||
"""Gets the current user for the revision."""
|
||||
self._assert_active()
|
||||
return self._user
|
||||
|
||||
def set_comment(self, comment):
|
||||
"""Sets the comments for the revision."""
|
||||
self._assert_active()
|
||||
self._comment = comment
|
||||
|
||||
def get_comment(self):
|
||||
"""Gets the current comment for the revision."""
|
||||
self._assert_active()
|
||||
return self._comment
|
||||
|
||||
def add_meta(self, cls, **kwargs):
|
||||
"""Adds a class of meta information to the current revision."""
|
||||
self._assert_active()
|
||||
self._meta.append((cls, kwargs))
|
||||
|
||||
def set_ignore_duplicates(self, ignore_duplicates):
|
||||
"""Sets whether to ignore duplicate revisions."""
|
||||
self._assert_active()
|
||||
self._ignore_duplicates = ignore_duplicates
|
||||
|
||||
def get_ignore_duplicates(self):
|
||||
"""Gets whether to ignore duplicate revisions."""
|
||||
self._assert_active()
|
||||
return self._ignore_duplicates
|
||||
|
||||
# Signal receivers.
|
||||
|
||||
def _request_finished_receiver(self, **kwargs):
|
||||
"""
|
||||
Called at the end of a request, ensuring that any open revisions
|
||||
are closed. Not closing all active revisions can cause memory leaks
|
||||
and weird behaviour.
|
||||
"""
|
||||
while self.is_active():
|
||||
self.end()
|
||||
|
||||
# High-level context management.
|
||||
|
||||
def create_revision(self, manage_manually=False):
|
||||
"""
|
||||
Marks up a block of code as requiring a revision to be created.
|
||||
|
||||
The returned context manager can also be used as a decorator.
|
||||
"""
|
||||
return RevisionContext(self, manage_manually)
|
||||
|
||||
|
||||
class RevisionContext(object):
|
||||
|
||||
"""An individual context for a revision."""
|
||||
|
||||
def __init__(self, context_manager, manage_manually):
|
||||
"""Initializes the revision context."""
|
||||
self._context_manager = context_manager
|
||||
self._manage_manually = manage_manually
|
||||
|
||||
def __enter__(self):
|
||||
"""Enters a block of revision management."""
|
||||
self._context_manager.start(self._manage_manually)
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
"""Leaves a block of revision management."""
|
||||
try:
|
||||
if exc_type is not None:
|
||||
self._context_manager.invalidate()
|
||||
finally:
|
||||
self._context_manager.end()
|
||||
|
||||
def __call__(self, func):
|
||||
"""Allows this revision context to be used as a decorator."""
|
||||
@wraps(func)
|
||||
def do_revision_context(*args, **kwargs):
|
||||
with self:
|
||||
return func(*args, **kwargs)
|
||||
return do_revision_context
|
||||
|
||||
|
||||
# A shared, thread-safe context manager.
|
||||
revision_context_manager = RevisionContextManager()
|
||||
|
||||
|
||||
class RegistrationError(Exception):
|
||||
|
||||
"""Exception thrown when registration with django-reversion goes wrong."""
|
||||
|
||||
|
||||
class RevisionManager(object):
|
||||
|
||||
"""Manages the configuration and creation of revisions."""
|
||||
|
||||
_created_managers = WeakValueDictionary()
|
||||
|
||||
@classmethod
|
||||
def get_created_managers(cls):
|
||||
"""Returns all created revision managers."""
|
||||
return list(cls._created_managers.items())
|
||||
|
||||
@classmethod
|
||||
def get_manager(cls, manager_slug):
|
||||
"""Returns the manager with the given slug."""
|
||||
if manager_slug in cls._created_managers:
|
||||
return cls._created_managers[manager_slug]
|
||||
raise RegistrationError("No revision manager exists with the slug %r" % manager_slug)
|
||||
|
||||
def __init__(self, manager_slug, revision_context_manager=revision_context_manager):
|
||||
"""Initializes the revision manager."""
|
||||
# Check the slug is unique for this revision manager.
|
||||
if manager_slug in RevisionManager._created_managers:
|
||||
raise RegistrationError("A revision manager has already been created with the slug %r" % manager_slug)
|
||||
# Store a reference to this manager.
|
||||
self.__class__._created_managers[manager_slug] = self
|
||||
# Store config params.
|
||||
self._manager_slug = manager_slug
|
||||
self._registered_models = {}
|
||||
self._revision_context_manager = revision_context_manager
|
||||
self._eager_signals = {}
|
||||
self._signals = {}
|
||||
# Proxies to common context methods.
|
||||
self._revision_context = revision_context_manager.create_revision()
|
||||
|
||||
# Registration methods.
|
||||
|
||||
def _registration_key_for_model(self, model):
|
||||
meta = model._meta
|
||||
return (
|
||||
meta.app_label,
|
||||
meta.model_name,
|
||||
)
|
||||
|
||||
def is_registered(self, model):
|
||||
"""
|
||||
Checks whether the given model has been registered with this revision
|
||||
manager.
|
||||
"""
|
||||
return self._registration_key_for_model(model) in self._registered_models
|
||||
|
||||
def get_registered_models(self):
|
||||
"""Returns an iterable of all registered models."""
|
||||
return [
|
||||
get_model(*key)
|
||||
for key
|
||||
in self._registered_models.keys()
|
||||
]
|
||||
|
||||
def register(self, model=None, adapter_cls=VersionAdapter, signals=None, eager_signals=None, **field_overrides):
|
||||
"""Registers a model with this revision manager."""
|
||||
# Default to post_save if no signals are given
|
||||
if signals is None and eager_signals is None:
|
||||
signals = [post_save]
|
||||
# Store signals for usage in the signal receiver
|
||||
self._eager_signals[model] = list(eager_signals or [])
|
||||
self._signals[model] = list(signals or [])
|
||||
# Return a class decorator if model is not given
|
||||
if model is None:
|
||||
return partial(self.register, adapter_cls=adapter_cls, **field_overrides)
|
||||
# Prevent multiple registration.
|
||||
if self.is_registered(model):
|
||||
raise RegistrationError("{model} has already been registered with django-reversion".format(
|
||||
model = model,
|
||||
))
|
||||
# Perform any customization.
|
||||
if field_overrides:
|
||||
adapter_cls = type(adapter_cls.__name__, (adapter_cls,), field_overrides)
|
||||
# Perform the registration.
|
||||
adapter_obj = adapter_cls(model)
|
||||
self._registered_models[self._registration_key_for_model(model)] = adapter_obj
|
||||
# Connect to the selected signals of the model.
|
||||
all_signals = self._signals[model] + self._eager_signals[model]
|
||||
for signal in all_signals:
|
||||
signal.connect(self._signal_receiver, model)
|
||||
return model
|
||||
|
||||
def get_adapter(self, model):
|
||||
"""Returns the registration information for the given model class."""
|
||||
if self.is_registered(model):
|
||||
return self._registered_models[self._registration_key_for_model(model)]
|
||||
raise RegistrationError("{model} has not been registered with django-reversion".format(
|
||||
model = model,
|
||||
))
|
||||
|
||||
def unregister(self, model):
|
||||
"""Removes a model from version control."""
|
||||
if not self.is_registered(model):
|
||||
raise RegistrationError("{model} has not been registered with django-reversion".format(
|
||||
model = model,
|
||||
))
|
||||
del self._registered_models[self._registration_key_for_model(model)]
|
||||
all_signals = self._signals[model] + self._eager_signals[model]
|
||||
for signal in all_signals:
|
||||
signal.disconnect(self._signal_receiver, model)
|
||||
del self._signals[model]
|
||||
del self._eager_signals[model]
|
||||
|
||||
def _follow_relationships(self, objects):
|
||||
"""Follows all relationships in the given set of objects."""
|
||||
followed = set()
|
||||
def _follow(obj, exclude_concrete):
|
||||
# Check the pk first because objects without a pk are not hashable
|
||||
if obj.pk is None or obj in followed or (obj.__class__, obj.pk) == exclude_concrete:
|
||||
return
|
||||
followed.add(obj)
|
||||
adapter = self.get_adapter(obj.__class__)
|
||||
for related in adapter.get_followed_relations(obj):
|
||||
_follow(related, exclude_concrete)
|
||||
for obj in objects:
|
||||
exclude_concrete = None
|
||||
if obj._meta.proxy:
|
||||
exclude_concrete = (obj._meta.concrete_model, obj.pk)
|
||||
_follow(obj, exclude_concrete)
|
||||
return followed
|
||||
|
||||
def _get_versions(self, db=None):
|
||||
"""Returns all versions that apply to this manager."""
|
||||
return Version.objects.using(db).filter(
|
||||
revision__manager_slug = self._manager_slug,
|
||||
).select_related("revision")
|
||||
|
||||
def save_revision(self, objects, ignore_duplicates=False, user=None, comment="", meta=(), db=None):
|
||||
"""Saves a new revision."""
|
||||
# Adapt the objects to a dict.
|
||||
if isinstance(objects, (list, tuple)):
|
||||
objects = dict(
|
||||
(obj, self.get_adapter(obj.__class__).get_version_data(obj, db))
|
||||
for obj in objects
|
||||
)
|
||||
# Create the revision.
|
||||
if objects:
|
||||
# Follow relationships.
|
||||
for obj in self._follow_relationships(objects.keys()):
|
||||
if not obj in objects:
|
||||
adapter = self.get_adapter(obj.__class__)
|
||||
objects[obj] = adapter.get_version_data(obj)
|
||||
# Create all the versions without saving them
|
||||
ordered_objects = list(objects.keys())
|
||||
new_versions = [Version(**objects[obj]) for obj in ordered_objects]
|
||||
# Check if there's some change in all the revision's objects.
|
||||
save_revision = True
|
||||
if ignore_duplicates:
|
||||
# Find the latest revision amongst the latest previous version of each object.
|
||||
subqueries = [Q(object_id=version.object_id, content_type=version.content_type) for version in new_versions]
|
||||
subqueries = reduce(operator.or_, subqueries)
|
||||
latest_revision = self._get_versions(db).filter(subqueries).aggregate(Max("revision"))["revision__max"]
|
||||
# If we have a latest revision, compare it to the current revision.
|
||||
if latest_revision is not None:
|
||||
previous_versions = self._get_versions(db).filter(revision=latest_revision).values_list("serialized_data", flat=True)
|
||||
if len(previous_versions) == len(new_versions):
|
||||
all_serialized_data = [version.serialized_data for version in new_versions]
|
||||
if sorted(previous_versions) == sorted(all_serialized_data):
|
||||
save_revision = False
|
||||
# Only save if we're always saving, or have changes.
|
||||
if save_revision:
|
||||
# Save a new revision.
|
||||
revision = Revision(
|
||||
manager_slug = self._manager_slug,
|
||||
user = user,
|
||||
comment = comment,
|
||||
)
|
||||
# Send the pre_revision_commit signal.
|
||||
pre_revision_commit.send(self,
|
||||
instances = ordered_objects,
|
||||
revision = revision,
|
||||
versions = new_versions,
|
||||
)
|
||||
# Save the revision.
|
||||
with transaction.atomic(using=db):
|
||||
revision.save(using=db)
|
||||
# Save version models.
|
||||
for version in new_versions:
|
||||
version.revision = revision
|
||||
version.save()
|
||||
# Save the meta information.
|
||||
for cls, kwargs in meta:
|
||||
cls._default_manager.db_manager(db).create(revision=revision, **kwargs)
|
||||
# Send the post_revision_commit signal.
|
||||
post_revision_commit.send(self,
|
||||
instances = ordered_objects,
|
||||
revision = revision,
|
||||
versions = new_versions,
|
||||
)
|
||||
# Return the revision.
|
||||
return revision
|
||||
|
||||
# Revision management API.
|
||||
|
||||
def get_for_object_reference(self, model, object_id, db=None):
|
||||
"""
|
||||
Returns all versions for the given object reference.
|
||||
|
||||
The results are returned with the most recent versions first.
|
||||
"""
|
||||
content_type = ContentType.objects.db_manager(db).get_for_model(model)
|
||||
versions = self._get_versions(db).filter(
|
||||
content_type = content_type,
|
||||
).select_related("revision")
|
||||
if has_int_pk(model):
|
||||
# We can do this as a fast, indexed lookup.
|
||||
object_id_int = int(object_id)
|
||||
versions = versions.filter(object_id_int=object_id_int)
|
||||
else:
|
||||
# We can't do this using an index. Never mind.
|
||||
object_id = force_text(object_id)
|
||||
versions = versions.filter(object_id=object_id)
|
||||
versions = versions.order_by("-pk")
|
||||
return versions
|
||||
|
||||
def get_for_object(self, obj, db=None):
|
||||
"""
|
||||
Returns all the versions of the given object, ordered by date created.
|
||||
|
||||
The results are returned with the most recent versions first.
|
||||
"""
|
||||
return self.get_for_object_reference(obj.__class__, obj.pk, db)
|
||||
|
||||
def get_unique_for_object(self, obj, db=None):
|
||||
"""
|
||||
Returns unique versions associated with the object.
|
||||
|
||||
The results are returned with the most recent versions first.
|
||||
"""
|
||||
versions = self.get_for_object(obj, db)
|
||||
changed_versions = []
|
||||
last_serialized_data = None
|
||||
for version in versions:
|
||||
if last_serialized_data != version.serialized_data:
|
||||
changed_versions.append(version)
|
||||
last_serialized_data = version.serialized_data
|
||||
return changed_versions
|
||||
|
||||
def get_for_date(self, object, date, db=None):
|
||||
"""Returns the latest version of an object for the given date."""
|
||||
versions = self.get_for_object(object, db)
|
||||
versions = versions.filter(revision__date_created__lte=date)
|
||||
try:
|
||||
version = versions[0]
|
||||
except IndexError:
|
||||
raise Version.DoesNotExist
|
||||
else:
|
||||
return version
|
||||
|
||||
def get_deleted(self, model_class, db=None, model_db=None):
|
||||
"""
|
||||
Returns all the deleted versions for the given model class.
|
||||
|
||||
The results are returned with the most recent versions first.
|
||||
"""
|
||||
model_db = model_db or db
|
||||
content_type = ContentType.objects.db_manager(db).get_for_model(model_class)
|
||||
live_pk_queryset = model_class._default_manager.db_manager(model_db).all().values_list("pk", flat=True)
|
||||
versioned_objs = self._get_versions(db).filter(
|
||||
content_type = content_type,
|
||||
)
|
||||
if has_int_pk(model_class):
|
||||
# If the model and version data are in different databases, decouple the queries.
|
||||
if model_db != db:
|
||||
live_pk_queryset = list(live_pk_queryset.iterator())
|
||||
# We can do this as a fast, in-database join.
|
||||
deleted_version_pks = versioned_objs.exclude(
|
||||
object_id_int__in = live_pk_queryset
|
||||
).values_list("object_id_int")
|
||||
else:
|
||||
# This join has to be done as two separate queries.
|
||||
deleted_version_pks = versioned_objs.exclude(
|
||||
object_id__in = list(live_pk_queryset.iterator())
|
||||
).values_list("object_id")
|
||||
deleted_version_pks = deleted_version_pks.annotate(
|
||||
latest_pk = Max("pk")
|
||||
).values_list("latest_pk", flat=True)
|
||||
# HACK: MySQL deals extremely badly with this as a subquery, and can hang infinitely.
|
||||
# TODO: If a version is identified where this bug no longer applies, we can add a version specifier.
|
||||
if connection.vendor == "mysql":
|
||||
deleted_version_pks = list(deleted_version_pks)
|
||||
# Return the deleted versions!
|
||||
return self._get_versions(db).filter(pk__in=deleted_version_pks).order_by("-pk")
|
||||
|
||||
# Signal receivers.
|
||||
|
||||
def _signal_receiver(self, instance, signal, **kwargs):
|
||||
"""Adds registered models to the current revision, if any."""
|
||||
if self._revision_context_manager.is_active() and not self._revision_context_manager.is_managing_manually():
|
||||
eager = signal in self._eager_signals[instance.__class__]
|
||||
adapter = self.get_adapter(instance.__class__)
|
||||
if eager:
|
||||
# pre_delete is a special case, because the instance will
|
||||
# be modified by django right after this.
|
||||
# don't use a lambda, but get the data out now.
|
||||
version_data = adapter.get_version_data(instance, self._revision_context_manager._db)
|
||||
self._revision_context_manager.add_to_context(self, copy.copy(instance), version_data)
|
||||
for obj in self._follow_relationships([instance]):
|
||||
adapter = self.get_adapter(obj.__class__)
|
||||
version_data = adapter.get_version_data(obj, self._revision_context_manager._db)
|
||||
self._revision_context_manager.add_to_context(self, copy.copy(obj), version_data)
|
||||
else:
|
||||
version_data = lambda: adapter.get_version_data(instance, self._revision_context_manager._db)
|
||||
self._revision_context_manager.add_to_context(self, instance, version_data)
|
||||
|
||||
|
||||
# A shared revision manager.
|
||||
default_revision_manager = RevisionManager("default")
|
|
@ -0,0 +1,104 @@
|
|||
# encoding: utf-8
|
||||
import datetime
|
||||
from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
from django.db import models
|
||||
|
||||
try:
|
||||
from django.contrib.auth import get_user_model
|
||||
except ImportError: # django < 1.5
|
||||
from django.contrib.auth.models import User
|
||||
else:
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class Migration(SchemaMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
|
||||
# Adding model 'Revision'
|
||||
db.create_table('reversion_revision', (
|
||||
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
|
||||
('date_created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
|
||||
('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm["%s.%s" % (User._meta.app_label, User._meta.object_name)], null=True, blank=True)),
|
||||
('comment', self.gf('django.db.models.fields.TextField')(blank=True)),
|
||||
))
|
||||
db.send_create_signal('reversion', ['Revision'])
|
||||
|
||||
# Adding model 'Version'
|
||||
db.create_table('reversion_version', (
|
||||
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
|
||||
('revision', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['reversion.Revision'])),
|
||||
('object_id', self.gf('django.db.models.fields.TextField')()),
|
||||
('content_type', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['contenttypes.ContentType'])),
|
||||
('format', self.gf('django.db.models.fields.CharField')(max_length=255)),
|
||||
('serialized_data', self.gf('django.db.models.fields.TextField')()),
|
||||
('object_repr', self.gf('django.db.models.fields.TextField')()),
|
||||
))
|
||||
db.send_create_signal('reversion', ['Version'])
|
||||
|
||||
def backwards(self, orm):
|
||||
|
||||
# Deleting model 'Revision'
|
||||
db.delete_table('reversion_revision')
|
||||
|
||||
# Deleting model 'Version'
|
||||
db.delete_table('reversion_version')
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
|
||||
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
|
||||
},
|
||||
'auth.permission': {
|
||||
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
|
||||
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
|
||||
},
|
||||
"%s.%s" % (User._meta.app_label, User._meta.module_name): {
|
||||
'Meta': {'object_name': User.__name__, "db_table": "'%s'" % User._meta.db_table},
|
||||
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
|
||||
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
User._meta.pk.column: ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
|
||||
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
|
||||
},
|
||||
'contenttypes.contenttype': {
|
||||
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
|
||||
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
|
||||
},
|
||||
'reversion.revision': {
|
||||
'Meta': {'object_name': 'Revision'},
|
||||
'comment': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
|
||||
'date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['%s.%s']"% (User._meta.app_label, User._meta.object_name), 'null': 'True', 'blank': 'True'})
|
||||
},
|
||||
'reversion.version': {
|
||||
'Meta': {'object_name': 'Version'},
|
||||
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
|
||||
'format': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'object_id': ('django.db.models.fields.TextField', [], {}),
|
||||
'object_repr': ('django.db.models.fields.TextField', [], {}),
|
||||
'revision': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['reversion.Revision']"}),
|
||||
'serialized_data': ('django.db.models.fields.TextField', [], {})
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['reversion']
|
|
@ -0,0 +1,77 @@
|
|||
# encoding: utf-8
|
||||
import datetime
|
||||
from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(SchemaMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
|
||||
# Adding field 'Version.type'
|
||||
db.add_column('reversion_version', 'type', self.gf('django.db.models.fields.PositiveSmallIntegerField')(default=1, db_index=True), keep_default=False)
|
||||
|
||||
def backwards(self, orm):
|
||||
|
||||
# Deleting field 'Version.type'
|
||||
db.delete_column('reversion_version', 'type')
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
|
||||
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
|
||||
},
|
||||
'auth.permission': {
|
||||
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
|
||||
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
|
||||
},
|
||||
'auth.user': {
|
||||
'Meta': {'object_name': 'User'},
|
||||
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
|
||||
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
|
||||
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
|
||||
},
|
||||
'contenttypes.contenttype': {
|
||||
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
|
||||
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
|
||||
},
|
||||
'reversion.revision': {
|
||||
'Meta': {'object_name': 'Revision'},
|
||||
'comment': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
|
||||
'date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'})
|
||||
},
|
||||
'reversion.version': {
|
||||
'Meta': {'object_name': 'Version'},
|
||||
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
|
||||
'format': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'object_id': ('django.db.models.fields.TextField', [], {}),
|
||||
'object_repr': ('django.db.models.fields.TextField', [], {}),
|
||||
'revision': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['reversion.Revision']"}),
|
||||
'serialized_data': ('django.db.models.fields.TextField', [], {}),
|
||||
'type': ('django.db.models.fields.PositiveSmallIntegerField', [], {'db_index': 'True'})
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['reversion']
|
|
@ -0,0 +1,79 @@
|
|||
# encoding: utf-8
|
||||
import datetime
|
||||
from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
from django.db import models
|
||||
|
||||
class Migration(SchemaMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
|
||||
# Adding field 'Version.object_id_int'
|
||||
db.add_column('reversion_version', 'object_id_int', self.gf('django.db.models.fields.IntegerField')(db_index=True, null=True, blank=True), keep_default=False)
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
|
||||
# Deleting field 'Version.object_id_int'
|
||||
db.delete_column('reversion_version', 'object_id_int')
|
||||
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
|
||||
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
|
||||
},
|
||||
'auth.permission': {
|
||||
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
|
||||
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
|
||||
},
|
||||
'auth.user': {
|
||||
'Meta': {'object_name': 'User'},
|
||||
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
|
||||
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
|
||||
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
|
||||
},
|
||||
'contenttypes.contenttype': {
|
||||
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
|
||||
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
|
||||
},
|
||||
'reversion.revision': {
|
||||
'Meta': {'object_name': 'Revision'},
|
||||
'comment': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
|
||||
'date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'})
|
||||
},
|
||||
'reversion.version': {
|
||||
'Meta': {'object_name': 'Version'},
|
||||
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
|
||||
'format': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'object_id': ('django.db.models.fields.TextField', [], {}),
|
||||
'object_id_int': ('django.db.models.fields.IntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}),
|
||||
'object_repr': ('django.db.models.fields.TextField', [], {}),
|
||||
'revision': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['reversion.Revision']"}),
|
||||
'serialized_data': ('django.db.models.fields.TextField', [], {}),
|
||||
'type': ('django.db.models.fields.PositiveSmallIntegerField', [], {'db_index': 'True'})
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['reversion']
|
|
@ -0,0 +1,89 @@
|
|||
# encoding: utf-8
|
||||
import datetime
|
||||
from south.db import db
|
||||
from south.v2 import DataMigration
|
||||
from django.db import models
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
from reversion.models import Version, has_int_pk
|
||||
|
||||
|
||||
class Migration(DataMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
"Write your forwards methods here."
|
||||
for version in orm.Version.objects.filter(object_id_int__isnull=True).iterator():
|
||||
try:
|
||||
content_type = ContentType.objects.get_for_id(version.content_type_id)
|
||||
except AttributeError:
|
||||
version.delete() # This version refers to a content type that doesn't exist any more.
|
||||
continue
|
||||
model = content_type.model_class()
|
||||
if has_int_pk(model):
|
||||
version.object_id_int = int(version.object_id)
|
||||
version.save()
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
"Write your backwards methods here."
|
||||
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
|
||||
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
|
||||
},
|
||||
'auth.permission': {
|
||||
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
|
||||
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
|
||||
},
|
||||
'auth.user': {
|
||||
'Meta': {'object_name': 'User'},
|
||||
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
|
||||
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
|
||||
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
|
||||
},
|
||||
'contenttypes.contenttype': {
|
||||
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
|
||||
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
|
||||
},
|
||||
'reversion.revision': {
|
||||
'Meta': {'object_name': 'Revision'},
|
||||
'comment': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
|
||||
'date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'})
|
||||
},
|
||||
'reversion.version': {
|
||||
'Meta': {'object_name': 'Version'},
|
||||
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
|
||||
'format': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'object_id': ('django.db.models.fields.TextField', [], {}),
|
||||
'object_id_int': ('django.db.models.fields.IntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}),
|
||||
'object_repr': ('django.db.models.fields.TextField', [], {}),
|
||||
'revision': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['reversion.Revision']"}),
|
||||
'serialized_data': ('django.db.models.fields.TextField', [], {}),
|
||||
'type': ('django.db.models.fields.PositiveSmallIntegerField', [], {'db_index': 'True'})
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['reversion']
|
|
@ -0,0 +1,80 @@
|
|||
# encoding: utf-8
|
||||
import datetime
|
||||
from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
from django.db import models
|
||||
|
||||
class Migration(SchemaMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
|
||||
# Adding field 'Revision.manager_slug'
|
||||
db.add_column('reversion_revision', 'manager_slug', self.gf('django.db.models.fields.CharField')(default='default', max_length=200, db_index=True), keep_default=False)
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
|
||||
# Deleting field 'Revision.manager_slug'
|
||||
db.delete_column('reversion_revision', 'manager_slug')
|
||||
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
|
||||
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
|
||||
},
|
||||
'auth.permission': {
|
||||
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
|
||||
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
|
||||
},
|
||||
'auth.user': {
|
||||
'Meta': {'object_name': 'User'},
|
||||
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
|
||||
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
|
||||
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
|
||||
},
|
||||
'contenttypes.contenttype': {
|
||||
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
|
||||
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
|
||||
},
|
||||
'reversion.revision': {
|
||||
'Meta': {'object_name': 'Revision'},
|
||||
'comment': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
|
||||
'date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'manager_slug': ('django.db.models.fields.CharField', [], {'default': "'default'", 'max_length': '200', 'db_index': 'True'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'})
|
||||
},
|
||||
'reversion.version': {
|
||||
'Meta': {'object_name': 'Version'},
|
||||
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
|
||||
'format': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'object_id': ('django.db.models.fields.TextField', [], {}),
|
||||
'object_id_int': ('django.db.models.fields.IntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}),
|
||||
'object_repr': ('django.db.models.fields.TextField', [], {}),
|
||||
'revision': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['reversion.Revision']"}),
|
||||
'serialized_data': ('django.db.models.fields.TextField', [], {}),
|
||||
'type': ('django.db.models.fields.PositiveSmallIntegerField', [], {'db_index': 'True'})
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['reversion']
|
|
@ -0,0 +1,77 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import datetime
|
||||
from south.db import db
|
||||
from south.v2 import DataMigration
|
||||
from django.db import models
|
||||
|
||||
class Migration(DataMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
"Write your forwards methods here."
|
||||
orm.Version.objects.filter(type=2).delete()
|
||||
orm.Revision.objects.filter(version__isnull=True).delete()
|
||||
|
||||
def backwards(self, orm):
|
||||
"Write your backwards methods here."
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
|
||||
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
|
||||
},
|
||||
'auth.permission': {
|
||||
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
|
||||
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
|
||||
},
|
||||
'auth.user': {
|
||||
'Meta': {'object_name': 'User'},
|
||||
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
|
||||
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
|
||||
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}),
|
||||
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
|
||||
},
|
||||
'contenttypes.contenttype': {
|
||||
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
|
||||
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
|
||||
},
|
||||
'reversion.revision': {
|
||||
'Meta': {'object_name': 'Revision'},
|
||||
'comment': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
|
||||
'date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'manager_slug': ('django.db.models.fields.CharField', [], {'default': "'default'", 'max_length': '200', 'db_index': 'True'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']", 'null': 'True', 'blank': 'True'})
|
||||
},
|
||||
'reversion.version': {
|
||||
'Meta': {'object_name': 'Version'},
|
||||
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
|
||||
'format': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'object_id': ('django.db.models.fields.TextField', [], {}),
|
||||
'object_id_int': ('django.db.models.fields.IntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}),
|
||||
'object_repr': ('django.db.models.fields.TextField', [], {}),
|
||||
'revision': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['reversion.Revision']"}),
|
||||
'serialized_data': ('django.db.models.fields.TextField', [], {}),
|
||||
'type': ('django.db.models.fields.PositiveSmallIntegerField', [], {'db_index': 'True'})
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['reversion']
|
||||
symmetrical = True
|
|
@ -0,0 +1,80 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import datetime
|
||||
from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(SchemaMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
# Deleting field 'Version.type'
|
||||
db.delete_column('reversion_version', 'type')
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
# Adding field 'Version.type'
|
||||
db.add_column('reversion_version', 'type',
|
||||
self.gf('django.db.models.fields.PositiveSmallIntegerField')(default=1, db_index=True),
|
||||
keep_default=False)
|
||||
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
|
||||
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
|
||||
},
|
||||
'auth.permission': {
|
||||
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
|
||||
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
|
||||
},
|
||||
'auth.user': {
|
||||
'Meta': {'object_name': 'User'},
|
||||
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
|
||||
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
|
||||
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}),
|
||||
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
|
||||
},
|
||||
'contenttypes.contenttype': {
|
||||
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
|
||||
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
|
||||
},
|
||||
'reversion.revision': {
|
||||
'Meta': {'object_name': 'Revision'},
|
||||
'comment': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
|
||||
'date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'manager_slug': ('django.db.models.fields.CharField', [], {'default': "'default'", 'max_length': '200', 'db_index': 'True'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']", 'null': 'True', 'blank': 'True'})
|
||||
},
|
||||
'reversion.version': {
|
||||
'Meta': {'object_name': 'Version'},
|
||||
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
|
||||
'format': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'object_id': ('django.db.models.fields.TextField', [], {}),
|
||||
'object_id_int': ('django.db.models.fields.IntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}),
|
||||
'object_repr': ('django.db.models.fields.TextField', [], {}),
|
||||
'revision': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['reversion.Revision']"}),
|
||||
'serialized_data': ('django.db.models.fields.TextField', [], {})
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['reversion']
|
|
@ -0,0 +1,78 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from south.utils import datetime_utils as datetime
|
||||
from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(SchemaMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
# Adding index on 'Revision', fields ['date_created']
|
||||
db.create_index(u'reversion_revision', ['date_created'])
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
# Removing index on 'Revision', fields ['date_created']
|
||||
db.delete_index(u'reversion_revision', ['date_created'])
|
||||
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
|
||||
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
|
||||
},
|
||||
'auth.permission': {
|
||||
'Meta': {'ordering': "(u'content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "((u'content_type', 'codename'),)", 'object_name': 'Permission'},
|
||||
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
|
||||
},
|
||||
'auth.user': {
|
||||
'Meta': {'object_name': 'User'},
|
||||
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
|
||||
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': "orm['auth.Group']"}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
|
||||
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': "orm['auth.Permission']"}),
|
||||
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
|
||||
},
|
||||
'contenttypes.contenttype': {
|
||||
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
|
||||
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
|
||||
},
|
||||
'reversion.revision': {
|
||||
'Meta': {'object_name': 'Revision'},
|
||||
'comment': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
|
||||
'date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'manager_slug': ('django.db.models.fields.CharField', [], {'default': "u'default'", 'max_length': '200', 'db_index': 'True'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'})
|
||||
},
|
||||
'reversion.version': {
|
||||
'Meta': {'object_name': 'Version'},
|
||||
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
|
||||
'format': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'object_id': ('django.db.models.fields.TextField', [], {}),
|
||||
'object_id_int': ('django.db.models.fields.IntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}),
|
||||
'object_repr': ('django.db.models.fields.TextField', [], {}),
|
||||
'revision': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['reversion.Revision']"}),
|
||||
'serialized_data': ('django.db.models.fields.TextField', [], {})
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['reversion']
|
|
@ -0,0 +1,10 @@
|
|||
{% extends "admin/change_list.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
|
||||
{% block object-tools-items %}
|
||||
{% if not is_popup %}
|
||||
<li><a href="{{recoverlist_url}}" class="recoverlink">{% blocktrans with cl.opts.verbose_name_plural|escape as name %}Recover deleted {{name}}{% endblocktrans %}</a></li>
|
||||
{% endif %}
|
||||
{{block.super}}
|
||||
{% endblock %}
|
|
@ -0,0 +1,43 @@
|
|||
{% extends "admin/object_history.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
<div id="content-main">
|
||||
|
||||
<p>{% blocktrans %}Choose a date from the list below to revert to a previous version of this object.{% endblocktrans %}</p>
|
||||
|
||||
<div class="module">
|
||||
{% if action_list %}
|
||||
<table id="change-history" class="table table-striped table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{% trans 'Date/time' %}</th>
|
||||
<th scope="col">{% trans 'User' %}</th>
|
||||
<th scope="col">{% trans 'Comment' %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for action in action_list %}
|
||||
<tr>
|
||||
<th scope="row"><a href="{{action.url}}">{{action.revision.date_created}}</a></th>
|
||||
<td>
|
||||
{% if action.revision.user %}
|
||||
{{action.revision.user.get_username}}
|
||||
{% if action.revision.user.get_full_name %} ({{action.revision.user.get_full_name}}){% endif %}
|
||||
{% else %}
|
||||
—
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{action.revision.comment|linebreaksbr|default:""}}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p>{% trans "This object doesn't have a change history. It probably wasn't added via this admin site." %}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
{% extends "reversion/revision_form.html" %}
|
||||
{% load url from future %}
|
||||
{% load i18n %}
|
||||
|
||||
|
||||
{% block breadcrumbs %}
|
||||
<div class="breadcrumbs">
|
||||
<a href="{% url 'admin:index' %}">{% trans "Home" %}</a> ›
|
||||
<a href="{% url 'admin:app_list' app_label %}">{{app_label|capfirst|escape}}</a> ›
|
||||
<a href="{{changelist_url}}">{{opts.verbose_name_plural|capfirst}}</a> ›
|
||||
<a href="{{recoverlist_url}}">{% blocktrans with opts.verbose_name_plural as name %}Recover deleted {{name}}{% endblocktrans %}</a> ›
|
||||
{{title}}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block form_top %}
|
||||
<p>{% blocktrans %}Press the save button below to recover this version of the object.{% endblocktrans %}</p>
|
||||
{% endblock %}
|
|
@ -0,0 +1,43 @@
|
|||
{% extends "admin/base_site.html" %}
|
||||
{% load url from future %}
|
||||
{% load i18n l10n %}
|
||||
|
||||
|
||||
{% block breadcrumbs %}
|
||||
<div class="breadcrumbs">
|
||||
<a href="{% url 'admin:index' %}">{% trans 'Home' %}</a> ›
|
||||
<a href="{% url 'admin:app_list' app_label %}">{{app_label|capfirst|escape}}</a> ›
|
||||
<a href="{{changelist_url}}">{{opts.verbose_name_plural|capfirst}}</a> ›
|
||||
{% blocktrans with opts.verbose_name_plural|escape as name %}Recover deleted {{name}}{% endblocktrans %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
<div id="content-main">
|
||||
<p>{% blocktrans %}Choose a date from the list below to recover a deleted version of an object.{% endblocktrans %}</p>
|
||||
<div class="module">
|
||||
{% if deleted %}
|
||||
<table id="change-history" class="table table-striped table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{% trans 'Date/time' %}</th>
|
||||
<th scope="col">{{opts.verbose_name|capfirst}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for deletion in deleted %}
|
||||
<tr>
|
||||
<th scope="row"><a href="{{deletion.pk|unlocalize}}/">{{deletion.revision.date_created}}</a></th>
|
||||
<td>{{deletion.object_repr}}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p>{% trans "There are no deleted objects to recover." %}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
{% extends "admin/change_form.html" %}
|
||||
{% load url from future %}
|
||||
{% load i18n %}
|
||||
|
||||
|
||||
{% block breadcrumbs %}
|
||||
<div class="breadcrumbs">
|
||||
<a href="{% url 'admin:index' %}">{% trans "Home" %}</a> ›
|
||||
<a href="{% url 'admin:app_list' app_label %}">{{app_label|capfirst|escape}}</a> ›
|
||||
<a href="{{changelist_url}}">{{opts.verbose_name_plural|capfirst}}</a> ›
|
||||
<a href="{{change_url}}">{{original|truncatewords:"18"}}</a> ›
|
||||
<a href="../">{% trans "History" %}</a> ›
|
||||
{% blocktrans with opts.verbose_name as verbose_name %}Revert {{verbose_name}}{% endblocktrans %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
{% with 1 as is_popup %}
|
||||
{{block.super}}
|
||||
{% endwith %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block form_top %}
|
||||
<p>{% blocktrans %}Press the save button below to revert to this version of the object.{% endblocktrans %}</p>
|
||||
{% endblock %}
|
||||
|
|
@ -0,0 +1 @@
|
|||
__version__ = (1, 8, 7)
|
|
@ -0,0 +1,82 @@
|
|||
#!/usr/bin/env python
|
||||
import sys
|
||||
from optparse import OptionParser
|
||||
|
||||
|
||||
def main():
|
||||
# Parse the command-line options.
|
||||
parser = OptionParser()
|
||||
parser.add_option("-v", "--verbosity",
|
||||
action = "store",
|
||||
dest = "verbosity",
|
||||
default = "1",
|
||||
type = "choice",
|
||||
choices = ["0", "1", "2", "3"],
|
||||
help = "Verbosity level; 0=minimal output, 1=normal output, 2=all output",
|
||||
)
|
||||
parser.add_option("--noinput",
|
||||
action = "store_false",
|
||||
dest = "interactive",
|
||||
default = True,
|
||||
help = "Tells Django to NOT prompt the user for input of any kind.",
|
||||
)
|
||||
parser.add_option("--failfast",
|
||||
action = "store_true",
|
||||
dest = "failfast",
|
||||
default = False,
|
||||
help = "Tells Django to stop running the test suite after first failed test.",
|
||||
)
|
||||
options, args = parser.parse_args()
|
||||
# Configure Django.
|
||||
from django.conf import settings
|
||||
settings.configure(
|
||||
DEBUG = False,
|
||||
DATABASES = {
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.sqlite3",
|
||||
}
|
||||
},
|
||||
ROOT_URLCONF = "urls",
|
||||
INSTALLED_APPS = (
|
||||
"django.contrib.auth",
|
||||
"django.contrib.contenttypes",
|
||||
"django.contrib.sessions",
|
||||
"django.contrib.sites",
|
||||
"django.contrib.messages",
|
||||
"django.contrib.staticfiles",
|
||||
"django.contrib.admin",
|
||||
"reversion",
|
||||
"test_reversion",
|
||||
),
|
||||
MIDDLEWARE_CLASSES = (
|
||||
"django.middleware.common.CommonMiddleware",
|
||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||
"django.contrib.messages.middleware.MessageMiddleware",
|
||||
),
|
||||
USE_TZ = True,
|
||||
STATIC_URL = "/static/",
|
||||
TEST_RUNNER = "django.test.runner.DiscoverRunner",
|
||||
)
|
||||
# Run Django setup (1.7+).
|
||||
import django
|
||||
try:
|
||||
django.setup()
|
||||
except AttributeError:
|
||||
pass # This is Django < 1.7
|
||||
# Configure the test runner.
|
||||
from django.test.utils import get_runner
|
||||
TestRunner = get_runner(settings)
|
||||
test_runner = TestRunner(
|
||||
verbosity = int(options.verbosity),
|
||||
interactive = options.interactive,
|
||||
failfast = options.failfast,
|
||||
)
|
||||
# Run the tests.
|
||||
failures = test_runner.run_tests(["test_reversion"])
|
||||
if failures:
|
||||
sys.exit(failures)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
|
@ -0,0 +1,56 @@
|
|||
from django.contrib import admin
|
||||
|
||||
import reversion
|
||||
|
||||
from test_reversion.models import (
|
||||
ChildTestAdminModel,
|
||||
InlineTestChildModel,
|
||||
InlineTestParentModel,
|
||||
InlineTestUnrelatedChildModel,
|
||||
InlineTestUnrelatedParentModel,
|
||||
)
|
||||
|
||||
|
||||
site = admin.AdminSite()
|
||||
|
||||
|
||||
class ChildTestAdminModelAdmin(reversion.VersionAdmin):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
site.register(ChildTestAdminModel, ChildTestAdminModelAdmin)
|
||||
|
||||
|
||||
class InlineTestChildModelInline(admin.TabularInline):
|
||||
|
||||
model = InlineTestChildModel
|
||||
|
||||
fk_name = "parent"
|
||||
|
||||
extra = 0
|
||||
|
||||
verbose_name = "Child"
|
||||
|
||||
verbose_name_plural = "Children"
|
||||
|
||||
|
||||
class InlineTestParentModelAdmin(reversion.VersionAdmin):
|
||||
|
||||
inlines = (InlineTestChildModelInline,)
|
||||
|
||||
|
||||
site.register(InlineTestParentModel, InlineTestParentModelAdmin)
|
||||
|
||||
|
||||
class InlineTestUnrelatedChildModelInline(admin.TabularInline):
|
||||
|
||||
model = InlineTestUnrelatedChildModel
|
||||
|
||||
|
||||
class InlineTestUnrelatedParentModelAdmin(reversion.VersionAdmin):
|
||||
|
||||
inlines = (InlineTestUnrelatedChildModelInline,)
|
||||
|
||||
|
||||
site.register(InlineTestUnrelatedParentModel, InlineTestUnrelatedParentModelAdmin)
|
|
@ -0,0 +1,127 @@
|
|||
from django.db import models
|
||||
from django.utils.encoding import force_text, python_2_unicode_compatible
|
||||
|
||||
from reversion.models import Revision
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class ReversionTestModelBase(models.Model):
|
||||
|
||||
name = models.CharField(
|
||||
max_length = 100,
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
class ReversionTestModel1(ReversionTestModelBase):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
str_pk_gen = 0;
|
||||
|
||||
def get_str_pk():
|
||||
global str_pk_gen
|
||||
str_pk_gen += 1;
|
||||
return force_text(str_pk_gen)
|
||||
|
||||
|
||||
class ReversionTestModel2(ReversionTestModelBase):
|
||||
|
||||
id = models.CharField(
|
||||
primary_key = True,
|
||||
max_length = 100,
|
||||
default = get_str_pk
|
||||
)
|
||||
|
||||
|
||||
class ReversionTestModel3(ReversionTestModelBase):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
|
||||
class TestFollowModel(ReversionTestModelBase):
|
||||
|
||||
test_model_1 = models.ForeignKey(
|
||||
ReversionTestModel1,
|
||||
)
|
||||
|
||||
test_model_2s = models.ManyToManyField(
|
||||
ReversionTestModel2,
|
||||
)
|
||||
|
||||
|
||||
class ReversionTestModel1Proxy(ReversionTestModel1):
|
||||
|
||||
class Meta:
|
||||
proxy = True
|
||||
|
||||
|
||||
class RevisionMeta(models.Model):
|
||||
|
||||
revision = models.OneToOneField(Revision)
|
||||
|
||||
age = models.IntegerField()
|
||||
|
||||
|
||||
# Admin test models.
|
||||
|
||||
class ParentTestAdminModel(models.Model):
|
||||
|
||||
parent_name = models.CharField(
|
||||
max_length = 200,
|
||||
)
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class ChildTestAdminModel(ParentTestAdminModel):
|
||||
|
||||
child_name = models.CharField(
|
||||
max_length = 200,
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.child_name
|
||||
|
||||
|
||||
class InlineTestParentModel(models.Model):
|
||||
|
||||
name = models.CharField(
|
||||
max_length = 100,
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class InlineTestChildModel(models.Model):
|
||||
|
||||
parent = models.ForeignKey(
|
||||
InlineTestParentModel,
|
||||
related_name = "children",
|
||||
)
|
||||
|
||||
name = models.CharField(
|
||||
max_length = 100,
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
# Test that reversion handles unrelated inlines.
|
||||
# Issue https://github.com/etianen/django-reversion/issues/277
|
||||
class InlineTestUnrelatedParentModel(models.Model):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class InlineTestUnrelatedChildModel(models.Model):
|
||||
|
||||
pass
|
|
@ -0,0 +1,896 @@
|
|||
"""
|
||||
Tests for the django-reversion API.
|
||||
|
||||
These tests require Python 2.5 to run.
|
||||
"""
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import datetime, os
|
||||
|
||||
from django.db import models
|
||||
from django.test import TestCase
|
||||
from django.core.management import call_command
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.conf import settings
|
||||
from django.contrib import admin
|
||||
try:
|
||||
from django.contrib.auth import get_user_model
|
||||
except ImportError: # django < 1.5
|
||||
from django.contrib.auth.models import User
|
||||
else:
|
||||
User = get_user_model()
|
||||
from django.utils.unittest import skipUnless
|
||||
from django.db.models.signals import pre_delete
|
||||
|
||||
import reversion
|
||||
from reversion.revisions import RegistrationError, RevisionManager
|
||||
from reversion.models import Revision, Version
|
||||
|
||||
from test_reversion.models import (
|
||||
ReversionTestModel1,
|
||||
ReversionTestModel2,
|
||||
ReversionTestModel3,
|
||||
TestFollowModel,
|
||||
ReversionTestModel1Proxy,
|
||||
RevisionMeta,
|
||||
ParentTestAdminModel,
|
||||
ChildTestAdminModel,
|
||||
InlineTestParentModel,
|
||||
InlineTestChildModel,
|
||||
)
|
||||
from test_reversion import admin # Force early registration of all admin models.
|
||||
|
||||
|
||||
ZERO = datetime.timedelta(0)
|
||||
|
||||
|
||||
class UTC(datetime.tzinfo):
|
||||
"""UTC"""
|
||||
|
||||
def utcoffset(self, dt):
|
||||
return ZERO
|
||||
|
||||
def tzname(self, dt):
|
||||
return "UTC"
|
||||
|
||||
def dst(self, dt):
|
||||
return ZERO
|
||||
|
||||
|
||||
class RegistrationTest(TestCase):
|
||||
|
||||
def check_registration(self, test_model):
|
||||
# Register the model and test.
|
||||
reversion.register(test_model)
|
||||
self.assertTrue(reversion.is_registered(test_model))
|
||||
self.assertRaises(RegistrationError, lambda: reversion.register(test_model))
|
||||
self.assertTrue(test_model in reversion.get_registered_models())
|
||||
self.assertTrue(isinstance(reversion.get_adapter(test_model), reversion.VersionAdapter))
|
||||
|
||||
def check_deregistration(self, test_model):
|
||||
# Unregister the model and text.
|
||||
reversion.unregister(test_model)
|
||||
self.assertFalse(reversion.is_registered(test_model))
|
||||
self.assertRaises(RegistrationError, lambda: reversion.unregister(test_model))
|
||||
self.assertTrue(test_model not in reversion.get_registered_models())
|
||||
self.assertRaises(RegistrationError, lambda: isinstance(reversion.get_adapter(test_model)))
|
||||
|
||||
def testRegistration(self):
|
||||
self.check_registration(ReversionTestModel1)
|
||||
self.check_deregistration(ReversionTestModel1)
|
||||
|
||||
def testProxyRegistration(self):
|
||||
# ProxyModel registered as usual model
|
||||
self.check_registration(ReversionTestModel1Proxy)
|
||||
self.check_deregistration(ReversionTestModel1Proxy)
|
||||
|
||||
def testDecorator(self):
|
||||
# Test the use of register as a decorator
|
||||
@reversion.register
|
||||
class DecoratorModel(models.Model):
|
||||
pass
|
||||
self.assertTrue(reversion.is_registered(DecoratorModel))
|
||||
|
||||
def testDecoratorArgs(self):
|
||||
# Test a decorator with arguments
|
||||
@reversion.register(format='yaml')
|
||||
class DecoratorArgsModel(models.Model):
|
||||
pass
|
||||
self.assertTrue(reversion.is_registered(DecoratorArgsModel))
|
||||
|
||||
def testEagerRegistration(self):
|
||||
# Register the model and test.
|
||||
reversion.register(ReversionTestModel3, eager_signals=[pre_delete])
|
||||
self.assertTrue(reversion.is_registered(ReversionTestModel3))
|
||||
self.assertRaises(RegistrationError, lambda: reversion.register(ReversionTestModel3, eager_signals=[pre_delete]))
|
||||
self.assertTrue(ReversionTestModel3 in reversion.get_registered_models())
|
||||
self.assertTrue(isinstance(reversion.get_adapter(ReversionTestModel3), reversion.VersionAdapter))
|
||||
self.assertEquals([], reversion.default_revision_manager._signals[ReversionTestModel3])
|
||||
self.assertEquals([pre_delete], reversion.default_revision_manager._eager_signals[ReversionTestModel3])
|
||||
# Unregister the model and text.
|
||||
reversion.unregister(ReversionTestModel3)
|
||||
self.assertFalse(reversion.is_registered(ReversionTestModel3))
|
||||
self.assertRaises(RegistrationError, lambda: reversion.unregister(ReversionTestModel3))
|
||||
self.assertTrue(ReversionTestModel3 not in reversion.get_registered_models())
|
||||
self.assertRaises(RegistrationError, lambda: isinstance(reversion.get_adapter(ReversionTestModel3)))
|
||||
self.assertFalse(ReversionTestModel3 in reversion.default_revision_manager._signals)
|
||||
self.assertFalse(ReversionTestModel3 in reversion.default_revision_manager._eager_signals)
|
||||
|
||||
|
||||
class ReversionTestBase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
# Unregister all registered models.
|
||||
self.initial_registered_models = []
|
||||
for registered_model in reversion.get_registered_models():
|
||||
self.initial_registered_models.append((registered_model, reversion.get_adapter(registered_model).__class__))
|
||||
reversion.unregister(registered_model)
|
||||
# Register the test models.
|
||||
reversion.register(ReversionTestModel1)
|
||||
reversion.register(ReversionTestModel2)
|
||||
reversion.register(ReversionTestModel3, eager_signals=[pre_delete])
|
||||
# Create some test data.
|
||||
self.test11 = ReversionTestModel1.objects.create(
|
||||
name = "model1 instance1 version1",
|
||||
)
|
||||
self.test12 = ReversionTestModel1.objects.create(
|
||||
name = "model1 instance2 version1",
|
||||
)
|
||||
self.test21 = ReversionTestModel2.objects.create(
|
||||
name = "model2 instance1 version1",
|
||||
)
|
||||
self.test22 = ReversionTestModel2.objects.create(
|
||||
name = "model2 instance2 version1",
|
||||
)
|
||||
self.test31 = ReversionTestModel3.objects.create(
|
||||
name = "model3 instance1 version1",
|
||||
)
|
||||
self.test32 = ReversionTestModel3.objects.create(
|
||||
name = "model3 instance2 version1",
|
||||
)
|
||||
self.user = User.objects.create(
|
||||
username = "user1",
|
||||
)
|
||||
|
||||
def tearDown(self):
|
||||
# Unregister the test models.
|
||||
reversion.unregister(ReversionTestModel1)
|
||||
reversion.unregister(ReversionTestModel2)
|
||||
reversion.unregister(ReversionTestModel3)
|
||||
# Delete the test models.
|
||||
ReversionTestModel1.objects.all().delete()
|
||||
ReversionTestModel2.objects.all().delete()
|
||||
ReversionTestModel3.objects.all().delete()
|
||||
User.objects.all().delete()
|
||||
del self.test11
|
||||
del self.test12
|
||||
del self.test21
|
||||
del self.test22
|
||||
del self.test31
|
||||
del self.test32
|
||||
del self.user
|
||||
# Delete the revisions index.
|
||||
Revision.objects.all().delete()
|
||||
# Unregister all remaining models.
|
||||
for registered_model in reversion.get_registered_models():
|
||||
reversion.unregister(registered_model)
|
||||
# Re-register initial registered models.
|
||||
for initial_model, adapter in self.initial_registered_models:
|
||||
reversion.register(initial_model, adapter_cls=adapter)
|
||||
del self.initial_registered_models
|
||||
|
||||
|
||||
class RevisionTestBase(ReversionTestBase):
|
||||
|
||||
@reversion.create_revision()
|
||||
def setUp(self):
|
||||
super(RevisionTestBase, self).setUp()
|
||||
|
||||
|
||||
class InternalsTest(RevisionTestBase):
|
||||
|
||||
def testRevisionsCreated(self):
|
||||
self.assertEqual(Revision.objects.count(), 1)
|
||||
self.assertEqual(Version.objects.count(), 4)
|
||||
|
||||
def testContextManager(self):
|
||||
# New revision should be created.
|
||||
with reversion.create_revision():
|
||||
self.test11.name = "model1 instance1 version2"
|
||||
self.test11.save()
|
||||
self.assertEqual(Revision.objects.count(), 2)
|
||||
self.assertEqual(Version.objects.count(), 5)
|
||||
|
||||
def testManualRevisionManagement(self):
|
||||
# When manage manually is on, no revisions created.
|
||||
with reversion.create_revision(manage_manually=True):
|
||||
self.test11.name = "model1 instance1 version2"
|
||||
self.test11.save()
|
||||
self.assertEqual(Revision.objects.count(), 1)
|
||||
self.assertEqual(Version.objects.count(), 4)
|
||||
# Save a manual revision.
|
||||
reversion.default_revision_manager.save_revision([self.test11])
|
||||
self.assertEqual(Revision.objects.count(), 2)
|
||||
self.assertEqual(Version.objects.count(), 5)
|
||||
|
||||
def testEmptyRevisionNotCreated(self):
|
||||
with reversion.create_revision():
|
||||
pass
|
||||
self.assertEqual(Revision.objects.count(), 1)
|
||||
self.assertEqual(Version.objects.count(), 4)
|
||||
|
||||
def testRevisionContextAbandonedOnError(self):
|
||||
try:
|
||||
with reversion.create_revision():
|
||||
self.test11.name = "model1 instance1 version2"
|
||||
self.test11.save()
|
||||
raise Exception("Foo")
|
||||
except:
|
||||
pass
|
||||
self.assertEqual(Revision.objects.count(), 1)
|
||||
self.assertEqual(Version.objects.count(), 4)
|
||||
|
||||
def testRevisionDecoratorAbandonedOnError(self):
|
||||
@reversion.create_revision()
|
||||
def make_revision():
|
||||
self.test11.name = "model1 instance1 version2"
|
||||
self.test11.save()
|
||||
raise Exception("Foo")
|
||||
try:
|
||||
make_revision()
|
||||
except:
|
||||
pass
|
||||
self.assertEqual(Revision.objects.count(), 1)
|
||||
self.assertEqual(Version.objects.count(), 4)
|
||||
|
||||
def testRevisionCreatedOnDelete(self):
|
||||
with reversion.create_revision():
|
||||
self.test31.delete()
|
||||
self.assertEqual(Revision.objects.count(), 2)
|
||||
self.assertEqual(Version.objects.count(), 5)
|
||||
|
||||
|
||||
class ApiTest(RevisionTestBase):
|
||||
|
||||
def setUp(self):
|
||||
super(ApiTest, self).setUp()
|
||||
with reversion.create_revision():
|
||||
self.test11.name = "model1 instance1 version2"
|
||||
self.test11.save()
|
||||
self.test12.name = "model1 instance2 version2"
|
||||
self.test12.save()
|
||||
self.test21.name = "model2 instance1 version2"
|
||||
self.test21.save()
|
||||
self.test22.name = "model2 instance2 version2"
|
||||
self.test22.save()
|
||||
|
||||
def testRevisionSignals(self):
|
||||
pre_revision_receiver_called = []
|
||||
|
||||
def pre_revision_receiver(**kwargs):
|
||||
self.assertEqual(kwargs["instances"], [self.test11])
|
||||
self.assertTrue(isinstance(kwargs["revision"], Revision))
|
||||
self.assertEqual(len(kwargs["versions"]), 1)
|
||||
pre_revision_receiver_called.append(True)
|
||||
post_revision_receiver_called = []
|
||||
|
||||
def post_revision_receiver(**kwargs):
|
||||
self.assertEqual(kwargs["instances"], [self.test11])
|
||||
self.assertTrue(isinstance(kwargs["revision"], Revision))
|
||||
self.assertEqual(len(kwargs["versions"]), 1)
|
||||
post_revision_receiver_called.append(True)
|
||||
reversion.pre_revision_commit.connect(pre_revision_receiver)
|
||||
reversion.post_revision_commit.connect(post_revision_receiver)
|
||||
# Create a revision.
|
||||
with reversion.create_revision():
|
||||
self.test11.save()
|
||||
# Check the signals were called.
|
||||
self.assertTrue(pre_revision_receiver_called)
|
||||
self.assertTrue(post_revision_receiver_called)
|
||||
|
||||
def testCanGetForObjectReference(self):
|
||||
# Test a model with an int pk.
|
||||
versions = reversion.get_for_object_reference(ReversionTestModel1, self.test11.pk)
|
||||
self.assertEqual(len(versions), 2)
|
||||
self.assertEqual(versions[0].field_dict["name"], "model1 instance1 version2")
|
||||
self.assertEqual(versions[1].field_dict["name"], "model1 instance1 version1")
|
||||
# Test a model with a str pk.
|
||||
versions = reversion.get_for_object_reference(ReversionTestModel2, self.test21.pk)
|
||||
self.assertEqual(len(versions), 2)
|
||||
self.assertEqual(versions[0].field_dict["name"], "model2 instance1 version2")
|
||||
self.assertEqual(versions[1].field_dict["name"], "model2 instance1 version1")
|
||||
|
||||
def testCanGetForObject(self):
|
||||
# Test a model with an int pk.
|
||||
versions = reversion.get_for_object(self.test11)
|
||||
self.assertEqual(len(versions), 2)
|
||||
self.assertEqual(versions[0].field_dict["name"], "model1 instance1 version2")
|
||||
self.assertEqual(versions[1].field_dict["name"], "model1 instance1 version1")
|
||||
# Test a model with a str pk.
|
||||
versions = reversion.get_for_object(self.test21)
|
||||
self.assertEqual(len(versions), 2)
|
||||
self.assertEqual(versions[0].field_dict["name"], "model2 instance1 version2")
|
||||
self.assertEqual(versions[1].field_dict["name"], "model2 instance1 version1")
|
||||
|
||||
def testCanGetUniqueForObject(self):
|
||||
with reversion.create_revision():
|
||||
self.test11.save()
|
||||
self.test21.save()
|
||||
# Test a model with an int pk.
|
||||
self.assertEqual(reversion.get_for_object(self.test11).count(), 3)
|
||||
self.assertEqual(len(reversion.get_unique_for_object(self.test11)), 2)
|
||||
# Test a model with a str pk.
|
||||
self.assertEqual(reversion.get_for_object(self.test21).count(), 3)
|
||||
self.assertEqual(len(reversion.get_unique_for_object(self.test21)), 2)
|
||||
|
||||
def testCanGetForDate(self):
|
||||
with self.settings(USE_TZ=True):
|
||||
now = datetime.datetime.now(UTC())
|
||||
# Test a model with an int pk.
|
||||
version = reversion.get_for_date(self.test11, now)
|
||||
self.assertEqual(version.field_dict["name"], "model1 instance1 version2")
|
||||
self.assertRaises(Version.DoesNotExist, lambda: reversion.get_for_date(self.test11, datetime.datetime(1970, 1, 1, tzinfo=UTC())))
|
||||
# Test a model with a str pk.
|
||||
version = reversion.get_for_date(self.test21, now)
|
||||
self.assertEqual(version.field_dict["name"], "model2 instance1 version2")
|
||||
self.assertRaises(Version.DoesNotExist, lambda: reversion.get_for_date(self.test21, datetime.datetime(1970, 1, 1, tzinfo=UTC())))
|
||||
|
||||
def testCanGetDeleted(self):
|
||||
with reversion.create_revision():
|
||||
self.test11.delete()
|
||||
self.test21.delete()
|
||||
# Test a model with an int pk.
|
||||
versions = reversion.get_deleted(ReversionTestModel1)
|
||||
self.assertEqual(len(versions), 1)
|
||||
self.assertEqual(versions[0].field_dict["name"], "model1 instance1 version2")
|
||||
# Test a model with a str pk.
|
||||
versions = reversion.get_deleted(ReversionTestModel2)
|
||||
self.assertEqual(len(versions), 1)
|
||||
self.assertEqual(versions[0].field_dict["name"], "model2 instance1 version2")
|
||||
|
||||
def testCanRevertVersion(self):
|
||||
reversion.get_for_object(self.test11)[1].revert()
|
||||
self.assertEqual(ReversionTestModel1.objects.get(id=self.test11.pk).name, "model1 instance1 version1")
|
||||
|
||||
def testCanRevertRevision(self):
|
||||
reversion.get_for_object(self.test11)[1].revision.revert()
|
||||
self.assertEqual(ReversionTestModel1.objects.get(id=self.test11.pk).name, "model1 instance1 version1")
|
||||
self.assertEqual(ReversionTestModel1.objects.get(id=self.test12.pk).name, "model1 instance2 version1")
|
||||
self.assertEqual(ReversionTestModel2.objects.get(id=self.test22.pk).name, "model2 instance2 version1")
|
||||
self.assertEqual(ReversionTestModel2.objects.get(id=self.test22.pk).name, "model2 instance2 version1")
|
||||
|
||||
def testCanRevertRevisionWithDeletedVersions(self):
|
||||
self.assertEqual(ReversionTestModel1.objects.count(), 2)
|
||||
self.assertEqual(ReversionTestModel2.objects.count(), 2)
|
||||
with reversion.create_revision():
|
||||
self.test11.name = "model1 instance1 version3"
|
||||
self.test11.save()
|
||||
self.test12.delete()
|
||||
self.test21.name = "model2 instance1 version3"
|
||||
self.test21.save()
|
||||
self.test22.delete()
|
||||
self.assertEqual(ReversionTestModel1.objects.count(), 1)
|
||||
self.assertEqual(ReversionTestModel2.objects.count(), 1)
|
||||
with reversion.create_revision():
|
||||
self.test11.name = "model1 instance1 version4"
|
||||
self.test11.save()
|
||||
self.test21.name = "model2 instance1 version4"
|
||||
self.test21.save()
|
||||
self.assertEqual(ReversionTestModel1.objects.count(), 1)
|
||||
self.assertEqual(ReversionTestModel2.objects.count(), 1)
|
||||
# Revert to a revision where some deletes were logged.
|
||||
reversion.get_for_object(self.test11)[1].revision.revert()
|
||||
self.assertEqual(ReversionTestModel1.objects.count(), 1)
|
||||
self.assertEqual(ReversionTestModel2.objects.count(), 1)
|
||||
self.assertEqual(ReversionTestModel1.objects.get(id=self.test11.id).name, "model1 instance1 version3")
|
||||
self.assertEqual(ReversionTestModel2.objects.get(id=self.test21.id).name, "model2 instance1 version3")
|
||||
# Revert the a revision before the deletes were logged.
|
||||
reversion.get_for_object(self.test11)[2].revision.revert()
|
||||
self.assertEqual(ReversionTestModel1.objects.count(), 2)
|
||||
self.assertEqual(ReversionTestModel2.objects.count(), 2)
|
||||
|
||||
def testCanSaveIgnoringDuplicates(self):
|
||||
with reversion.create_revision():
|
||||
self.test11.save()
|
||||
self.test12.save()
|
||||
self.test21.save()
|
||||
self.test22.save()
|
||||
self.assertFalse(reversion.get_ignore_duplicates())
|
||||
reversion.set_ignore_duplicates(True)
|
||||
self.assertTrue(reversion.get_ignore_duplicates())
|
||||
self.assertEqual(reversion.get_for_object(self.test11).count(), 2)
|
||||
# Save a non-duplicate revision.
|
||||
with reversion.create_revision():
|
||||
self.test11.save()
|
||||
self.assertFalse(reversion.get_ignore_duplicates())
|
||||
reversion.set_ignore_duplicates(True)
|
||||
self.assertEqual(reversion.get_for_object(self.test11).count(), 3)
|
||||
|
||||
def testCanAddMetaToRevision(self):
|
||||
# Create a revision with lots of meta data.
|
||||
with reversion.create_revision():
|
||||
self.test11.save()
|
||||
reversion.set_comment("Foo bar")
|
||||
self.assertEqual(reversion.get_comment(), "Foo bar")
|
||||
reversion.set_user(self.user)
|
||||
self.assertEqual(reversion.get_user(), self.user)
|
||||
reversion.add_meta(RevisionMeta, age=5)
|
||||
# Test the revision data.
|
||||
revision = reversion.get_for_object(self.test11)[0].revision
|
||||
self.assertEqual(revision.user, self.user)
|
||||
self.assertEqual(revision.comment, "Foo bar")
|
||||
self.assertEqual(revision.revisionmeta.age, 5)
|
||||
|
||||
|
||||
class ReversionTestModel1Child(ReversionTestModel1):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class MultiTableInheritanceApiTest(RevisionTestBase):
|
||||
|
||||
def setUp(self):
|
||||
super(MultiTableInheritanceApiTest, self).setUp()
|
||||
reversion.register(ReversionTestModel1Child, follow=("reversiontestmodel1_ptr",))
|
||||
with reversion.create_revision():
|
||||
self.testchild1 = ReversionTestModel1Child.objects.create(
|
||||
name = "modelchild1 instance1 version 1",
|
||||
)
|
||||
|
||||
def testCanRetreiveFullFieldDict(self):
|
||||
self.assertEqual(reversion.get_for_object(self.testchild1)[0].field_dict["name"], "modelchild1 instance1 version 1")
|
||||
|
||||
def tearDown(self):
|
||||
super(MultiTableInheritanceApiTest, self).tearDown()
|
||||
del self.testchild1
|
||||
|
||||
|
||||
class ReversionTestModel1ChildProxy(ReversionTestModel1Child):
|
||||
class Meta:
|
||||
proxy = True
|
||||
|
||||
|
||||
class ProxyModelApiTest(RevisionTestBase):
|
||||
|
||||
def setUp(self):
|
||||
super(ProxyModelApiTest, self).setUp()
|
||||
reversion.register(ReversionTestModel1Proxy)
|
||||
self.concrete = self.test11
|
||||
self.proxy = ReversionTestModel1Proxy.objects.get(pk=self.concrete.pk)
|
||||
|
||||
with reversion.create_revision():
|
||||
self.proxy.name = "proxy model"
|
||||
self.proxy.save()
|
||||
|
||||
def testCanGetForObjectReference(self):
|
||||
# Can get version for proxy model
|
||||
proxy_versions = reversion.get_for_object_reference(ReversionTestModel1Proxy, self.proxy.id)
|
||||
self.assertEqual(len(proxy_versions), 2)
|
||||
self.assertEqual(proxy_versions[0].field_dict["name"], self.proxy.name)
|
||||
self.assertEqual(proxy_versions[1].field_dict["name"], self.concrete.name)
|
||||
|
||||
# Can get the same version for concrete model
|
||||
concrete_versions = reversion.get_for_object_reference(ReversionTestModel1, self.concrete.id)
|
||||
self.assertEqual(list(concrete_versions), list(proxy_versions))
|
||||
|
||||
def testCanGetForObject(self):
|
||||
# Can get version for proxy model
|
||||
proxy_versions = reversion.get_for_object(self.proxy)
|
||||
self.assertEqual(len(proxy_versions), 2)
|
||||
self.assertEqual(proxy_versions[0].field_dict["name"], self.proxy.name)
|
||||
self.assertEqual(proxy_versions[1].field_dict["name"], self.concrete.name)
|
||||
|
||||
# Can get the same version for concrete model
|
||||
concrete_versions = reversion.get_for_object(self.concrete)
|
||||
self.assertEqual(list(concrete_versions), list(proxy_versions))
|
||||
|
||||
def testCanRevertVersion(self):
|
||||
self.assertEqual(ReversionTestModel1.objects.get(pk=self.concrete.pk).name, self.proxy.name)
|
||||
reversion.get_for_object(self.proxy)[1].revert()
|
||||
self.assertEqual(ReversionTestModel1.objects.get(pk=self.concrete.pk).name, self.concrete.name)
|
||||
|
||||
def testMultiTableInheritanceProxyModel(self):
|
||||
reversion.register(ReversionTestModel1Child, follow=("reversiontestmodel1_ptr",))
|
||||
reversion.register(ReversionTestModel1ChildProxy, follow=("reversiontestmodel1_ptr",))
|
||||
|
||||
with reversion.create_revision():
|
||||
concrete = ReversionTestModel1Child.objects.create(name="modelchild1 instance1 version 1")
|
||||
|
||||
proxy = ReversionTestModel1ChildProxy.objects.get(pk=concrete.pk)
|
||||
with reversion.create_revision():
|
||||
proxy.name = "proxy model"
|
||||
proxy.save()
|
||||
|
||||
proxy_versions = reversion.get_for_object(proxy)
|
||||
|
||||
self.assertEqual(proxy_versions[0].field_dict["name"], proxy.name)
|
||||
self.assertEqual(proxy_versions[1].field_dict["name"], concrete.name)
|
||||
|
||||
|
||||
class FollowModelsTest(ReversionTestBase):
|
||||
|
||||
@reversion.create_revision()
|
||||
def setUp(self):
|
||||
super(FollowModelsTest, self).setUp()
|
||||
reversion.unregister(ReversionTestModel1)
|
||||
reversion.register(ReversionTestModel1, follow=("testfollowmodel_set",))
|
||||
reversion.register(TestFollowModel, follow=("test_model_1", "test_model_2s",))
|
||||
self.follow1 = TestFollowModel.objects.create(
|
||||
name = "related instance1 version 1",
|
||||
test_model_1 = self.test11,
|
||||
)
|
||||
self.follow1.test_model_2s.add(self.test21, self.test22)
|
||||
|
||||
def testRelationsFollowed(self):
|
||||
self.assertEqual(Revision.objects.count(), 1)
|
||||
self.assertEqual(Version.objects.count(), 5)
|
||||
with reversion.create_revision():
|
||||
self.follow1.save()
|
||||
self.assertEqual(Revision.objects.count(), 2)
|
||||
self.assertEqual(Version.objects.count(), 9)
|
||||
|
||||
def testRevertWithDelete(self):
|
||||
with reversion.create_revision():
|
||||
test23 = ReversionTestModel2.objects.create(
|
||||
name = "model2 instance3 version1",
|
||||
)
|
||||
self.follow1.test_model_2s.add(test23)
|
||||
self.follow1.save()
|
||||
self.assertEqual(reversion.get_for_object(test23).count(), 1)
|
||||
self.assertEqual(self.follow1.test_model_2s.all().count(), 3)
|
||||
# Test that a revert with delete works.
|
||||
test23_pk = test23.pk
|
||||
self.assertEqual(ReversionTestModel2.objects.count(), 3)
|
||||
with reversion.create_revision():
|
||||
reversion.get_for_object(self.follow1)[1].revision.revert(delete=True)
|
||||
self.assertEqual(ReversionTestModel1.objects.get(id=self.test11.pk).name, "model1 instance1 version1")
|
||||
self.assertEqual(ReversionTestModel2.objects.get(id=self.test22.pk).name, "model2 instance2 version1")
|
||||
self.assertEqual(ReversionTestModel2.objects.get(id=self.test22.pk).name, "model2 instance2 version1")
|
||||
self.assertEqual(ReversionTestModel2.objects.count(), 2)
|
||||
self.assertRaises(ReversionTestModel2.DoesNotExist, lambda: ReversionTestModel2.objects.get(id=test23_pk))
|
||||
# Roll back to the revision where all models were present.
|
||||
reversion.get_for_object(self.follow1)[1].revision.revert()
|
||||
self.assertEqual(self.follow1.test_model_2s.all().count(), 3)
|
||||
# Roll back to a revision where a delete flag is present.
|
||||
reversion.get_for_object(self.follow1)[0].revision.revert(delete=True)
|
||||
self.assertEqual(self.follow1.test_model_2s.all().count(), 2)
|
||||
|
||||
def testReverseRelationsFollowed(self):
|
||||
self.assertEqual(Revision.objects.count(), 1)
|
||||
self.assertEqual(Version.objects.count(), 5)
|
||||
with reversion.create_revision():
|
||||
self.test11.save()
|
||||
self.assertEqual(Revision.objects.count(), 2)
|
||||
self.assertEqual(Version.objects.count(), 9)
|
||||
|
||||
def testReverseFollowRevertWithDelete(self):
|
||||
with reversion.create_revision():
|
||||
follow2 = TestFollowModel.objects.create(
|
||||
name = "related instance2 version 1",
|
||||
test_model_1 = self.test11,
|
||||
)
|
||||
# Test that a revert with delete works.
|
||||
follow2_pk = follow2.pk
|
||||
reversion.get_for_object(self.test11)[1].revision.revert(delete=True)
|
||||
self.assertEqual(TestFollowModel.objects.count(), 1)
|
||||
self.assertRaises(TestFollowModel.DoesNotExist, lambda: TestFollowModel.objects.get(id=follow2_pk))
|
||||
|
||||
def testRecoverDeleted(self):
|
||||
# Delete the test model.
|
||||
with reversion.create_revision():
|
||||
self.test11.delete()
|
||||
self.assertEqual(TestFollowModel.objects.count(), 0)
|
||||
self.assertEqual(ReversionTestModel1.objects.count(), 1)
|
||||
# Recover the test model.
|
||||
with reversion.create_revision():
|
||||
reversion.get_deleted(ReversionTestModel1)[0].revision.revert()
|
||||
# Make sure it was recovered.
|
||||
self.assertEqual(TestFollowModel.objects.count(), 1)
|
||||
self.assertEqual(ReversionTestModel1.objects.count(), 2)
|
||||
|
||||
def tearDown(self):
|
||||
reversion.unregister(TestFollowModel)
|
||||
TestFollowModel.objects.all().delete()
|
||||
del self.follow1
|
||||
super(FollowModelsTest, self).tearDown()
|
||||
|
||||
|
||||
excluded_revision_manager = RevisionManager("excluded")
|
||||
|
||||
|
||||
class ExcludedFieldsTest(RevisionTestBase):
|
||||
|
||||
def setUp(self):
|
||||
excluded_revision_manager.register(ReversionTestModel1, fields=("id",))
|
||||
excluded_revision_manager.register(ReversionTestModel2, exclude=("name",))
|
||||
super(ExcludedFieldsTest, self).setUp()
|
||||
|
||||
def testExcludedRevisionManagerIsSeparate(self):
|
||||
self.assertEqual(excluded_revision_manager.get_for_object(self.test11).count(), 1)
|
||||
|
||||
def testExcludedFieldsAreRespected(self):
|
||||
self.assertEqual(excluded_revision_manager.get_for_object(self.test11)[0].field_dict["id"], self.test11.id)
|
||||
self.assertEqual(excluded_revision_manager.get_for_object(self.test11)[0].field_dict["name"], "")
|
||||
self.assertEqual(excluded_revision_manager.get_for_object(self.test21)[0].field_dict["id"], self.test21.id)
|
||||
self.assertEqual(excluded_revision_manager.get_for_object(self.test21)[0].field_dict["name"], "")
|
||||
|
||||
def tearDown(self):
|
||||
super(ExcludedFieldsTest, self).tearDown()
|
||||
excluded_revision_manager.unregister(ReversionTestModel1)
|
||||
excluded_revision_manager.unregister(ReversionTestModel2)
|
||||
|
||||
|
||||
class CreateInitialRevisionsTest(ReversionTestBase):
|
||||
|
||||
def testCreateInitialRevisions(self):
|
||||
self.assertEqual(Revision.objects.count(), 0)
|
||||
self.assertEqual(Version.objects.count(), 0)
|
||||
call_command("createinitialrevisions")
|
||||
revcount = Revision.objects.count()
|
||||
vercount = Version.objects.count()
|
||||
self.assertTrue(revcount >= 4)
|
||||
self.assertTrue(vercount >= 4)
|
||||
call_command("createinitialrevisions")
|
||||
self.assertEqual(Revision.objects.count(), revcount)
|
||||
self.assertEqual(Version.objects.count(), vercount)
|
||||
|
||||
def testCreateInitialRevisionsSpecificApps(self):
|
||||
call_command("createinitialrevisions", "test_reversion")
|
||||
self.assertEqual(Revision.objects.count(), 6)
|
||||
self.assertEqual(Version.objects.count(), 6)
|
||||
|
||||
def testCreateInitialRevisionsSpecificModels(self):
|
||||
call_command("createinitialrevisions", "test_reversion.ReversionTestModel1")
|
||||
self.assertEqual(Revision.objects.count(), 2)
|
||||
self.assertEqual(Version.objects.count(), 2)
|
||||
call_command("createinitialrevisions", "test_reversion.ReversionTestModel2")
|
||||
self.assertEqual(Revision.objects.count(), 4)
|
||||
self.assertEqual(Version.objects.count(), 4)
|
||||
|
||||
def testCreateInitialRevisionsSpecificComment(self):
|
||||
call_command("createinitialrevisions", comment="Foo bar")
|
||||
self.assertEqual(Revision.objects.all()[0].comment, "Foo bar")
|
||||
|
||||
|
||||
# Tests for reversion functionality that's tied to requests.
|
||||
|
||||
class RevisionMiddlewareTest(ReversionTestBase):
|
||||
|
||||
urls = "test_reversion.urls"
|
||||
|
||||
def testRevisionMiddleware(self):
|
||||
self.assertEqual(Revision.objects.count(), 0)
|
||||
self.assertEqual(Version.objects.count(), 0)
|
||||
self.client.get("/success/")
|
||||
self.assertEqual(Revision.objects.count(), 1)
|
||||
self.assertEqual(Version.objects.count(), 4)
|
||||
|
||||
def testRevisionMiddlewareInvalidatesRevisionOnError(self):
|
||||
self.assertEqual(Revision.objects.count(), 0)
|
||||
self.assertEqual(Version.objects.count(), 0)
|
||||
self.assertRaises(Exception, lambda: self.client.get("/error/"))
|
||||
self.assertEqual(Revision.objects.count(), 0)
|
||||
self.assertEqual(Version.objects.count(), 0)
|
||||
|
||||
def testRevisionMiddlewareErrorOnDoubleMiddleware(self):
|
||||
self.assertRaises(ImproperlyConfigured, lambda: self.client.get("/double/"))
|
||||
|
||||
|
||||
class VersionAdminTest(TestCase):
|
||||
|
||||
urls = "test_reversion.urls"
|
||||
|
||||
def setUp(self):
|
||||
self.old_TEMPLATE_DIRS = settings.TEMPLATE_DIRS
|
||||
settings.TEMPLATE_DIRS = (
|
||||
os.path.join(os.path.dirname(admin.__file__), "templates"),
|
||||
)
|
||||
self.user = User(
|
||||
username = "foo",
|
||||
is_staff = True,
|
||||
is_superuser = True,
|
||||
)
|
||||
self.user.set_password("bar")
|
||||
self.user.save()
|
||||
# Log the user in.
|
||||
if hasattr(self, "settings"):
|
||||
with self.settings(INSTALLED_APPS=tuple(set(tuple(settings.INSTALLED_APPS) + ("django.contrib.sessions",)))): # HACK: Without this the client won't log in, for some reason.
|
||||
self.client.login(
|
||||
username = "foo",
|
||||
password = "bar",
|
||||
)
|
||||
else:
|
||||
self.client.login(
|
||||
username = "foo",
|
||||
password = "bar",
|
||||
)
|
||||
|
||||
@skipUnless('django.contrib.admin' in settings.INSTALLED_APPS,
|
||||
"django.contrib.admin not activated")
|
||||
def testAutoRegisterWorks(self):
|
||||
self.assertTrue(reversion.is_registered(ChildTestAdminModel))
|
||||
self.assertTrue(reversion.is_registered(ParentTestAdminModel))
|
||||
|
||||
@skipUnless('django.contrib.admin' in settings.INSTALLED_APPS,
|
||||
"django.contrib.admin not activated")
|
||||
def testRevisionSavedOnPost(self):
|
||||
self.assertEqual(ChildTestAdminModel.objects.count(), 0)
|
||||
# Create an instance via the admin.
|
||||
response = self.client.post("/admin/test_reversion/childtestadminmodel/add/", {
|
||||
"parent_name": "parent instance1 version1",
|
||||
"child_name": "child instance1 version1",
|
||||
"_continue": 1,
|
||||
})
|
||||
self.assertEqual(response.status_code, 302)
|
||||
obj_pk = response["Location"].split("/")[-2]
|
||||
obj = ChildTestAdminModel.objects.get(id=obj_pk)
|
||||
# Check that a version is created.
|
||||
versions = reversion.get_for_object(obj)
|
||||
self.assertEqual(versions.count(), 1)
|
||||
self.assertEqual(versions[0].field_dict["parent_name"], "parent instance1 version1")
|
||||
self.assertEqual(versions[0].field_dict["child_name"], "child instance1 version1")
|
||||
# Save a new version.
|
||||
response = self.client.post("/admin/test_reversion/childtestadminmodel/%s/" % obj_pk, {
|
||||
"parent_name": "parent instance1 version2",
|
||||
"child_name": "child instance1 version2",
|
||||
"_continue": 1,
|
||||
})
|
||||
self.assertEqual(response.status_code, 302)
|
||||
# Check that a version is created.
|
||||
versions = reversion.get_for_object(obj)
|
||||
self.assertEqual(versions.count(), 2)
|
||||
self.assertEqual(versions[0].field_dict["parent_name"], "parent instance1 version2")
|
||||
self.assertEqual(versions[0].field_dict["child_name"], "child instance1 version2")
|
||||
# Check that the versions can be listed.
|
||||
response = self.client.get("/admin/test_reversion/childtestadminmodel/%s/history/" % obj_pk)
|
||||
self.assertContains(response, "child instance1 version2")
|
||||
self.assertContains(response, "child instance1 version1")
|
||||
# Check that a version can be rolled back.
|
||||
response = self.client.post("/admin/test_reversion/childtestadminmodel/%s/history/%s/" % (obj_pk, versions[1].pk), {
|
||||
"parent_name": "parent instance1 version3",
|
||||
"child_name": "child instance1 version3",
|
||||
})
|
||||
self.assertEqual(response.status_code, 302)
|
||||
# Check that a version is created.
|
||||
versions = reversion.get_for_object(obj)
|
||||
self.assertEqual(versions.count(), 3)
|
||||
self.assertEqual(versions[0].field_dict["parent_name"], "parent instance1 version3")
|
||||
self.assertEqual(versions[0].field_dict["child_name"], "child instance1 version3")
|
||||
# Check that a deleted version can be viewed.
|
||||
obj.delete()
|
||||
response = self.client.get("/admin/test_reversion/childtestadminmodel/recover/")
|
||||
self.assertContains(response, "child instance1 version3")
|
||||
# Check that a deleted version can be recovered.
|
||||
response = self.client.post("/admin/test_reversion/childtestadminmodel/recover/%s/" % versions[0].pk, {
|
||||
"parent_name": "parent instance1 version4",
|
||||
"child_name": "child instance1 version4",
|
||||
})
|
||||
obj = ChildTestAdminModel.objects.get(id=obj_pk)
|
||||
|
||||
|
||||
def createInlineObjects(self, should_delete):
|
||||
# Create an instance via the admin without a child.
|
||||
response = self.client.post("/admin/test_reversion/inlinetestparentmodel/add/", {
|
||||
"name": "parent version1",
|
||||
"children-TOTAL_FORMS": "0",
|
||||
"children-INITIAL_FORMS": "0",
|
||||
# "children-0-name": "child version 1",
|
||||
"_continue": 1,
|
||||
})
|
||||
self.assertEqual(response.status_code, 302)
|
||||
parent_pk = response["Location"].split("/")[-2]
|
||||
parent = InlineTestParentModel.objects.get(id=parent_pk)
|
||||
|
||||
# Update instance via the admin to add a child
|
||||
response = self.client.post("/admin/test_reversion/inlinetestparentmodel/%s/" % parent_pk, {
|
||||
"name": "parent version1",
|
||||
"children-TOTAL_FORMS": "1",
|
||||
"children-INITIAL_FORMS": "0",
|
||||
"children-0-name": "child version 1",
|
||||
"_continue": 1,
|
||||
})
|
||||
self.assertEqual(response.status_code, 302)
|
||||
children = InlineTestChildModel.objects.filter(parent=parent_pk)
|
||||
self.assertEqual(children.count(), 1)
|
||||
|
||||
# get list of versions
|
||||
version_list = reversion.get_for_object(parent)
|
||||
self.assertEqual(len(version_list), 2)
|
||||
|
||||
# check if reversion page has the checkbox for the inline checked
|
||||
response = self.client.get("/admin/test_reversion/inlinetestparentmodel/%s/history/%s/" %
|
||||
(parent_pk, version_list[1].id))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
if should_delete:
|
||||
self.assertContains(response, '<input checked="checked" id="id_children-0-DELETE" name="children-0-DELETE" type="checkbox" />') # this is crude
|
||||
else:
|
||||
self.assertNotContains(response, '<input checked="checked" id="id_children-0-DELETE" name="children-0-DELETE" type="checkbox" />') # this is crude
|
||||
|
||||
# don't actually submit a post since the values we submit would be from the test, not what the admin defaults
|
||||
|
||||
|
||||
@skipUnless('django.contrib.admin' in settings.INSTALLED_APPS,
|
||||
"django.contrib.admin not activated")
|
||||
def testInlineAdmin(self):
|
||||
self.assertTrue(reversion.is_registered(InlineTestParentModel))
|
||||
|
||||
# make sure model is following the child FK
|
||||
self.assertTrue('children' in reversion.get_adapter(InlineTestParentModel).follow)
|
||||
|
||||
self.createInlineObjects(True)
|
||||
|
||||
# unregister model
|
||||
reversion.unregister(InlineTestParentModel)
|
||||
self.assertFalse(reversion.is_registered(InlineTestParentModel))
|
||||
|
||||
# re-register without following
|
||||
reversion.register(InlineTestParentModel)
|
||||
self.assertTrue(reversion.is_registered(InlineTestParentModel))
|
||||
|
||||
# make sure model is NOT following the child FK
|
||||
self.assertFalse('children' in reversion.get_adapter(InlineTestParentModel).follow)
|
||||
|
||||
self.createInlineObjects(False)
|
||||
|
||||
|
||||
def tearDown(self):
|
||||
self.client.logout()
|
||||
self.user.delete()
|
||||
del self.user
|
||||
ChildTestAdminModel.objects.all().delete()
|
||||
settings.TEMPLATE_DIRS = self.old_TEMPLATE_DIRS
|
||||
|
||||
|
||||
# Tests for optional patch generation methods.
|
||||
|
||||
try:
|
||||
from reversion.helpers import generate_patch, generate_patch_html
|
||||
except ImportError:
|
||||
can_test_patch = False
|
||||
else:
|
||||
can_test_patch = True
|
||||
|
||||
|
||||
class PatchTest(RevisionTestBase):
|
||||
|
||||
def setUp(self):
|
||||
super(PatchTest, self).setUp()
|
||||
with reversion.create_revision():
|
||||
self.test11.name = "model1 instance1 version2"
|
||||
self.test11.save()
|
||||
self.version2, self.version1 = reversion.get_for_object(self.test11)
|
||||
|
||||
@skipUnless(can_test_patch, "Diff match patch library not installed")
|
||||
def testCanGeneratePatch(self):
|
||||
self.assertEqual(
|
||||
generate_patch(self.version1, self.version2, "name"),
|
||||
"@@ -17,9 +17,9 @@\n version\n-1\n+2\n",
|
||||
)
|
||||
|
||||
@skipUnless(can_test_patch, "Diff match patch library not installed")
|
||||
def testCanGeneratePathHtml(self):
|
||||
self.assertEqual(
|
||||
generate_patch_html(self.version1, self.version2, "name"),
|
||||
'<span>model1 instance1 version</span><del style="background:#ffe6e6;">1</del><ins style="background:#e6ffe6;">2</ins>',
|
||||
)
|
||||
|
||||
def tearDown(self):
|
||||
super(PatchTest, self).tearDown()
|
||||
del self.version1
|
||||
del self.version2
|
||||
|
||||
|
||||
# test preserve deleted User Revisions
|
||||
class DeleteUserTest(RevisionTestBase):
|
||||
|
||||
def testDeleteUser(self):
|
||||
self.assertEqual(Revision.objects.count(), 1)
|
||||
self.assertEqual(Version.objects.count(), 4)
|
||||
rev = Revision.objects.all()[0]
|
||||
rev.user = self.user
|
||||
rev.save()
|
||||
self.user.delete()
|
||||
self.assertEqual(Revision.objects.count(), 1)
|
||||
self.assertEqual(Version.objects.count(), 4)
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue