From 7ddda91ed2cba0b1b6476b29f3bb3f2394cd4781 Mon Sep 17 00:00:00 2001 From: Michael Fladischer Date: Fri, 8 Dec 2017 19:55:32 +0100 Subject: [PATCH] Import django-reversion_2.0.12.orig.tar.gz [dgit import orig django-reversion_2.0.12.orig.tar.gz] --- .coveragerc | 21 + .gitignore | 15 + .travis.yml | 39 + CHANGELOG.rst | 686 ++++++++++++++++++ LICENSE | 27 + MANIFEST.in | 9 + README.rst | 60 ++ docs/_include/admin.rst | 13 + docs/_include/create-revision-args.rst | 8 + docs/_include/create-revision-atomic.rst | 1 + .../create-revision-manage-manually.rst | 1 + docs/_include/create-revision-using.rst | 1 + docs/_include/model-db-arg.rst | 2 + docs/_include/post-register.rst | 2 + docs/_include/signal-args.rst | 8 + docs/_include/throws-registration-error.rst | 1 + docs/_include/throws-revert-error.rst | 1 + docs/_include/throws-revision-error.rst | 1 + docs/admin.rst | 109 +++ docs/api.rst | 479 ++++++++++++ docs/changelog.rst | 1 + docs/commands.rst | 45 ++ docs/common-problems.rst | 12 + docs/conf.py | 335 +++++++++ docs/django-versions.rst | 19 + docs/errors.rst | 30 + docs/index.rst | 73 ++ docs/middleware.rst | 34 + docs/signals.rst | 22 + docs/views.rst | 45 ++ reversion/__init__.py | 39 + reversion/admin.py | 305 ++++++++ reversion/compat.py | 17 + reversion/errors.py | 13 + reversion/locale/ar/LC_MESSAGES/django.mo | Bin 0 -> 2637 bytes reversion/locale/ar/LC_MESSAGES/django.po | 125 ++++ reversion/locale/cs/LC_MESSAGES/django.mo | Bin 0 -> 2474 bytes reversion/locale/cs/LC_MESSAGES/django.po | 127 ++++ reversion/locale/da/LC_MESSAGES/django.mo | Bin 0 -> 2384 bytes reversion/locale/da/LC_MESSAGES/django.po | 122 ++++ reversion/locale/de/LC_MESSAGES/django.mo | Bin 0 -> 2599 bytes reversion/locale/de/LC_MESSAGES/django.po | 133 ++++ reversion/locale/es/LC_MESSAGES/django.mo | Bin 0 -> 2506 bytes reversion/locale/es/LC_MESSAGES/django.po | 126 ++++ reversion/locale/es_AR/LC_MESSAGES/django.mo | Bin 0 -> 2507 bytes reversion/locale/es_AR/LC_MESSAGES/django.po | 134 ++++ reversion/locale/fr/LC_MESSAGES/django.mo | Bin 0 -> 2582 bytes reversion/locale/fr/LC_MESSAGES/django.po | 127 ++++ reversion/locale/he/LC_MESSAGES/django.mo | Bin 0 -> 2530 bytes reversion/locale/he/LC_MESSAGES/django.po | 123 ++++ reversion/locale/it/LC_MESSAGES/django.mo | Bin 0 -> 2395 bytes reversion/locale/it/LC_MESSAGES/django.po | 113 +++ reversion/locale/nb/LC_MESSAGES/django.mo | Bin 0 -> 2506 bytes reversion/locale/nb/LC_MESSAGES/django.po | 116 +++ reversion/locale/nl/LC_MESSAGES/django.mo | Bin 0 -> 2450 bytes reversion/locale/nl/LC_MESSAGES/django.po | 135 ++++ reversion/locale/pl/LC_MESSAGES/django.mo | Bin 0 -> 2526 bytes reversion/locale/pl/LC_MESSAGES/django.po | 124 ++++ reversion/locale/pt_BR/LC_MESSAGES/django.mo | Bin 0 -> 2377 bytes reversion/locale/pt_BR/LC_MESSAGES/django.po | 113 +++ reversion/locale/ru/LC_MESSAGES/django.mo | Bin 0 -> 2989 bytes reversion/locale/ru/LC_MESSAGES/django.po | 118 +++ reversion/locale/sk/LC_MESSAGES/django.mo | Bin 0 -> 2489 bytes reversion/locale/sk/LC_MESSAGES/django.po | 128 ++++ reversion/locale/sv/LC_MESSAGES/django.mo | Bin 0 -> 2535 bytes reversion/locale/sv/LC_MESSAGES/django.po | 138 ++++ reversion/locale/uk/LC_MESSAGES/django.mo | Bin 0 -> 3451 bytes reversion/locale/uk/LC_MESSAGES/django.po | 134 ++++ reversion/locale/zh_CN/LC_MESSAGES/django.mo | Bin 0 -> 2183 bytes reversion/locale/zh_CN/LC_MESSAGES/django.po | 121 +++ .../locale/zh_Hans/LC_MESSAGES/django.mo | Bin 0 -> 2183 bytes .../locale/zh_Hans/LC_MESSAGES/django.po | 121 +++ reversion/management/__init__.py | 0 reversion/management/commands/__init__.py | 57 ++ .../commands/createinitialrevisions.py | 74 ++ .../management/commands/deleterevisions.py | 95 +++ reversion/middleware.py | 51 ++ reversion/migrations/0001_initial.py | 48 ++ .../0001_squashed_0004_auto_20160611_1202.py | 54 ++ .../migrations/0002_auto_20141216_1509.py | 19 + .../migrations/0003_auto_20160601_1600.py | 108 +++ .../migrations/0004_auto_20160611_1202.py | 24 + reversion/migrations/__init__.py | 0 reversion/models.py | 356 +++++++++ reversion/revisions.py | 433 +++++++++++ reversion/signals.py | 10 + .../templates/reversion/change_list.html | 10 + .../templates/reversion/object_history.html | 42 ++ .../templates/reversion/recover_form.html | 25 + .../templates/reversion/recover_list.html | 41 ++ .../templates/reversion/revision_form.html | 26 + reversion/views.py | 68 ++ setup.cfg | 2 + setup.py | 45 ++ tests/manage.py | 22 + tests/test_app/__init__.py | 0 tests/test_app/admin.py | 14 + tests/test_app/migrations/0001_initial.py | 103 +++ tests/test_app/migrations/__init__.py | 0 tests/test_app/models.py | 111 +++ tests/test_app/tests/__init__.py | 0 tests/test_app/tests/base.py | 112 +++ tests/test_app/tests/test_admin.py | 259 +++++++ tests/test_app/tests/test_api.py | 328 +++++++++ tests/test_app/tests/test_commands.py | 195 +++++ tests/test_app/tests/test_middleware.py | 37 + tests/test_app/tests/test_models.py | 375 ++++++++++ tests/test_app/tests/test_views.py | 42 ++ tests/test_app/urls.py | 10 + tests/test_app/views.py | 24 + tests/test_project/__init__.py | 0 tests/test_project/settings.py | 135 ++++ tests/test_project/urls.py | 12 + tests/test_project/wsgi.py | 16 + tox.ini | 46 ++ 115 files changed, 8281 insertions(+) create mode 100644 .coveragerc create mode 100644 .gitignore create mode 100644 .travis.yml create mode 100644 CHANGELOG.rst create mode 100644 LICENSE create mode 100644 MANIFEST.in create mode 100644 README.rst create mode 100644 docs/_include/admin.rst create mode 100644 docs/_include/create-revision-args.rst create mode 100644 docs/_include/create-revision-atomic.rst create mode 100644 docs/_include/create-revision-manage-manually.rst create mode 100644 docs/_include/create-revision-using.rst create mode 100644 docs/_include/model-db-arg.rst create mode 100644 docs/_include/post-register.rst create mode 100644 docs/_include/signal-args.rst create mode 100644 docs/_include/throws-registration-error.rst create mode 100644 docs/_include/throws-revert-error.rst create mode 100644 docs/_include/throws-revision-error.rst create mode 100644 docs/admin.rst create mode 100644 docs/api.rst create mode 100644 docs/changelog.rst create mode 100644 docs/commands.rst create mode 100644 docs/common-problems.rst create mode 100644 docs/conf.py create mode 100644 docs/django-versions.rst create mode 100644 docs/errors.rst create mode 100644 docs/index.rst create mode 100644 docs/middleware.rst create mode 100644 docs/signals.rst create mode 100644 docs/views.rst create mode 100644 reversion/__init__.py create mode 100644 reversion/admin.py create mode 100644 reversion/compat.py create mode 100644 reversion/errors.py create mode 100644 reversion/locale/ar/LC_MESSAGES/django.mo create mode 100644 reversion/locale/ar/LC_MESSAGES/django.po create mode 100644 reversion/locale/cs/LC_MESSAGES/django.mo create mode 100644 reversion/locale/cs/LC_MESSAGES/django.po create mode 100644 reversion/locale/da/LC_MESSAGES/django.mo create mode 100644 reversion/locale/da/LC_MESSAGES/django.po create mode 100644 reversion/locale/de/LC_MESSAGES/django.mo create mode 100644 reversion/locale/de/LC_MESSAGES/django.po create mode 100644 reversion/locale/es/LC_MESSAGES/django.mo create mode 100644 reversion/locale/es/LC_MESSAGES/django.po create mode 100644 reversion/locale/es_AR/LC_MESSAGES/django.mo create mode 100644 reversion/locale/es_AR/LC_MESSAGES/django.po create mode 100644 reversion/locale/fr/LC_MESSAGES/django.mo create mode 100644 reversion/locale/fr/LC_MESSAGES/django.po create mode 100644 reversion/locale/he/LC_MESSAGES/django.mo create mode 100644 reversion/locale/he/LC_MESSAGES/django.po create mode 100644 reversion/locale/it/LC_MESSAGES/django.mo create mode 100644 reversion/locale/it/LC_MESSAGES/django.po create mode 100644 reversion/locale/nb/LC_MESSAGES/django.mo create mode 100644 reversion/locale/nb/LC_MESSAGES/django.po create mode 100644 reversion/locale/nl/LC_MESSAGES/django.mo create mode 100644 reversion/locale/nl/LC_MESSAGES/django.po create mode 100644 reversion/locale/pl/LC_MESSAGES/django.mo create mode 100644 reversion/locale/pl/LC_MESSAGES/django.po create mode 100644 reversion/locale/pt_BR/LC_MESSAGES/django.mo create mode 100644 reversion/locale/pt_BR/LC_MESSAGES/django.po create mode 100644 reversion/locale/ru/LC_MESSAGES/django.mo create mode 100644 reversion/locale/ru/LC_MESSAGES/django.po create mode 100644 reversion/locale/sk/LC_MESSAGES/django.mo create mode 100644 reversion/locale/sk/LC_MESSAGES/django.po create mode 100644 reversion/locale/sv/LC_MESSAGES/django.mo create mode 100644 reversion/locale/sv/LC_MESSAGES/django.po create mode 100644 reversion/locale/uk/LC_MESSAGES/django.mo create mode 100644 reversion/locale/uk/LC_MESSAGES/django.po create mode 100644 reversion/locale/zh_CN/LC_MESSAGES/django.mo create mode 100644 reversion/locale/zh_CN/LC_MESSAGES/django.po create mode 100644 reversion/locale/zh_Hans/LC_MESSAGES/django.mo create mode 100644 reversion/locale/zh_Hans/LC_MESSAGES/django.po create mode 100644 reversion/management/__init__.py create mode 100644 reversion/management/commands/__init__.py create mode 100644 reversion/management/commands/createinitialrevisions.py create mode 100644 reversion/management/commands/deleterevisions.py create mode 100644 reversion/middleware.py create mode 100644 reversion/migrations/0001_initial.py create mode 100644 reversion/migrations/0001_squashed_0004_auto_20160611_1202.py create mode 100644 reversion/migrations/0002_auto_20141216_1509.py create mode 100644 reversion/migrations/0003_auto_20160601_1600.py create mode 100644 reversion/migrations/0004_auto_20160611_1202.py create mode 100644 reversion/migrations/__init__.py create mode 100644 reversion/models.py create mode 100644 reversion/revisions.py create mode 100644 reversion/signals.py create mode 100644 reversion/templates/reversion/change_list.html create mode 100644 reversion/templates/reversion/object_history.html create mode 100644 reversion/templates/reversion/recover_form.html create mode 100644 reversion/templates/reversion/recover_list.html create mode 100644 reversion/templates/reversion/revision_form.html create mode 100644 reversion/views.py create mode 100644 setup.cfg create mode 100644 setup.py create mode 100755 tests/manage.py create mode 100644 tests/test_app/__init__.py create mode 100644 tests/test_app/admin.py create mode 100644 tests/test_app/migrations/0001_initial.py create mode 100644 tests/test_app/migrations/__init__.py create mode 100644 tests/test_app/models.py create mode 100644 tests/test_app/tests/__init__.py create mode 100644 tests/test_app/tests/base.py create mode 100644 tests/test_app/tests/test_admin.py create mode 100644 tests/test_app/tests/test_api.py create mode 100644 tests/test_app/tests/test_commands.py create mode 100644 tests/test_app/tests/test_middleware.py create mode 100644 tests/test_app/tests/test_models.py create mode 100644 tests/test_app/tests/test_views.py create mode 100644 tests/test_app/urls.py create mode 100644 tests/test_app/views.py create mode 100644 tests/test_project/__init__.py create mode 100644 tests/test_project/settings.py create mode 100644 tests/test_project/urls.py create mode 100644 tests/test_project/wsgi.py create mode 100644 tox.ini diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..e2170e5 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,21 @@ +[run] +source = + reversion + test_app + test_project + +[report] +exclude_lines = + # Have to re-enable the standard pragma + pragma: no cover + + # Don't complain if tests don't hit defensive assertion code: + raise AssertionError + raise NotImplementedError + assert False + + # Don't complain if tests don't hit model __str__ methods. + def __str__ + +show_missing = True +skip_covered = True diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9ea259e --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +.DS_Store +*.db +.project +.pydevproject +.settings +*.pyc +*.pyo +dist +build +MANIFEST +*.egg-info/ +docs/_build +.coverage +*.sqlite3 +.tox diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..11c2535 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,39 @@ +sudo: false +language: python +python: +- 3.6 +addons: + apt: + packages: + - libmysqlclient-dev +cache: + directories: + - "$HOME/.cache/pip" +env: + global: + - PYTHONWARNINGS=default,ignore::PendingDeprecationWarning,ignore::ResourceWarning + - DJANGO_DATABASE_USER_POSTGRES=postgres + - DJANGO_DATABASE_USER_MYSQL=travis +matrix: + fast_finish: true +services: +- postgresql +- mysql +install: +- pyenv shell 2.7 3.5 3.6 +- pip install 'tox>=2.3.1' +before_script: +- mysql -e 'create database test_project' +- psql -c 'create database test_project;' -U postgres; +script: tox +deploy: + provider: pypi + user: etianen + password: + secure: XW4/9HiChbPJSJe4d/MRcO+ViPGhW1iQ8kVi814KJh7mCxOAKijpW5hfdc9oSKB6d8iYB3OzZ7naIUU9GMce40bpeTgPDLVBLCSYKRNLuVoJdh+Q6ItGUiFf8kAJz5jgopG80QnCpLA9JvYxKVJ4amfYWWm204eQmIEnRRAd+Jk= + on: + tags: true + distributions: sdist bdist_wheel + repo: etianen/django-reversion +notifications: + email: false diff --git a/CHANGELOG.rst b/CHANGELOG.rst new file mode 100644 index 0000000..422bbc0 --- /dev/null +++ b/CHANGELOG.rst @@ -0,0 +1,686 @@ +.. _changelog: + +django-reversion changelog +========================== + +2.0.12 - 05/12/2017 +------------------- + +- Fixed MySQL error in ``get_deleted()``. + + +2.0.11 - 27/11/2017 +------------------- + +- Dramatically improved performance of ``get_deleted()`` over large datasets (@alexey-v-paramonov, @etianen). +- Ukranian translation (@illia-v). +- Bugfixes (@achidlow, @claudep, @etianen). + + +2.0.10 - 18/08/2017 +------------------- + +- Bugfix: Handling case of `None` user in request (@pawelad). +- Documentation corrections (@danielquinn). +- Bugfix: "invalid literal for int() with base 10: 'None'" for unversioned admin inline relations. + + If, after updating, you still experience this issue, run the following in a Django shell: + + .. code:: + + from reversion.models import Version + Version.objects.filter(object_id="None").delete() + + **Important:** Ensure that none of your versioned models contain a string primary key where `"None"` is a valid value + before running this snippet! + + +2.0.9 - 19/06/2017 +------------------ + +- Bugfix: Deleted inline admin instances no longer added to revision. +- Bugfix: M2M relations correctly added to revision (@etianen, @claudep). +- Improved performance of 0003 migration (@mkurek). +- Documentation improvements (@orlra, @guettli, @meilinger). +- Django 1.11 support (@claudep). +- Added ``atomic=True`` parameter to ``create_revision`` (Ernesto Ferro). + + +2.0.8 - 28/11/2016 +------------------ + +- Setting ``revision.user`` in ``process_response`` for middleware (@etianen). +- Fixing localization of model primary keys in `recover_list.html` (@w4rri0r3k). +- Documentation tweaks (@jaywink). + + +2.0.7 - 31/10/2016 +------------------ + +- Database migrations now db-aware (@alukach). +- Added "revert" and "recover" context variables to admin templates (@kezabelle). +- Added ``post_revision_commit`` and ``pre_revision_commit`` signals back in (@carlosxl). +- Fixing datetime in admin change message (@arogachev). +- Fixing performance bug in postgres (@st4lk). +- Fixing admin change messages in Django 1.10+ (@claudep). +- Fixing revision middleware behavior in Django 1.10+ (@etianen). +- Documentation tweaks (@jschneier). +- Deprecation fixes (@KhasanovBI, @zsiciarz, @claudep). +- Releasing as a universal wheel (@adamchainz). + + +2.0.6 - 21/07/2016 +------------------ + +- Fixed ``RevisionMiddleware`` always rolling back transactions in gunicorn (@stebunovd, @etianen). +- Tweaks and minor bugfixes (@SahilMak). + + +2.0.5 - 29/06/2016 +------------------ + +- Fixed LookupError when running migration 0003 with stale content types (@etianen). + + +2.0.4 - 20/06/2016 +------------------ + +- Fixed LookupError when running migration 0003 (@etianen). +- Fixed duplicate versions using ``get_deleted()`` (@etianen). +- Fixed unexpected deletion of underflowing revisions when using ``--keep`` switch with ``deleterevisions`` (@etianen). + + +2.0.3 - 14/06/2016 +------------------ + +- Added support for m2m fields with a custom ``through`` model (@etianen). + + +2.0.2 - 13/06/2016 +------------------ + +- Fixing migration 0003 in MySQL (@etianen). + + +2.0.1 - 13/06/2016 +------------------ + +- Improved performance of migration 0003 (@BertrandBordage). +- De-duplicating ``Version`` table before applying migration 0004 (@BertrandBordage, @etianen). + + +2.0.0 - 11/06/2016 +------------------ + +django-reversion was first released in May 2008, and has been in active development ever since. Over this time it's developed a certain amount of cruft from legacy and unused features, resulting in needless complexity and multiple ways of achieving the same task. + +This release substantially cleans and refactors the codebase. Much of the top-level functionality remains unchanged or is very similar. The release notes are divided into subsections to make it easier to find out where you need to update your code. + +This release includes a migration for the ``Version`` model that may take some time to complete. + + +General improvements +^^^^^^^^^^^^^^^^^^^^ + +* Dramatically improved performance of version lookup for models with a non-integer primary key (@etianen, @mshannon1123). +* Documentation refactor (@etianen). +* Test refactor (@etianen). +* Minor tweaks and bugfixes (@etianen, @bmarika, @ticosax). + + +Admin +^^^^^ + +* Fixed issue with empty revisions being created in combination with ``RevisionMiddleware`` (@etianen). + +* **Breaking:** Removed ``reversion_format`` property from ``VersionAdmin`` (@etianen). + + Use ``VersionAdmin.reversion_register`` instead. + + .. code:: + + class YourVersionAdmin(VersionAdmin): + + def reversion_register(self, model, **options): + options["format"] = "yaml" + super(YourVersionAdmin, self).reversion_register(model, **options) + +* **Breaking:** Removed ``ignore_duplicate_revisions`` property from ``VersionAdmin`` (@etianen). + + Use ``VersionAdmin.reversion_register`` instead. + + .. code:: + + class YourVersionAdmin(VersionAdmin): + + def reversion_register(self, model, **options): + options["ignore_duplicate_revisions"] = True + super(YourVersionAdmin, self).reversion_register(model, **options) + + + + +Management commands +^^^^^^^^^^^^^^^^^^^ + +* **Breaking:** Refactored arguments to ``createinitialrevisions`` (@etianen). + + All existing functionality should still be supported, but several parameter names have been updated to match Django coding conventions. + + Check the command ``--help`` for details. + +* **Breaking:** Refactored arguments to ``deleterevisions`` (@etianen). + + All existing functionality should still be supported, but several parameter names have been updated to match Django coding conventions, and some duplicate parameters have been removed. The confirmation prompt has been removed entirely, and the command now always runs in the ``--force`` mode from the previous version. + + Check the command ``--help`` for details. + + +Middleware +^^^^^^^^^^ + +* Added support for using ``RevisionMiddleware`` with new-style Django 1.10 ``MIDDLEWARE`` (@etianen). +* Middleware wraps entire request in ``transaction.atomic()`` to preserve transactional integrity of revision and models (@etianen). + + +View helpers +^^^^^^^^^^^^ + +* Added ``reversion.views.create_revision`` view decorator (@etianen). +* Added ``reversion.views.RevisionMixin`` class-based view mixin (@etianen). + + +Low-level API +^^^^^^^^^^^^^ + +* Restored many of the django-reversion API methods back to the top-level namespace (@etianen). +* Revision blocks are now automatically wrapped in ``transaction.atomic()`` (@etianen). +* Added ``for_concrete_model`` argument to ``reversion.register()`` (@etianen). +* Added ``Version.objects.get_for_model()`` lookup function (@etianen). +* Added ``reversion.add_to_revision()`` for manually adding model instances to an active revision (@etianen). +* Removed ``Version.object_id_int`` field, in favor of a unified ``Version.object_id`` field for all primary key types (@etianen). + +* **Breaking:** ``reversion.get_for_object_reference()`` has been moved to ``Version.objects.get_for_object_reference()`` (@etianen). + +* **Breaking:** ``reversion.get_for_object()`` has been moved to ``Version.objects.get_for_object()`` (@etianen). + +* **Breaking:** ``reversion.get_deleted()`` has been moved to ``Version.objects.get_deleted()`` (@etianen). + +* **Breaking:** ``Version.object_version`` has been renamed to ``Version._object_version`` (@etianen). + +* **Breaking:** Refactored multi-db support (@etianen). + + django-reversion now supports restoring model instances to their original database automatically. Several parameter names have also be updated to match Django coding conventions. + + If you made use of the previous multi-db functionality, check the latest docs for details. Otherwise, everything should *just work*. + +* **Breaking:** Removed ``get_ignore_duplicates`` and ``set_ignore_duplicates`` (@etianen). + + ``ignore_duplicates`` is now set in reversion.register() on a per-model basis. + +* **Breaking:** Removed ``get_for_date()`` function (@etianen). + + Use ``get_for_object().filter(revision__date_created__lte=date)`` instead. + +* **Breaking:** Removed ``get_unique_for_object()`` function (@etianen). + + Use ``get_for_object().get_unique()`` instead. + +* **Breaking:** Removed ``signal`` and ``eager_signals`` argument from ``reversion.register()`` (@etianen). + + To create revisions on signals other than ``post_save`` and ``m2m_changed``, call ``reversion.add_to_revision()`` in a signal handler for the appropriate signal. + + .. code:: python + + from django.dispatch import receiver + import reversion + from your_app import your_custom_signal + + @reciever(your_custom_signal) + def your_custom_signal_handler(instance, **kwargs): + if reversion.is_active(): + reversion.add_to_revision(instance) + + This approach will work for both eager and non-eager signals. + +* **Breaking:** Removed ``adapter_cls`` argument from ``reversion.register()`` (@etianen). + +* **Breaking:** Removed ``reversion.save_revision()`` (@etianen). + + Use reversion.add_to_revision() instead. + + .. code:: python + + import reversion + + with reversion.create_revision(): + reversion.add_to_revision(your_obj) + + +Signals +^^^^^^^ + +* **Breaking:** Removed ``pre_revision_commit`` signal (@etianen). + + Use the Django standard ``pre_save`` signal for ``Revision`` instead. + +* **Breaking:** Removed ``post_revision_commit`` signal (@etianen). + + Use the Django standard ``post_save`` signal for ``Revision`` instead. + + +Helpers +^^^^^^^ + +* **Breaking:** Removed ``patch_admin`` function (@etianen). + + Use ``VersionAdmin`` as a mixin to 3rd party ModelAdmins instead. + + .. code:: + + @admin.register(SomeModel) + class YourModelAdmin(VersionAdmin, SomeModelAdmin): + + pass + +* **Breaking:** Removed ``generate_diffs`` function (@etianen). + + django-reversion no supports an official diff helper. There are much better ways of achieving this now, such as `django-reversion-compare `_. + + The old implementation is available for reference from the `previous release `_. + +* **Breaking:** Removed ``generate_patch`` function (@etianen). + + django-reversion no supports an official diff helper. There are much better ways of achieving this now, such as `django-reversion-compare `_. + + The old implementation is available for reference from the `previous release `_. + +* **Breaking:** Removed ``generate_patch_html`` function (@etianen). + + django-reversion no supports an official diff helper. There are much better ways of achieving this now, such as `django-reversion-compare `_. + + The old implementation is available for reference from the `previous release `_. + +Models +^^^^^^ + +* **Breaking:** Ordering of ``-pk`` added to models ``Revision`` and ``Version``. Previous was the default ``pk``. + +1.10.2 - 18/04/2016 +------------------- + +* Fixing deprecation warnings (@claudep). +* Minor tweaks and bug fixes (@fladi, @claudep, @etianen). + + +1.10.1 - 27/01/2016 +------------------- + +* Fixing some deprecation warnings (@ticosax). +* Minor tweaks (@claudep, @etianen). + + +1.10 - 02/12/2015 +----------------- + +* **Breaking:** Updated the location of ``VersionAdmin``. + + Prior to this change, you could access the ``VersionAdmin`` class using the following import: + + .. code:: python + + # Old-style import for accessing the admin class. + import reversion + + # Access admin class from the reversion namespace. + class YourModelAdmin(reversion.VersionAdmin): + + pass + + In order to support Django 1.9, the admin class has been moved to the following + import: + + .. code:: python + + # New-style import for accesssing admin class. + from reversion.admin import VersionAdmin + + # Use the admin class directly. + class YourModelAdmin(VersionAdmin): + + pass + +* **Breaking:** Updated the location of low-level API methods. + Prior to this change, you could access the low-level API using the following import: + + .. code:: python + + # Old-style import for accessing the low-level API. + import reversion + + # Use low-level API methods from the reversion namespace. + @reversion.register + class YourModel(models.Model): + + pass + + In order to support Django 1.9, the low-level API + methods have been moved to the following import: + + .. code:: python + + # New-style import for accesssing the low-level API. + from reversion import revisions as reversion + + # Use low-level API methods from the revisions namespace. + @reversion.register + class YourModel(models.Model): + + pass + +* **Breaking:** Updated the location of http://django-reversion.readthedocs.org/en/latest/signals.html. + Prior to this change, you could access the reversion signals using the following import: + + .. code:: python + + # Old-style import for accessing the reversion signals + import reversion + + # Use signals from the reversion namespace. + reversion.post_revision_commit.connect(...) + + In order to support Django 1.9, the reversion signals have been moved to the following + import: + + .. code:: python + + # New-style import for accesssing the reversion signals. + from reversion.signals import pre_revision_commit, post_revision_commit + + # Use reversion signals directly. + post_revision_commit.connect(...) + +* Django 1.9 compatibility (@etianen). +* Added spanish (argentina) translation (@gonzalobustos). +* Minor bugfixes and tweaks (@Blitzstok, @IanLee1521, @lutoma, @siamalekpour, @etianen). + + +1.9.3 - 07/08/2015 +------------------ + +* Fixing regression with admin redirects following save action (@etianen). + + +1.9.2 - 07/08/2015 +------------------ + +* Fixing regression with "delete", "save as new" and "save and continue" button being shown in recover and revision admin views (@etianen). +* Fixing regression where VersionAdmin.ignore_duplicate_revisions was ignored (@etianen). + + +1.9.1 - 04/08/2015 +------------------ + +* Fixing packaging error that rendered the 1.9.0 release unusable. No way to cover up the mistake, so here's a brand new bugfix release! (@etianen). + + +1.9.0 - 04/08/2015 +------------------ + +* Using database transactions do render consistent views of past revisions in database admin, fixing a lot of lingering minor issues (@etianen). +* Correct handling of readonly fields in admin (@etianen). +* Updates to Czech translation (@cuchac). +* Arabic translation (@RamezIssac). +* Fixing deleterevisions to work with Python2 (@jmurty). +* Fixing edge-cases where an object does not have a PK (@johnfraney). +* Tweaks, code cleanups and documentation fixes (@claudep, @johnfraney, @podloucky-init, Drew Hubl, @JanMalte, @jmurty, @etianen). + + +1.8.7 - 21/05/2015 +------------------ + +* Fixing deleterevisions command on Python 3 (@davidfsmith). +* Fixing Django 1.6 compatibility (@etianen). +* Removing some Django 1.9 deprecation warnings (@BATCOH, @niknokseyer). +* Minor tweaks (@nikolas, @etianen). + + +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. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e71c9b7 --- /dev/null +++ b/LICENSE @@ -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. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..7f73ee4 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,9 @@ +include reversion/templates/reversion/*.html +include reversion/locale/*/LC_MESSAGES/django.* +include LICENSE +include README.rst +include CHANGELOG.rst +include MANIFEST.in +recursive-include docs * +recursive-include tests *.py +prune docs/_build diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..d302636 --- /dev/null +++ b/README.rst @@ -0,0 +1,60 @@ +django-reversion +================ + +**django-reversion** is an extension to the Django web framework that provides +version control for model instances. + +Features +-------- + +- Roll back to any point in a model instance's history. +- Recover deleted model instances. +- Simple admin integration. + + +Documentation +------------- + +Please read the `Getting Started `_ +guide for more information. + +Issue tracking and source code can be found at the +`main project website `_. + +You can keep up to date with the latest announcements by joining the +`django-reversion discussion group `_. + + +Upgrading +--------- + +Please check the `Changelog `_ before upgrading +your installation of django-reversion. + + +Contributing +------------ + +Bug reports, bug fixes, and new features are always welcome. Please raise issues on the +`django-reversion project site `_, and submit +pull requests for any new code. + +You can run the test suite yourself from within a virtual environment with the following +commands. The test suite requires that both MySQL and PostgreSQL be installed. + +.. code:: bash + + pip install 'tox>=2.3.1' + tox + +The django-reversion project is built on every push with `Travis CI `_. + +.. image:: https://travis-ci.org/etianen/django-reversion.svg?branch=master + :target: https://travis-ci.org/etianen/django-reversion + + +Contributors +------------ + +The django-reversion project was developed by `Dave Hall `_ and contributed +to by `many other people `_. diff --git a/docs/_include/admin.rst b/docs/_include/admin.rst new file mode 100644 index 0000000..f5f052b --- /dev/null +++ b/docs/_include/admin.rst @@ -0,0 +1,13 @@ +Register your models with a subclass of :ref:`VersionAdmin`. + +.. code:: python + + from django.contrib import admin + from reversion.admin import VersionAdmin + + @admin.register(YourModel) + class YourModelAdmin(VersionAdmin): + + pass + +.. include:: /_include/post-register.rst diff --git a/docs/_include/create-revision-args.rst b/docs/_include/create-revision-args.rst new file mode 100644 index 0000000..b035887 --- /dev/null +++ b/docs/_include/create-revision-args.rst @@ -0,0 +1,8 @@ +``manage_manually`` + .. include:: /_include/create-revision-manage-manually.rst + +``using`` + .. include:: /_include/create-revision-using.rst + +``atomic`` + .. include:: /_include/create-revision-atomic.rst diff --git a/docs/_include/create-revision-atomic.rst b/docs/_include/create-revision-atomic.rst new file mode 100644 index 0000000..d111a30 --- /dev/null +++ b/docs/_include/create-revision-atomic.rst @@ -0,0 +1 @@ +If ``True``, the revision block will be wrapped in a ``transaction.atomic()``. diff --git a/docs/_include/create-revision-manage-manually.rst b/docs/_include/create-revision-manage-manually.rst new file mode 100644 index 0000000..969ef1a --- /dev/null +++ b/docs/_include/create-revision-manage-manually.rst @@ -0,0 +1 @@ +If ``True``, versions will not be saved when a model's ``save()`` method is called. This allows version control to be switched off for a given revision block. diff --git a/docs/_include/create-revision-using.rst b/docs/_include/create-revision-using.rst new file mode 100644 index 0000000..eaec79c --- /dev/null +++ b/docs/_include/create-revision-using.rst @@ -0,0 +1 @@ +The database to save the revision data. The revision block will be wrapped in a transaction using this database. If ``None``, the default database for :ref:`Revision` will be used. diff --git a/docs/_include/model-db-arg.rst b/docs/_include/model-db-arg.rst new file mode 100644 index 0000000..3fa0974 --- /dev/null +++ b/docs/_include/model-db-arg.rst @@ -0,0 +1,2 @@ +``model_db`` + The database where the model is saved. Defaults to the default database for the model. diff --git a/docs/_include/post-register.rst b/docs/_include/post-register.rst new file mode 100644 index 0000000..2e23c4e --- /dev/null +++ b/docs/_include/post-register.rst @@ -0,0 +1,2 @@ +.. Hint:: + Whenever you register a model with django-reversion, run :ref:`createinitialrevisions`. diff --git a/docs/_include/signal-args.rst b/docs/_include/signal-args.rst new file mode 100644 index 0000000..dfae77e --- /dev/null +++ b/docs/_include/signal-args.rst @@ -0,0 +1,8 @@ +``sender`` + The ``reversion.create_revision`` object. + +``revision`` + The :ref:`Revision` model. + +``versions`` + The :ref:`Version` models in the revision. diff --git a/docs/_include/throws-registration-error.rst b/docs/_include/throws-registration-error.rst new file mode 100644 index 0000000..a099af0 --- /dev/null +++ b/docs/_include/throws-registration-error.rst @@ -0,0 +1 @@ +Throws :ref:`RegistrationError` if the model has not been registered with django-reversion. diff --git a/docs/_include/throws-revert-error.rst b/docs/_include/throws-revert-error.rst new file mode 100644 index 0000000..eec2aea --- /dev/null +++ b/docs/_include/throws-revert-error.rst @@ -0,0 +1 @@ +Throws :ref:`RevertError` if the model could not be deserialized or reverted, e.g. the serialized data is not compatible with the current database schema. diff --git a/docs/_include/throws-revision-error.rst b/docs/_include/throws-revision-error.rst new file mode 100644 index 0000000..af4b865 --- /dev/null +++ b/docs/_include/throws-revision-error.rst @@ -0,0 +1 @@ +Throws :ref:`RevisionManagementError` if there is no active revision block. diff --git a/docs/admin.rst b/docs/admin.rst new file mode 100644 index 0000000..ce6a08d --- /dev/null +++ b/docs/admin.rst @@ -0,0 +1,109 @@ +.. _admin: + +Admin integration +================= + +django-reversion can be used to add rollback and recovery to your admin site. + +.. Warning:: + The admin integration requires that your database engine supports transactions. This is the case for PostgreSQL, SQLite and MySQL InnoDB. If you are using MySQL MyISAM, upgrade your database tables to InnoDB! + + +Overview +-------- + +Registering models +^^^^^^^^^^^^^^^^^^ + +.. include:: /_include/admin.rst + +.. Note:: + + If you've registered your models using :ref:`reversion.register() `, the admin class will use the configuration you specify there. Otherwise, the admin class will auto-register your model, following all inline model relations and parent superclasses. Customize the admin registration by overriding :ref:`VersionAdmin.register() `. + + +Integration with 3rd party apps +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +You can use :ref:`VersionAdmin` as a mixin with a 3rd party admin class. + +.. code:: python + + @admin.register(SomeModel) + class YourModelAdmin(VersionAdmin, SomeModelAdmin): + + pass + +If the 3rd party model is already registered with the Django admin, you may have to unregister it first. + +.. code:: python + + admin.site.unregister(SomeModel) + + @admin.register(SomeModel) + class YourModelAdmin(VersionAdmin, SomeModelAdmin): + + pass + + +.. _VersionAdmin: + +reversion.admin.VersionAdmin +---------------------------- + +A subclass of ``django.contrib.ModelAdmin`` providing rollback and recovery. + + +``revision_form_template = None`` + + A custom template to render the revision form. + + Alternatively, create specially named templates to override the default templates on a per-model or per-app basis. + + * ``'reversion/app_label/model_name/revision_form.html'`` + * ``'reversion/app_label/revision_form.html'`` + * ``'reversion/revision_form.html'`` + + +``recover_list_template = None`` + + A custom template to render the recover list. + + Alternatively, create specially named templates to override the default templates on a per-model or per-app basis. + + * ``'reversion/app_label/model_name/recover_list.html'`` + * ``'reversion/app_label/recover_list.html'`` + * ``'reversion/recover_list.html'`` + + +``recover_form_template = None`` + + A custom template to render the recover form. + + * ``'reversion/app_label/model_name/recover_form.html'`` + * ``'reversion/app_label/recover_form.html'`` + * ``'reversion/recover_form.html'`` + + +``history_latest_first = False`` + + If ``True``, revisions will be displayed with the most recent revision first. + + +.. _VersionAdmin_register: + +``reversion_register(model, **options)`` + + Callback used by the auto-registration machinery to register the model with django-reversion. Override this to customize how models are registered. + + .. code:: python + + def reversion_register(self, model, **options): + options["exclude"] = ("some_field",) + super(YourModelAdmin, self).reversion_register(model, **options) + + ``model`` + The model that will be registered with django-reversion. + + ``options`` + Registeration options, see :ref:`reversion.register() `. diff --git a/docs/api.rst b/docs/api.rst new file mode 100644 index 0000000..8bc961b --- /dev/null +++ b/docs/api.rst @@ -0,0 +1,479 @@ +.. _api: + +django-reversion API +==================== + +Use the django-reversion API to build version-controlled apps. See also :ref:`Views` and :ref:`Middleware`. + + +Overview +-------- + +Registering models +^^^^^^^^^^^^^^^^^^ + +Models must be registered with django-reversion before they can be used with the API. + +.. code:: python + + from django.db import models + import reversion + + @reversion.register() + class YourModel(models.Model): + + pass + +.. Hint:: + If you're using the :ref:`admin`, model registration is automatic. If you’re using django-reversion in a management command, make sure you call ``django.contrib.admin.autodiscover()`` to load the admin modules before using the django-reversion API. + +.. include:: /_include/post-register.rst + + +Creating revisions +^^^^^^^^^^^^^^^^^^ + +A *revision* represents one or more changes made to your model instances, grouped together as a single unit. You create a revision by creating a *revision block*. When you call ``save()`` on a registered model inside a revision block, it will be added to that revision. + +.. code:: python + + # Declare a revision block. + with reversion.create_revision(): + + # Save a new model instance. + obj = YourModel() + obj.name = "obj v1" + obj.save() + + # Store some meta-information. + reversion.set_user(request.user) + reversion.set_comment("Created revision 1") + + # Declare a new revision block. + with reversion.create_revision(): + + # Update the model instance. + obj.name = "obj v2" + obj.save() + + # Store some meta-information. + reversion.set_user(request.user) + reversion.set_comment("Created revision 2") + +.. Important:: + + Bulk actions, such as ``Queryset.update()``, do not send signals, so won't be noticed by django-reversion. + + +Loading revisions +^^^^^^^^^^^^^^^^^ + +Each model instance saved in a revision block is serialized as a :ref:`Version`. All versions in a revision block are associated with a single :ref:`Revision`. + +You can load a :ref:`VersionQuerySet` of versions from the database. Versions are loaded with the most recent version first. + +.. code:: python + + from reversion.models import Version + + # Load a queryset of versions for a specific model instance. + versions = Version.objects.get_for_object(instance) + assert len(versions) == 2 + + # Check the serialized data for the first version. + assert versions[1].field_dict["name"] = "obj v1" + + # Check the serialized data for the second version. + assert versions[0].field_dict["name"] = "obj v2" + + +Revision metadata +^^^^^^^^^^^^^^^^^ + +:ref:`Revision` stores meta-information about the revision. + +.. code:: python + + # Check the revision metadata for the first revision. + assert versions[1].revision.comment = "Created revision 1" + assert versions[1].revision.user = request.user + assert isinstance(versions[1].revision.date_created, datetime.datetime) + + # Check the revision metadata for the second revision. + assert versions[0].revision.comment = "Created revision 2" + assert versions[0].revision.user = request.user + assert isinstance(versions[0].revision.date_created, datetime.datetime) + + +Reverting revisions +^^^^^^^^^^^^^^^^^^^ + +Revert a :ref:`Revision` to restore the serialized model instances. + +.. code:: python + + # Revert the first revision. + versions[1].revision.revert() + + # Check the model instance has been reverted. + obj.refresh_from_db() + assert obj.name == "version 1" + + # Revert the second revision. + versions[0].revision.revert() + + # Check the model instance has been reverted. + obj.refresh_from_db() + assert obj.name == "version 2" + + +Restoring deleted model instances +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Reverting a :ref:`Revision` will restore any serialized model instances that have been deleted. + +.. code:: python + + # Delete the model instance, but store the pk. + pk = obj.pk + obj.delete() + + # Revert the second revision. + versions[0].revision.revert() + + # Check the model has been restored to the database. + obj = YourModel.objects.get(pk=obj.pk) + assert obj.name == "version 2" + + +.. _registration-api: + +Registration API +---------------- + +.. _register: + +``reversion.register(model, **options)`` + + Registers a model with django-reversion. + + Throws :ref:`RegistrationError` if the model has already been registered. + + ``model`` + The Django model to register. + + ``fields=None`` + An iterable of field names to include in the serialized data. If ``None``, all fields will be included. + + ``exclude=()`` + An iterable of field names to exclude from the serialized data. + + ``follow=()`` + An iterable of model relationships to follow when saving a version of this model. ``ForeignKey``, ``ManyToManyField`` and reversion ``ForeignKey`` relationships are supported. Any property that returns a ``Model`` or ``QuerySet`` is also supported. + + ``format="json"`` + The name of a Django serialization format to use when saving the model instance. + + ``for_concrete_model=True`` + If ``True`` proxy models will be saved under the same content type as their concrete model. If ``False``, proxy models will be saved under their own content type, effectively giving proxy models their own distinct history. + + ``ignore_duplicates=False`` + If ``True``, then an additional check is performed to avoid saving duplicate versions for this model. + + Checking for duplicate revisions adds significant overhead to the process of creating a revision. Don't enable it unless you really need it! + + .. Hint:: + By default, django-reversion will not register any parent classes of a model that uses multi-table inheritance. If you wish to also add parent models to your revision, you must explicitly add their ``parent_ptr`` fields to the ``follow`` parameter when you register the model. + + .. include:: /_include/post-register.rst + + +``reversion.is_registered(model)`` + + Returns whether the given model has been registered with django-reversion. + + ``model`` + The Django model to check. + + +``reversion.unregister(model)`` + + Unregisters the given model from django-reversion. + + .. include:: /_include/throws-registration-error.rst + + ``model`` + The Django model to unregister. + + +``reversion.get_registered_models()`` + + Returns an iterable of all registered models. + + +.. _revision-api: + +Revision API +------------ + +``reversion.create_revision(manage_manually=False, using=None, atomic=True)`` + + Marks a block of code as a *revision block*. Can also be used as a decorator. + + .. include:: /_include/create-revision-args.rst + + +``reversion.is_active()`` + + Returns whether there is currently an active revision block. + + +``reversion.is_manage_manually()`` + + Returns whether the current revision block is in ``manage_manually`` mode. + + +``reversion.set_user(user)`` + + Sets the user for the current revision. + + .. include:: /_include/throws-revision-error.rst + + ``user`` + A ``User`` model instance (or whatever your ``settings.AUTH_USER_MODEL`` is). + + +``reversion.get_user()`` + + Returns the user for the current revision. + + .. include:: /_include/throws-revision-error.rst + + +.. _set_comment: + +``reversion.set_comment(comment)`` + + Sets the comment for the current revision. + + .. include:: /_include/throws-revision-error.rst + + ``comment`` + The text comment for the revision. + + +``reversion.get_comment()`` + + Returns the comment for the current revision. + + .. include:: /_include/throws-revision-error.rst + + +``reversion.set_date_created(date_created)`` + + Sets the creation date for the current revision. + + .. include:: /_include/throws-revision-error.rst + + ``date_created`` + The creation date for the revision. + + +``reversion.get_date_created()`` + + Returns the creation date for the current revision. + + .. include:: /_include/throws-revision-error.rst + + +``reversion.add_meta(model, **values)`` + + Adds custom metadata to a revision. + + .. include:: /_include/throws-revision-error.rst + + ``model`` + A Django model to store the custom metadata. The model must have a ``ForeignKey`` or ``OneToOneField`` to :ref:`Revision`. + + ``**values`` + Values to be stored on ``model`` when it is saved. + + +``reversion.add_to_revision(obj, model_db=None)`` + + Adds a model instance to a revision. + + .. include:: /_include/throws-revision-error.rst + + ``obj`` + A model instance to add to the revision. + + .. include:: /_include/model-db-arg.rst + + +.. _VersionQuerySet: + +reversion.models.VersionQuerySet +-------------------------------- + +A ``QuerySet`` of :ref:`Version`. The results are ordered with the most recent :ref:`Version` first. + + +``Version.objects.get_for_model(model, model_db=None)`` + + Returns a :ref:`VersionQuerySet` for the given model. + + .. include:: /_include/throws-registration-error.rst + + ``model`` + A registered model. + + .. include:: /_include/model-db-arg.rst + + +``Version.objects.get_for_object(obj, model_db=None)`` + + Returns a :ref:`VersionQuerySet` for the given model instance. + + .. include:: /_include/throws-registration-error.rst + + ``obj`` + An instance of a registered model. + + .. include:: /_include/model-db-arg.rst + + +``Version.objects.get_for_object_reference(model, pk, model_db=None)`` + + Returns a :ref:`VersionQuerySet` for the given model and primary key. + + .. include:: /_include/throws-registration-error.rst + + ``model`` + A registered model. + + ``pk`` + The database primary key of a model instance. + + .. include:: /_include/model-db-arg.rst + + +``Version.objects.get_deleted(model, model_db=None)`` + + Returns a :ref:`VersionQuerySet` for the given model containing versions where the serialized model no longer exists in the database. + + .. include:: /_include/throws-registration-error.rst + + ``model`` + A registered model. + + ``db`` + The database to load the versions from. + + .. include:: /_include/model-db-arg.rst + + +``Version.objects.get_unique()`` + + Returns an iterable of :ref:`Version`, where each version is unique for a given database, model instance, and set of serialized fields. + + +.. _Version: + +reversion.models.Version +------------------------ + +Represents a single model instance serialized in a revision. + + +``Version.id`` + + The database primary key of the :ref:`Version`. + + +``Version.revision`` + + A ``ForeignKey`` to a :ref:`Revision` instance. + + +``Version.content_type`` + + The ``ContentType`` of the serialized model instance. + + +``Version.object_id`` + + The string representation of the serialized model instance's primary key. + + +``Version.db`` + + The Django database alias where the serialized model was saved. + + +``Version.format`` + + The name of the Django serialization format used to serialize the model instance. + + +``Version.serialized_data`` + + The raw serialized data of the model instance. + + +``Version.object_repr`` + + The stored snapshot of the model instance's ``__str__`` method when the instance was serialized. + + +``Version.field_dict`` + + A dictionary of stored model fields. This includes fields from any parent models in the same revision. + + .. include:: /_include/throws-revert-error.rst + + +``Version.revert()`` + + Restores the serialized model instance to the database. To restore the entire revision, use :ref:`Revision.revert() `. + + .. include:: /_include/throws-revert-error.rst + + +.. _Revision: + +reversion.models.Revision +------------------------- + +Contains metadata about a revision, and groups together all :ref:`Version` instances created in that revision. + +``Revision.id`` + + The database primary key of the :ref:`Revision`. + + +``Revision.date_created`` + + A ``datetime`` when the revision was created. + + +``Revision.user`` + + The ``User`` that created the revision, or None. + + +``Revision.comment`` + + A text comment on the revision. + + +.. _Revision-revert: + +``Revision.revert(delete=False)`` + + Restores all contained serialized model instances to the database. + + .. include:: /_include/throws-revert-error.rst + + ``delete`` + If ``True``, any model instances which have been created and are reachable by the ``follow`` clause of any model instances in this revision will be deleted. This effectively restores a group of related models to the state they were in when the revision was created. diff --git a/docs/changelog.rst b/docs/changelog.rst new file mode 100644 index 0000000..565b052 --- /dev/null +++ b/docs/changelog.rst @@ -0,0 +1 @@ +.. include:: ../CHANGELOG.rst diff --git a/docs/commands.rst b/docs/commands.rst new file mode 100644 index 0000000..839d935 --- /dev/null +++ b/docs/commands.rst @@ -0,0 +1,45 @@ +.. _commands: + +Management commands +=================== + +django-reversion includes a number of ``django-admin.py`` management commands. + + +.. _createinitialrevisions: + +createinitialrevisions +---------------------- + +Creates an initial revision for all registered models in your project. It should be run after installing django-reversion, or registering a new model with django-reversion. + +.. code:: bash + + ./manage.py createinitialrevisions + ./manage.py createinitialrevisions your_app.YourModel --comment="Initial revision." + +Run ``./manage.py createinitialrevisions --help`` for more information. + +.. Warning:: + For large databases, this command can take a long time to run. + + +deleterevisions +--------------- + +Deletes old revisions. It can be run regularly to keep revision history manageable. + +.. code:: bash + + ./manage.py deleterevisions + # keep any changes from last 30 days + ./manage.py deleterevisions your_app.YourModel --days=30 + # keep 30 most recent changes for each item. + ./manage.py deleterevisions your_app.YourModel --keep=30 + # Keep anything from last 30 days and at least 3 from older changes. + ./manage.py deleterevisions your_app.YourModel --keep=3 --days=30 + +Run ``./manage.py deleterevisions --help`` for more information. + +.. Warning:: + With no arguments, this command will delete your entire revision history! Read the command help for ways to limit which revisions should be deleted. diff --git a/docs/common-problems.rst b/docs/common-problems.rst new file mode 100644 index 0000000..0d3148e --- /dev/null +++ b/docs/common-problems.rst @@ -0,0 +1,12 @@ +.. _common-problems: + +Common problems +=============== + + +RegistrationError: class 'myapp.MyModel' has already been registered with Reversion +----------------------------------------------------------------------------------- + +This is caused by your ``models.py`` file being imported twice, resulting in ``reversion.register()`` being called twice for the same model. + +This problem is almost certainly due to relative import statements in your codebase. Try converting all your relative imports into absolute imports. diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..e3fcb42 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,335 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# django-reversion documentation build configuration file, created by +# sphinx-quickstart on Thu Jun 2 08:41:36 2016. +# +# 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. + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +# import os +# import sys +# sys.path.insert(0, os.path.abspath('.')) + +from reversion import __version__ + +# -- 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 = [] + +# Add any paths that contain templates here, relative to this directory. +templates_path = [] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +# source_suffix = ['.rst', '.md'] +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 = '2016, Dave Hall' +author = 'Dave Hall' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = '.'.join(str(x) for x in __version__[:2]) +# The full version, including alpha/beta/rc tags. +release = '.'.join(str(x) for x in __version__) + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = None + +# 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. +# This patterns also effect to html_static_path and html_extra_path +exclude_patterns = ['_build', '_include', 'Thumbs.db', '.DS_Store'] + +# 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 = [] + +# If true, keep warnings as "system message" paragraphs in the built documents. +# keep_warnings = False + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = False + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +# html_theme = 'alabaster' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# +# html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +# html_theme_path = [] + +# The name for this set of Sphinx documents. +# " v documentation" by default. +# +# html_title = 'django-reversion v1.10.3' + +# 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 (relative to this directory) to use as a favicon of +# the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# 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 = [] + +# Add any extra paths that contain custom files (such as robots.txt or +# .htaccess) here, relative to this directory. These files are copied +# directly to the root of the documentation. +# +# html_extra_path = [] + +# If not None, a 'Last updated on:' timestamp is inserted at every page +# bottom, using the given strftime format. +# The empty string is equivalent to '%b %d, %Y'. +# +# html_last_updated_fmt = None + +# If 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 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 + +# Language to be used for generating the HTML full-text search index. +# Sphinx supports the following languages: +# 'da', 'de', 'en', 'es', 'fi', 'fr', 'h', 'it', 'ja' +# 'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr', 'zh' +# +# html_search_language = 'en' + +# A dictionary with options for the search language support, empty by default. +# 'ja' uses this config value. +# 'zh' user can custom change `jieba` dictionary path. +# +# html_search_options = {'type': 'default'} + +# The name of a javascript file (relative to the configuration directory) that +# implements a search results scorer. If empty, the default will be used. +# +# html_search_scorer = 'scorer.js' + +# 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': '', + + # Latex figure (float) alignment + # + # 'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (master_doc, '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 = [ + (master_doc, 'django-reversion', 'django-reversion Documentation', + [author], 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 = [ + (master_doc, 'django-reversion', 'django-reversion Documentation', + author, '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' + +# If true, do not generate a @detailmenu in the "Top" node's menu. +# +# texinfo_no_detailmenu = False diff --git a/docs/django-versions.rst b/docs/django-versions.rst new file mode 100644 index 0000000..4933b04 --- /dev/null +++ b/docs/django-versions.rst @@ -0,0 +1,19 @@ +.. _django-versions: + +Compatible Django versions +========================== + +django-reversion aims to stay compatible with the latest LTS release of Django, along with more recent releases. See :ref:`changelog`. + +Older versions of Django require an older version of django-reversion to be installed. + +============== ================= +Django version Reversion release +============== ================= +1.8 - current 2.0.0 +1.7 1.10.x +1.6 1.8.x +============== ================= + +.. Warning:: + Older versions of django-reversion receive very limited support. It's advised to upgrade your Django to remain compatible with the latest release of django-reversion. diff --git a/docs/errors.rst b/docs/errors.rst new file mode 100644 index 0000000..f38c8b2 --- /dev/null +++ b/docs/errors.rst @@ -0,0 +1,30 @@ +.. _errors: + +Errors +====== + +django-reversion defines several custom errors. + + +.. _RegistrationError: + +reversion.RegistrationError +--------------------------- + +Something went wrong with the :ref:`registration-api`. + + +.. _RevisionManagementError: + +reversion.RevisionManagementError +--------------------------------- + +Something went wrong using the :ref:`revision-api`. + + +.. _RevertError: + +reversion.RevertError +--------------------- + +Something went wrong reverting a revision. diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..2589728 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,73 @@ +.. _index: + +django-reversion +================ + +**django-reversion** is an extension to the Django web framework that provides +version control for model instances. + + +Features +-------- + +- Roll back to any point in a model instance's history. +- Recover deleted model instances. +- Simple admin integration. + + +Installation +------------ + +To install django-reversion: + +1. Install with pip: ``pip install django-reversion``. +2. Add ``'reversion'`` to ``INSTALLED_APPS``. +3. Run ``manage.py migrate``. + +.. Important:: + See :ref:`django-versions` if you're not using the latest release of Django. + + +Admin integration +----------------- + +django-reversion can be used to add rollback and recovery to your admin site. + +.. include:: /_include/admin.rst + +For more information about admin integration, see :ref:`admin`. + + +Low-level API +------------- + +You can use the django-reversion API to build version-controlled applications. See :ref:`api`. + + +More information +---------------- + +Installation +^^^^^^^^^^^^ + +.. toctree:: + :maxdepth: 1 + + django-versions + common-problems + changelog + + +Usage +^^^^^ + +.. toctree:: + :maxdepth: 2 + + admin + commands + api + views + middleware + errors + signals diff --git a/docs/middleware.rst b/docs/middleware.rst new file mode 100644 index 0000000..38b2289 --- /dev/null +++ b/docs/middleware.rst @@ -0,0 +1,34 @@ +.. _middleware: + +Middleware +========== + +Shortcuts when using django-reversion in views. + + +reversion.middleware.RevisionMiddleware +--------------------------------------- + +Wrap the every request that isn't ``GET``, ``HEAD`` or ``OPTIONS`` in a revision block. + +The request user will also be added to the revision metadata. + +To enable ``RevisionMiddleware``, add ``'reversion.middleware.RevisionMiddleware'`` to your ``MIDDLEWARE_CLASSES`` setting. For Django >= 1.10, add it to your ``MIDDLEWARE`` setting. + +.. Warning:: + This will wrap every request that isn't ``GET``, ``HEAD`` or ``OPTIONS`` in a database transaction. For best performance, consider marking individual views instead. + + +``RevisionMiddleware.manage_manually = False`` + + .. include:: /_include/create-revision-manage-manually.rst + + +``RevisionMiddleware.using = None`` + + .. include:: /_include/create-revision-using.rst + + +``RevisionMiddleware.atomic = True`` + + .. include:: /_include/create-revision-atomic.rst diff --git a/docs/signals.rst b/docs/signals.rst new file mode 100644 index 0000000..32f6d84 --- /dev/null +++ b/docs/signals.rst @@ -0,0 +1,22 @@ +.. _signals: + +Signals +======= + +django-reversion provides two custom signals. + + +reversion.signals.pre_revision_commit +------------------------------------- + +Sent just before a revision is saved to the database. + +.. include:: /_include/signal-args.rst + + +reversion.signals.post_revision_commit +-------------------------------------- + +Sent just after a revision and its related versions are saved to the database. + +.. include:: /_include/signal-args.rst diff --git a/docs/views.rst b/docs/views.rst new file mode 100644 index 0000000..3582436 --- /dev/null +++ b/docs/views.rst @@ -0,0 +1,45 @@ +.. _views: + +Views +===== + +Shortcuts when using django-reversion in views. + + +Decorators +---------- + +``reversion.views.create_revision(manage_manually=False, using=None, atomic=True)`` + + Decorates a view to wrap every request that isn't ``GET``, ``HEAD`` or ``OPTIONS`` in a revision block. + + The request user will also be added to the revision metadata. You can set the revision comment by calling :ref:`reversion.set_comment() ` within your view. + + .. include:: /_include/create-revision-args.rst + + +reversion.views.RevisionMixin +----------------------------- + +Mixin a class-based view to wrap every request that isn't ``GET``, ``HEAD`` or ``OPTIONS`` in a revision block. + +The request user will also be added to the revision metadata. You can set the revision comment by calling :ref:`reversion.set_comment() ` within your view. + +.. code:: python + + from django.contrib.auth.views import FormView + from reversion.views import RevisionMixin + + class RevisionFormView(RevisionMixin, FormView): + + pass + + +``RevisionMixin.revision_manage_manually = False`` + + .. include:: /_include/create-revision-manage-manually.rst + + +``RevisionMixin.revision_using = None`` + + .. include:: /_include/create-revision-using.rst diff --git a/reversion/__init__.py b/reversion/__init__.py new file mode 100644 index 0000000..72eafec --- /dev/null +++ b/reversion/__init__.py @@ -0,0 +1,39 @@ +""" +An extension to the Django web framework that provides version control for model instances. + +Developed by Dave Hall. + + +""" + +try: + import django # noqa +except ImportError: # pragma: no cover + # The top-level API requires Django, which might not be present if setup.py + # is importing reversion to get __version__. + pass +else: + from reversion.errors import ( # noqa + RevertError, + RevisionManagementError, + RegistrationError, + ) + from reversion.revisions import ( # noqa + is_active, + is_manage_manually, + get_user, + set_user, + get_comment, + set_comment, + get_date_created, + set_date_created, + add_meta, + add_to_revision, + create_revision, + register, + is_registered, + unregister, + get_registered_models, + ) + +__version__ = VERSION = (2, 0, 12) diff --git a/reversion/admin.py b/reversion/admin.py new file mode 100644 index 0000000..cdefd91 --- /dev/null +++ b/reversion/admin.py @@ -0,0 +1,305 @@ +from __future__ import unicode_literals +import json +from contextlib import contextmanager +from django.db import models, transaction, connection +from django.conf.urls import url +from django.contrib import admin, messages +from django.contrib.admin import options +from django.contrib.admin.models import LogEntry +from django.contrib.admin.utils import unquote, quote +try: + from django.contrib.contenttypes.admin import GenericInlineModelAdmin + from django.contrib.contenttypes.fields import GenericRelation +except ImportError: # Django < 1.9 pragma: no cover + from django.contrib.contenttypes.generic import GenericInlineModelAdmin, GenericRelation +try: + from django.urls import reverse +except ImportError: # Django < 1.10 pragma: no cover + from django.core.urlresolvers import reverse +from django.core.exceptions import PermissionDenied, ImproperlyConfigured +from django.shortcuts import get_object_or_404, render, redirect +from django.utils.text import capfirst +from django.utils.timezone import template_localtime +from django.utils.translation import ugettext as _ +from django.utils.encoding import force_text +from django.utils.formats import localize +from reversion.compat import remote_field, remote_model +from reversion.errors import RevertError +from reversion.models import Version +from reversion.revisions import is_active, register, is_registered, set_comment, create_revision, set_user +from reversion.views import _RollBackRevisionView + + +def private_fields(meta): + try: + return meta.private_fields + except AttributeError: # Django < 1.10 pragma: no cover + return meta.virtual_fields + + +class VersionAdmin(admin.ModelAdmin): + + object_history_template = "reversion/object_history.html" + + change_list_template = "reversion/change_list.html" + + revision_form_template = None + + recover_list_template = None + + recover_form_template = None + + history_latest_first = False + + def reversion_register(self, model, **kwargs): + """Registers the model with reversion.""" + register(model, **kwargs) + + @contextmanager + def create_revision(self, request): + with create_revision(): + set_user(request.user) + yield + + # Revision helpers. + + def _reversion_get_template_list(self, template_name): + opts = self.model._meta + return ( + "reversion/%s/%s/%s" % (opts.app_label, opts.object_name.lower(), template_name), + "reversion/%s/%s" % (opts.app_label, template_name), + "reversion/%s" % template_name, + ) + + def _reversion_order_version_queryset(self, queryset): + """Applies the correct ordering to the given version queryset.""" + if not self.history_latest_first: + queryset = queryset.order_by("pk") + return queryset + + # Messages. + + def log_addition(self, request, object, change_message=None): + change_message = change_message or _("Initial version.") + if is_active(): + # If https://code.djangoproject.com/ticket/27218 is implemented, we + # could first call super() and get the change_message from the returned + # LogEntry. + if isinstance(change_message, list): + set_comment(LogEntry(change_message=json.dumps(change_message)).get_change_message()) + else: + set_comment(change_message) + try: + super(VersionAdmin, self).log_addition(request, object, change_message) + except TypeError: # Django < 1.9 pragma: no cover + super(VersionAdmin, self).log_addition(request, object) + + def log_change(self, request, object, message): + if is_active(): + if isinstance(message, list): + set_comment(LogEntry(change_message=json.dumps(message)).get_change_message()) + else: + set_comment(message) + super(VersionAdmin, self).log_change(request, object, message) + + # Auto-registration. + + def _reversion_autoregister(self, model, follow): + if not is_registered(model): + for parent_model, field in model._meta.concrete_model._meta.parents.items(): + follow += (field.name,) + self._reversion_autoregister(parent_model, ()) + self.reversion_register(model, follow=follow) + + def _reversion_introspect_inline_admin(self, inline): + inline_model = None + follow_field = None + fk_name = None + if issubclass(inline, GenericInlineModelAdmin): + inline_model = inline.model + ct_field = inline.ct_field + fk_name = inline.ct_fk_field + for field in private_fields(self.model._meta): + if ( + isinstance(field, GenericRelation) and + remote_model(field) == inline_model and + field.object_id_field_name == fk_name and + field.content_type_field_name == ct_field + ): + follow_field = field.name + break + elif issubclass(inline, options.InlineModelAdmin): + inline_model = inline.model + fk_name = inline.fk_name + if not fk_name: + for field in inline_model._meta.get_fields(): + if ( + isinstance(field, (models.ForeignKey, models.OneToOneField)) and + issubclass(self.model, remote_model(field)) + ): + fk_name = field.name + break + if fk_name and not remote_field(inline_model._meta.get_field(fk_name)).is_hidden(): + field = inline_model._meta.get_field(fk_name) + accessor = remote_field(field).get_accessor_name() + follow_field = accessor + return inline_model, follow_field + + def __init__(self, *args, **kwargs): + super(VersionAdmin, self).__init__(*args, **kwargs) + # Automatically register models if required. + if not is_registered(self.model): + inline_fields = () + for inline in self.inlines: + inline_model, follow_field = self._reversion_introspect_inline_admin(inline) + if inline_model: + self._reversion_autoregister(inline_model, ()) + if follow_field: + inline_fields += (follow_field,) + self._reversion_autoregister(self.model, inline_fields) + + def get_urls(self): + urls = super(VersionAdmin, self).get_urls() + admin_site = self.admin_site + opts = self.model._meta + info = opts.app_label, opts.model_name, + reversion_urls = [ + url("^recover/$", admin_site.admin_view(self.recoverlist_view), name='%s_%s_recoverlist' % info), + url("^recover/(\d+)/$", admin_site.admin_view(self.recover_view), name='%s_%s_recover' % info), + url("^([^/]+)/history/(\d+)/$", admin_site.admin_view(self.revision_view), name='%s_%s_revision' % info), + ] + return reversion_urls + urls + + # Views. + + def add_view(self, request, form_url='', extra_context=None): + with self.create_revision(request): + return super(VersionAdmin, self).add_view(request, form_url, extra_context) + + def change_view(self, request, object_id, form_url='', extra_context=None): + with self.create_revision(request): + return super(VersionAdmin, self).change_view(request, object_id, form_url, extra_context) + + def _reversion_revisionform_view(self, request, version, template_name, extra_context=None): + # Check that database transactions are supported. + if not connection.features.uses_savepoints: + raise ImproperlyConfigured("Cannot use VersionAdmin with a database that does not support savepoints.") + # Run the view. + try: + with transaction.atomic(using=version.db): + # Revert the revision. + version.revision.revert(delete=True) + # Run the normal changeform view. + with self.create_revision(request): + response = self.changeform_view(request, quote(version.object_id), request.path, extra_context) + # Decide on whether the keep the changes. + if request.method == "POST" and response.status_code == 302: + set_comment(_("Reverted to previous version, saved on %(datetime)s") % { + "datetime": localize(template_localtime(version.revision.date_created)), + }) + else: + response.template_name = template_name # Set the template name to the correct template. + response.render() # Eagerly render the response, so it's using the latest version. + raise _RollBackRevisionView(response) # Raise exception to undo the transaction and revision. + except RevertError as ex: + opts = self.model._meta + messages.error(request, force_text(ex)) + return redirect("{}:{}_{}_changelist".format(self.admin_site.name, opts.app_label, opts.model_name)) + except _RollBackRevisionView as ex: + return ex.response + return response + + def recover_view(self, request, version_id, extra_context=None): + """Displays a form that can recover a deleted model.""" + # The revisionform view will check for change permission (via changeform_view), + # but we also need to check for add permissions here. + if not self.has_add_permission(request): + raise PermissionDenied + # Render the recover view. + version = get_object_or_404(Version, pk=version_id) + context = { + "title": _("Recover %(name)s") % {"name": version.object_repr}, + "recover": True, + } + context.update(extra_context or {}) + return self._reversion_revisionform_view( + request, + version, + self.recover_form_template or self._reversion_get_template_list("recover_form.html"), + context, + ) + + def revision_view(self, request, object_id, version_id, extra_context=None): + """Displays the contents of the given revision.""" + object_id = unquote(object_id) # Underscores in primary key get quoted to "_5F" + version = get_object_or_404(Version, pk=version_id, object_id=object_id) + context = { + "title": _("Revert %(name)s") % {"name": version.object_repr}, + "revert": True, + } + context.update(extra_context or {}) + return self._reversion_revisionform_view( + request, + version, + self.revision_form_template or self._reversion_get_template_list("revision_form.html"), + context, + ) + + def changelist_view(self, request, extra_context=None): + with self.create_revision(request): + context = { + "has_change_permission": self.has_change_permission(request), + } + context.update(extra_context or {}) + return super(VersionAdmin, self).changelist_view(request, context) + + def recoverlist_view(self, request, extra_context=None): + """Displays a deleted model to allow recovery.""" + # Check if user has change and add permissions for model + if not self.has_change_permission(request) or not self.has_add_permission(request): + raise PermissionDenied + model = self.model + opts = model._meta + deleted = self._reversion_order_version_queryset(Version.objects.get_deleted(self.model)) + # Set the app name. + request.current_app = self.admin_site.name + # Get the rest of the context. + context = dict( + self.admin_site.each_context(request), + opts=opts, + app_label=opts.app_label, + module_name=capfirst(opts.verbose_name), + title=_("Recover deleted %(name)s") % {"name": force_text(opts.verbose_name_plural)}, + deleted=deleted, + ) + context.update(extra_context or {}) + return render( + request, + self.recover_list_template or self._reversion_get_template_list("recover_list.html"), + context, + ) + + def history_view(self, request, object_id, extra_context=None): + """Renders the history view.""" + # Check if user has change permissions for model + if not self.has_change_permission(request): + raise PermissionDenied + opts = self.model._meta + action_list = [ + { + "revision": version.revision, + "url": reverse( + "%s:%s_%s_revision" % (self.admin_site.name, opts.app_label, opts.model_name), + args=(quote(version.object_id), version.id) + ), + } + for version + in self._reversion_order_version_queryset(Version.objects.get_for_object_reference( + self.model, + unquote(object_id), # Underscores in primary key get quoted to "_5F" + ).select_related("revision__user")) + ] + # Compile the context. + context = {"action_list": action_list} + context.update(extra_context or {}) + return super(VersionAdmin, self).history_view(request, object_id, context) diff --git a/reversion/compat.py b/reversion/compat.py new file mode 100644 index 0000000..b1460b9 --- /dev/null +++ b/reversion/compat.py @@ -0,0 +1,17 @@ +import django + + +def remote_field(field): + # remote_field is new in Django 1.9 + return field.remote_field if hasattr(field, 'remote_field') else field.rel + + +def remote_model(field): + # remote_field is new in Django 1.9 + return field.remote_field.model if hasattr(field, 'remote_field') else field.rel.to + + +def is_authenticated(user): + if django.VERSION < (1, 10): + return user.is_authenticated() + return user.is_authenticated diff --git a/reversion/errors.py b/reversion/errors.py new file mode 100644 index 0000000..b06b122 --- /dev/null +++ b/reversion/errors.py @@ -0,0 +1,13 @@ +class RevertError(Exception): + + """Exception thrown when something goes wrong with reverting a model.""" + + +class RevisionManagementError(Exception): + + """Exception that is thrown when something goes wrong with revision managment.""" + + +class RegistrationError(Exception): + + """Exception thrown when registration with django-reversion goes wrong.""" diff --git a/reversion/locale/ar/LC_MESSAGES/django.mo b/reversion/locale/ar/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..1547399bbe817c0751b348a32d7333bb8be99f9d GIT binary patch literal 2637 zcmbVM+fN*26d&7qTkoy+OFSW=Lh3BLy`^l~+AS<)<1Qd9jWjW4cK2f$Gdn|OW~n45 zrj!LFn(Bk5G4Y`ix-Aiq;De-zY2vH#`I{I2hWhB6zcVw-!nV=sz_-8ea=za==XcJ` z9|sS7t>AeP<8_SRFy6rUnW!Tz?Ndfb}ZyS>Oin z9PkdX54eASus;BN8S8Q2Ge8^o5^xrH4EQZ@82CG|1NbIL-vbJiMHd=2=BV|g9}ZvEOJP5)tTexAU9^vc5+tkcfZJ84;tAdO0zD`>*D zD&$TIGIYnKf-tO0J_03ATrCIQ3fn5GN$h8n$fr(RfcWhIDQ6>HZ7=7 zbzMYxPa?jLT{&>djU@h+q7nW-k&J|jwRf0WMRYn!W1}J9t^=R%-Zi!hD9wjE1Z)TC zAzZ6$Zxd%*BBa{h!G&a{;53i4+B+&1oOU|Y*6`6rmox`O4GEoUu?W>Bs)jKWrE#lD z6>Ww@Np}hVT3ORW1*1%8qggOaYo`|i;h_3Xqp;yl5vgPe$9&7BNv?=S#YxR93xWaH zF-jR1`B(+5V9an_Y^jw>++(_iN`gl#RS<%syCSNLIl@+oVORJ)TeJl-E-6($8MZBM zMKx1WF9a=`py5>SyQ%YOx{w|j%?u4jM#Pk5yK2rU>m~JEwd|;QD?yRrp}gA5TZzt3 zhczK=HEkBHl5Un0bgH1ckzu21Ylhly*%c>2=9Is5k|$47Fio1tWQ^XS7~wRTjAI%P zrbjV7%G36EEOsV&j1C_T<~_+c-#FGhi^q2zc6a0OI1W#ojwo_juFF-qN%*PDO>arA zlUMWRN!GnNZx(wuuv{S6ZFuw8Tp({wuE;IEB!9s9Her8DuCvh=);GObHewEMLH;P~ z0o4^y*S-1w>2O(YgL;FB|C2K$fk3yB%91y$a86i;d3FVXEh5X+mZM;~iQJdu7OpP% z8-Z(i%fBa=y+x*#w~&%I4-#m|jy@x~=3SOQw@9|US28GrWPFjsgdU7$r76VyD1yRl zBdiU4hr$mTYRBaYc72yMxd#7>-sPZ9yDhi(+-w$l8+b&;R56jyyQ zyvyFw-cO}m^=hn9qpP{J>W@QoL|sYt{S`t%JX&G!o-8Y0kE^cu1Z{Bwh ARsaA1 literal 0 HcmV?d00001 diff --git a/reversion/locale/ar/LC_MESSAGES/django.po b/reversion/locale/ar/LC_MESSAGES/django.po new file mode 100644 index 0000000..07ebf72 --- /dev/null +++ b/reversion/locale/ar/LC_MESSAGES/django.po @@ -0,0 +1,125 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-06-15 01:49+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 " +"&& n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;\n" + +#: reversion/admin.py:161 +msgid "Initial version." +msgstr "النسخة الأولية" + +#: reversion/admin.py:195 reversion/templates/reversion/change_list.html:7 +#: reversion/templates/reversion/recover_form.html:10 +#: reversion/templates/reversion/recover_list.html:10 +#, python-format +msgid "Recover deleted %(name)s" +msgstr "أستعيد المحذوف من %(name)s" + +#: reversion/admin.py:312 +#, python-format +msgid "Reverted to previous version, saved on %(datetime)s" +msgstr "اُعيد لنسخه سابقه، حُفظ في %(datetime)s" + +#: reversion/admin.py:314 +#, python-format +msgid "" +"The %(model)s \"%(name)s\" was reverted successfully. You may edit it again " +"below." +msgstr "" + "تم أعاده %(model)s \"%(name)s\" بنجاح ، يمكنك/ي التعديل مجددا" + "" + +#: reversion/admin.py:399 +#, python-format +msgid "Recover %(name)s" +msgstr "إستعيد %(name)s" + +#: reversion/admin.py:413 +#, python-format +msgid "Revert %(name)s" +msgstr "أعد %(name)s" + +#: reversion/models.py:59 +msgid "date created" +msgstr "تاريخ الأنشاء" + +#: reversion/models.py:66 +msgid "user" +msgstr "المستخدم" + +#: reversion/models.py:70 +msgid "comment" +msgstr "التعليق" + +#: reversion/templates/reversion/object_history.html:8 +msgid "" +"Choose a date from the list below to revert to a previous version of this " +"object." +msgstr "أختر تاريخ من القائمه أدناه لأعاده نسخه سابقه من هذا الكيان" + +#: reversion/templates/reversion/object_history.html:15 +#: reversion/templates/reversion/recover_list.html:23 +msgid "Date/time" +msgstr "التاريخ/ الوقت" + +#: reversion/templates/reversion/object_history.html:16 +msgid "User" +msgstr "المستخدم" + +#: reversion/templates/reversion/object_history.html:17 +msgid "Comment" +msgstr "التعليق" + +#: reversion/templates/reversion/object_history.html:38 +msgid "" +"This object doesn't have a change history. It probably wasn't added via this " +"admin site." +msgstr "لا يوجد تاريخ تعديل لهذا الكيان. ربما لم يُنشأ من موقع الإداره" + +#: reversion/templates/reversion/recover_form.html:7 +#: reversion/templates/reversion/recover_list.html:7 +#: reversion/templates/reversion/revision_form.html:7 +msgid "Home" +msgstr "الرئيسيه" + +#: reversion/templates/reversion/recover_form.html:17 +msgid "Press the save button below to recover this version of the object." +msgstr "أنقر على حفظ أدناه لأسترجاع هذه النسخه" + +#: reversion/templates/reversion/recover_list.html:17 +msgid "" +"Choose a date from the list below to recover a deleted version of an object." +msgstr "أختر تاريخ من القائمه أدناه لإسترجاع نسخه سابقه من هذا الكيان" + + +#: reversion/templates/reversion/recover_list.html:37 +msgid "There are no deleted objects to recover." +msgstr "لا يوجد كيانات محذوفه لإسترجاعها" + +#: reversion/templates/reversion/revision_form.html:11 +msgid "History" +msgstr "التاريخ" + +#: reversion/templates/reversion/revision_form.html:12 +#, python-format +msgid "Revert %(verbose_name)s" +msgstr "إسترجع %(verbose_name)s" + +#: reversion/templates/reversion/revision_form.html:25 +msgid "Press the save button below to revert to this version of the object." +msgstr "أنقر على حفظ أدناه لإعاده هذه النسخه" + diff --git a/reversion/locale/cs/LC_MESSAGES/django.mo b/reversion/locale/cs/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..f060570da88f7b1ce431b2fb8f5cff3fa7d0668a GIT binary patch literal 2474 zcma)-OK%%h6vqc>d6@DlR+{{#!*-{42!;^UU}6!;DJ0{A`n4EV!v{wHt` z-tU4hg7?5P;4fenq$dXR5_}!+Es(&kK(==ad=2~=JPzImm%)87Is<+JehhvI;?MdG zw-~tzg%?>P*ltY4$&LB0;~E#ta0H4}&iQT_&R_k~P(m5wDWIG}0x!X-|cu)=A)Ay(DYKc~oQg*sxha%}BO9 z73=NxOvBR?iz*0YXsm3elB?v4xr+;>axqh}vhen(@d9a`gQqHLTjvyB@}Xxu;g8HY z)^a4Z?nk6WOHws9244Rsu>PLgV&W8xg8E+*qi}XJQXXb-ocGbhD51Oj0Q*(c=;E+q zW!c|w+Xut<9oL~&$n?W`K0=%hmynC88Lm915^AU*?x~qT!K*`>4vzLXNSB3%3V0)Y zrQ=!+X8BFup`_6E@;n4$6C=cf(#TZ)Xuy1 z>Oi?^TF%TbX3pklHCH~LFD)g?vZ*3t7j@lp?K5#*+ZC0j#B!-(&qq=i$lYeyrRi8Q zm9mp5J9UgwsdVc2;bbbA#6laT@%UH_*ImeE%6742XESuJlrH8Eua%Z^iGt9^u0$f# zzCb`}T39I*Xem?7(WzW9lP{dkWV7Ym`SWvF8rEY`mv%*pAWenL(%BVu`Bb5B+V>n4 z)#nmLoQg?MDJPkjSE0d~u`BH+oHg<@bF}HBCMP+Qk(Oq5rLth3n5<)8)@5Yp!kTiu zu%4z9RnH`r{Wuc7y`Z8%rzvdqU-j(qlQejo4QFRl^dTh)zs{v-dYZyh?_(l`iDM@d z)^hKv2XRfB&00(O=rWj#1L{c9vI9}a6n1a)?ntgGPF;vrg9*}s=!md;oewkCvS>C; z>{yRrB@uFT zvMT7^gq~XLV&5}s(f2mOt!oG@g#LW7b)|d5Y$%F-)w_cO@nBe{dmY<96w{-am;^O3 z;Z5QwapJJs=*(K9iE$ox`vv}N?=efF6?Hca%;A5-PFWn=nyj3fdC`SG7@po=b)wWOF+q$`Rt+$CXj1=Thv3C=yLRlJ$I!I1Tok49V zZbqV|T2RP9d5+clvAgN=#^Y=a1ky$nV@aY*K`JBdP$+}$=E!F>!QLIVfrmk-@;vGU zTh~H|))43=JmL#>7IAwnvfEDpcR`r!cPuox2p+PsBGqZ&Saxrqfk8xYRr=Py&t&`Q literal 0 HcmV?d00001 diff --git a/reversion/locale/cs/LC_MESSAGES/django.po b/reversion/locale/cs/LC_MESSAGES/django.po new file mode 100644 index 0000000..94bc354 --- /dev/null +++ b/reversion/locale/cs/LC_MESSAGES/django.po @@ -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. +# FIRST AUTHOR , YEAR. +# +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 \n" +"Language-Team: 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>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áceno do předchozí verze 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 "První verze" + +#: 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 verzi objektu." diff --git a/reversion/locale/da/LC_MESSAGES/django.mo b/reversion/locale/da/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..bc359e0edbf79774386c711e31505ca6c82e0fc1 GIT binary patch literal 2384 zcmb7EO>7%Q6rR!_!2Fk=A_U@5DZ){^j*}u| z#!Hy5V*Z5r8s>+`@WA*JSOxwCd>44-5g{H0egb?P_<6ei3U~tRZ-CDNzXvV@?*dD} zmYp(xCUhYFY(~>Zv&qMewXh50PMoY zyFj)-e@cj(z>k6K=WoErfPbX;HxQ=c7|7V)Q@{%F4DdQoru*Ll+20?5r-8o!UjhCO zd>eQYq}#yDz&YTjzz}!<#1SHk!}cvS+&eLS9>>A?~NeZP`)V8KaepgW(IZsU$ zn_J`!S=BQ8%CeP;l~*BQi)VFFsG6Ofw=XWD_$Y^>! zrM)P@yEz~Alvsmj#ty}r!J?ugAIW(9c}~==axRI$$$dr5f%ow8NFd1>Q!#MJ;~@T( zV(k9INH)`q&1ALgsaYq+8{-5{y-4_uuCd)jX>Lvk*bb>j;_gq|dE#h8LZ~xYE+l6P zhc*z_Otxom=&Yl&<1^0EE$L9yl+X=YEo7}dh~r_7cFcf!a!4wSJmDuhGD zXEs&eSRd3}Cklh*LB|CRQ=m+JqYZJtd}!Y>3q$;RjagKtd;3StdTkP`XYV~Ry<&5nd+Q8LXrB*(SZ$6jLtQhTa zPlCp<55K+oz|Zw#6mF5zZJqKy`;4-6_zeX5=&Vcwl85tQy;M z1=9Uw>6R82Y4mp~tLcp;nx9?Fi0y;hafg)V)?v^(WYRUThDd0jLp<9CO~X@1=}>B| z$VV}M;G-~(IxHnfFzF+*o!q}Jq?`L6#Ha&b3J^VvIaClIykVhs!GC=uA8EO*7_^lz z?J7}MtnI0uSUtF79ok{)9pr3>UFsivjYxXb)w1756k3H!-yf&z$|i0G2m8R-?qOo5 zj zz1?Q~S?urz1$|)W*_W0^4F~-u%u#7TUASz~ZW2K-EB@(;hD3>V6hTgo_&F8csGfK| zC9xdrr>l*wWAPmr>EN~wE&jSu>xsHJ9k%%1NgcYOh9+^XDe86;J+%++?BPAy5CjA@ gByA{gQHVCQYzJ^xbSFwO=*cib?2fO4XpRj20r8Bq;{X5v literal 0 HcmV?d00001 diff --git a/reversion/locale/da/LC_MESSAGES/django.po b/reversion/locale/da/LC_MESSAGES/django.po new file mode 100644 index 0000000..074b908 --- /dev/null +++ b/reversion/locale/da/LC_MESSAGES/django.po @@ -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 , 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 \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=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." diff --git a/reversion/locale/de/LC_MESSAGES/django.mo b/reversion/locale/de/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..807f2b26c2c5f0ff4da2840cf7edc85a703e1575 GIT binary patch literal 2599 zcma)-&u<$=6vqcBv^75p1qw*0JXJ)bBD-;04#BillO|09PFqrk(jKVB-tl^x-C1j9 zHX)WOao~tRLgK(3A@zzxPvvBBLE?Zw;syu!7dUa@`_}6pZ9-w?@yD|}^L~8a+wpIQ z_kAI7y@2sD#xEGJV0`irzHl9QScoUUgJ2yz0=^5j!2RI2;34ok@F4hOdH+{%ALhS< zPlJDg3*cYiA~^TR;QlSJg88T5i{O_af!}~^_h*n}`vW`${taFRXJLFA{0Mv>yba!pGt-A6Z^82%h; zyKBw(0bDgvt{Y|X)>YwqVnaFKa~;YC(eIahhFF=1Aj{M=Hvy}P#%Mr-sp&L>^W%<= z4SgJ=ZRyYu>Z5_ksL-g% zc;To4D9V&GuX*Zl^(3`A(!`3efNLJosz*LqQ#O+wj*C~yDB}Lq5>NA9_dRnSl}tx~s;eS0DP>Zi2GB#bR2g{F#l$gYHBP z?%$1-5m)PmA>wtE7L& zwK_O_%5q?nqf0nZHa&i9YXu`{onDT834DbQaEkIjz$-s2Jooa#xuva)O>1N__O6wi z44{|W{@vDQFW*!h%iiA^97)oOn;~7KYcsS`(|jQ1=`1HNmKE~RoRYZWrS)RNx-N{E zIKkafo}swHQ(K5vgcY&taUhdX>?Oml?RYTIS4>T) zKD@U)F4Mnnyl2t9N6@&ALyIFbWn_c65Y~Mg?|pndAJ=SGaxvudJwJnUYkZKQ!(3Mt zN_$w}>p;tGgzmNNEQu|zgnUKKOM79_-JAV8hWk6kxyEh1prmc0+_{OVo!Pb=bjOmz zVPhiNyUnnr**IMtc6Ff!o8V5A{DOpHak2w(^7+zO|6@G2SnLp3*4JYV*GV4D3hZxtoR3q8Vnl% literal 0 HcmV?d00001 diff --git a/reversion/locale/de/LC_MESSAGES/django.po b/reversion/locale/de/LC_MESSAGES/django.po new file mode 100644 index 0000000..dee2e8f --- /dev/null +++ b/reversion/locale/de/LC_MESSAGES/django.po @@ -0,0 +1,133 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +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 \n" +"Language-Team: LANGUAGE \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" + +#: management/commands/createinitialrevisions.py:76 +msgid "Initial version." +msgstr "Ursprüngliche Version." + +#: 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." diff --git a/reversion/locale/es/LC_MESSAGES/django.mo b/reversion/locale/es/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..520f0fab76daf4dcbcb5b6e6306ef10597dbd13c GIT binary patch literal 2506 zcmb7FO>Y}T7#?VW)_fPrhXCtWj{`3PcYu!ozXl!$eg}LJ_*1!l3wRXs-+@m7{{)@` z-T^KHXOHyPuK{1c{8QkQ!0SNf@FQ>r_zSQBWM@^tMc^giH6T9XCSJ_rH{fHyKg#vL zfM0<79pHOd|KwpIJ^9)WWU90$UM_zKA9Uk9?kZ_D+cft>P(FgOp)f%l)l zV0)xbJc_{&Qd9UUCV1hfA_tsDewY_O&tPy&{2(3TB*wEm!SguAbH%rQ{97^g^5 zEWM((H7WUBMTvHvS}HMD$Q!aMGJDFhl}eOXF=6Jk$;h-x;@!HeA|L)&XZW#ax01S6 z?P-&{QGb3{yM2!)O;zT_GT2Rcohq?hG8n7EO$*#V%w(#@T_{$;+1Q>~H85MtwAV5j zI1WX_D(8yWoZM5?%Do5wdqONuABm2WF$nLzE(U@9o0F|Fmt)mFZ!uUHq;_PZ@OSV0 z?!e)I?NAh?^k70y6Gs~pLLIAei8+VkPBanLST!{`bllOY!5OFMigYM<>Coj-gsipm zBSu|C*v9UTYf@{S9drbd;Xj zcVdmEC#PnD$+=)=il%03Z@d*u&Q4CkFhDUh-;6`--mFbe_wClD^Fh#S3wxt8{oMv_~Z1S`g-u0~n6n7W1OdFp>%sAlxq0!@w2S47=K z=CY(blhjr?28~0BEW}5ms-0Yek)p1&k~JvrDl2Vajs|u5#t#{(M56^{Tu4)KQ@#10 ze!1B-iyQM8I+i%uYGav`a$v#D4Btyom&{9Si52ENv1+Uo2i0iE%eYdPAVw@o5q?+n zACuQS$2toOdIkq2)H0s8P)-{o*aNYH$p}Z>-T|%qyBLP~k3OJ8Wr_!Sz~XdZX*9CF z@*sC3_T@1L6H=;_j#>5(P4sE%SlQxABC5N@s?PI6w2IofD#eQZI$TiKmJx20#$N*J z7g8fvTt6t5w27fCS9|>}5}O9P(<}4mM#2KYVY8u?hNWj^^XNzg{-sFqKMedwGHq$& za9m%mRB<}nGeddfdl@r9hzq6QQV1lgR`IPbV$eJ*BW`I38ETI, 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 \n" +"Language-Team: LANGUAGE \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" diff --git a/reversion/locale/es_AR/LC_MESSAGES/django.mo b/reversion/locale/es_AR/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..fd04fc8ff593c60affdc05059167d55643804a2e GIT binary patch literal 2507 zcmbVN&u<$=6ds^JYks%<6d+zBML1G-W2Y(#j&f+6q>YpmH@HQoZ$AHa^4RAB z?G^Ml(C?zZh5p$ic%U7BRETxp3E*YmRp67rZ-6I&cYv<}zc1(S0*_(*EAV;XAHXHx zJ>W9%++)T3F7Q>1KLtJq{0hi1dqiGVmI37l@zu84r%*7vR&t-^=+w zfg{9z4|pB((Gx;^1pEbxpFY*BG8T6O)w<3Sh9vI^kNs6Uc zv}MhZ{6JBnou{5k%no@&Rz+r8S+-J%@+u~boHi+$Es}WlZmP(K|J4~j_Uu;D$f|8^ zGI!9QA85Dl(F}(w^HVvVDzS$-*(|x7t`~8OYNDd{z(Vft!SVfx z!vWjjP(bNnLeCIq8xvBUuJaKyhcixek=As5Xt3#wqp50-DcX?^$6Y#fSrmb_tt?5# zA$?#n8p<)LSbM@x_N6Wv4B4SY8KI|UQY8g(V0}_TY&bbyij8vV+nxq|B9fwkO#6yp zz~>m!nnymSCws}5<6=u0$6S3{;w0fm#zTbQv{#|H;*=FpSuc4tM^+)@SY*7?v4)E@ zSc`+JMe(+%wy8$O`e4KLbsQ{ZeHU~MraSL-gEoJR5)ZiaS~S;ao(r0dpxLD6yDhvl z&Ndn_48XqTt8ld4Tw%8^oe#QJrY@1ltwk41x+@byOBr%?wRI$($-WA@N)B7}{z#_U z4XA$J_EG&hl`|8Q7Abe*{AO)qZDVCWlV;ecwN2__9dySd1nkux;b;>VZ z>8=Lz`*s}9ma@T08ktz9{T9vlw6ArN%*rHKHFoG)l#cSDTbx^<;&ri}(&`vPpMA>Ich!AQ8J0;XJp4V{hHb%~eC}1x3gJkZi z%vpsV?qibZKRel+a9v$}$Vf$hHFYq#LS#T+wOGndK7$PpC~>utpYxGipDDA(nIf_K zglUDxN9Yo5WvUb=4aX=S+Pi&AqZFulC-!|X{NUEkAP!6TxOxTWS}+Ul1g_V zzeDt+zV=Ij70qkZISQu~C;n%_o{=2(v~eM=xJs!MeT+cy1@@6pCMKb8RllfuZ{_yh pZI|B#sFr4_EJO^NXN$|gN`{(1{XopB60EKev8LsB2C7zA{S5#`;_(0g literal 0 HcmV?d00001 diff --git a/reversion/locale/es_AR/LC_MESSAGES/django.po b/reversion/locale/es_AR/LC_MESSAGES/django.po new file mode 100644 index 0000000..240762b --- /dev/null +++ b/reversion/locale/es_AR/LC_MESSAGES/django.po @@ -0,0 +1,134 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# Gonzalo Bustos, 2015. +# +msgid "" +msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-10-11 19:10-0300\n" +"PO-Revision-Date: 2015-10-11 19:12-0300\n" +"Last-Translator: Gonzalo Bustos\n" +"Language-Team: Spanish (Argentina)\n" +"Language: es_AR\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Poedit 1.6.10\n" + +#: admin.py:144 +msgid "Initial version." +msgstr "Versión inicial." + +#: admin.py:166 +#, python-format +msgid "Deleted %(verbose_name)s." +msgstr "%(verbose_name)s eliminados" + +#: admin.py:189 templates/reversion/change_list.html:7 +#: templates/reversion/recover_form.html:11 +#: templates/reversion/recover_list.html:11 +#, python-format +msgid "Recover deleted %(name)s" +msgstr "Restaurar %(name)s eliminados" + +#: admin.py:304 +#, python-format +msgid "Reverted to previous version, saved on %(datetime)s" +msgstr "Revertido a una versión anterior, guardada el %(datetime)s" + +#: admin.py:306 +#, python-format +msgid "" +"The %(model)s \"%(name)s\" was reverted successfully. You may edit it again " +"below." +msgstr "" +"El %(model)s \"%(name)s\" fue revertido con éxito. Puede editarlo " +"nuevamente a continuación." + +#: admin.py:392 +#, python-format +msgid "Recover %(name)s" +msgstr "Restaurar %(name)s" + +#: admin.py:406 +#, python-format +msgid "Revert %(name)s" +msgstr "Revertir %(name)s" + +#: models.py:59 +msgid "date created" +msgstr "fecha de creación" + +#: models.py:65 +msgid "user" +msgstr "usuario" + +#: models.py:69 +msgid "comment" +msgstr "comentario" + +#: templates/reversion/object_history.html:8 +msgid "" +"Choose a date from the list below to revert to a previous version of this " +"object." +msgstr "" +"Elija una fecha del listado a continuación para revertir a una versión " +"anterior de este objeto" + +#: templates/reversion/object_history.html:15 +#: templates/reversion/recover_list.html:24 +msgid "Date/time" +msgstr "Fecha/hora" + +#: templates/reversion/object_history.html:16 +msgid "User" +msgstr "Usuario" + +#: templates/reversion/object_history.html:17 +msgid "Comment" +msgstr "Comentario" + +#: templates/reversion/object_history.html:36 +msgid "" +"This object doesn't have a change history. It probably wasn't added via this " +"admin site." +msgstr "" +"Este objeto no tiene un historial de cambios. Es probable que no haya sido " +"agregado a través del sitio de administración." + +#: templates/reversion/recover_form.html:8 +#: templates/reversion/recover_list.html:8 +#: templates/reversion/revision_form.html:8 +msgid "Home" +msgstr "Inicio" + +#: templates/reversion/recover_form.html:18 +msgid "Press the save button below to recover this version of the object." +msgstr "Presione el botón guardar para restaurar esta versión del objeto." + +#: templates/reversion/recover_list.html:18 +msgid "" +"Choose a date from the list below to recover a deleted version of an object." +msgstr "" +"Elija una fecha del listado a continuación para restaurar una versión " +"eliminada del objeto." + +#: templates/reversion/recover_list.html:38 +msgid "There are no deleted objects to recover." +msgstr "No hay objetos eliminados para restaurar." + +#: templates/reversion/revision_form.html:12 +msgid "History" +msgstr "Historial" + +#: templates/reversion/revision_form.html:13 +#, python-format +msgid "Revert %(verbose_name)s" +msgstr "Revertir %(verbose_name)s" + +#: templates/reversion/revision_form.html:26 +msgid "Press the save button below to revert to this version of the object." +msgstr "Presione el botón guardar para revertir a esta versión del objeto." diff --git a/reversion/locale/fr/LC_MESSAGES/django.mo b/reversion/locale/fr/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..f25ee02144cc69ef272a1efd906ff99f6c7e895c GIT binary patch literal 2582 zcmbtWO^+Kz5N$&EG7unufIvhk(u$45%-Tta5@!?Q?0ORmZ?bHh6*-0WOxxSh%(Ut5 z*{~5pIdVYaga{G`#3{aWg9DNyMf`;v5OCsW@M`Q?J0BtlTJCu>?VhT7Rn^b_ZSU@{ z1;(qGZ(-iX+>e<&h6~2;z&7v?;D^9)mk>_@ZvbBaegS+2coX;r@Q2CwKLdB;`8M!H z;IF_lz`MX@;Qq(Q>s{dMcy>Ux_a*Q(;CH~oz#oBYz`ua=z~eCb5wHtn`#<6GIb!=| zvi|;)LTIet1U?JggXs7F<=8p`3iU)$o?yE&MDvXe z9C;v&VlW3Da3h|;g1N9Ztgw!JV3dKW`->%r=PZGeHfYgSn3>t>nsMTg^_`aDjXUM0*eI91<7tyCA}QLGSzi%0 za2-QxdGIk^*-f@NF5W5Qn7dI+R1$V%oFW9Ly$Z!8r!1;$xHW@T9BhnRwoYq}r4Jfs zo3znfzu3BPzOt?c#`<8@^>rMa$@?zom^xL~E_8w=tE5Mt2i$LUI#jJy1AMAA+}7Va z8dQ%|tJoMI<(kjKbYCr~9uE%HsCKk|827xdEu9ZKR%R}dAXKNO*DA{tEh{H;uM$U# zs0S~Fak+H5pGuvCkx5U%K-SM?Uj-c{(>k>q=g(eZB8%BVovNLvupl5W&Z+B^gJ_{KF)UId3yDz_=>S|EYk=&b zcCjKm8b$)2&(58Ypaj}dGN9f+OSc;JTBv+ylMY+<6m^32ku_U2Y zmlED^9Rw{U&?1ww7A-n&P1Zlv%&h9;3?T5ug~{5Z@^(Lo|K(s{5EtZfl04W(ra-j# z1wh79R>&(yCJl7}u%3+f, 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 \n" +"Language-Team: LANGUAGE \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 Enregistrer 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 Enregistrer ci-dessous pour " +"restaurer cette version de l’élément." diff --git a/reversion/locale/he/LC_MESSAGES/django.mo b/reversion/locale/he/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..09271c373004746bc9a6da346905de7854d6057a GIT binary patch literal 2530 zcmbW1&u<$=6vqcBzYK*^Kp+*Qr;0)3nwqp}Dfw~X*bCwU{0(zMg&RWR3=$H2-}*NggV<#E1@AQHzXbPTd>1?o zeg($CKfnWE`~e}J2NysBOCbBX3_b_m0AB;|f^*={;1D6c2OSn#l(_Qz9gF*TT5E>RFU+Oq=M^f%F2Se zL|Tz2b81C;Y$Xd)ONTJ>T~#8rNH(6?_hn9p|En`B*6h}%WlyfSs_b|B(@U=3@u-SL zS<)gN%Oo<>slozF5pk#;(QZ+SDJWIms+dw3%z4uHg8+PcMN+n`HAMa`j$n(f z5IEvi4FBq)rGAi;bTeV?G`A*N`5W*8{zn0R>O2&m8od2l5|{6 z__y=6+e~qo9eQXc+$GiSl}2V!mUbhtVY|rWDCw78(X_-RvnjV^m-3Qez;zANv_?EC zYiA3qtcxve$Kf7yZIl{*Y^MkbzN=+eEcntx8z^o_Yud5SHX1cbbFqoHV`ma{Hj$p6 zo}CG$<+Ad$mGbkhW1TMNeJi7)6q=jOSQDPKHM-m4E{oDwB=V*;I%bVVXfzTXd*jsz zW?*QcG%lZlzILx4YuF`iUt1Z^F8Ku;0YzzYA(^C^SSmr|iBxPlc`6o%$Bg+NU z##hI91_eAV!`ZLW#I z-Yamj3(HMjU5C25xzV-RZfmo8XS;2;5y-WFBbx?_i z2^u)hL6+C8o-bRE=sq7f)T-U1T}@L#{pu*(Mwp$~&=RO-J?NJzw>(Q)Z>}{g+j3CAo7NzvYHAR59ly8, 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 \n" +"Language-Team: LANGUAGE \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 "נא ללחוץ על לחצן השמירה מתחת כדי לשחזר לגרסה זו של האובייקט" diff --git a/reversion/locale/it/LC_MESSAGES/django.mo b/reversion/locale/it/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..cb1b6a3eb633f1db754e0603e1a10c03eb326e69 GIT binary patch literal 2395 zcmb7_O>Y}T7{>=FZ|1Fp*8@n8Dx#!nckQ%2ByOncyfh8OiR)}CdJ5y+@p{nij@g+_ z$%jCkDskh4gt#Da=>>5q5+8soLgmB(gt&15!T(u1w$ntRtTOxC-ON1myv^S~O`WG{xB>nS-T*H@D#VlE$KVU#9q?)J^Wpcez*Bg?3oe1* zfGglX;AwE>F(FkU_do@*{cmwwgirUt7x6Bh z5MmdY0^bL7@G0;Iumb)#eE$W+pZEhew(}?01pgZH9R$brJ_R{1UxIIe--GP`Zy@{s zH@E=4icKWg0zL3+(1Q0t-X}nE;AXj&_v2XkLOROp~~O--C=>+s@!m5!SMaqc_w@8wGpKh6gQn^nm(vI*aW2uKJ z_Ss;LX2QtKs8u_{L!7;Z>!J@Rf28nsTX#`3$3wN<*g+TLE@+N`uy-{j6~ z+E_>4QW0A(FmM@OU*&rB3|Nq*t&aiwhYmFHkbA(?(-+ zZHGNy+}OC9Xy4@Vl}Zz5VI13>U#l#e%;BVX!G0e;JN1#P_7jxmGAA^*%Duf4T=V9~ z^Vp@X%DvSrG?C8YI?Z>qtB6J%>mrNwSR=MW6(^dCtW*iUCCLng01N5ELX$N(X4Hp3 zKqv~38-Qn{fjB>kLYajsK?1N?V~s*>$;7e3LSOt3`x+_C*t{$^K%<8$LfC4&*D)I` z4^+g6sG3%9ou{?BW(AQ|lS{D;lUk1SBh{o6>KBQXY<62Fdy=@rrL{+bEF+nS69*}o z8N~%Dc?fG0A)RDSKtjsgj_Y#d%Q(+^a{6JV7*8^yJ=lE0e)y+a~%tias+f=WxH4jEU{r&eJ!scskDQE;uZ0>3^4nbKVyLbC9Cm9V^69pUKs zViUR+avMT)2FcgsAT`=`qAz`M<52fOs4^%|*-b1s&=fS#XfHTZYL8C%XNYAjBlvC| i&N8HrPYORcmqWaAG{~^GAx7{}Q-h@ms>DZAiGKk_NWdEa literal 0 HcmV?d00001 diff --git a/reversion/locale/it/LC_MESSAGES/django.po b/reversion/locale/it/LC_MESSAGES/django.po new file mode 100644 index 0000000..13a1526 --- /dev/null +++ b/reversion/locale/it/LC_MESSAGES/django.po @@ -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 , 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 \n" +"Language-Team: LANGUAGE \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" + diff --git a/reversion/locale/nb/LC_MESSAGES/django.mo b/reversion/locale/nb/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..af21cdd44f868d1399f5f91c2de532d7a21fe98c GIT binary patch literal 2506 zcma)7OK%)S5N;s6%sYSpiJ&5qg{@>Jy9px62BX9;Y$cA3JqgJv>fLFN+dI>}>h7^w zi9?PYaz$|9gv3AK1`d9 z_WRg(u#aHpPvHyeAK)e6zrZhm@zX*)54;I{4fqZ4Md0_q_kce(_kRT*f_(@0I`9wR zEbwpOJn+af_4O|BUDzJTe7At_0Dl0U0{#qK1KtBp0ndW;8n6pw{$KF*8)Ew%cm?*& z=Y+Tc{0qo&eS%;ofaifP051YL&ZMyoka>22eE!!!&hu7-cY$xgZsB3pWdX=q^%{E! z?7;pGcoFy~@C5J!93Fs|fe1z12C}5A1yZTb0*E)U-@xWGcyY|k{}T4g*c=-#gi$Td zg%^$y&te~D1M3xRNL!utDqk3_xh-qGB1x$XiZ-1sC~PUpwGY%)x!t8;$*G>*QI5S- zu7XMlmeWt=A(mRvF4QbRjyv{ zDC(9WAfE#%SJCXtmc<#R`&f!mqK_h3Z?v79s^b=;3nQub-Kh8u+V>|80`^0ZAn}6& zA1BT>C8Rnz#U*DAr@cs!*5p)Sap<(CqoXs9(ysIAEeckV8_b z4up@)q;3?9nb4uFu#Mg8np!xhzSmOhxL0mUt@7rhKwDf9Nxdy;GDUFUI>xjdkdN)k zZa(C=*i)t{ccYf5BzR<6AOx?2ip3SL9I9-&HKXM;x>~nvhth4(+NjEkn|NYf4Xg{% ziqCW!&6b&u65FAPwbdk=b4mvIMBH#4I@xNsqjoE5pQhH?&Z&=&wNAELaEze2X0C&N z?N+CK`dF(Czf01GC~?yGTq2_mZD^B1p$+S{@D6cHEknvnJq;iIU*}0SEO}I5Egkm~$+r^D4|-Nfx6sV?T~(Q@Lot z^lYk4)}b?99VXWD(#bqpw65?SGK0$WGbhhc{c~o@(1$bBo}RcCty%7YXdKsN>vmP9 zF&buVzmWOZXU>);xM7DbudaW2;rjT-hAmxB&DAk>s0^D3Qe0JeM#}JXvY1Y?8ceW_ zN|o8Pj(FexiLz~F>;T;!FvW1+V@#pBtJi%S%`SHc7WZEtbLxp%!5nnnm3_s?#6hRI z&bYVnPWXm8t&XkP75FdKQ%gFr9n&IRr=^bOk!3NmQnAs*_OOYil~~$V#eE|S2Z_WD z`+XW9rd(!DMSUX&gU4wBnap0SaE1`$I3LDXniSK1B<%wtJd*MAaUC#Enu)gmVqKV& zr%iJ=sRi~BIDhC>h`n#Ni+zkJE0+erI@D!UmE*NW0i)-EN%y}~uA8g-S}4RcUs50a z>(JR%xtU0Fo9*>pEEYAyb%d7&m-jMj^gYm*G5)K#JxBUu%#EH^m;+$(^{CS-rq!xd wksSEFG{z4N`VK>ebRXT=&2, 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 \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." + diff --git a/reversion/locale/nl/LC_MESSAGES/django.mo b/reversion/locale/nl/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..9a0bc83b74ae1380ad2d9e5ea0b0807dc2303984 GIT binary patch literal 2450 zcmbVMzi%8x6doWzIKr>+D@Z_wNEWt|-Q1ZJ;est3-#K;?$Hu-`1Sx{K+vmNpcV|vJ zvvzFJLkA6lnu?B&1}dPWph-hPLrFva03^P*yLbMDLd0lizunolZ{GLbH}Ch8$G#94 zFJZol`3vT2m>)lc8^&M2>%e=!_khz63-LJcbKn!euYj)rzpmDQ06vTNpMjTwcY!V7 zsYlB7+rU$JH^66rp8#J5ehGXF_$_c5_y=$tc;T23?*nfGaf$D6|1lf(H@xbgm*K=!v?t$zyS6!YGoDxSi85%W1r&dYF| z#KZHLFc&pU&?^RLh!-%iT@e?;D+ceugZD)0#iN+ab2MiA);f|(M#__1&ul7vpJbw) zmt9Kiw)9qJ)U#WZu@xocDUx_|+8SxsrNX_tK|LRe2Xuy?J-bzMkkOX5x!dp0_qE&e zXw#I87cE40-s_Y^tBPQ39B#UZ{X?TtnsA|*N6a?c5er6pt&*Y7P%LNUTtUmJEt1{b zdxU<3U9s^#Vw{H|v;TB46>t(16pI_sl}!fPM#U=aYk+{he}rtUEb>*aQ1_x8|~V4<$Tb|lyQjy)ux$O>HJkC~D(OC7cv6t8Y-@{%kcr^bzZV@ftAYGM_KwdVO%sWWm!gT}M-L@blV%Y%{g}orm*R*vC{l z^zipDeF$b{_t~<^p&vqd6=}>3`Ua$7a-`U3%C{xdvx`EbpzTcV>J1~~!@4Sk#4?1s zDlJ9q%wTQXLVEbS1#wo2>(Z9;rJK?_B;v{jr74tal%*EMhmJRySrL#%RG`fg=g!on lK5O4wifIeCh(*)Qa*Z=CusTv+c`~4q7!5#edJ&Jz{sFoZ(pvxk literal 0 HcmV?d00001 diff --git a/reversion/locale/nl/LC_MESSAGES/django.po b/reversion/locale/nl/LC_MESSAGES/django.po new file mode 100644 index 0000000..a93ee95 --- /dev/null +++ b/reversion/locale/nl/LC_MESSAGES/django.po @@ -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 , 2012. +# Bouke Haarsma , 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 \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." diff --git a/reversion/locale/pl/LC_MESSAGES/django.mo b/reversion/locale/pl/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..3fe12d28fea2e167969ec4198a5af9d91e9e7904 GIT binary patch literal 2526 zcmb`I&u<$=6vqcB(3&5mEtKEzR4HmIvKyxrqB5mw+N4b&NkigNS_y^m?!+0#p0Reu zb=SvAhzk;25UTnQaIcU!1fi<&fddzW#Ek?00o=GCzR$ZGr)g9`V&w71JNx#{d*AnV z{OiFzUkZ%pF<-&_8S{0_&mO`T#@>g8coN(P&Vh%)_rVB!9Q+2{4}J&k1Ahcx1b@lz z{|@fK^`GD~;9uZbaQ`DhOo2-vuYU!;0DcV;cn4&^zkwXvAK(eF1ZRujX>b($9Q+Xc z7Q{y!cvOgY5&L2AX-v2Vl@zuc@ z@OsWW;2~U>VEi)p8aM)0KwkeiU-#fiTz>(61pWZN1s=tw3Ah4s96y09Il>$0%08aM zJb=miBYp7{CJ)Y&V=hJsKRDNe`2r*!jKlcGc7ymK$6&CQ&th_J#o+zLWE|7TsF5vo z%!zYr);dL!V(AsFCbmU>O;J-jPc_xFUGkO^71?VlVJp>CUd4nfr>!BoN)q3-hKhXn zZ=K;`&u%5P6Ln47j@$0fuW2{*Xj?61yqKD-PFH8=r&rEjT$-P(iYY`p?sZFvsXU^Q zQMj!kmz6BDP|P3)o1|jK!fe)PuVu5?C=`o{a&C~Tlh+j0I^H9n`%)f6vn^W|r%3l7 zT@;Bv$jMT!?N&vKg+l5F+_*Pmv7`nYZhIiV-qj9OL=ARUV2oIfn4rbTDE9=5j6SI% z?~&1#MO4Qe9VzxWLS5<5AG{Hsa-ArGtgD@7GY#pA?NCdmq+;y}A6b_=mpEjH3HlDx z*sZq8lto##dX5dZ`hjAroH^=gjY}peT9c-(2nJl}kY+vdu{Bw1rW_Yr$~fkB)e@zK z9~rk0g414w;*wJd+Q4$}2D5SSLDtiiEFEs|(voW1#0T@PuH)crr|yEPtx#$4Vl_CI zDCuDna4S~mczI$XD4z(9PvB>z{N@|wiE3mR4q;XA&R4R0} zrt3zlE-mXw%cMuAdily3m#u_G`O~F22+)yr6;zdMRcLN<;licK3)6IJZthG|hc>AX zZ7S4mmgeyi4Ktkx%cXPHc)VgkHEkmgude&?b`ynqo8?WM^6y-#o)1nA?KsR;l?2l! zvavSx3Z1NJUlLO`mW|=bayhMOg~-uFn>VRPZEJLY!}U~SaJr;pR7+8=UfNCW-j4L< zr_^yBqc?B*%}=O{S~T#Og^zXL2}%5ScgQM#9({xDv6qS=Y9sTmL-8I?2IC>GXoR#t zSH@_jqWNr*<55pKbVwGEMOHiBq3m$cQY`z#8cFs3jk~wIMv6rw-E}>gZ6ZUlAS2zs zWprbZY^2>fU6pO^3Pn`oekEA3of77U_E?cNL^|mXaL)M(CfpTHJC8@t5W0OW}TYB*CtQ%41K|>X-tBgjOtHW*^ zY;K*FvCPjVt*O`2(uA~_$Zl-gHnuf3<5`3rj)Ap-9J literal 0 HcmV?d00001 diff --git a/reversion/locale/pl/LC_MESSAGES/django.po b/reversion/locale/pl/LC_MESSAGES/django.po new file mode 100644 index 0000000..2b2b2b3 --- /dev/null +++ b/reversion/locale/pl/LC_MESSAGES/django.po @@ -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 , 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 \n" +"Language-Team: LANGUAGE \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" + diff --git a/reversion/locale/pt_BR/LC_MESSAGES/django.mo b/reversion/locale/pt_BR/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..a8754138f78e44026867066b7de5d96b2c936896 GIT binary patch literal 2377 zcma)-&u`pB6vqveU*Shf0Tm>Wo+2VrHFh>_h0<+7O_OaCHNToV5j|C7d$K#o_AE2w zZSqGTPUVUULI@BzOTz-v=eg`hURVDs1Y4$FV;8 zl;>Rs$H6VI13m}-432@nf^6rX)%rfjdj0~Jz<t@%=Uv31FthZX&tqSCAROg*r6SwLxnNt_?k6#M@R_57 zb;8ur#uq`(5kgev1`5tMjy!71&39J&5qRyY8%;6y0D}ATua%ZMxA9ccizHW z*a(+a8aFSmZLG|N-U6(hb}EzJA}rNr*IU$BtQ%=<>3|g-Nm1b(jC{b4@`w=^*yFwr z|Er>J{iBp@Q~?`=x&NXczejw4{s<|#StV#gmIsb(sMZI{AvFl>{;*CFha(}xVq%gL z&5=X$L-;*0nQ2&c($aYUjB&apERxO#VV5o97-8Km(sVbV8@ixO?2=5BBm6{Ds4Bz( zD>Nu3%v=v@r59PGWl%^gI7l)jTH5?=M=eg7ptvRSrX&b(UISWk@JB}?N_W{VmJ~_C zHK+tq4Lu^sV1iXn2HvKX24$eRApNDp-|7`=oz~~(ug+a*&{kvP`qJ9!*oJItqO=_Lk>$D}P$jY|CkFsG15|L0ls6Sx@H;tk#4K}hIYRPs#*Cco1 zwCH|=9f`z;^4d8ac!$+l)}-m3FsfI;U8A&tT8l=;HOFe_(G_yny@O9)GM!D|n4&9n z#qC2(x3m#u`{;gWRIEoj2>gW#inL>stw=kW9VL@2S=BU|Hq{afli_-YVcz2_tJk93qhLesg0IYb{JGot~>avLVBwN1l}rVrPaMth1w21vip+5 zbv394%6VT~TU#hDJ?=2Q)N?InMI!Rg_*, 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 \n" +"Language-Team: Tangerina Lab \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" + diff --git a/reversion/locale/ru/LC_MESSAGES/django.mo b/reversion/locale/ru/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..c78390c64a158025fe0a39f842832d1ab024e877 GIT binary patch literal 2989 zcmb_d-)~e!6dn~(7ZpYP6*Z1ch$Z$e+m;7hkx<$a8Y$FeQAkuW+dJJ}-M!P?J6l+T zhEfQ@q9h0jF*e2+UwkUH3;ntE%@<7O`sNFXe}Veulkq#VyWK5qfkb!r?zb~@=FE4# zbI$CoP3x{Oyq>}HBA%OgUcz(oK78TzC-70=U%>sqe}HcT-@2c%hk)b2jll1Lj{r;6 z`9Mea0Q>{E9k>Bbb^><;iRUN4mw{J+9{@{01>B4v-vgcjP6B@iQoNTpFb2Bp zNj#uzH32ZlWM5BL5pFDR(h6JHgVp&>RjdJMF@$JiBK;^9NM>_H@jZrTGalCAx_nEs zFY-fDo!)8rx1s@9Ju%`-Pqdz_9g>{cApO?O7T^;G}bWd;hvAqXVy&Y-R1-9F? zmzQiGn5v-2_9+bd0vU!jfKUueo+)Szk}Kk{M>K>$5!Yh4tBV@@J2^>J1zVDNX(D8` zpEW60*whzrR0i2OR?SsP+Or!rRL85@AxqHsc7&QcaQJQogyXWaz)D`5HHvJOAO z&4WVdX&GmSLm8kV6xF8F<2vtDYBpI3hsrmWl0z!cPH&j?TxU-q8#-x~vfy91HWDq~uH^~6Z77H2yg`x%q1104PVaSg z)a{7cfDD{&zhAkYpH1=|8BfQKIDLw;>(p2*5vVV9s)DbBB32F`I@Hr~fbZ?@>P~f} zdk!9mF>~3J%{4P&Ce3toI=WE5c+90}3@fGR6s?#NH&bQ;CZpU;n;SeDF^gCjF|%f( zg-7RL0!Pspho^Ehg7;~7UBG0`b2LgG%$zA^~47 z(sTj^WEL?fnJGjwg{ku^4~{;!l^DU%1i)|#FO-GYe-D+04 zN0ku_<>-#eg3JG$hMRBg-QVRbYDsHAwL^{2Q$LPe7fw{LDBv zUt{h%Vz3=dtx!f6mQhjHD;37UfGXcsFZCyN-?X)xtkj_?p%=&L0$9Mr47L*~>{#B< zW6{~=?Ssyz>Ya-&u68F>t^xE1oPWpHY(Y8{, 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 \n" +"Language-Team: Russian \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 "Нажмите кнопку \"Сохранить\" далее, чтобы вернуть эту версию объекта." + diff --git a/reversion/locale/sk/LC_MESSAGES/django.mo b/reversion/locale/sk/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..cb6a257a4dbed813e9730b7a86e63fbfbfad1ac3 GIT binary patch literal 2489 zcmb7_&u<$=6vwx;{BrqOeiR81k4kM*wY#<3BDHQ^nkFd;O=^?65h)UA?48&f@6K4e zvo5x{z>PyygoFg#j8vo+0S7o0HLB$JhB$KM%z*>^2Ylap*GYefmQiLup4qo=-miK2 zW5>2HEQ}X1U%~tp^Ht1`w%~#BH#iUe1HJ>Ee8{pM1wR8H2fs|$-+!f2cQ2f_yqVvy8jD! z1vcIStJtqRVp;EjS3$OO3qINoZU=e26MPta9ee@Y3!Vi_Ap83%_$>H4_%iq>$o6b| z%(C*J4YIxOgUjIO;4$zo@FKVehdc>>4Dxwb@L+vkrTgE39O9=i;c6nwcCZYO9U~;Z zK8N3|BZmq1lEJZi9uvnUxr3X@;MntEUlIDmM~>5YOf|KRBnfhbk+c%(fJ{@8??r}c z($|;BXbNRrcVx&?(w9cMgjM8eMS6tOSXi)qAmPbd+{eUX)IWC7gtBGGsE8rwCb=7X^UmP6tU1~4J=jC8CEkqoVRS`|sn)|RVJs-PQEom zOEZh-=gQ}Di?XdlV=qJv&$Um-jmWO*BIPROsy)TJP}ersTagas3rFmH!7d!3!m(oh zjs5vTJ`X{A5oOEupUN%IOfK3BWqW#(X3NEex&0T*=Vo&AA~JS06e{usTrJYuaVS>l zbX-%OXweCizJi|U)M_oRIWj(puT&!z4QW@U2#R#x*Bus~pFib$jt(0e60mt;ZeeD_ z(SnoDO=)FtVeD$R4F`<;&>U?0$l!5~dl*Smx==l1AKj2+BUWT+a~J=G}E(VAy+ z6(7d>_8A=pQIS+TSw^M9$7%GlG@(kR!ajPF@>HY=b+UAb_Us{bqI85e3)nn#JjeGD z7fYdgC{I$S8;G7za+NP^4;x%MEfeEKjfexpUl87J?qSKP z$Yy_>hjH`{jDnE7ysd|olM=ai8>?xb1~;O%9QM_?&IUIfS{VF1T*E&q;, 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 \n" +"Language-Team: Slovak \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." diff --git a/reversion/locale/sv/LC_MESSAGES/django.mo b/reversion/locale/sv/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..5aefd0334c1047d91cc6b05682f12cb95192c99f GIT binary patch literal 2535 zcma)-&u<$=6vqcBzf6Hr3bYc3M@2-^vaXYgD!2`*8#|3vJ28n&YvHDMu=m$J_Vit z&w$s!b6^#W^7{`!e)kAue-FX8z`wwY;4wI9fs5cQ_!amm_yEM8ck_K$%|;!9`qu1AYSj1YQ8&#Y+{~1`FU1AnW)MAFTi9{Qg6bOYjQz^g=k9o;UKF zFX!akO)t(l`(xiINwyHWcoQ2^vc-Al7_&Trtbek&1{~jO*a$UStXC{=$FWr;$(K%1 zFNq^^+loSM9d%R~-z67QqPp==C2XZa<&;mjvN|>t_ekQiv!lAM_k9L!S54C>k~qqndOpm4(F_BBdnR(D8@_YES;Ua z<1F2k7UjHJ%uVaneedSDnak9wEZ?YHuhGrg#%6tet+1g6apJtD4YcoF zO9Sh*<1!Um>uqnDec=Rn+=*pcD9tZ;rAyxYMJjz*{@~L2(n6^OLl4E!d{T0|TeZrD z*If6i6{H|ytbudme#4QZX%PNcD+M5tw2*=jUstZ(OZZs~fe=&5vMe zf>Z{|YbzO*sZm+GzQvNSG#Xb!U5u09zy?yAI5P+F%@<3B<=8l!D6hRcfNrNga~B37 z3U`^)oml0Tw%RM+;(;B@>Z!!5nQrWB6O?JOqg|mDril!_l{krPnam)&v`Y(@Y5a9* z*3dgkG(T4mH%Irw9a7HmkVzxb*?7U+*dEb1iiq>OE+^2n, 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 \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=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." diff --git a/reversion/locale/uk/LC_MESSAGES/django.mo b/reversion/locale/uk/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..e8baa9ee2a33f6dc50d558e90e1ea9448f639991 GIT binary patch literal 3451 zcmb_d+iw(A96l;uSiItm1RsvoLK`}JDJH_wMWKMyP+OaVXky6h>&Z*_qAE+13(* zw1g53p$VzQ7zxH0AADL0y#ZSueDU$ji@q5D02AZ8KIr#5)7@@c0!?(%={M*0yL`Xz zclNi<8@^z8w%~mQ@2_~9@qYaf{^3bJ%-CbV6z~Xe7w~=Hd0;2-7vMX1%14*UVQ0XPkO7B~mo5Bv#u0JsT@G~WxP7);1^e0DlG^2mS-Z zA3OeNM;?EbxJ9YKBv{1o^C@b#yn z{C5NC+`B-U9{^GbK(CI6a;QDD{^HVrVx)&SY045m1W1w0;NRhpSm+C{cA-;(# zP^7s7)F0yhdAytJGpH4P@5@Qsb6ID>^8&$5ZkbZ>0pBZfSrFW@1Ie?(@rJqdxG!?v zknm}jaD)^V$0)#Y?hSAg|Fb7WPR7@342LC|HF?PwL$+5A8tut~9V}b)%8tcdPjbgI zE#BM`xjNyClHa;~%;2`0^NJ-?+F3``tsy59QLWcT*&TGfVV4KOw@t@Bg^Zh920XuL z%GRLKPGAm+`@~YTgTQw4Agd%?OSrj_I6DA(6VfgUb{N!p{s=qlVbJB;6258?$62p0 zf8ZgI_`Xbo6xqNad`1#NHb7}Hs3 z>qOlE?Spnu`PFrAx}SWQ|~k_i!^PUARcy1t7t<7wiI!86k=eIR#CW; z=^n}X0s&gAOe?*P;h+GLk!asy&@- z-;r!jCb7}zLuXNoF75m9;J!YiyT>@Nk00(ycX#dhsOP8 zaG9A@JQ?fsTnR3X{*e;Gm*NwdC^+nh>fGAX`o8>i3YtC>`azr~Xr+cDmoX|FAH zWZs}peInVBO7hdEqa)apY{i)r&a}7gi7_=9PK6UpT~_nzx|#{kVmKCFw|N7Y?5kN>wYqpH|D8(vT|Z9M!;EvoBWO=H_lEG(*N>>E?_n8Gd?QMc8Ms?xD( zZGIFEE^#`wJ)DHSF;$8GXZLrtV?*>6=SD4njsMr_QgIfM$GlpgTIw=WX{fkVXHar>#ZSY%L(l>NAvcKPMhY+SduFUv zK{;c8l?p=Cksf9#Yb-!%NOw|)P)R^im2i})aay5}um{qd1pVL)oZnIvrmkUyHjZIP za;r23GNIX{MN%s{(PF-(rFmD&w@F=zlwM1uX`PCON+8^NrS75Pt2(Uz>iIrmMhH{k zI$IJq%LJ91 zBvVt}EDBb|)QTTe5-M%Nf?9xe+5$H#a)K7lH#RnDi6Xg8l5D)W^i@Slg^Iq1sfET< zx`8>Gs1i=%R&3BHZj(8!&}y6`o(f$zhya1KMfR&)T}R_+1(onDBZu^f&IjNM N)849-r{28A!oS$L+Is*1 literal 0 HcmV?d00001 diff --git a/reversion/locale/uk/LC_MESSAGES/django.po b/reversion/locale/uk/LC_MESSAGES/django.po new file mode 100644 index 0000000..93a1db2 --- /dev/null +++ b/reversion/locale/uk/LC_MESSAGES/django.po @@ -0,0 +1,134 @@ +# Translation of django-reversion into Ukrainian. +# This file is distributed under the same license as the django-reversion package. +# Illia Volochii , 2017. +msgid "" +msgstr "" +"Project-Id-Version: django-reversion\n" +"Report-Msgid-Bugs-To: https://github.com/etianen/django-reversion/issues\n" +"POT-Creation-Date: 2017-11-03 12:02+0200\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Illia Volochii \n" +"Language-Team: Ukrainian\n" +"Language: uk\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" +"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" +#: reversion/admin.py:83 +msgid "Initial version." +msgstr "Початкова версія." + +#: reversion/admin.py:197 +#, python-format +msgid "Reverted to previous version, saved on %(datetime)s" +msgstr "Повернуто до попередньої версії, яка збережена %(datetime)s" + +#: reversion/admin.py:221 +#, python-format +msgid "Recover %(name)s" +msgstr "Відновити %(name)s" + +#: reversion/admin.py:237 +#, python-format +msgid "Revert %(name)s" +msgstr "Повернути %(name)s" + +#: reversion/admin.py:272 reversion/templates/reversion/change_list.html:7 +#: reversion/templates/reversion/recover_form.html:10 +#: reversion/templates/reversion/recover_list.html:10 +#, python-format +msgid "Recover deleted %(name)s" +msgstr "Відновити видалені %(name)s" + +#: reversion/models.py:31 +#, python-format +msgid "Could not save %(object_repr)s version - missing dependency." +msgstr "Неможливо зберегти версію \"%(object_repr)s\" - відсутня залежність." + +#: reversion/models.py:45 +msgid "date created" +msgstr "дата створення" + +#: reversion/models.py:54 +msgid "user" +msgstr "користувач" + +#: reversion/models.py:60 +msgid "comment" +msgstr "коментар" + +#: reversion/models.py:242 +#, python-format +msgid "Could not load %(object_repr)s version - incompatible version data." +msgstr "" +"Неможливо завантажити версію \"%(object_repr)s\" - несумісні дані версій." + +#: reversion/models.py:246 +#, python-format +msgid "Could not load %(object_repr)s version - unknown serializer %(format)s." +msgstr "" +"Неможливо завантажити версію \"%(object_repr)s\" - невідомий серіалізатор " +"%(format)s." + +#: reversion/templates/reversion/object_history.html:8 +msgid "" +"Choose a date from the list below to revert to a previous version of this " +"object." +msgstr "" +"Виберіть дату із списку нижче, щоб повернутися до попередньої версії цього " +"об'єкта." + +#: reversion/templates/reversion/object_history.html:15 +#: reversion/templates/reversion/recover_list.html:23 +msgid "Date/time" +msgstr "Дата/час" + +#: reversion/templates/reversion/object_history.html:16 +msgid "User" +msgstr "Користувач" + +#: reversion/templates/reversion/object_history.html:17 +msgid "Action" +msgstr "Дія" + +#: reversion/templates/reversion/object_history.html:38 +msgid "" +"This object doesn't have a change history. It probably wasn't added via this " +"admin site." +msgstr "" +"Цей об'єкт не має історії змін. Напевно, він був доданий не через цей сайт " +"адміністрування." + +#: reversion/templates/reversion/recover_form.html:7 +#: reversion/templates/reversion/recover_list.html:7 +#: reversion/templates/reversion/revision_form.html:7 +msgid "Home" +msgstr "Домівка" + +#: reversion/templates/reversion/recover_form.html:20 +msgid "Press the save button below to recover this version of the object." +msgstr "Натисніть кнопку \"Зберегти\" нижче, щоб відновити цю версію об'єкта." + +#: reversion/templates/reversion/recover_list.html:17 +msgid "" +"Choose a date from the list below to recover a deleted version of an object." +msgstr "Виберіть дату із списку нижче, щоб відновити видалену версію об'єкта." + +#: reversion/templates/reversion/recover_list.html:37 +msgid "There are no deleted objects to recover." +msgstr "Не знайдено видалених об'єктів для відновлення." + +#: reversion/templates/reversion/revision_form.html:11 +msgid "History" +msgstr "Історія" + +#: reversion/templates/reversion/revision_form.html:12 +#, python-format +msgid "Revert %(verbose_name)s" +msgstr "Повернути %(verbose_name)s" + +#: reversion/templates/reversion/revision_form.html:21 +msgid "Press the save button below to revert to this version of the object." +msgstr "" +"Натисніть кнопку \"Зберегти\" нижче, щоб повернутися до цієї версії об'єкта." diff --git a/reversion/locale/zh_CN/LC_MESSAGES/django.mo b/reversion/locale/zh_CN/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..6b0fa29e1c9f710f0b308a53f41ba40fcd7b06ee GIT binary patch literal 2183 zcma)*+fy4=9LHB%Z}C>^bw(Y}bcTUiH%+jkHI&*E(voQc5kg0N>1?uxu+CtLW;e-`I{pKGPj-_a?O4y`x1XG|zw^8A zZ@YJna#$}xUW5Dsc^&fELwI2Q35LMGzz@K~4|CjO;0X8xIOgnU!JXLO0C#~);8E}{ z*a+^}VV^gFuVCK+J_~B#%V0nFHh2|`fWLw@;2SXdA=m^m`!jej|MTEe;G%P$0Y$`k z7km%rRgZGq``{@MKW@t5yu$^DD>a`_E>?TCo`j(E+#?X?x4asT2!V66$y5_q$))1Bm$nXk7?X+bN|yAE*s3OK)N;BBdIBUt39Jj(km9#WRfZd z*NEu$8&Z;TO^8g@dbuVQo1h{Yk|39^ySa!)b-kE@F7!|mOBn{DyeE;Ok1aW{luJoG z&_pTx|1#3*RII8-5t4MT&Xo>I1>EwYXz$)Vj$>%esj>m4hti{P+spPAVcCiVsaDmn zMzTu5X%uNy)g)Co-KB_DgpX+cL*9fNu*9!J$WQ?lIaCLL;uB!yl=Madxe2?;@R zD(GegH4FK(H<5@c)sA`sgM>1No>iAtv{- zxR|COifqOt0iA>$K};e9T{5VfYuBm9#hs}r&TL$x$XMi3>`g>d*(mZskw0mVsE}sXms%G$EJt2ww(Nsd`qpFX%A}vwAf!V@b^K9OHq}Jm-zC-p)ck6bc=YCAX?2 zDmn-?91I64`FY(QSA(h;_)hp}Z#UvL=qJYhZW)cMW98O#YSgz!kMRd9dMvC?s__9O zu8NY9@R5Tt$#6yFlqSghF;z?IKB9CNw|c#|j@W;t7omt z*Q|-DZ04Hv`K&oNlwIq~uH7(Km(BEa;mRBo%%%1G4>P~_^*`XIx%ss@IAm*^>6_X0 z3)td*ZZK_4e9!LZm(J!FH#qC^Pld}XocYCoIlRmj<}Vdye&o#br1|XyTesxs&ZN08 zW?dXCe6_eGz71<(u42>4W{awMa{wB49Cl8K=3jU0Q+!*_$LVNo$$9&``|P)U;AKxx zt$f3zoIiKpFBx^V(j(^iDf{{#w=QNkr_JFdq-#!GM~@1Vv-rcJzjY~p`%8223@fCy zJY!7^GQHd5_D$BOQv8*MmFtjQ8$v0W$xJ4@Icg4%S>r2q1K<-@VJm-T6KdAjQlal^ u{?57F;!JL2AotysT>rYYnlaB$!v}Mo`{ufpUM)4#OplwH4SR@6ul@nBrxDoz literal 0 HcmV?d00001 diff --git a/reversion/locale/zh_CN/LC_MESSAGES/django.po b/reversion/locale/zh_CN/LC_MESSAGES/django.po new file mode 100644 index 0000000..75e7231 --- /dev/null +++ b/reversion/locale/zh_CN/LC_MESSAGES/django.po @@ -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 , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2014-06-12 14:21+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#: admin.py:160 +msgid "Initial version." +msgstr "初始版本" + +#: admin.py:194 templates/reversion/change_list.html:7 +#: templates/reversion/recover_form.html:11 +#: templates/reversion/recover_list.html:11 +#, python-format +msgid "Recover deleted %(name)s" +msgstr "恢复已删除的 %(name)s" + +#: admin.py:311 +#, python-format +msgid "Reverted to previous version, saved on %(datetime)s" +msgstr "恢复到 %(datetime)s 的版本" + +#: admin.py:313 +#, python-format +msgid "" +"The %(model)s \"%(name)s\" was reverted successfully. You may edit it again " +"below." +msgstr "%(model)s \"%(name)s\" 已成功恢复,你可以在下面在此编辑它。" + +#: admin.py:398 +#, python-format +msgid "Recover %(name)s" +msgstr "恢复 %(name)s" + +#: admin.py:412 +#, python-format +msgid "Revert %(name)s" +msgstr "恢复 %(name)s" + +#: models.py:55 +msgid "date created" +msgstr "创建日期" + +#: models.py:62 +msgid "user" +msgstr "用户" + +#: models.py:66 +msgid "comment" +msgstr "评论" + +#: templates/reversion/object_history.html:8 +msgid "" +"Choose a date from the list below to revert to a previous version of this " +"object." +msgstr "单击下方的日期以恢复当前对象到之前的版本。" + +#: templates/reversion/object_history.html:15 +#: templates/reversion/recover_list.html:24 +msgid "Date/time" +msgstr "时间" + +#: templates/reversion/object_history.html:16 +msgid "User" +msgstr "用户" + +#: templates/reversion/object_history.html:17 +msgid "Comment" +msgstr "评论" + +#: templates/reversion/object_history.html:38 +msgid "" +"This object doesn't have a change history. It probably wasn't added via this " +"admin site." +msgstr "此对象不存在任何变更历史,它可能不是通过管理站点添加的。" + +#: templates/reversion/recover_form.html:8 +#: templates/reversion/recover_list.html:8 +#: templates/reversion/revision_form.html:8 +msgid "Home" +msgstr "首页" + +#: templates/reversion/recover_form.html:18 +msgid "Press the save button below to recover this version of the object." +msgstr "单击保存按钮以恢复为此版本。" + +#: templates/reversion/recover_list.html:18 +msgid "" +"Choose a date from the list below to recover a deleted version of an object." +msgstr "单击下方的日期以恢复一个已删除的对象。" + +#: templates/reversion/recover_list.html:38 +msgid "There are no deleted objects to recover." +msgstr "没有可供恢复的已删除对象。" + +#: templates/reversion/revision_form.html:12 +msgid "History" +msgstr "历史" + +#: templates/reversion/revision_form.html:13 +#, python-format +msgid "Revert %(verbose_name)s" +msgstr "恢复 %(verbose_name)s" + +#: templates/reversion/revision_form.html:26 +msgid "Press the save button below to revert to this version of the object." +msgstr "单击保存按钮将此对象恢复到此版本。" diff --git a/reversion/locale/zh_Hans/LC_MESSAGES/django.mo b/reversion/locale/zh_Hans/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..6b0fa29e1c9f710f0b308a53f41ba40fcd7b06ee GIT binary patch literal 2183 zcma)*+fy4=9LHB%Z}C>^bw(Y}bcTUiH%+jkHI&*E(voQc5kg0N>1?uxu+CtLW;e-`I{pKGPj-_a?O4y`x1XG|zw^8A zZ@YJna#$}xUW5Dsc^&fELwI2Q35LMGzz@K~4|CjO;0X8xIOgnU!JXLO0C#~);8E}{ z*a+^}VV^gFuVCK+J_~B#%V0nFHh2|`fWLw@;2SXdA=m^m`!jej|MTEe;G%P$0Y$`k z7km%rRgZGq``{@MKW@t5yu$^DD>a`_E>?TCo`j(E+#?X?x4asT2!V66$y5_q$))1Bm$nXk7?X+bN|yAE*s3OK)N;BBdIBUt39Jj(km9#WRfZd z*NEu$8&Z;TO^8g@dbuVQo1h{Yk|39^ySa!)b-kE@F7!|mOBn{DyeE;Ok1aW{luJoG z&_pTx|1#3*RII8-5t4MT&Xo>I1>EwYXz$)Vj$>%esj>m4hti{P+spPAVcCiVsaDmn zMzTu5X%uNy)g)Co-KB_DgpX+cL*9fNu*9!J$WQ?lIaCLL;uB!yl=Madxe2?;@R zD(GegH4FK(H<5@c)sA`sgM>1No>iAtv{- zxR|COifqOt0iA>$K};e9T{5VfYuBm9#hs}r&TL$x$XMi3>`g>d*(mZskw0mVsE}sXms%G$EJt2ww(Nsd`qpFX%A}vwAf!V@b^K9OHq}Jm-zC-p)ck6bc=YCAX?2 zDmn-?91I64`FY(QSA(h;_)hp}Z#UvL=qJYhZW)cMW98O#YSgz!kMRd9dMvC?s__9O zu8NY9@R5Tt$#6yFlqSghF;z?IKB9CNw|c#|j@W;t7omt z*Q|-DZ04Hv`K&oNlwIq~uH7(Km(BEa;mRBo%%%1G4>P~_^*`XIx%ss@IAm*^>6_X0 z3)td*ZZK_4e9!LZm(J!FH#qC^Pld}XocYCoIlRmj<}Vdye&o#br1|XyTesxs&ZN08 zW?dXCe6_eGz71<(u42>4W{awMa{wB49Cl8K=3jU0Q+!*_$LVNo$$9&``|P)U;AKxx zt$f3zoIiKpFBx^V(j(^iDf{{#w=QNkr_JFdq-#!GM~@1Vv-rcJzjY~p`%8223@fCy zJY!7^GQHd5_D$BOQv8*MmFtjQ8$v0W$xJ4@Icg4%S>r2q1K<-@VJm-T6KdAjQlal^ u{?57F;!JL2AotysT>rYYnlaB$!v}Mo`{ufpUM)4#OplwH4SR@6ul@nBrxDoz literal 0 HcmV?d00001 diff --git a/reversion/locale/zh_Hans/LC_MESSAGES/django.po b/reversion/locale/zh_Hans/LC_MESSAGES/django.po new file mode 100644 index 0000000..75e7231 --- /dev/null +++ b/reversion/locale/zh_Hans/LC_MESSAGES/django.po @@ -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 , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2014-06-12 14:21+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#: admin.py:160 +msgid "Initial version." +msgstr "初始版本" + +#: admin.py:194 templates/reversion/change_list.html:7 +#: templates/reversion/recover_form.html:11 +#: templates/reversion/recover_list.html:11 +#, python-format +msgid "Recover deleted %(name)s" +msgstr "恢复已删除的 %(name)s" + +#: admin.py:311 +#, python-format +msgid "Reverted to previous version, saved on %(datetime)s" +msgstr "恢复到 %(datetime)s 的版本" + +#: admin.py:313 +#, python-format +msgid "" +"The %(model)s \"%(name)s\" was reverted successfully. You may edit it again " +"below." +msgstr "%(model)s \"%(name)s\" 已成功恢复,你可以在下面在此编辑它。" + +#: admin.py:398 +#, python-format +msgid "Recover %(name)s" +msgstr "恢复 %(name)s" + +#: admin.py:412 +#, python-format +msgid "Revert %(name)s" +msgstr "恢复 %(name)s" + +#: models.py:55 +msgid "date created" +msgstr "创建日期" + +#: models.py:62 +msgid "user" +msgstr "用户" + +#: models.py:66 +msgid "comment" +msgstr "评论" + +#: templates/reversion/object_history.html:8 +msgid "" +"Choose a date from the list below to revert to a previous version of this " +"object." +msgstr "单击下方的日期以恢复当前对象到之前的版本。" + +#: templates/reversion/object_history.html:15 +#: templates/reversion/recover_list.html:24 +msgid "Date/time" +msgstr "时间" + +#: templates/reversion/object_history.html:16 +msgid "User" +msgstr "用户" + +#: templates/reversion/object_history.html:17 +msgid "Comment" +msgstr "评论" + +#: templates/reversion/object_history.html:38 +msgid "" +"This object doesn't have a change history. It probably wasn't added via this " +"admin site." +msgstr "此对象不存在任何变更历史,它可能不是通过管理站点添加的。" + +#: templates/reversion/recover_form.html:8 +#: templates/reversion/recover_list.html:8 +#: templates/reversion/revision_form.html:8 +msgid "Home" +msgstr "首页" + +#: templates/reversion/recover_form.html:18 +msgid "Press the save button below to recover this version of the object." +msgstr "单击保存按钮以恢复为此版本。" + +#: templates/reversion/recover_list.html:18 +msgid "" +"Choose a date from the list below to recover a deleted version of an object." +msgstr "单击下方的日期以恢复一个已删除的对象。" + +#: templates/reversion/recover_list.html:38 +msgid "There are no deleted objects to recover." +msgstr "没有可供恢复的已删除对象。" + +#: templates/reversion/revision_form.html:12 +msgid "History" +msgstr "历史" + +#: templates/reversion/revision_form.html:13 +#, python-format +msgid "Revert %(verbose_name)s" +msgstr "恢复 %(verbose_name)s" + +#: templates/reversion/revision_form.html:26 +msgid "Press the save button below to revert to this version of the object." +msgstr "单击保存按钮将此对象恢复到此版本。" diff --git a/reversion/management/__init__.py b/reversion/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/reversion/management/commands/__init__.py b/reversion/management/commands/__init__.py new file mode 100644 index 0000000..1abae72 --- /dev/null +++ b/reversion/management/commands/__init__.py @@ -0,0 +1,57 @@ +from __future__ import unicode_literals +from django.apps import apps +from django.contrib import admin +from django.core.management.base import BaseCommand, CommandError +from reversion.revisions import is_registered + + +class BaseRevisionCommand(BaseCommand): + + def add_arguments(self, parser): + super(BaseRevisionCommand, self).add_arguments(parser) + parser.add_argument( + "app_label", + metavar="app_label", + nargs="*", + help="Optional app_label or app_label.model_name list.", + ) + parser.add_argument( + "--using", + default=None, + help="The database to query for revision data.", + ) + parser.add_argument( + "--model-db", + default=None, + help="The database to query for model data.", + ) + + def get_models(self, options): + # Load admin classes. + admin.autodiscover() + # Get options. + app_labels = options["app_label"] + # Parse model classes. + if len(app_labels) == 0: + selected_models = apps.get_models() + else: + selected_models = set() + for label in app_labels: + if "." in label: + # This is an app.Model specifier. + try: + model = apps.get_model(label) + except LookupError: + raise CommandError("Unknown model: {}".format(label)) + selected_models.add(model) + else: + # This is just an app - no model qualifier. + app_label = label + try: + app = apps.get_app_config(app_label) + except LookupError: + raise CommandError("Unknown app: {}".format(app_label)) + selected_models.update(app.get_models()) + for model in selected_models: + if is_registered(model): + yield model diff --git a/reversion/management/commands/createinitialrevisions.py b/reversion/management/commands/createinitialrevisions.py new file mode 100644 index 0000000..d528f80 --- /dev/null +++ b/reversion/management/commands/createinitialrevisions.py @@ -0,0 +1,74 @@ +from __future__ import unicode_literals +from django.db import reset_queries, transaction, router +from reversion.models import Revision, Version, _safe_subquery +from reversion.management.commands import BaseRevisionCommand +from reversion.revisions import create_revision, set_comment, add_to_revision + + +class Command(BaseRevisionCommand): + + help = "Creates initial revisions for a given app [and model]." + + def add_arguments(self, parser): + super(Command, self).add_arguments(parser) + parser.add_argument( + "--comment", + action="store", + default="Initial version.", + help="Specify the comment to add to the revisions. Defaults to 'Initial version'.") + parser.add_argument( + "--batch-size", + action="store", + type=int, + default=500, + help="For large sets of data, revisions will be populated in batches. Defaults to 500.", + ) + + def handle(self, *app_labels, **options): + verbosity = options["verbosity"] + using = options["using"] + model_db = options["model_db"] + comment = options["comment"] + batch_size = options["batch_size"] + # Create revisions. + using = using or router.db_for_write(Revision) + with transaction.atomic(using=using): + for model in self.get_models(options): + # Check all models for empty revisions. + if verbosity >= 1: + self.stdout.write("Creating revisions for {name}".format( + name=model._meta.verbose_name, + )) + created_count = 0 + live_objs = _safe_subquery( + "exclude", + model._default_manager.using(model_db), + model._meta.pk.name, + Version.objects.using(using).get_for_model( + model, + model_db=model_db, + ), + "object_id", + ) + # Save all the versions. + ids = list(live_objs.values_list("pk", flat=True).order_by()) + total = len(ids) + for i in range(0, total, batch_size): + chunked_ids = ids[i:i+batch_size] + objects = live_objs.in_bulk(chunked_ids) + for obj in objects.values(): + with create_revision(using=using): + set_comment(comment) + add_to_revision(obj, model_db=model_db) + created_count += 1 + reset_queries() + if verbosity >= 2: + self.stdout.write("- Created {created_count} / {total}".format( + created_count=created_count, + total=total, + )) + # Print out a message, if feeling verbose. + if verbosity >= 1: + self.stdout.write("- Created {total} / {total}".format( + total=total, + )) diff --git a/reversion/management/commands/deleterevisions.py b/reversion/management/commands/deleterevisions.py new file mode 100644 index 0000000..bfe25a1 --- /dev/null +++ b/reversion/management/commands/deleterevisions.py @@ -0,0 +1,95 @@ +from __future__ import unicode_literals +from datetime import timedelta +from django.db import transaction, models, router +from django.utils import timezone +from reversion.models import Revision, Version +from reversion.management.commands import BaseRevisionCommand + + +class Command(BaseRevisionCommand): + + help = "Deletes revisions for a given app [and model]." + + def add_arguments(self, parser): + super(Command, self).add_arguments(parser) + parser.add_argument( + "--days", + default=0, + type=int, + help="Delete only revisions older than the specified number of days.", + ) + parser.add_argument( + "--keep", + default=0, + type=int, + help="Keep the specified number of revisions (most recent) for each object.", + ) + + def handle(self, *app_labels, **options): + verbosity = options["verbosity"] + using = options["using"] + model_db = options["model_db"] + days = options["days"] + keep = options["keep"] + # Delete revisions. + using = using or router.db_for_write(Revision) + with transaction.atomic(using=using): + revision_query = models.Q() + keep_revision_ids = set() + # By default, delete nothing. + can_delete = False + # Get all revisions for the given revision manager and model. + for model in self.get_models(options): + if verbosity >= 1: + self.stdout.write("Finding stale revisions for {name}".format( + name=model._meta.verbose_name, + )) + # Find all matching revision IDs. + model_query = Version.objects.using(using).get_for_model( + model, + model_db=model_db, + ) + if keep: + overflow_object_ids = list(Version.objects.using(using).get_for_model( + model, + model_db=model_db, + ).order_by().values_list("object_id").annotate( + count=models.Count("object_id"), + ).filter( + count__gt=keep, + ).values_list("object_id", flat=True).iterator()) + # Only delete overflow revisions. + model_query = model_query.filter(object_id__in=overflow_object_ids) + for object_id in overflow_object_ids: + if verbosity >= 2: + self.stdout.write("- Finding stale revisions for {name} #{object_id}".format( + name=model._meta.verbose_name, + object_id=object_id, + )) + # But keep the underflow revisions. + keep_revision_ids.update(Version.objects.using(using).get_for_object_reference( + model, + object_id, + model_db=model_db, + ).values_list("revision_id", flat=True)[:keep].iterator()) + # Add to revision query. + revision_query |= models.Q( + pk__in=model_query.order_by().values_list("revision_id", flat=True) + ) + # If we have at least one model, then we can delete. + can_delete = True + if can_delete: + revisions_to_delete = Revision.objects.using(using).filter( + revision_query, + date_created__lt=timezone.now() - timedelta(days=days), + ).exclude( + pk__in=keep_revision_ids + ).order_by() + else: + revisions_to_delete = Revision.objects.using(using).none() + # Print out a message, if feeling verbose. + if verbosity >= 1: + self.stdout.write("Deleting {total} revisions...".format( + total=revisions_to_delete.count(), + )) + revisions_to_delete.delete() diff --git a/reversion/middleware.py b/reversion/middleware.py new file mode 100644 index 0000000..7f0e89c --- /dev/null +++ b/reversion/middleware.py @@ -0,0 +1,51 @@ +import sys +from reversion.revisions import create_revision as create_revision_base +from reversion.views import _request_creates_revision, _set_user_from_request, create_revision + + +class RevisionMiddleware(object): + + """Wraps the entire request in a revision.""" + + manage_manually = False + + using = None + + atomic = True + + def __init__(self, get_response=None): + super(RevisionMiddleware, self).__init__() + # Support Django 1.10 middleware. + if get_response is not None: + self.get_response = create_revision( + manage_manually=self.manage_manually, + using=self.using, + atomic=self.atomic + )(get_response) + + def process_request(self, request): + if _request_creates_revision(request): + context = create_revision_base( + manage_manually=self.manage_manually, + using=self.using, + atomic=self.atomic + ) + context.__enter__() + if not hasattr(request, "_revision_middleware"): + setattr(request, "_revision_middleware", {}) + request._revision_middleware[self] = context + + def _close_revision(self, request, is_exception): + if self in getattr(request, "_revision_middleware", {}): + _set_user_from_request(request) + request._revision_middleware.pop(self).__exit__(*sys.exc_info() if is_exception else (None, None, None)) + + def process_response(self, request, response): + self._close_revision(request, False) + return response + + def process_exception(self, request, exception): + self._close_revision(request, True) + + def __call__(self, request): + return self.get_response(request) diff --git a/reversion/migrations/0001_initial.py b/reversion/migrations/0001_initial.py new file mode 100644 index 0000000..3301759 --- /dev/null +++ b/reversion/migrations/0001_initial.py @@ -0,0 +1,48 @@ +# -*- 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={ + "ordering": ("-pk",) + }, + 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.', on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')), + ('revision', models.ForeignKey(help_text='The revision that contains this version.', on_delete=django.db.models.deletion.CASCADE, to='reversion.Revision')), + ], + options={ + "ordering": ("-pk",) + }, + bases=(models.Model,), + ), + ] diff --git a/reversion/migrations/0001_squashed_0004_auto_20160611_1202.py b/reversion/migrations/0001_squashed_0004_auto_20160611_1202.py new file mode 100644 index 0000000..fa7c936 --- /dev/null +++ b/reversion/migrations/0001_squashed_0004_auto_20160611_1202.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.7 on 2016-06-06 13:22 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + replaces = [('reversion', '0001_initial'), ('reversion', '0002_auto_20141216_1509'), ('reversion', '0003_auto_20160601_1600'), ('reversion', '0004_auto_20160611_1202')] + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('contenttypes', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Revision', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('date_created', models.DateTimeField(db_index=True, help_text='The date and time this revision was created.', verbose_name='date created')), + ('comment', models.TextField(blank=True, help_text='A text comment on this revision.', verbose_name='comment')), + ('user', models.ForeignKey(blank=True, help_text='The user who created this revision.', null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='user')), + ], + options={ + "ordering": ("-pk",) + }, + ), + migrations.CreateModel( + name='Version', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('object_id', models.CharField(help_text='Primary key of the model under version control.', max_length=191)), + ('format', models.CharField(help_text='The serialization format used by this model.', max_length=255)), + ('serialized_data', models.TextField(help_text='The serialized form of this version of the model.')), + ('object_repr', models.TextField(help_text='A string representation of the object.')), + ('content_type', models.ForeignKey(help_text='Content type of the model under version control.', on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')), + ('revision', models.ForeignKey(help_text='The revision that contains this version.', on_delete=django.db.models.deletion.CASCADE, to='reversion.Revision')), + ('db', models.CharField(help_text='The database the model under version control is stored in.', max_length=191)), + ], + options={ + "ordering": ("-pk",) + }, + ), + migrations.AlterUniqueTogether( + name='version', + unique_together=set([('db', 'content_type', 'object_id', 'revision')]), + ), + ] diff --git a/reversion/migrations/0002_auto_20141216_1509.py b/reversion/migrations/0002_auto_20141216_1509.py new file mode 100644 index 0000000..9c50385 --- /dev/null +++ b/reversion/migrations/0002_auto_20141216_1509.py @@ -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), + ), + ] diff --git a/reversion/migrations/0003_auto_20160601_1600.py b/reversion/migrations/0003_auto_20160601_1600.py new file mode 100644 index 0000000..7aa2cdb --- /dev/null +++ b/reversion/migrations/0003_auto_20160601_1600.py @@ -0,0 +1,108 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.6 on 2016-06-01 16:00 +from __future__ import unicode_literals + +from collections import defaultdict +from django.db import DEFAULT_DB_ALIAS, migrations, models, router +from django.apps import apps as live_apps + + +def de_dupe_version_table(apps, schema_editor): + """ + Removes some duplicate Version models that may have crept into the database and will prevent the + unique index being added by migration 0004. + """ + db_alias = schema_editor.connection.alias + Version = apps.get_model("reversion", "Version") + keep_version_ids = Version.objects.using(db_alias).order_by().values_list( + # Group by the unique constraint we intend to enforce. + "revision_id", + "content_type_id", + "object_id", + ).annotate( + # Add in the most recent id for each duplicate row. + max_pk=models.Max("pk"), + ).values_list("max_pk", flat=True) + # Do not do anything if we're keeping all ids anyway. + if keep_version_ids.count() == Version.objects.using(db_alias).all().count(): + return + # Delete all duplicate versions. Can't do this as a delete with subquery because MySQL doesn't like running a + # subquery on the table being updated/deleted. + delete_version_ids = list(Version.objects.using(db_alias).exclude( + pk__in=keep_version_ids, + ).values_list("pk", flat=True)) + Version.objects.using(db_alias).filter( + pk__in=delete_version_ids, + ).delete() + + +def set_version_db(apps, schema_editor): + """ + Updates the db field in all Version models to point to the correct write + db for the model. + """ + db_alias = schema_editor.connection.alias + Version = apps.get_model("reversion", "Version") + content_types = Version.objects.using(db_alias).order_by().values_list( + "content_type_id", + "content_type__app_label", + "content_type__model" + ).distinct() + model_dbs = defaultdict(list) + for content_type_id, app_label, model_name in content_types: + # We need to be able to access all models in the project, and we can't + # specify them up-front in the migration dependencies. So we have to + # just get the live model. This should be fine, since we don't actually + # manipulate the live model in any way. + try: + model = live_apps.get_model(app_label, model_name) + except LookupError: + # If the model appears not to exist, play it safe and use the default db. + db = "default" + else: + db = router.db_for_write(model) + model_dbs[db].append(content_type_id) + # Update db field. + # speedup for case when there is only default db + if DEFAULT_DB_ALIAS in model_dbs and len(model_dbs) == 1: + Version.objects.using(db_alias).update(db=DEFAULT_DB_ALIAS) + else: + for db, content_type_ids in model_dbs.items(): + Version.objects.using(db_alias).filter( + content_type__in=content_type_ids + ).update(db=db) + + +class Migration(migrations.Migration): + + dependencies = [ + ('reversion', '0002_auto_20141216_1509'), + ] + + operations = [ + migrations.RemoveField( + model_name='revision', + name='manager_slug', + ), + migrations.RemoveField( + model_name='version', + name='object_id_int', + ), + migrations.AlterField( + model_name='version', + name='object_id', + field=models.CharField(help_text='Primary key of the model under version control.', max_length=191), + ), + migrations.AlterField( + model_name='revision', + name='date_created', + field=models.DateTimeField(db_index=True, help_text='The date and time this revision was created.', verbose_name='date created'), + ), + migrations.AddField( + model_name='version', + name='db', + field=models.CharField(null=True, help_text='The database the model under version control is stored in.', max_length=191), + ), + migrations.RunPython(de_dupe_version_table), + migrations.RunPython(set_version_db), + ] diff --git a/reversion/migrations/0004_auto_20160611_1202.py b/reversion/migrations/0004_auto_20160611_1202.py new file mode 100644 index 0000000..67558bb --- /dev/null +++ b/reversion/migrations/0004_auto_20160611_1202.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.7 on 2016-06-11 12:02 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('reversion', '0003_auto_20160601_1600'), + ] + + operations = [ + migrations.AlterField( + model_name='version', + name='db', + field=models.CharField(help_text='The database the model under version control is stored in.', max_length=191), + ), + migrations.AlterUniqueTogether( + name='version', + unique_together=set([('db', 'content_type', 'object_id', 'revision')]), + ), + ] diff --git a/reversion/migrations/__init__.py b/reversion/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/reversion/models.py b/reversion/models.py new file mode 100644 index 0000000..f75266e --- /dev/null +++ b/reversion/models.py @@ -0,0 +1,356 @@ +from __future__ import unicode_literals +from collections import defaultdict +from itertools import chain +from django.contrib.contenttypes.models import ContentType +try: + from django.contrib.contenttypes.fields import GenericForeignKey +except ImportError: # Django < 1.9 pragma: no cover + from django.contrib.contenttypes.generic import GenericForeignKey +from django.conf import settings +from django.core import serializers +from django.core.serializers.base import DeserializationError +from django.core.exceptions import ObjectDoesNotExist +from django.db import models, IntegrityError, transaction, router, connections +from django.db.models.deletion import Collector +from django.db.models.expressions import RawSQL +from django.utils.functional import cached_property +from django.utils.translation import ugettext_lazy as _, ugettext +from django.utils.encoding import force_text, python_2_unicode_compatible +from reversion.errors import RevertError +from reversion.revisions import _get_options, _get_content_type, _follow_relations_recursive + + +def _safe_revert(versions): + unreverted_versions = [] + for version in versions: + try: + with transaction.atomic(using=version.db): + version.revert() + except (IntegrityError, ObjectDoesNotExist): + unreverted_versions.append(version) + if len(unreverted_versions) == len(versions): + raise RevertError(ugettext("Could not save %(object_repr)s version - missing dependency.") % { + "object_repr": unreverted_versions[0], + }) + if unreverted_versions: + _safe_revert(unreverted_versions) + + +@python_2_unicode_compatible +class Revision(models.Model): + + """A group of related serialized versions.""" + + date_created = models.DateTimeField( + db_index=True, + verbose_name=_("date created"), + help_text="The date and time this revision was created.", + ) + + user = models.ForeignKey( + settings.AUTH_USER_MODEL, + blank=True, + null=True, + on_delete=models.SET_NULL, + verbose_name=_("user"), + help_text="The user who created this revision.", + ) + + comment = models.TextField( + blank=True, + verbose_name=_("comment"), + help_text="A text comment on this revision.", + ) + + def revert(self, delete=False): + # Group the models by the database of the serialized model. + versions_by_db = defaultdict(list) + for version in self.version_set.iterator(): + versions_by_db[version.db].append(version) + # For each db, perform a separate atomic revert. + for version_db, versions in versions_by_db.items(): + with transaction.atomic(using=version_db): + # Optionally delete objects no longer in the current revision. + if delete: + # Get a set of all objects in this revision. + old_revision = set() + for version in versions: + model = version._model + try: + # Load the model instance from the same DB as it was saved under. + old_revision.add(model._default_manager.using(version.db).get(pk=version.object_id)) + except model.DoesNotExist: + pass + # Calculate the set of all objects that are in the revision now. + current_revision = chain.from_iterable( + _follow_relations_recursive(obj) + for obj in old_revision + ) + # Delete objects that are no longer in the current revision. + collector = Collector(using=version_db) + collector.collect([item for item in current_revision if item not in old_revision]) + collector.delete() + # Attempt to revert all revisions. + _safe_revert(versions) + + def __str__(self): + return ", ".join(force_text(version) for version in self.version_set.all()) + + class Meta: + app_label = "reversion" + ordering = ("-pk",) + + +class SubquerySQL(RawSQL): + + def as_sql(self, compiler, connection): + return self.sql, self.params + + +class VersionQuerySet(models.QuerySet): + + def get_for_model(self, model, model_db=None): + model_db = model_db or router.db_for_write(model) + content_type = _get_content_type(model, self.db) + return self.filter( + content_type=content_type, + db=model_db, + ) + + def get_for_object_reference(self, model, object_id, model_db=None): + return self.get_for_model(model, model_db=model_db).filter( + object_id=object_id, + ) + + def get_for_object(self, obj, model_db=None): + return self.get_for_object_reference(obj.__class__, obj.pk, model_db=model_db) + + def get_deleted(self, model, model_db=None): + # Try to do a faster JOIN. + model_db = model_db or router.db_for_write(model) + connection = connections[self.db] + if self.db == model_db and connection.vendor in ("sqlite", "postgresql"): + content_type = _get_content_type(model, self.db) + subquery = SubquerySQL( + """ + SELECT MAX(V.{id}) + FROM {version} AS V + LEFT JOIN {model} ON V.{object_id} = CAST({model}.{model_id} as {str}) + WHERE + V.{db} = %s AND + V.{content_type_id} = %s AND + {model}.{model_id} IS NULL + GROUP BY V.{object_id} + """.format( + id=connection.ops.quote_name("id"), + version=connection.ops.quote_name(Version._meta.db_table), + model=connection.ops.quote_name(model._meta.db_table), + model_id=connection.ops.quote_name(model._meta.pk.db_column or model._meta.pk.attname), + object_id=connection.ops.quote_name("object_id"), + str=Version._meta.get_field("object_id").db_type(connection), + db=connection.ops.quote_name("db"), + content_type_id=connection.ops.quote_name("content_type_id"), + ), + (model_db, content_type.id), + output_field=Version._meta.pk, + ) + else: + # We have to use a slow subquery. + subquery = self.get_for_model(model, model_db=model_db).exclude( + object_id__in=list( + model._default_manager.using(model_db).values_list("pk", flat=True).order_by().iterator() + ), + ).values_list("object_id").annotate( + latest_pk=models.Max("pk") + ).order_by().values_list("latest_pk", flat=True) + # Perform the subquery. + return self.filter( + pk__in=subquery, + ) + + def get_unique(self): + last_key = None + for version in self.iterator(): + key = (version.object_id, version.content_type_id, version.db, version._local_field_dict) + if last_key != key: + yield version + last_key = key + + +@python_2_unicode_compatible +class Version(models.Model): + + """A saved version of a database model.""" + + objects = VersionQuerySet.as_manager() + + revision = models.ForeignKey( + Revision, + on_delete=models.CASCADE, + help_text="The revision that contains this version.", + ) + + object_id = models.CharField( + max_length=191, + help_text="Primary key of the model under version control.", + ) + + content_type = models.ForeignKey( + ContentType, + on_delete=models.CASCADE, + help_text="Content type of the model under version control.", + ) + + @property + def _content_type(self): + return ContentType.objects.db_manager(self._state.db).get_for_id(self.content_type_id) + + @property + def _model(self): + return self._content_type.model_class() + + # A link to the current instance, not the version stored in this Version! + object = GenericForeignKey( + ct_field="content_type", + fk_field="object_id", + ) + + db = models.CharField( + max_length=191, + help_text="The database the model under version control is stored in.", + ) + + format = models.CharField( + max_length=255, + help_text="The serialization format used by this model.", + ) + + serialized_data = models.TextField( + help_text="The serialized form of this version of the model.", + ) + + object_repr = models.TextField( + help_text="A string representation of the object.", + ) + + @cached_property + def _object_version(self): + data = self.serialized_data + data = force_text(data.encode("utf8")) + try: + return list(serializers.deserialize(self.format, data, ignorenonexistent=True))[0] + except DeserializationError: + raise RevertError(ugettext("Could not load %(object_repr)s version - incompatible version data.") % { + "object_repr": self.object_repr, + }) + except serializers.SerializerDoesNotExist: + raise RevertError(ugettext("Could not load %(object_repr)s version - unknown serializer %(format)s.") % { + "object_repr": self.object_repr, + "format": self.format, + }) + + @cached_property + def _local_field_dict(self): + """ + A dictionary mapping field names to field values in this version + of the model. + + Parent links of inherited multi-table models will not be followed. + """ + version_options = _get_options(self._model) + object_version = self._object_version + obj = object_version.object + model = self._model + field_dict = {} + for field_name in version_options.fields: + field = model._meta.get_field(field_name) + if isinstance(field, models.ManyToManyField): + # M2M fields with a custom through are not stored in m2m_data, but as a separate model. + if field.attname in object_version.m2m_data: + field_dict[field.attname] = object_version.m2m_data[field.attname] + else: + field_dict[field.attname] = getattr(obj, field.attname) + return field_dict + + @cached_property + def field_dict(self): + """ + A dictionary mapping field names to field values in this version + of the model. + + This method will follow parent links, if present. + """ + field_dict = self._local_field_dict + # Add parent data. + for parent_model, field in self._model._meta.concrete_model._meta.parents.items(): + content_type = _get_content_type(parent_model, self._state.db) + parent_id = field_dict[field.attname] + parent_version = self.revision.version_set.get( + content_type=content_type, + object_id=parent_id, + db=self.db, + ) + field_dict.update(parent_version.field_dict) + return field_dict + + def revert(self): + self._object_version.save(using=self.db) + + def __str__(self): + return self.object_repr + + class Meta: + app_label = 'reversion' + unique_together = ( + ("db", "content_type", "object_id", "revision"), + ) + ordering = ("-pk",) + + +class _Str(models.Func): + + """Casts a value to the database's text type.""" + + function = "CAST" + template = "%(function)s(%(expressions)s as %(db_type)s)" + + def __init__(self, expression): + super(_Str, self).__init__(expression, output_field=models.TextField()) + + def as_sql(self, compiler, connection): + self.extra["db_type"] = self.output_field.db_type(connection) + return super(_Str, self).as_sql(compiler, connection) + + +def _safe_subquery(method, left_query, left_field_name, right_subquery, right_field_name): + right_subquery = right_subquery.order_by().values_list(right_field_name, flat=True) + left_field = left_query.model._meta.get_field(left_field_name) + right_field = right_subquery.model._meta.get_field(right_field_name) + # If the databases don't match, we have to do it in-memory. + # If it's not a supported database, we also have to do it in-memory. + if ( + left_query.db != right_subquery.db or not + ( + left_field.get_internal_type() != right_field.get_internal_type() and + connections[left_query.db].vendor in ("sqlite", "postgresql") + ) + ): + right_subquery = list(right_subquery.iterator()) + else: + # If the left hand side is not a text field, we need to cast it. + if not isinstance(left_field, (models.CharField, models.TextField)): + left_field_name_str = "{}_str".format(left_field_name) + left_query = left_query.annotate(**{ + left_field_name_str: _Str(left_field_name), + }) + left_field_name = left_field_name_str + # If the right hand side is not a text field, we need to cast it. + if not isinstance(right_field, (models.CharField, models.TextField)): + right_field_name_str = "{}_str".format(right_field_name) + right_subquery = right_subquery.annotate(**{ + right_field_name_str: _Str(right_field_name), + }).values_list(right_field_name_str, flat=True) + # All done! + return getattr(left_query, method)(**{ + "{}__in".format(left_field_name): right_subquery, + }) diff --git a/reversion/revisions.py b/reversion/revisions.py new file mode 100644 index 0000000..fca53d0 --- /dev/null +++ b/reversion/revisions.py @@ -0,0 +1,433 @@ +from __future__ import unicode_literals +from collections import namedtuple, defaultdict +from contextlib import contextmanager +from functools import wraps +from threading import local +from django.apps import apps +from django.core import serializers +from django.core.exceptions import ObjectDoesNotExist +from django.db import models, transaction, router +from django.db.models.query import QuerySet +from django.db.models.signals import post_save, m2m_changed +from django.utils.encoding import force_text +from django.utils import timezone, six +from reversion.compat import remote_field +from reversion.errors import RevisionManagementError, RegistrationError +from reversion.signals import pre_revision_commit, post_revision_commit + + +_VersionOptions = namedtuple("VersionOptions", ( + "fields", + "follow", + "format", + "for_concrete_model", + "ignore_duplicates", +)) + + +_StackFrame = namedtuple("StackFrame", ( + "manage_manually", + "user", + "comment", + "date_created", + "db_versions", + "meta", +)) + + +class _Local(local): + + def __init__(self): + self.stack = () + + +_local = _Local() + + +def is_active(): + return bool(_local.stack) + + +def _current_frame(): + if not is_active(): + raise RevisionManagementError("There is no active revision for this thread") + return _local.stack[-1] + + +def _copy_db_versions(db_versions): + return { + db: versions.copy() + for db, versions + in db_versions.items() + } + + +def _push_frame(manage_manually, using): + if is_active(): + current_frame = _current_frame() + db_versions = _copy_db_versions(current_frame.db_versions) + db_versions.setdefault(using, {}) + stack_frame = current_frame._replace( + manage_manually=manage_manually, + db_versions=db_versions, + ) + else: + stack_frame = _StackFrame( + manage_manually=manage_manually, + user=None, + comment="", + date_created=timezone.now(), + db_versions={using: {}}, + meta=(), + ) + _local.stack += (stack_frame,) + + +def _update_frame(**kwargs): + _local.stack = _local.stack[:-1] + (_current_frame()._replace(**kwargs),) + + +def _pop_frame(): + prev_frame = _current_frame() + _local.stack = _local.stack[:-1] + if is_active(): + current_frame = _current_frame() + db_versions = { + db: prev_frame.db_versions[db] + for db + in current_frame.db_versions.keys() + } + _update_frame( + user=prev_frame.user, + comment=prev_frame.comment, + date_created=prev_frame.date_created, + db_versions=db_versions, + meta=prev_frame.meta, + ) + + +def is_manage_manually(): + return _current_frame().manage_manually + + +def set_user(user): + _update_frame(user=user) + + +def get_user(): + return _current_frame().user + + +def set_comment(comment): + _update_frame(comment=comment) + + +def get_comment(): + return _current_frame().comment + + +def set_date_created(date_created): + _update_frame(date_created=date_created) + + +def get_date_created(): + return _current_frame().date_created + + +def add_meta(model, **values): + _update_frame(meta=_current_frame().meta + ((model, values),)) + + +def _follow_relations(obj): + version_options = _get_options(obj.__class__) + for follow_name in version_options.follow: + try: + follow_obj = getattr(obj, follow_name) + except ObjectDoesNotExist: + continue + if isinstance(follow_obj, models.Model): + yield follow_obj + elif isinstance(follow_obj, (models.Manager, QuerySet)): + for follow_obj_instance in follow_obj.all(): + yield follow_obj_instance + elif follow_obj is not None: + raise RegistrationError("{name}.{follow_name} should be a Model or QuerySet".format( + name=obj.__class__.__name__, + follow_name=follow_name, + )) + + +def _follow_relations_recursive(obj): + def do_follow(obj): + if obj not in relations: + relations.add(obj) + for related in _follow_relations(obj): + do_follow(related) + relations = set() + do_follow(obj) + return relations + + +def _add_to_revision(obj, using, model_db, explicit): + from reversion.models import Version + # Exit early if the object is not fully-formed. + if obj.pk is None: + return + version_options = _get_options(obj.__class__) + content_type = _get_content_type(obj.__class__, using) + object_id = force_text(obj.pk) + version_key = (content_type, object_id) + # If the obj is already in the revision, stop now. + db_versions = _current_frame().db_versions + versions = db_versions[using] + if version_key in versions and not explicit: + return + # Get the version data. + version = Version( + content_type=content_type, + object_id=object_id, + db=model_db, + format=version_options.format, + serialized_data=serializers.serialize( + version_options.format, + (obj,), + fields=version_options.fields, + ), + object_repr=force_text(obj), + ) + # If the version is a duplicate, stop now. + if version_options.ignore_duplicates and explicit: + previous_version = Version.objects.using(using).get_for_object(obj, model_db=model_db).first() + if previous_version and previous_version._local_field_dict == version._local_field_dict: + return + # Store the version. + db_versions = _copy_db_versions(db_versions) + db_versions[using][version_key] = version + _update_frame(db_versions=db_versions) + # Follow relations. + for follow_obj in _follow_relations(obj): + _add_to_revision(follow_obj, using, model_db, False) + + +def add_to_revision(obj, model_db=None): + model_db = model_db or router.db_for_write(obj.__class__, instance=obj) + for db in _current_frame().db_versions.keys(): + _add_to_revision(obj, db, model_db, True) + + +def _save_revision(versions, user=None, comment="", meta=(), date_created=None, using=None): + from reversion.models import Revision + # Only save versions that exist in the database. + model_db_pks = defaultdict(lambda: defaultdict(set)) + for version in versions: + model_db_pks[version._model][version.db].add(version.object_id) + model_db_existing_pks = { + model: { + db: frozenset(map( + force_text, + model._default_manager.using(db).filter(pk__in=pks).values_list("pk", flat=True), + )) + for db, pks in db_pks.items() + } + for model, db_pks in model_db_pks.items() + } + versions = [ + version for version in versions + if version.object_id in model_db_existing_pks[version._model][version.db] + ] + # Bail early if there are no objects to save. + if not versions: + return + # Save a new revision. + revision = Revision( + date_created=date_created, + user=user, + comment=comment, + ) + # Send the pre_revision_commit signal. + pre_revision_commit.send( + sender=create_revision, + revision=revision, + versions=versions, + ) + # Save the revision. + revision.save(using=using) + # Save version models. + for version in versions: + version.revision = revision + version.save(using=using) + # Save the meta information. + for meta_model, meta_fields in meta: + meta_model._default_manager.db_manager(using=using).create( + revision=revision, + **meta_fields + ) + # Send the post_revision_commit signal. + post_revision_commit.send( + sender=create_revision, + revision=revision, + versions=versions, + ) + + +@contextmanager +def _dummy_context(): + yield + + +@contextmanager +def _create_revision_context(manage_manually, using, atomic): + _push_frame(manage_manually, using) + try: + context = transaction.atomic(using=using) if atomic else _dummy_context() + with context: + yield + # Only save for a db if that's the last stack frame for that db. + if not any(using in frame.db_versions for frame in _local.stack[:-1]): + current_frame = _current_frame() + _save_revision( + versions=current_frame.db_versions[using].values(), + user=current_frame.user, + comment=current_frame.comment, + meta=current_frame.meta, + date_created=current_frame.date_created, + using=using, + ) + finally: + _pop_frame() + + +def create_revision(manage_manually=False, using=None, atomic=True): + from reversion.models import Revision + using = using or router.db_for_write(Revision) + return _ContextWrapper(_create_revision_context, (manage_manually, using, atomic)) + + +class _ContextWrapper(object): + + def __init__(self, func, args): + self._func = func + self._args = args + self._context = func(*args) + + def __enter__(self): + return self._context.__enter__() + + def __exit__(self, exc_type, exc_value, traceback): + return self._context.__exit__(exc_type, exc_value, traceback) + + def __call__(self, func): + @wraps(func) + def do_revision_context(*args, **kwargs): + with self._func(*self._args): + return func(*args, **kwargs) + return do_revision_context + + +def _post_save_receiver(sender, instance, using, **kwargs): + if is_registered(sender) and is_active() and not is_manage_manually(): + add_to_revision(instance, model_db=using) + + +def _m2m_changed_receiver(instance, using, action, model, reverse, **kwargs): + if action.startswith("post_") and not reverse: + if is_registered(instance) and is_active() and not is_manage_manually(): + add_to_revision(instance, model_db=using) + + +def _get_registration_key(model): + return (model._meta.app_label, model._meta.model_name) + + +_registered_models = {} + + +def is_registered(model): + return _get_registration_key(model) in _registered_models + + +def get_registered_models(): + return (apps.get_model(*key) for key in _registered_models.keys()) + + +def _get_senders_and_signals(model): + yield model, post_save, _post_save_receiver + opts = model._meta.concrete_model._meta + for field in opts.local_many_to_many: + m2m_model = remote_field(field).through + if isinstance(m2m_model, six.string_types): + if "." not in m2m_model: + m2m_model = "{app_label}.{m2m_model}".format( + app_label=opts.app_label, + m2m_model=m2m_model + ) + yield m2m_model, m2m_changed, _m2m_changed_receiver + + +def register(model=None, fields=None, exclude=(), follow=(), format="json", + for_concrete_model=True, ignore_duplicates=False): + def register(model): + # Prevent multiple registration. + if is_registered(model): + raise RegistrationError("{model} has already been registered with django-reversion".format( + model=model, + )) + # Parse fields. + opts = model._meta.concrete_model._meta + version_options = _VersionOptions( + fields=tuple( + field_name + for field_name + in ([ + field.name + for field + in opts.local_fields + opts.local_many_to_many + ] if fields is None else fields) + if field_name not in exclude + ), + follow=tuple(follow), + format=format, + for_concrete_model=for_concrete_model, + ignore_duplicates=ignore_duplicates, + ) + # Register the model. + _registered_models[_get_registration_key(model)] = version_options + # Connect signals. + for sender, signal, signal_receiver in _get_senders_and_signals(model): + signal.connect(signal_receiver, sender=sender) + # All done! + return model + # Return a class decorator if model is not given + if model is None: + return register + # Register the model. + return register(model) + + +def _assert_registered(model): + if not is_registered(model): + raise RegistrationError("{model} has not been registered with django-reversion".format( + model=model, + )) + + +def _get_options(model): + _assert_registered(model) + return _registered_models[_get_registration_key(model)] + + +def unregister(model): + _assert_registered(model) + del _registered_models[_get_registration_key(model)] + # Disconnect signals. + for sender, signal, signal_receiver in _get_senders_and_signals(model): + signal.disconnect(signal_receiver, sender=sender) + + +def _get_content_type(model, using): + from django.contrib.contenttypes.models import ContentType + version_options = _get_options(model) + return ContentType.objects.db_manager(using).get_for_model( + model, + for_concrete_model=version_options.for_concrete_model, + ) diff --git a/reversion/signals.py b/reversion/signals.py new file mode 100644 index 0000000..99e0085 --- /dev/null +++ b/reversion/signals.py @@ -0,0 +1,10 @@ +from django.dispatch.dispatcher import Signal + + +_signal_args = [ + "revision", + "versions", +] + +pre_revision_commit = Signal(providing_args=_signal_args) +post_revision_commit = Signal(providing_args=_signal_args) diff --git a/reversion/templates/reversion/change_list.html b/reversion/templates/reversion/change_list.html new file mode 100644 index 0000000..4cf742d --- /dev/null +++ b/reversion/templates/reversion/change_list.html @@ -0,0 +1,10 @@ +{% extends "admin/change_list.html" %} +{% load i18n admin_urls %} + + +{% block object-tools-items %} + {% if not is_popup and has_add_permission and has_change_permission %} +
  • {% blocktrans with cl.opts.verbose_name_plural|escape as name %}Recover deleted {{name}}{% endblocktrans %}
  • + {% endif %} + {{block.super}} +{% endblock %} diff --git a/reversion/templates/reversion/object_history.html b/reversion/templates/reversion/object_history.html new file mode 100644 index 0000000..62d14aa --- /dev/null +++ b/reversion/templates/reversion/object_history.html @@ -0,0 +1,42 @@ +{% extends "admin/object_history.html" %} +{% load i18n %} + + +{% block content %} +
    + +

    {% blocktrans %}Choose a date from the list below to revert to a previous version of this object.{% endblocktrans %}

    + +
    + {% if action_list %} + + + + + + + + + + {% for action in action_list %} + + + + + + {% endfor %} + +
    {% trans 'Date/time' %}{% trans 'User' %}{% trans 'Action' %}
    {{action.revision.date_created|date:"DATETIME_FORMAT"}} + {% 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 %} + {{action.revision.comment|linebreaksbr|default:""}}
    + {% else %} +

    {% trans "This object doesn't have a change history. It probably wasn't added via this admin site." %}

    + {% endif %} +
    +
    +{% endblock %} diff --git a/reversion/templates/reversion/recover_form.html b/reversion/templates/reversion/recover_form.html new file mode 100644 index 0000000..78cc0c3 --- /dev/null +++ b/reversion/templates/reversion/recover_form.html @@ -0,0 +1,25 @@ +{% extends "reversion/revision_form.html" %} +{% load i18n admin_urls %} + + +{% block breadcrumbs %} + +{% endblock %} + + +{% block object-tools %}{% endblock %} + + +{% block form_top %} +

    {% blocktrans %}Press the save button below to recover this version of the object.{% endblocktrans %}

    +{% endblock %} + + +{% block submit_buttons_top %}{% with is_popup=1 %}{{block.super}}{% endwith %}{% endblock %} +{% block submit_buttons_bottom %}{% with is_popup=1 %}{{block.super}}{% endwith %}{% endblock %} diff --git a/reversion/templates/reversion/recover_list.html b/reversion/templates/reversion/recover_list.html new file mode 100644 index 0000000..6decea3 --- /dev/null +++ b/reversion/templates/reversion/recover_list.html @@ -0,0 +1,41 @@ +{% extends "admin/base_site.html" %} +{% load i18n l10n admin_urls %} + + +{% block breadcrumbs %} + +{% endblock %} + + +{% block content %} +
    +

    {% blocktrans %}Choose a date from the list below to recover a deleted version of an object.{% endblocktrans %}

    +
    + {% if deleted %} + + + + + + + + + {% for deletion in deleted %} + + + + + {% endfor %} + +
    {% trans 'Date/time' %}{{opts.verbose_name|capfirst}}
    {{deletion.revision.date_created}}{{deletion.object_repr}}
    + {% else %} +

    {% trans "There are no deleted objects to recover." %}

    + {% endif %} +
    +
    +{% endblock %} diff --git a/reversion/templates/reversion/revision_form.html b/reversion/templates/reversion/revision_form.html new file mode 100644 index 0000000..083b492 --- /dev/null +++ b/reversion/templates/reversion/revision_form.html @@ -0,0 +1,26 @@ +{% extends "admin/change_form.html" %} +{% load i18n admin_urls %} + + +{% block breadcrumbs %} + +{% endblock %} + + +{% block object-tools %}{% endblock %} + + +{% block form_top %} +

    {% blocktrans %}Press the save button below to revert to this version of the object.{% endblocktrans %}

    +{% endblock %} + + +{% block submit_buttons_top %}{% with is_popup=1 %}{{block.super}}{% endwith %}{% endblock %} +{% block submit_buttons_bottom %}{% with is_popup=1 %}{{block.super}}{% endwith %}{% endblock %} diff --git a/reversion/views.py b/reversion/views.py new file mode 100644 index 0000000..68afb27 --- /dev/null +++ b/reversion/views.py @@ -0,0 +1,68 @@ +from functools import wraps + +from reversion.compat import is_authenticated +from reversion.revisions import create_revision as create_revision_base, set_user, get_user + + +class _RollBackRevisionView(Exception): + + def __init__(self, response): + self.response = response + + +def _request_creates_revision(request): + return request.method not in ("OPTIONS", "GET", "HEAD") + + +def _set_user_from_request(request): + if getattr(request, "user", None) and is_authenticated(request.user) and get_user() is None: + set_user(request.user) + + +def create_revision(manage_manually=False, using=None, atomic=True): + """ + View decorator that wraps the request in a revision. + + The revision will have it's user set from the request automatically. + """ + def decorator(func): + @wraps(func) + def do_revision_view(request, *args, **kwargs): + if _request_creates_revision(request): + try: + with create_revision_base(manage_manually=manage_manually, using=using, atomic=atomic): + response = func(request, *args, **kwargs) + # Check for an error response. + if response.status_code >= 400: + raise _RollBackRevisionView(response) + # Otherwise, we're good. + _set_user_from_request(request) + return response + except _RollBackRevisionView as ex: + return ex.response + return func(request, *args, **kwargs) + return do_revision_view + return decorator + + +class RevisionMixin(object): + + """ + A class-based view mixin that wraps the request in a revision. + + The revision will have it's user set from the request automatically. + """ + + revision_manage_manually = False + + revision_using = None + + revision_atomic = True + + def __init__(self, *args, **kwargs): + super(RevisionMixin, self).__init__(*args, **kwargs) + self.dispatch = create_revision( + manage_manually=self.revision_manage_manually, + using=self.revision_using, + atomic=self.revision_atomic + )(self.dispatch) diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..2a9acf1 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,2 @@ +[bdist_wheel] +universal = 1 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..3497dcf --- /dev/null +++ b/setup.py @@ -0,0 +1,45 @@ +from setuptools import setup, find_packages +from reversion import __version__ + + +# Load in babel support, if available. +try: + from babel.messages import frontend as babel + cmdclass = { + "compile_catalog": babel.compile_catalog, + "extract_messages": babel.extract_messages, + "init_catalog": babel.init_catalog, + "update_catalog": babel.update_catalog, + } +except ImportError: + cmdclass = {} + +setup( + name="django-reversion", + version='.'.join(str(x) for x in __version__), + license="BSD", + description="An extension to the Django web framework that provides version control for model instances.", + author="Dave Hall", + author_email="dave@etianen.com", + url="http://github.com/etianen/django-reversion", + zip_safe=False, + packages=find_packages(), + package_data={ + "reversion": ["locale/*/LC_MESSAGES/django.*", "templates/reversion/*.html"]}, + cmdclass=cmdclass, + install_requires=[ + "django>=1.8", + ], + classifiers=[ + "Development Status :: 5 - Production/Stable", + "Environment :: Web Environment", + "Intended Audience :: Developers", + "License :: OSI Approved :: BSD License", + "Operating System :: OS Independent", + "Programming Language :: Python", + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', + "Framework :: Django", + ] +) diff --git a/tests/manage.py b/tests/manage.py new file mode 100755 index 0000000..29ba849 --- /dev/null +++ b/tests/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +import os +import sys + +if __name__ == "__main__": + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_project.settings") + try: + from django.core.management import execute_from_command_line + except ImportError: + # The above import may fail for some other reason. Ensure that the + # issue is really that Django is missing to avoid masking other + # exceptions on Python 2. + try: + import django # noqa + except ImportError: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) + raise + execute_from_command_line(sys.argv) diff --git a/tests/test_app/__init__.py b/tests/test_app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_app/admin.py b/tests/test_app/admin.py new file mode 100644 index 0000000..c664123 --- /dev/null +++ b/tests/test_app/admin.py @@ -0,0 +1,14 @@ +from django.contrib import admin +from reversion.admin import VersionAdmin +from test_app.models import TestModel, TestModelRelated + + +class TestModelAdmin(VersionAdmin): + + filter_horizontal = ("related",) + + +admin.site.register(TestModel, TestModelAdmin) + + +admin.site.register(TestModelRelated, admin.ModelAdmin) diff --git a/tests/test_app/migrations/0001_initial.py b/tests/test_app/migrations/0001_initial.py new file mode 100644 index 0000000..330b6c2 --- /dev/null +++ b/tests/test_app/migrations/0001_initial.py @@ -0,0 +1,103 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.7 on 2016-06-14 10:27 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('contenttypes', '0002_remove_content_type_name'), + ('reversion', '0001_squashed_0004_auto_20160611_1202'), + ] + + operations = [ + migrations.CreateModel( + name='TestMeta', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=191)), + ('revision', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='reversion.Revision')), + ], + ), + migrations.CreateModel( + name='TestModel', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(default='v1', max_length=191)), + ], + ), + migrations.CreateModel( + name='TestModelGenericInline', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('object_id', models.IntegerField()), + ('inline_name', models.CharField(default='v1', max_length=191)), + ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')), + ], + ), + migrations.CreateModel( + name='TestModelInline', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('inline_name', models.CharField(default='v1', max_length=191)), + ], + ), + migrations.CreateModel( + name='TestModelRelated', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(default='v1', max_length=191)), + ], + ), + migrations.CreateModel( + name='TestModelThrough', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(default='v1', max_length=191)), + ], + ), + migrations.CreateModel( + name='TestModelParent', + fields=[ + ('testmodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='test_app.TestModel')), + ('parent_name', models.CharField(default='parent v1', max_length=191)), + ], + bases=('test_app.testmodel',), + ), + migrations.CreateModel( + name='TestModelEscapePK', + fields=[ + ('name', models.CharField(max_length=191, primary_key=True, serialize=False)), + ], + ), + migrations.AddField( + model_name='testmodelthrough', + name='test_model', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to='test_app.TestModel'), + ), + migrations.AddField( + model_name='testmodelthrough', + name='test_model_related', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to='test_app.TestModelRelated'), + ), + migrations.AddField( + model_name='testmodelinline', + name='test_model', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='test_app.TestModel'), + ), + migrations.AddField( + model_name='testmodel', + name='related', + field=models.ManyToManyField(blank=True, related_name='_testmodel_related_+', to='test_app.TestModelRelated'), + ), + migrations.AddField( + model_name='testmodel', + name='related_through', + field=models.ManyToManyField(blank=True, related_name='_testmodel_related_through_+', through='test_app.TestModelThrough', to='test_app.TestModelRelated'), + ), + ] diff --git a/tests/test_app/migrations/__init__.py b/tests/test_app/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_app/models.py b/tests/test_app/models.py new file mode 100644 index 0000000..0714000 --- /dev/null +++ b/tests/test_app/models.py @@ -0,0 +1,111 @@ +from django.db import models +from django.contrib.contenttypes.models import ContentType +try: + from django.contrib.contenttypes.fields import GenericRelation +except ImportError: # Django < 1.9 pragma: no cover + from django.contrib.contenttypes.generic import GenericRelation +from reversion.models import Revision + + +class TestModelGenericInline(models.Model): + + object_id = models.IntegerField() + + content_type = models.ForeignKey( + ContentType, + on_delete=models.CASCADE, + ) + + inline_name = models.CharField( + max_length=191, + default="v1", + ) + + +class TestModel(models.Model): + + name = models.CharField( + max_length=191, + default="v1", + ) + + related = models.ManyToManyField( + "TestModelRelated", + blank=True, + related_name="+", + ) + + related_through = models.ManyToManyField( + "TestModelRelated", + blank=True, + through="TestModelThrough", + related_name="+", + ) + + generic_inlines = GenericRelation(TestModelGenericInline) + + +class TestModelEscapePK(models.Model): + + name = models.CharField(max_length=191, primary_key=True) + + +class TestModelThrough(models.Model): + + test_model = models.ForeignKey( + "TestModel", + related_name="+", + on_delete=models.CASCADE, + ) + + test_model_related = models.ForeignKey( + "TestModelRelated", + related_name="+", + on_delete=models.CASCADE, + ) + + name = models.CharField( + max_length=191, + default="v1", + ) + + +class TestModelRelated(models.Model): + + name = models.CharField( + max_length=191, + default="v1", + ) + + +class TestModelParent(TestModel): + + parent_name = models.CharField( + max_length=191, + default="parent v1", + ) + + +class TestModelInline(models.Model): + + test_model = models.ForeignKey( + TestModel, + on_delete=models.CASCADE, + ) + + inline_name = models.CharField( + max_length=191, + default="v1", + ) + + +class TestMeta(models.Model): + + revision = models.ForeignKey( + Revision, + on_delete=models.CASCADE, + ) + + name = models.CharField( + max_length=191, + ) diff --git a/tests/test_app/tests/__init__.py b/tests/test_app/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_app/tests/base.py b/tests/test_app/tests/base.py new file mode 100644 index 0000000..65f7c03 --- /dev/null +++ b/tests/test_app/tests/base.py @@ -0,0 +1,112 @@ +from datetime import timedelta +from django.conf import settings +from django.contrib.auth.models import User +from django.core.management import call_command +try: + from django.urls import clear_url_caches +except ImportError: # Django < 1.10 pragma: no cover + from django.core.urlresolvers import clear_url_caches +from django.test import TestCase, TransactionTestCase +from django.test.utils import override_settings +from django.utils import timezone +from django.utils.six import StringIO, assertRegex +import reversion +from reversion.models import Revision, Version +from test_app.models import TestModel, TestModelParent +from importlib import import_module +try: + from importlib import reload +except ImportError: # Python 2.7 + pass + + +# Test helpers. + +class TestBaseMixin(object): + + multi_db = True + + def reloadUrls(self): + reload(import_module(settings.ROOT_URLCONF)) + clear_url_caches() + + def setUp(self): + super(TestBaseMixin, self).setUp() + for model in list(reversion.get_registered_models()): + reversion.unregister(model) + + def tearDown(self): + super(TestBaseMixin, self).tearDown() + for model in list(reversion.get_registered_models()): + reversion.unregister(model) + + def callCommand(self, command, *args, **kwargs): + kwargs.setdefault("stdout", StringIO()) + kwargs.setdefault("stderr", StringIO()) + kwargs.setdefault("verbosity", 2) + return call_command(command, *args, **kwargs) + + def assertSingleRevision(self, objects, user=None, comment="", meta_names=(), date_created=None, + using=None, model_db=None): + revision = Version.objects.using(using).get_for_object(objects[0], model_db=model_db).get().revision + self.assertEqual(revision.user, user) + if hasattr(comment, 'pattern'): + assertRegex(self, revision.comment, comment) + elif comment is not None: # Allow a wildcard comment. + self.assertEqual(revision.comment, comment) + self.assertAlmostEqual(revision.date_created, date_created or timezone.now(), delta=timedelta(seconds=1)) + # Check meta. + self.assertEqual(revision.testmeta_set.count(), len(meta_names)) + for meta_name in meta_names: + self.assertTrue(revision.testmeta_set.filter(name=meta_name).exists()) + # Check objects. + self.assertEqual(revision.version_set.count(), len(objects)) + for obj in objects: + self.assertTrue(Version.objects.using(using).get_for_object( + obj, + model_db=model_db, + ).filter( + revision=revision, + ).exists()) + + def assertNoRevision(self, using=None): + self.assertEqual(Revision.objects.using(using).all().count(), 0) + + +class TestBase(TestBaseMixin, TestCase): + pass + + +class TestBaseTransaction(TestBaseMixin, TransactionTestCase): + pass + + +class TestModelMixin(object): + + def setUp(self): + super(TestModelMixin, self).setUp() + reversion.register(TestModel) + + +class TestModelParentMixin(TestModelMixin): + + def setUp(self): + super(TestModelParentMixin, self).setUp() + reversion.register(TestModelParent, follow=("testmodel_ptr",)) + + +@override_settings(PASSWORD_HASHERS=["django.contrib.auth.hashers.MD5PasswordHasher"]) +class UserMixin(TestBase): + + def setUp(self): + super(UserMixin, self).setUp() + self.user = User(username="test", is_staff=True, is_superuser=True) + self.user.set_password("password") + self.user.save() + + +class LoginMixin(UserMixin): + + def setUp(self): + super(LoginMixin, self).setUp() + self.client.login(username="test", password="password") diff --git a/tests/test_app/tests/test_admin.py b/tests/test_app/tests/test_admin.py new file mode 100644 index 0000000..2c902cd --- /dev/null +++ b/tests/test_app/tests/test_admin.py @@ -0,0 +1,259 @@ +import re +from django.contrib import admin +try: + from django.contrib.contenttypes.admin import GenericTabularInline +except ImportError: # Django < 1.9 pragma: no cover + from django.contrib.contenttypes.generic import GenericTabularInline +from django.shortcuts import resolve_url +import reversion +from reversion.admin import VersionAdmin +from reversion.models import Version +from test_app.models import TestModel, TestModelParent, TestModelInline, TestModelGenericInline, TestModelEscapePK +from test_app.tests.base import TestBase, LoginMixin + + +class AdminMixin(TestBase): + + def setUp(self): + super(AdminMixin, self).setUp() + admin.site.register(TestModelParent, VersionAdmin) + self.reloadUrls() + + def tearDown(self): + super(AdminMixin, self).tearDown() + admin.site.unregister(TestModelParent) + self.reloadUrls() + + +class AdminRegisterTest(AdminMixin, TestBase): + + def setAutoRegister(self): + self.assertTrue(reversion.is_registered(TestModelParent)) + + def setAutoRegisterFollowsParent(self): + self.assertTrue(reversion.is_registered(TestModel)) + + +class AdminAddViewTest(LoginMixin, AdminMixin, TestBase): + + def testAddView(self): + self.client.post(resolve_url("admin:test_app_testmodelparent_add"), { + "name": "v1", + "parent_name": "parent_v1", + }) + obj = TestModelParent.objects.get() + self.assertSingleRevision( + (obj, obj.testmodel_ptr), user=self.user, + # Django 1.8 gives "Initial version.", Django > 1.8 "Added." + comment=re.compile(r"(Initial version\.|Added\.)") + ) + + +class AdminUpdateViewTest(LoginMixin, AdminMixin, TestBase): + + def testUpdateView(self): + obj = TestModelParent.objects.create() + self.client.post(resolve_url("admin:test_app_testmodelparent_change", obj.pk), { + "name": "v2", + "parent_name": "parent v2", + }) + self.assertSingleRevision( + (obj, obj.testmodel_ptr), user=self.user, + comment="Changed name and parent_name." + ) + + +class AdminChangelistView(LoginMixin, AdminMixin, TestBase): + + def testChangelistView(self): + obj = TestModelParent.objects.create() + response = self.client.get(resolve_url("admin:test_app_testmodelparent_changelist")) + self.assertContains(response, resolve_url("admin:test_app_testmodelparent_change", obj.pk)) + + +class AdminRevisionViewTest(LoginMixin, AdminMixin, TestBase): + + def setUp(self): + super(AdminRevisionViewTest, self).setUp() + with reversion.create_revision(): + self.obj = TestModelParent.objects.create() + with reversion.create_revision(): + self.obj.name = "v2" + self.obj.parent_name = "parent v2" + self.obj.save() + + def testRevisionView(self): + response = self.client.get(resolve_url( + "admin:test_app_testmodelparent_revision", + self.obj.pk, + Version.objects.get_for_object(self.obj)[1].pk, + )) + self.assertContains(response, 'value="v1"') + self.assertContains(response, 'value="parent v1"') + # Test that the changes were rolled back. + self.obj.refresh_from_db() + self.assertEqual(self.obj.name, "v2") + self.assertEqual(self.obj.parent_name, "parent v2") + self.assertIn("revert", response.context) + self.assertTrue(response.context["revert"]) + + def testRevisionViewOldRevision(self): + response = self.client.get(resolve_url( + "admin:test_app_testmodelparent_revision", + self.obj.pk, + Version.objects.get_for_object(self.obj)[0].pk, + )) + self.assertContains(response, 'value="v2"') + self.assertContains(response, 'value="parent v2"') + + def testRevisionViewRevertError(self): + Version.objects.get_for_object(self.obj).update(format="boom") + response = self.client.get(resolve_url( + "admin:test_app_testmodelparent_revision", + self.obj.pk, + Version.objects.get_for_object(self.obj)[1].pk, + )) + self.assertEqual( + response["Location"].replace("http://testserver", ""), + resolve_url("admin:test_app_testmodelparent_changelist"), + ) + + def testRevisionViewRevert(self): + self.client.post(resolve_url( + "admin:test_app_testmodelparent_revision", + self.obj.pk, + Version.objects.get_for_object(self.obj)[1].pk, + ), { + "name": "v1", + "parent_name": "parent v1", + }) + self.obj.refresh_from_db() + self.assertEqual(self.obj.name, "v1") + self.assertEqual(self.obj.parent_name, "parent v1") + + +class AdminRecoverViewTest(LoginMixin, AdminMixin, TestBase): + + def setUp(self): + super(AdminRecoverViewTest, self).setUp() + with reversion.create_revision(): + obj = TestModelParent.objects.create() + obj.delete() + + def testRecoverView(self): + response = self.client.get(resolve_url( + "admin:test_app_testmodelparent_recover", + Version.objects.get_for_model(TestModelParent).get().pk, + )) + self.assertContains(response, 'value="v1"') + self.assertContains(response, 'value="parent v1"') + self.assertIn("recover", response.context) + self.assertTrue(response.context["recover"]) + + def testRecoverViewRecover(self): + self.client.post(resolve_url( + "admin:test_app_testmodelparent_recover", + Version.objects.get_for_model(TestModelParent).get().pk, + ), { + "name": "v1", + "parent_name": "parent v1", + }) + obj = TestModelParent.objects.get() + self.assertEqual(obj.name, "v1") + self.assertEqual(obj.parent_name, "parent v1") + + +class AdminRecoverlistViewTest(LoginMixin, AdminMixin, TestBase): + + def testRecoverlistView(self): + with reversion.create_revision(): + obj = TestModelParent.objects.create() + obj.delete() + response = self.client.get(resolve_url("admin:test_app_testmodelparent_recoverlist")) + self.assertContains(response, resolve_url( + "admin:test_app_testmodelparent_recover", + Version.objects.get_for_model(TestModelParent).get().pk, + )) + + +class AdminHistoryViewTest(LoginMixin, AdminMixin, TestBase): + + def testHistorylistView(self): + with reversion.create_revision(): + obj = TestModelParent.objects.create() + response = self.client.get(resolve_url("admin:test_app_testmodelparent_history", obj.pk)) + self.assertContains(response, resolve_url( + "admin:test_app_testmodelparent_revision", + obj.pk, + Version.objects.get_for_model(TestModelParent).get().pk, + )) + + +class AdminQuotingTest(LoginMixin, AdminMixin, TestBase): + + def setUp(self): + super(AdminQuotingTest, self).setUp() + admin.site.register(TestModelEscapePK, VersionAdmin) + self.reloadUrls() + + def tearDown(self): + super(AdminQuotingTest, self).tearDown() + admin.site.unregister(TestModelEscapePK) + self.reloadUrls() + + def testHistoryWithQuotedPrimaryKey(self): + pk = 'ABC_123' + quoted_pk = admin.utils.quote(pk) + # test is invalid if quoting does not change anything + assert quoted_pk != pk + + with reversion.create_revision(): + obj = TestModelEscapePK.objects.create(name=pk) + + revision_url = resolve_url( + "admin:test_app_testmodelescapepk_revision", + quoted_pk, + Version.objects.get_for_object(obj).get().pk, + ) + history_url = resolve_url( + "admin:test_app_testmodelescapepk_history", + quoted_pk + ) + response = self.client.get(history_url) + self.assertContains(response, revision_url) + response = self.client.get(revision_url) + self.assertContains(response, 'value="{}"'.format(pk)) + + +class TestModelInlineAdmin(admin.TabularInline): + + model = TestModelInline + + +class TestModelGenericInlineAdmin(GenericTabularInline): + + model = TestModelGenericInline + + +class TestModelParentAdmin(VersionAdmin): + + inlines = (TestModelInlineAdmin, TestModelGenericInlineAdmin) + + +class AdminRegisterInlineTest(TestBase): + + def setUp(self): + super(AdminRegisterInlineTest, self).setUp() + admin.site.register(TestModelParent, TestModelParentAdmin) + self.reloadUrls() + + def tearDown(self): + super(AdminRegisterInlineTest, self).tearDown() + admin.site.unregister(TestModelParent) + self.reloadUrls() + + def testAutoRegisterInline(self): + self.assertTrue(reversion.is_registered(TestModelInline)) + + def testAutoRegisterGenericInline(self): + self.assertTrue(reversion.is_registered(TestModelGenericInline)) diff --git a/tests/test_app/tests/test_api.py b/tests/test_app/tests/test_api.py new file mode 100644 index 0000000..e12b102 --- /dev/null +++ b/tests/test_app/tests/test_api.py @@ -0,0 +1,328 @@ +from datetime import timedelta +from django.contrib.auth.models import User +from django.db import models +from django.db.transaction import get_connection +from django.utils import timezone +import reversion +from test_app.models import TestModel, TestModelRelated, TestModelThrough, TestModelParent, TestMeta +from test_app.tests.base import TestBase, TestBaseTransaction, TestModelMixin, UserMixin + +try: + from unittest.mock import MagicMock +except ImportError: + from mock import MagicMock + + +class SaveTest(TestModelMixin, TestBase): + + def testModelSave(self): + TestModel.objects.create() + self.assertNoRevision() + + +class IsRegisteredTest(TestModelMixin, TestBase): + + def testIsRegistered(self): + self.assertTrue(reversion.is_registered(TestModel)) + + +class IsRegisterUnregisteredTest(TestBase): + + def testIsRegisteredFalse(self): + self.assertFalse(reversion.is_registered(TestModel)) + + +class GetRegisteredModelsTest(TestModelMixin, TestBase): + + def testGetRegisteredModels(self): + self.assertEqual(set(reversion.get_registered_models()), set((TestModel,))) + + +class RegisterTest(TestBase): + + def testRegister(self): + reversion.register(TestModel) + self.assertTrue(reversion.is_registered(TestModel)) + + def testRegisterDecorator(self): + @reversion.register() + class TestModelDecorater(models.Model): + pass + self.assertTrue(reversion.is_registered(TestModelDecorater)) + + def testRegisterAlreadyRegistered(self): + reversion.register(TestModel) + with self.assertRaises(reversion.RegistrationError): + reversion.register(TestModel) + + def testRegisterM2MSThroughLazy(self): + # When register is used as a decorator in models.py, lazy relations haven't had a chance to be resolved, so + # will still be a string. + @reversion.register() + class TestModelLazy(models.Model): + related = models.ManyToManyField( + TestModelRelated, + through="TestModelThroughLazy", + ) + + class TestModelThroughLazy(models.Model): + pass + + +class UnregisterTest(TestModelMixin, TestBase): + + def testUnregister(self): + reversion.unregister(TestModel) + self.assertFalse(reversion.is_registered(TestModel)) + + +class UnregisterUnregisteredTest(TestBase): + + def testUnregisterNotRegistered(self): + with self.assertRaises(reversion.RegistrationError): + reversion.unregister(User) + + +class CreateRevisionTest(TestModelMixin, TestBase): + + def testCreateRevision(self): + with reversion.create_revision(): + obj = TestModel.objects.create() + self.assertSingleRevision((obj,)) + + def testCreateRevisionNested(self): + with reversion.create_revision(): + with reversion.create_revision(): + obj = TestModel.objects.create() + self.assertSingleRevision((obj,)) + + def testCreateRevisionEmpty(self): + with reversion.create_revision(): + pass + self.assertNoRevision() + + def testCreateRevisionException(self): + try: + with reversion.create_revision(): + TestModel.objects.create() + raise Exception("Boom!") + except Exception as ex: + pass + self.assertNoRevision() + + def testCreateRevisionDecorator(self): + obj = reversion.create_revision()(TestModel.objects.create)() + self.assertSingleRevision((obj,)) + + def testPreRevisionCommitSignal(self): + _callback = MagicMock() + reversion.signals.pre_revision_commit.connect(_callback) + + with reversion.create_revision(): + TestModel.objects.create() + self.assertEqual(_callback.call_count, 1) + + def testPostRevisionCommitSignal(self): + _callback = MagicMock() + reversion.signals.post_revision_commit.connect(_callback) + + with reversion.create_revision(): + TestModel.objects.create() + self.assertEqual(_callback.call_count, 1) + + +class CreateRevisionAtomicTest(TestModelMixin, TestBaseTransaction): + def testCreateRevisionAtomic(self): + self.assertFalse(get_connection().in_atomic_block) + with reversion.create_revision(): + self.assertTrue(get_connection().in_atomic_block) + + def testCreateRevisionNonAtomic(self): + self.assertFalse(get_connection().in_atomic_block) + with reversion.create_revision(atomic=False): + self.assertFalse(get_connection().in_atomic_block) + + +class CreateRevisionManageManuallyTest(TestModelMixin, TestBase): + + def testCreateRevisionManageManually(self): + with reversion.create_revision(manage_manually=True): + TestModel.objects.create() + self.assertNoRevision() + + def testCreateRevisionManageManuallyNested(self): + with reversion.create_revision(): + with reversion.create_revision(manage_manually=True): + TestModel.objects.create() + self.assertNoRevision() + + +class CreateRevisionDbTest(TestModelMixin, TestBase): + + def testCreateRevisionMultiDb(self): + with reversion.create_revision(using="mysql"), reversion.create_revision(using="postgres"): + obj = TestModel.objects.create() + self.assertNoRevision() + self.assertSingleRevision((obj,), using="mysql") + self.assertSingleRevision((obj,), using="postgres") + + +class CreateRevisionFollowTest(TestBase): + + def testCreateRevisionFollow(self): + reversion.register(TestModel, follow=("related",)) + reversion.register(TestModelRelated) + obj_related = TestModelRelated.objects.create() + with reversion.create_revision(): + obj = TestModel.objects.create() + obj.related.add(obj_related) + self.assertSingleRevision((obj, obj_related)) + + def testCreateRevisionFollowThrough(self): + reversion.register(TestModel, follow=("related_through",)) + reversion.register(TestModelThrough, follow=("test_model", "test_model_related",)) + reversion.register(TestModelRelated) + obj_related = TestModelRelated.objects.create() + with reversion.create_revision(): + obj = TestModel.objects.create() + obj_through = TestModelThrough.objects.create( + test_model=obj, + test_model_related=obj_related, + ) + self.assertSingleRevision((obj, obj_through, obj_related)) + + def testCreateRevisionFollowInvalid(self): + reversion.register(TestModel, follow=("name",)) + with reversion.create_revision(): + with self.assertRaises(reversion.RegistrationError): + TestModel.objects.create() + + +class CreateRevisionIgnoreDuplicatesTest(TestBase): + + def testCreateRevisionIgnoreDuplicates(self): + reversion.register(TestModel, ignore_duplicates=True) + with reversion.create_revision(): + obj = TestModel.objects.create() + with reversion.create_revision(): + obj.save() + self.assertSingleRevision((obj,)) + + +class CreateRevisionInheritanceTest(TestModelMixin, TestBase): + + def testCreateRevisionInheritance(self): + reversion.register(TestModelParent, follow=("testmodel_ptr",)) + with reversion.create_revision(): + obj = TestModelParent.objects.create() + self.assertSingleRevision((obj, obj.testmodel_ptr)) + + +class SetCommentTest(TestModelMixin, TestBase): + + def testSetComment(self): + with reversion.create_revision(): + reversion.set_comment("comment v1") + obj = TestModel.objects.create() + self.assertSingleRevision((obj,), comment="comment v1") + + def testSetCommentNoBlock(self): + with self.assertRaises(reversion.RevisionManagementError): + reversion.set_comment("comment v1") + + +class GetCommentTest(TestBase): + + def testGetComment(self): + with reversion.create_revision(): + reversion.set_comment("comment v1") + self.assertEqual(reversion.get_comment(), "comment v1") + + def testGetCommentDefault(self): + with reversion.create_revision(): + self.assertEqual(reversion.get_comment(), "") + + def testGetCommentNoBlock(self): + with self.assertRaises(reversion.RevisionManagementError): + reversion.get_comment() + + +class SetUserTest(UserMixin, TestModelMixin, TestBase): + + def testSetUser(self): + with reversion.create_revision(): + reversion.set_user(self.user) + obj = TestModel.objects.create() + self.assertSingleRevision((obj,), user=self.user) + + def testSetUserNoBlock(self): + with self.assertRaises(reversion.RevisionManagementError): + reversion.set_user(self.user) + + +class GetUserTest(UserMixin, TestBase): + + def testGetUser(self): + with reversion.create_revision(): + reversion.set_user(self.user) + self.assertEqual(reversion.get_user(), self.user) + + def testGetUserDefault(self): + with reversion.create_revision(): + self.assertEqual(reversion.get_user(), None) + + def testGetUserNoBlock(self): + with self.assertRaises(reversion.RevisionManagementError): + reversion.get_user() + + +class SetDateCreatedTest(TestModelMixin, TestBase): + + def testSetDateCreated(self): + date_created = timezone.now() - timedelta(days=20) + with reversion.create_revision(): + reversion.set_date_created(date_created) + obj = TestModel.objects.create() + self.assertSingleRevision((obj,), date_created=date_created) + + def testDateCreatedNoBlock(self): + with self.assertRaises(reversion.RevisionManagementError): + reversion.set_date_created(timezone.now()) + + +class GetDateCreatedTest(TestBase): + + def testGetDateCreated(self): + date_created = timezone.now() - timedelta(days=20) + with reversion.create_revision(): + reversion.set_date_created(date_created) + self.assertEqual(reversion.get_date_created(), date_created) + + def testGetDateCreatedDefault(self): + with reversion.create_revision(): + self.assertAlmostEqual(reversion.get_date_created(), timezone.now(), delta=timedelta(seconds=1)) + + def testGetDateCreatedNoBlock(self): + with self.assertRaises(reversion.RevisionManagementError): + reversion.get_date_created() + + +class AddMetaTest(TestModelMixin, TestBase): + + def testAddMeta(self): + with reversion.create_revision(): + reversion.add_meta(TestMeta, name="meta v1") + obj = TestModel.objects.create() + self.assertSingleRevision((obj,), meta_names=("meta v1",)) + + def testAddMetaNoBlock(self): + with self.assertRaises(reversion.RevisionManagementError): + reversion.add_meta(TestMeta, name="meta v1") + + def testAddMetaMultDb(self): + with reversion.create_revision(using="mysql"), reversion.create_revision(using="postgres"): + obj = TestModel.objects.create() + reversion.add_meta(TestMeta, name="meta v1") + self.assertNoRevision() + self.assertSingleRevision((obj,), meta_names=("meta v1",), using="mysql") + self.assertSingleRevision((obj,), meta_names=("meta v1",), using="postgres") diff --git a/tests/test_app/tests/test_commands.py b/tests/test_app/tests/test_commands.py new file mode 100644 index 0000000..bb950ad --- /dev/null +++ b/tests/test_app/tests/test_commands.py @@ -0,0 +1,195 @@ +from datetime import timedelta +from django.core.management import CommandError +from django.utils import timezone +import reversion +from test_app.models import TestModel +from test_app.tests.base import TestBase, TestModelMixin + + +class CreateInitialRevisionsTest(TestModelMixin, TestBase): + + def testCreateInitialRevisions(self): + obj = TestModel.objects.create() + self.callCommand("createinitialrevisions") + self.assertSingleRevision((obj,), comment="Initial version.") + + def testCreateInitialRevisionsAlreadyCreated(self): + obj = TestModel.objects.create() + self.callCommand("createinitialrevisions") + self.callCommand("createinitialrevisions") + self.assertSingleRevision((obj,), comment="Initial version.") + + +class CreateInitialRevisionsAppLabelTest(TestModelMixin, TestBase): + + def testCreateInitialRevisionsAppLabel(self): + obj = TestModel.objects.create() + self.callCommand("createinitialrevisions", "test_app") + self.assertSingleRevision((obj,), comment="Initial version.") + + def testCreateInitialRevisionsAppLabelMissing(self): + with self.assertRaises(CommandError): + self.callCommand("createinitialrevisions", "boom") + + def testCreateInitialRevisionsModel(self): + obj = TestModel.objects.create() + self.callCommand("createinitialrevisions", "test_app.TestModel") + self.assertSingleRevision((obj,), comment="Initial version.") + + def testCreateInitialRevisionsModelMissing(self): + with self.assertRaises(CommandError): + self.callCommand("createinitialrevisions", "test_app.boom") + + def testCreateInitialRevisionsModelMissingApp(self): + with self.assertRaises(CommandError): + self.callCommand("createinitialrevisions", "boom.boom") + + def testCreateInitialRevisionsModelNotRegistered(self): + TestModel.objects.create() + self.callCommand("createinitialrevisions", "auth.User") + self.assertNoRevision() + + +class CreateInitialRevisionsDbTest(TestModelMixin, TestBase): + + def testCreateInitialRevisionsDb(self): + obj = TestModel.objects.create() + self.callCommand("createinitialrevisions", using="postgres") + self.assertNoRevision() + self.assertSingleRevision((obj,), comment="Initial version.", using="postgres") + + def testCreateInitialRevisionsDbMySql(self): + obj = TestModel.objects.create() + self.callCommand("createinitialrevisions", using="mysql") + self.assertNoRevision() + self.assertSingleRevision((obj,), comment="Initial version.", using="mysql") + + +class CreateInitialRevisionsModelDbTest(TestModelMixin, TestBase): + + def testCreateInitialRevisionsModelDb(self): + obj = TestModel.objects.db_manager("postgres").create() + self.callCommand("createinitialrevisions", model_db="postgres") + self.assertSingleRevision((obj,), comment="Initial version.", model_db="postgres") + + +class CreateInitialRevisionsCommentTest(TestModelMixin, TestBase): + + def testCreateInitialRevisionsComment(self): + obj = TestModel.objects.create() + self.callCommand("createinitialrevisions", comment="comment v1") + self.assertSingleRevision((obj,), comment="comment v1") + + +class DeleteRevisionsTest(TestModelMixin, TestBase): + + def testDeleteRevisions(self): + with reversion.create_revision(): + TestModel.objects.create() + self.callCommand("deleterevisions") + self.assertNoRevision() + + +class DeleteRevisionsAppLabelTest(TestModelMixin, TestBase): + + def testDeleteRevisionsAppLabel(self): + with reversion.create_revision(): + TestModel.objects.create() + self.callCommand("deleterevisions", "test_app") + self.assertNoRevision() + + def testDeleteRevisionsAppLabelMissing(self): + with self.assertRaises(CommandError): + self.callCommand("deleterevisions", "boom") + + def testDeleteRevisionsModel(self): + with reversion.create_revision(): + TestModel.objects.create() + self.callCommand("deleterevisions", "test_app.TestModel") + self.assertNoRevision() + + def testDeleteRevisionsModelMissing(self): + with self.assertRaises(CommandError): + self.callCommand("deleterevisions", "test_app.boom") + + def testDeleteRevisionsModelMissingApp(self): + with self.assertRaises(CommandError): + self.callCommand("deleterevisions", "boom.boom") + + def testDeleteRevisionsModelNotRegistered(self): + with reversion.create_revision(): + obj = TestModel.objects.create() + self.callCommand("deleterevisions", "auth.User") + self.assertSingleRevision((obj,)) + + +class DeleteRevisionsDbTest(TestModelMixin, TestBase): + + def testDeleteRevisionsDb(self): + with reversion.create_revision(using="postgres"): + TestModel.objects.create() + self.callCommand("deleterevisions", using="postgres") + self.assertNoRevision(using="postgres") + + def testDeleteRevisionsDbMySql(self): + with reversion.create_revision(using="mysql"): + TestModel.objects.create() + self.callCommand("deleterevisions", using="mysql") + self.assertNoRevision(using="mysql") + + def testDeleteRevisionsDbNoMatch(self): + with reversion.create_revision(): + obj = TestModel.objects.create() + self.callCommand("deleterevisions", using="postgres") + self.assertSingleRevision((obj,)) + + +class DeleteRevisionsModelDbTest(TestModelMixin, TestBase): + + def testDeleteRevisionsModelDb(self): + with reversion.create_revision(): + TestModel.objects.db_manager("postgres").create() + self.callCommand("deleterevisions", model_db="postgres") + self.assertNoRevision(using="postgres") + + +class DeleteRevisionsDaysTest(TestModelMixin, TestBase): + + def testDeleteRevisionsDays(self): + date_created = timezone.now() - timedelta(days=20) + with reversion.create_revision(): + TestModel.objects.create() + reversion.set_date_created(date_created) + self.callCommand("deleterevisions", days=19) + self.assertNoRevision() + + def testDeleteRevisionsDaysNoMatch(self): + date_created = timezone.now() - timedelta(days=20) + with reversion.create_revision(): + obj = TestModel.objects.create() + reversion.set_date_created(date_created) + self.callCommand("deleterevisions", days=21) + self.assertSingleRevision((obj,), date_created=date_created) + + +class DeleteRevisionsKeepTest(TestModelMixin, TestBase): + + def testDeleteRevisionsKeep(self): + with reversion.create_revision(): + obj_1 = TestModel.objects.create() + reversion.set_comment("obj_1 v1") + with reversion.create_revision(): + obj_1.save() + reversion.set_comment("obj_1 v2") + with reversion.create_revision(): + obj_2 = TestModel.objects.create() + reversion.set_comment("obj_2 v1") + with reversion.create_revision(): + obj_2.save() + reversion.set_comment("obj_2 v2") + with reversion.create_revision(): + obj_3 = TestModel.objects.create() + self.callCommand("deleterevisions", keep=1) + self.assertSingleRevision((obj_1,), comment="obj_1 v2") + self.assertSingleRevision((obj_2,), comment="obj_2 v2") + self.assertSingleRevision((obj_3,)) diff --git a/tests/test_app/tests/test_middleware.py b/tests/test_app/tests/test_middleware.py new file mode 100644 index 0000000..538c5d0 --- /dev/null +++ b/tests/test_app/tests/test_middleware.py @@ -0,0 +1,37 @@ +from django.conf import settings +from django.test.utils import override_settings +from test_app.models import TestModel +from test_app.tests.base import TestBase, TestModelMixin, LoginMixin + + +use_middleware = override_settings( + MIDDLEWARE=settings.MIDDLEWARE + ["reversion.middleware.RevisionMiddleware"], + MIDDLEWARE_CLASSES=settings.MIDDLEWARE_CLASSES + ["reversion.middleware.RevisionMiddleware"], +) + + +@use_middleware +class RevisionMiddlewareTest(TestModelMixin, TestBase): + + def testCreateRevision(self): + response = self.client.post("/test-app/save-obj/") + obj = TestModel.objects.get(pk=response.content) + self.assertSingleRevision((obj,)) + + def testCreateRevisionError(self): + with self.assertRaises(Exception): + self.client.post("/test-app/save-obj-error/") + self.assertNoRevision() + + def testCreateRevisionGet(self): + self.client.get("/test-app/create-revision/") + self.assertNoRevision() + + +@use_middleware +class RevisionMiddlewareUserTest(TestModelMixin, LoginMixin, TestBase): + + def testCreateRevisionUser(self): + response = self.client.post("/test-app/save-obj/") + obj = TestModel.objects.get(pk=response.content) + self.assertSingleRevision((obj,), user=self.user) diff --git a/tests/test_app/tests/test_models.py b/tests/test_app/tests/test_models.py new file mode 100644 index 0000000..80f893d --- /dev/null +++ b/tests/test_app/tests/test_models.py @@ -0,0 +1,375 @@ +from django.utils.encoding import force_text +import reversion +from reversion.models import Version +from test_app.models import TestModel, TestModelRelated, TestModelParent +from test_app.tests.base import TestBase, TestModelMixin, TestModelParentMixin + + +class GetForModelTest(TestModelMixin, TestBase): + + def testGetForModel(self): + with reversion.create_revision(): + obj = TestModel.objects.create() + self.assertEqual(Version.objects.get_for_model(obj.__class__).count(), 1) + + +class GetForModelDbTest(TestModelMixin, TestBase): + + def testGetForModelDb(self): + with reversion.create_revision(using="postgres"): + obj = TestModel.objects.create() + self.assertEqual(Version.objects.using("postgres").get_for_model(obj.__class__).count(), 1) + + def testGetForModelDbMySql(self): + with reversion.create_revision(using="mysql"): + obj = TestModel.objects.create() + self.assertEqual(Version.objects.using("mysql").get_for_model(obj.__class__).count(), 1) + + +class GetForObjectTest(TestModelMixin, TestBase): + + def testGetForObject(self): + with reversion.create_revision(): + obj = TestModel.objects.create() + self.assertEqual(Version.objects.get_for_object(obj).count(), 1) + + def testGetForObjectEmpty(self): + obj = TestModel.objects.create() + self.assertEqual(Version.objects.get_for_object(obj).count(), 0) + + def testGetForObjectOrdering(self): + with reversion.create_revision(): + obj = TestModel.objects.create() + with reversion.create_revision(): + obj.name = "v2" + obj.save() + self.assertEqual(Version.objects.get_for_object(obj)[0].field_dict["name"], "v2") + self.assertEqual(Version.objects.get_for_object(obj)[1].field_dict["name"], "v1") + + def testGetForObjectFiltering(self): + with reversion.create_revision(): + obj_1 = TestModel.objects.create() + with reversion.create_revision(): + obj_2 = TestModel.objects.create() + self.assertEqual(Version.objects.get_for_object(obj_1).get().object, obj_1) + self.assertEqual(Version.objects.get_for_object(obj_2).get().object, obj_2) + + +class GetForObjectDbTest(TestModelMixin, TestBase): + + def testGetForObjectDb(self): + with reversion.create_revision(using="postgres"): + obj = TestModel.objects.create() + self.assertEqual(Version.objects.get_for_object(obj).count(), 0) + self.assertEqual(Version.objects.using("postgres").get_for_object(obj).count(), 1) + + def testGetForObjectDbMySql(self): + with reversion.create_revision(using="mysql"): + obj = TestModel.objects.create() + self.assertEqual(Version.objects.get_for_object(obj).count(), 0) + self.assertEqual(Version.objects.using("mysql").get_for_object(obj).count(), 1) + + +class GetForObjectModelDbTest(TestModelMixin, TestBase): + + def testGetForObjectModelDb(self): + with reversion.create_revision(): + obj = TestModel.objects.db_manager("postgres").create() + self.assertEqual(Version.objects.get_for_object(obj).count(), 0) + self.assertEqual(Version.objects.get_for_object(obj, model_db="postgres").count(), 1) + + +class GetForObjectUniqueTest(TestModelMixin, TestBase): + + def testGetForObjectUnique(self): + with reversion.create_revision(): + obj = TestModel.objects.create() + with reversion.create_revision(): + obj.save() + self.assertEqual(len(list(Version.objects.get_for_object(obj).get_unique())), 1) + + def testGetForObjectUniqueMiss(self): + with reversion.create_revision(): + obj = TestModel.objects.create() + with reversion.create_revision(): + obj.name = "v2" + obj.save() + self.assertEqual(len(list(Version.objects.get_for_object(obj).get_unique())), 2) + + +class GetForObjectReferenceTest(TestModelMixin, TestBase): + + def testGetForObjectReference(self): + with reversion.create_revision(): + obj = TestModel.objects.create() + self.assertEqual(Version.objects.get_for_object_reference(TestModel, obj.pk).count(), 1) + + def testGetForObjectReferenceEmpty(self): + obj = TestModel.objects.create() + self.assertEqual(Version.objects.get_for_object_reference(TestModel, obj.pk).count(), 0) + + def testGetForObjectReferenceOrdering(self): + with reversion.create_revision(): + obj = TestModel.objects.create() + with reversion.create_revision(): + obj.name = "v2" + obj.save() + self.assertEqual(Version.objects.get_for_object_reference(TestModel, obj.pk)[0].field_dict["name"], "v2") + self.assertEqual(Version.objects.get_for_object_reference(TestModel, obj.pk)[1].field_dict["name"], "v1") + + def testGetForObjectReferenceFiltering(self): + with reversion.create_revision(): + obj_1 = TestModel.objects.create() + with reversion.create_revision(): + obj_2 = TestModel.objects.create() + self.assertEqual(Version.objects.get_for_object_reference(TestModel, obj_1.pk).get().object, obj_1) + self.assertEqual(Version.objects.get_for_object_reference(TestModel, obj_2.pk).get().object, obj_2) + + +class GetForObjectReferenceDbTest(TestModelMixin, TestBase): + + def testGetForObjectReferenceModelDb(self): + with reversion.create_revision(using="postgres"): + obj = TestModel.objects.create() + self.assertEqual(Version.objects.get_for_object_reference(TestModel, obj.pk).count(), 0) + self.assertEqual(Version.objects.using("postgres").get_for_object_reference(TestModel, obj.pk).count(), 1) + + +class GetForObjectReferenceModelDbTest(TestModelMixin, TestBase): + + def testGetForObjectReferenceModelDb(self): + with reversion.create_revision(): + obj = TestModel.objects.db_manager("postgres").create() + self.assertEqual(Version.objects.get_for_object_reference(TestModel, obj.pk).count(), 0) + self.assertEqual(Version.objects.get_for_object_reference(TestModel, obj.pk, model_db="postgres").count(), 1) + + def testGetForObjectReferenceModelDbMySql(self): + with reversion.create_revision(): + obj = TestModel.objects.db_manager("mysql").create() + self.assertEqual(Version.objects.get_for_object_reference(TestModel, obj.pk).count(), 0) + self.assertEqual(Version.objects.get_for_object_reference(TestModel, obj.pk, model_db="mysql").count(), 1) + + +class GetDeletedTest(TestModelMixin, TestBase): + + def testGetDeleted(self): + with reversion.create_revision(): + obj = TestModel.objects.create() + with reversion.create_revision(): + obj.save() + obj.delete() + self.assertEqual(Version.objects.get_deleted(TestModel).count(), 1) + + def testGetDeletedEmpty(self): + with reversion.create_revision(): + TestModel.objects.create() + self.assertEqual(Version.objects.get_deleted(TestModel).count(), 0) + + def testGetDeletedOrdering(self): + with reversion.create_revision(): + obj_1 = TestModel.objects.create() + with reversion.create_revision(): + obj_2 = TestModel.objects.create() + pk_1 = obj_1.pk + obj_1.delete() + pk_2 = obj_2.pk + obj_2.delete() + self.assertEqual(Version.objects.get_deleted(TestModel)[0].object_id, force_text(pk_2)) + self.assertEqual(Version.objects.get_deleted(TestModel)[1].object_id, force_text(pk_1)) + + def testGetDeletedPostgres(self): + with reversion.create_revision(using="postgres"): + obj = TestModel.objects.using("postgres").create() + with reversion.create_revision(using="postgres"): + obj.save() + obj.delete() + self.assertEqual(Version.objects.using("postgres").get_deleted(TestModel, model_db="postgres").count(), 1) + + def testGetDeletedMySQL(self): + with reversion.create_revision(using="mysql"): + obj = TestModel.objects.using("mysql").create() + with reversion.create_revision(using="mysql"): + obj.save() + obj.delete() + self.assertEqual(Version.objects.using("mysql").get_deleted(TestModel, model_db="mysql").count(), 1) + + +class GetDeletedDbTest(TestModelMixin, TestBase): + + def testGetDeletedDb(self): + with reversion.create_revision(using="postgres"): + obj = TestModel.objects.create() + obj.delete() + self.assertEqual(Version.objects.get_deleted(TestModel).count(), 0) + self.assertEqual(Version.objects.using("postgres").get_deleted(TestModel).count(), 1) + + def testGetDeletedDbMySql(self): + with reversion.create_revision(using="mysql"): + obj = TestModel.objects.create() + obj.delete() + self.assertEqual(Version.objects.get_deleted(TestModel).count(), 0) + self.assertEqual(Version.objects.using("mysql").get_deleted(TestModel).count(), 1) + + +class GetDeletedModelDbTest(TestModelMixin, TestBase): + + def testGetDeletedModelDb(self): + with reversion.create_revision(): + obj = TestModel.objects.db_manager("postgres").create() + obj.delete() + self.assertEqual(Version.objects.get_deleted(TestModel).count(), 0) + self.assertEqual(Version.objects.get_deleted(TestModel, model_db="postgres").count(), 1) + + +class FieldDictTest(TestModelMixin, TestBase): + + def testFieldDict(self): + with reversion.create_revision(): + obj = TestModel.objects.create() + self.assertEqual(Version.objects.get_for_object(obj).get().field_dict, { + "id": obj.pk, + "name": "v1", + "related": [], + }) + + def testFieldDictM2M(self): + obj_related = TestModelRelated.objects.create() + with reversion.create_revision(): + obj = TestModel.objects.create() + obj.related.add(obj_related) + self.assertEqual(Version.objects.get_for_object(obj).get().field_dict, { + "id": obj.pk, + "name": "v1", + "related": [obj_related.pk], + }) + + +class FieldDictFieldsTest(TestBase): + + def testFieldDictFieldFields(self): + reversion.register(TestModel, fields=("name",)) + with reversion.create_revision(): + obj = TestModel.objects.create() + self.assertEqual(Version.objects.get_for_object(obj).get().field_dict, { + "name": "v1", + }) + + +class FieldDictExcludeTest(TestBase): + + def testFieldDictFieldExclude(self): + reversion.register(TestModel, exclude=("name",)) + with reversion.create_revision(): + obj = TestModel.objects.create() + self.assertEqual(Version.objects.get_for_object(obj).get().field_dict, { + "id": obj.pk, + "related": [], + }) + + +class FieldDictInheritanceTest(TestModelParentMixin, TestBase): + + def testFieldDictInheritance(self): + with reversion.create_revision(): + obj = TestModelParent.objects.create() + self.assertEqual(Version.objects.get_for_object(obj).get().field_dict, { + "id": obj.pk, + "name": "v1", + "related": [], + "parent_name": "parent v1", + "testmodel_ptr_id": obj.pk, + }) + + def testFieldDictInheritanceUpdate(self): + obj = TestModelParent.objects.create() + with reversion.create_revision(): + obj.name = "v2" + obj.parent_name = "parent v2" + obj.save() + self.assertEqual(Version.objects.get_for_object(obj).get().field_dict, { + "id": obj.pk, + "name": "v2", + "parent_name": "parent v2", + "related": [], + "testmodel_ptr_id": obj.pk, + }) + + +class M2MTest(TestModelMixin, TestBase): + + def testM2MSave(self): + v1 = TestModelRelated.objects.create(name="v1") + v2 = TestModelRelated.objects.create(name="v2") + with reversion.create_revision(): + obj = TestModel.objects.create() + obj.related.add(v1) + obj.related.add(v2) + version = Version.objects.get_for_object(obj).first() + self.assertEqual(set(version.field_dict["related"]), set((v1.pk, v2.pk,))) + + +class RevertTest(TestModelMixin, TestBase): + + def testRevert(self): + with reversion.create_revision(): + obj = TestModel.objects.create() + with reversion.create_revision(): + obj.name = "v2" + obj.save() + Version.objects.get_for_object(obj)[1].revert() + obj.refresh_from_db() + self.assertEqual(obj.name, "v1") + + def testRevertBadSerializedData(self): + with reversion.create_revision(): + obj = TestModel.objects.create() + Version.objects.get_for_object(obj).update(serialized_data="boom") + with self.assertRaises(reversion.RevertError): + Version.objects.get_for_object(obj).get().revert() + + def testRevertBadFormat(self): + with reversion.create_revision(): + obj = TestModel.objects.create() + Version.objects.get_for_object(obj).update(format="boom") + with self.assertRaises(reversion.RevertError): + Version.objects.get_for_object(obj).get().revert() + + +class RevisionRevertTest(TestModelMixin, TestBase): + + def testRevert(self): + with reversion.create_revision(): + obj_1 = TestModel.objects.create( + name="obj_1 v1" + ) + obj_2 = TestModel.objects.create( + name="obj_2 v1" + ) + with reversion.create_revision(): + obj_1.name = "obj_1 v2" + obj_1.save() + obj_2.name = "obj_2 v2" + obj_2.save() + Version.objects.get_for_object(obj_1)[1].revision.revert() + obj_1.refresh_from_db() + self.assertEqual(obj_1.name, "obj_1 v1") + obj_2.refresh_from_db() + self.assertEqual(obj_2.name, "obj_2 v1") + + +class RevisionRevertDeleteTest(TestBase): + + def testRevertDelete(self): + reversion.register(TestModel, follow=("related",)) + reversion.register(TestModelRelated) + with reversion.create_revision(): + obj = TestModel.objects.create() + obj_related = TestModelRelated.objects.create() + with reversion.create_revision(): + obj.related.add(obj_related) + obj.name = "v2" + obj.save() + Version.objects.get_for_object(obj)[1].revision.revert(delete=True) + obj.refresh_from_db() + self.assertEqual(obj.name, "v1") + self.assertFalse(TestModelRelated.objects.filter(pk=obj_related.pk).exists()) diff --git a/tests/test_app/tests/test_views.py b/tests/test_app/tests/test_views.py new file mode 100644 index 0000000..ea5fed2 --- /dev/null +++ b/tests/test_app/tests/test_views.py @@ -0,0 +1,42 @@ +from test_app.models import TestModel +from test_app.tests.base import TestBase, TestModelMixin, LoginMixin + + +class CreateRevisionTest(TestModelMixin, TestBase): + + def testCreateRevision(self): + response = self.client.post("/test-app/create-revision/") + obj = TestModel.objects.get(pk=response.content) + self.assertSingleRevision((obj,)) + + def testCreateRevisionGet(self): + self.client.get("/test-app/create-revision/") + self.assertNoRevision() + + +class CreateRevisionUserTest(LoginMixin, TestModelMixin, TestBase): + + def testCreateRevisionUser(self): + response = self.client.post("/test-app/create-revision/") + obj = TestModel.objects.get(pk=response.content) + self.assertSingleRevision((obj,), user=self.user) + + +class RevisionMixinTest(TestModelMixin, TestBase): + + def testRevisionMixin(self): + response = self.client.post("/test-app/revision-mixin/") + obj = TestModel.objects.get(pk=response.content) + self.assertSingleRevision((obj,)) + + def testRevisionMixinGet(self): + self.client.get("/test-app/revision-mixin/") + self.assertNoRevision() + + +class RevisionMixinUserTest(LoginMixin, TestModelMixin, TestBase): + + def testCreateRevisionUser(self): + response = self.client.post("/test-app/revision-mixin/") + obj = TestModel.objects.get(pk=response.content) + self.assertSingleRevision((obj,), user=self.user) diff --git a/tests/test_app/urls.py b/tests/test_app/urls.py new file mode 100644 index 0000000..a23682e --- /dev/null +++ b/tests/test_app/urls.py @@ -0,0 +1,10 @@ +from django.conf.urls import url +from test_app import views + + +urlpatterns = [ + url("^save-obj/", views.save_obj_view), + url("^save-obj-error/", views.save_obj_error_view), + url("^create-revision/", views.create_revision_view), + url("^revision-mixin/", views.RevisionMixinView.as_view()), +] diff --git a/tests/test_app/views.py b/tests/test_app/views.py new file mode 100644 index 0000000..eff02a0 --- /dev/null +++ b/tests/test_app/views.py @@ -0,0 +1,24 @@ +from django.http import HttpResponse +from django.views.generic.base import View +from reversion.views import create_revision, RevisionMixin +from test_app.models import TestModel + + +def save_obj_view(request): + return HttpResponse(TestModel.objects.create().id) + + +def save_obj_error_view(request): + TestModel.objects.create() + raise Exception("Boom!") + + +@create_revision() +def create_revision_view(request): + return save_obj_view(request) + + +class RevisionMixinView(RevisionMixin, View): + + def dispatch(self, request): + return save_obj_view(request) diff --git a/tests/test_project/__init__.py b/tests/test_project/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_project/settings.py b/tests/test_project/settings.py new file mode 100644 index 0000000..2df52e2 --- /dev/null +++ b/tests/test_project/settings.py @@ -0,0 +1,135 @@ +""" +Django settings for test_project project. + +Generated by "django-admin startproject" using Django 1.10a1. + +For more information on this file, see +https://docs.djangoproject.com/en/dev/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/dev/ref/settings/ +""" + +import os +import getpass + +# Build paths inside the project like this: os.path.join(BASE_DIR, ...) +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/dev/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = "lzu78x^s$rit0p*vdt)$1e&hh*)4y=xv))=@zsx(am7t=7406a" + +# SECURITY WARNING: don"t run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = [] + + +# Application definition + +INSTALLED_APPS = [ + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "reversion", + "test_app", +] + +MIDDLEWARE = MIDDLEWARE_CLASSES = [ + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", +] + +ROOT_URLCONF = "test_project.urls" + +TEMPLATES = [ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + ], + }, + }, +] + +WSGI_APPLICATION = "test_project.wsgi.application" + + +# Database +# https://docs.djangoproject.com/en/dev/ref/settings/#databases + +DATABASES = { + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": os.path.join(BASE_DIR, "db.sqlite3"), + }, + "postgres": { + "ENGINE": "django.db.backends.postgresql_psycopg2", + "NAME": os.environ.get("DJANGO_DATABASE_NAME_POSTGRES", "test_project"), + "USER": os.environ.get("DJANGO_DATABASE_USER_POSTGRES", getpass.getuser()), + "PASSWORD": os.environ.get("DJANGO_DATABASE_PASSWORD_POSTGRES", ""), + }, + "mysql": { + "ENGINE": "django.db.backends.mysql", + "NAME": os.environ.get("DJANGO_DATABASE_NAME_MYSQL", "test_project"), + "USER": os.environ.get("DJANGO_DATABASE_USER_MYSQL", getpass.getuser()), + "PASSWORD": os.environ.get("DJANGO_DATABASE_PASSWORD_MYSQL", ""), + }, +} + + +# Password validation +# https://docs.djangoproject.com/en/dev/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/dev/topics/i18n/ + +LANGUAGE_CODE = "en-us" + +TIME_ZONE = "UTC" + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/dev/howto/static-files/ + +STATIC_URL = "/static/" diff --git a/tests/test_project/urls.py b/tests/test_project/urls.py new file mode 100644 index 0000000..34c81e5 --- /dev/null +++ b/tests/test_project/urls.py @@ -0,0 +1,12 @@ +from django.conf.urls import url, include +from django.contrib import admin + +admin.autodiscover() + +urlpatterns = [ + + url(r"^admin/", admin.site.urls), + + url(r"^test-app/", include("test_app.urls")), + +] diff --git a/tests/test_project/wsgi.py b/tests/test_project/wsgi.py new file mode 100644 index 0000000..ee57abe --- /dev/null +++ b/tests/test_project/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for test_project project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/dev/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_project.settings") + +application = get_wsgi_application() diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..bd6c472 --- /dev/null +++ b/tox.ini @@ -0,0 +1,46 @@ +[tox] +envlist = + coverage-erase + test-{py27,py35,py36}-django{18,19,110,111} + test-{py35,py36}-djangomaster + coverage-report + flake8 + docs + +[testenv] +usedevelop = True +deps = + py27: mock + django18: Django>=1.8,<1.9 + django19: Django>=1.9,<1.10 + django110: Django>=1.10,<1.11 + django111: Django>=1.11a,<2.0 + djangomaster: https://github.com/django/django/archive/master.tar.gz + psycopg2>=2.6.1 + mysqlclient>=1.3.12 + coverage>=4.1 +ignore_outcome = + djangomaster: True +commands = + coverage-erase: coverage erase + test: coverage run --append tests/manage.py test tests + coverage-report: coverage report + +[testenv:flake8] +basepython = python3.5 +deps = + flake8>=2.5.4 +commands = + flake8 + +[flake8] +max-line-length=120 +exclude=venv,migrations,.tox + +[testenv:docs] +basepython = python3.5 +changedir = docs +deps = + sphinx>=1.4.2 +commands= + sphinx-build -n -W . _build