[Python-modules-commits] [django-reversion] 01/08: New upstream version 2.0.12

Michael Fladischer fladi at moszumanska.debian.org
Fri Dec 8 19:02:55 UTC 2017


This is an automated email from the git hooks/post-receive script.

fladi pushed a commit to branch debian/master
in repository django-reversion.

commit 7e23235da90c77a816e187502b550c112edc048e
Author: Michael Fladischer <FladischerMichael at fladi.at>
Date:   Fri Dec 8 19:26:12 2017 +0100

    New upstream version 2.0.12
---
 .travis.yml                               |  12 ++-
 CHANGELOG.rst                             |  14 ++++
 docs/admin.rst                            |   4 +-
 reversion/__init__.py                     |   2 +-
 reversion/admin.py                        |  14 +++-
 reversion/locale/uk/LC_MESSAGES/django.mo | Bin 0 -> 3451 bytes
 reversion/locale/uk/LC_MESSAGES/django.po | 134 ++++++++++++++++++++++++++++++
 reversion/models.py                       |  60 ++++++++++---
 tests/test_app/migrations/0001_initial.py |   6 ++
 tests/test_app/models.py                  |   7 ++
 tests/test_app/tests/base.py              |   5 ++
 tests/test_app/tests/test_admin.py        |  38 ++++++++-
 tests/test_app/tests/test_api.py          |   2 +-
 tests/test_app/tests/test_models.py       |  16 ++++
 tests/test_project/settings.py            |   1 -
 tests/test_project/urls.py                |   2 +-
 tox.ini                                   |  10 ++-
 17 files changed, 299 insertions(+), 28 deletions(-)

diff --git a/.travis.yml b/.travis.yml
index 6034fce..11c2535 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,7 +1,11 @@
+sudo: false
 language: python
 python:
-- 3.5
-sudo: false
+- 3.6
+addons:
+  apt:
+    packages:
+    - libmysqlclient-dev
 cache:
   directories:
   - "$HOME/.cache/pip"
@@ -14,9 +18,9 @@ matrix:
   fast_finish: true
 services:
 - postgresql
-addons:
-  mariadb: '10.1'
+- mysql
 install:
+- pyenv shell 2.7 3.5 3.6
 - pip install 'tox>=2.3.1'
 before_script:
 - mysql -e 'create database test_project'
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index a5ed2a7..422bbc0 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -3,6 +3,20 @@
 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
 -------------------
 
diff --git a/docs/admin.rst b/docs/admin.rst
index 8085a3f..ce6a08d 100644
--- a/docs/admin.rst
+++ b/docs/admin.rst
@@ -19,7 +19,7 @@ Registering models
 
 .. Note::
 
-    If you've registered your models using :ref:`reversion.register() <register>`, the admin class will use the configuration you specify there. Otherwise, the admin class will auto-register your model, following all inline model relations and parent superclasses.
+    If you've registered your models using :ref:`reversion.register() <register>`, the admin class will use the configuration you specify there. Otherwise, the admin class will auto-register your model, following all inline model relations and parent superclasses. Customize the admin registration by overriding :ref:`VersionAdmin.register() <VersionAdmin_register>`.
 
 
 Integration with 3rd party apps
@@ -90,6 +90,8 @@ A subclass of ``django.contrib.ModelAdmin`` providing rollback and recovery.
     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.
diff --git a/reversion/__init__.py b/reversion/__init__.py
index e3b63c4..72eafec 100644
--- a/reversion/__init__.py
+++ b/reversion/__init__.py
@@ -36,4 +36,4 @@ else:
         get_registered_models,
     )
 
-__version__ = VERSION = (2, 0, 10)
+__version__ = VERSION = (2, 0, 12)
diff --git a/reversion/admin.py b/reversion/admin.py
index a9331e3..cdefd91 100644
--- a/reversion/admin.py
+++ b/reversion/admin.py
@@ -30,6 +30,13 @@ from reversion.revisions import is_active, register, is_registered, set_comment,
 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"
@@ -112,7 +119,7 @@ class VersionAdmin(admin.ModelAdmin):
             inline_model = inline.model
             ct_field = inline.ct_field
             fk_name = inline.ct_fk_field
-            for field in self.model._meta.virtual_fields:
+            for field in private_fields(self.model._meta):
                 if (
                     isinstance(field, GenericRelation) and
                     remote_model(field) == inline_model and
@@ -184,7 +191,7 @@ class VersionAdmin(admin.ModelAdmin):
                 version.revision.revert(delete=True)
                 # Run the normal changeform view.
                 with self.create_revision(request):
-                    response = self.changeform_view(request, version.object_id, request.path, extra_context)
+                    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") % {
@@ -277,7 +284,6 @@ class VersionAdmin(admin.ModelAdmin):
         # Check if user has change permissions for model
         if not self.has_change_permission(request):
             raise PermissionDenied
-        object_id = unquote(object_id)  # Underscores in primary key get quoted to "_5F"
         opts = self.model._meta
         action_list = [
             {
@@ -290,7 +296,7 @@ class VersionAdmin(admin.ModelAdmin):
             for version
             in self._reversion_order_version_queryset(Version.objects.get_for_object_reference(
                 self.model,
-                object_id,
+                unquote(object_id),  # Underscores in primary key get quoted to "_5F"
             ).select_related("revision__user"))
         ]
         # Compile the context.
diff --git a/reversion/locale/uk/LC_MESSAGES/django.mo b/reversion/locale/uk/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..e8baa9e
Binary files /dev/null and b/reversion/locale/uk/LC_MESSAGES/django.mo differ
diff --git a/reversion/locale/uk/LC_MESSAGES/django.po b/reversion/locale/uk/LC_MESSAGES/django.po
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 <illia.volochii at gmail.com>, 2017.
+msgid ""
+msgstr ""
+"Project-Id-Version: django-reversion\n"
+"Report-Msgid-Bugs-To: https://github.com/etianen/django-reversion/issues\n"
+"POT-Creation-Date: 2017-11-03 12:02+0200\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: Illia Volochii <illia.volochii at gmail.com>\n"
+"Language-Team: Ukrainian\n"
+"Language: uk\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
+"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
+#: reversion/admin.py:83
+msgid "Initial version."
+msgstr "Початкова версія."
+
+#: reversion/admin.py:197
+#, python-format
+msgid "Reverted to previous version, saved on %(datetime)s"
+msgstr "Повернуто до попередньої версії, яка збережена %(datetime)s"
+
+#: reversion/admin.py:221
+#, python-format
+msgid "Recover %(name)s"
+msgstr "Відновити %(name)s"
+
+#: reversion/admin.py:237
+#, python-format
+msgid "Revert %(name)s"
+msgstr "Повернути %(name)s"
+
+#: reversion/admin.py:272 reversion/templates/reversion/change_list.html:7
+#: reversion/templates/reversion/recover_form.html:10
+#: reversion/templates/reversion/recover_list.html:10
+#, python-format
+msgid "Recover deleted %(name)s"
+msgstr "Відновити видалені %(name)s"
+
+#: reversion/models.py:31
+#, python-format
+msgid "Could not save %(object_repr)s version - missing dependency."
+msgstr "Неможливо зберегти версію \"%(object_repr)s\" - відсутня залежність."
+
+#: reversion/models.py:45
+msgid "date created"
+msgstr "дата створення"
+
+#: reversion/models.py:54
+msgid "user"
+msgstr "користувач"
+
+#: reversion/models.py:60
+msgid "comment"
+msgstr "коментар"
+
+#: reversion/models.py:242
+#, python-format
+msgid "Could not load %(object_repr)s version - incompatible version data."
+msgstr ""
+"Неможливо завантажити версію \"%(object_repr)s\" - несумісні дані версій."
+
+#: reversion/models.py:246
+#, python-format
+msgid "Could not load %(object_repr)s version - unknown serializer %(format)s."
+msgstr ""
+"Неможливо завантажити версію \"%(object_repr)s\" - невідомий серіалізатор "
+"%(format)s."
+
+#: reversion/templates/reversion/object_history.html:8
+msgid ""
+"Choose a date from the list below to revert to a previous version of this "
+"object."
+msgstr ""
+"Виберіть дату із списку нижче, щоб повернутися до попередньої версії цього "
+"об'єкта."
+
+#: reversion/templates/reversion/object_history.html:15
+#: reversion/templates/reversion/recover_list.html:23
+msgid "Date/time"
+msgstr "Дата/час"
+
+#: reversion/templates/reversion/object_history.html:16
+msgid "User"
+msgstr "Користувач"
+
+#: reversion/templates/reversion/object_history.html:17
+msgid "Action"
+msgstr "Дія"
+
+#: reversion/templates/reversion/object_history.html:38
+msgid ""
+"This object doesn't have a change history. It probably wasn't added via this "
+"admin site."
+msgstr ""
+"Цей об'єкт не має історії змін. Напевно, він був доданий не через цей сайт "
+"адміністрування."
+
+#: reversion/templates/reversion/recover_form.html:7
+#: reversion/templates/reversion/recover_list.html:7
+#: reversion/templates/reversion/revision_form.html:7
+msgid "Home"
+msgstr "Домівка"
+
+#: reversion/templates/reversion/recover_form.html:20
+msgid "Press the save button below to recover this version of the object."
+msgstr "Натисніть кнопку \"Зберегти\" нижче, щоб відновити цю версію об'єкта."
+
+#: reversion/templates/reversion/recover_list.html:17
+msgid ""
+"Choose a date from the list below to recover a deleted version of an object."
+msgstr "Виберіть дату із списку нижче, щоб відновити видалену версію об'єкта."
+
+#: reversion/templates/reversion/recover_list.html:37
+msgid "There are no deleted objects to recover."
+msgstr "Не знайдено видалених об'єктів для відновлення."
+
+#: reversion/templates/reversion/revision_form.html:11
+msgid "History"
+msgstr "Історія"
+
+#: reversion/templates/reversion/revision_form.html:12
+#, python-format
+msgid "Revert %(verbose_name)s"
+msgstr "Повернути %(verbose_name)s"
+
+#: reversion/templates/reversion/revision_form.html:21
+msgid "Press the save button below to revert to this version of the object."
+msgstr ""
+"Натисніть кнопку \"Зберегти\" нижче, щоб повернутися до цієї версії об'єкта."
diff --git a/reversion/models.py b/reversion/models.py
index 1eee6a9..f75266e 100644
--- a/reversion/models.py
+++ b/reversion/models.py
@@ -11,6 +11,8 @@ 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
@@ -85,9 +87,9 @@ class Revision(models.Model):
                         for obj in old_revision
                     )
                     # Delete objects that are no longer in the current revision.
-                    for item in current_revision:
-                        if item not in old_revision:
-                            item.delete(using=version.db)
+                    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)
 
@@ -99,6 +101,12 @@ class Revision(models.Model):
         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):
@@ -118,16 +126,46 @@ class VersionQuerySet(models.QuerySet):
         return self.get_for_object_reference(obj.__class__, obj.pk, model_db=model_db)
 
     def get_deleted(self, model, model_db=None):
-        return self.get_for_model(model, model_db=model_db).filter(
-            pk__in=_safe_subquery(
-                "exclude",
-                self.get_for_model(model, model_db=model_db),
-                "object_id",
-                model._default_manager.using(model_db),
-                model._meta.pk.name,
+        # 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),
+            ).order_by().values_list("latest_pk", flat=True)
+        # Perform the subquery.
+        return self.filter(
+            pk__in=subquery,
         )
 
     def get_unique(self):
diff --git a/tests/test_app/migrations/0001_initial.py b/tests/test_app/migrations/0001_initial.py
index 6e9f85b..330b6c2 100644
--- a/tests/test_app/migrations/0001_initial.py
+++ b/tests/test_app/migrations/0001_initial.py
@@ -69,6 +69,12 @@ class Migration(migrations.Migration):
             ],
             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',
diff --git a/tests/test_app/models.py b/tests/test_app/models.py
index ddc8b15..0714000 100644
--- a/tests/test_app/models.py
+++ b/tests/test_app/models.py
@@ -45,16 +45,23 @@ class TestModel(models.Model):
     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(
diff --git a/tests/test_app/tests/base.py b/tests/test_app/tests/base.py
index 5bf2fd7..65f7c03 100644
--- a/tests/test_app/tests/base.py
+++ b/tests/test_app/tests/base.py
@@ -30,6 +30,11 @@ class TestBaseMixin(object):
         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()):
diff --git a/tests/test_app/tests/test_admin.py b/tests/test_app/tests/test_admin.py
index 6491afd..2c902cd 100644
--- a/tests/test_app/tests/test_admin.py
+++ b/tests/test_app/tests/test_admin.py
@@ -8,7 +8,7 @@ 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
+from test_app.models import TestModel, TestModelParent, TestModelInline, TestModelGenericInline, TestModelEscapePK
 from test_app.tests.base import TestBase, LoginMixin
 
 
@@ -189,6 +189,42 @@ class AdminHistoryViewTest(LoginMixin, AdminMixin, TestBase):
         ))
 
 
+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
diff --git a/tests/test_app/tests/test_api.py b/tests/test_app/tests/test_api.py
index 24201a2..e12b102 100644
--- a/tests/test_app/tests/test_api.py
+++ b/tests/test_app/tests/test_api.py
@@ -106,7 +106,7 @@ class CreateRevisionTest(TestModelMixin, TestBase):
             with reversion.create_revision():
                 TestModel.objects.create()
                 raise Exception("Boom!")
-        except:
+        except Exception as ex:
             pass
         self.assertNoRevision()
 
diff --git a/tests/test_app/tests/test_models.py b/tests/test_app/tests/test_models.py
index f3f13c7..80f893d 100644
--- a/tests/test_app/tests/test_models.py
+++ b/tests/test_app/tests/test_models.py
@@ -177,6 +177,22 @@ class GetDeletedTest(TestModelMixin, TestBase):
         self.assertEqual(Version.objects.get_deleted(TestModel)[0].object_id, force_text(pk_2))
         self.assertEqual(Version.objects.get_deleted(TestModel)[1].object_id, force_text(pk_1))
 
+    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):
 
diff --git a/tests/test_project/settings.py b/tests/test_project/settings.py
index 395a30e..2df52e2 100644
--- a/tests/test_project/settings.py
+++ b/tests/test_project/settings.py
@@ -44,7 +44,6 @@ INSTALLED_APPS = [
 
 MIDDLEWARE = MIDDLEWARE_CLASSES = [
     "django.middleware.security.SecurityMiddleware",
-    "django.contrib.auth.middleware.SessionAuthenticationMiddleware",
     "django.contrib.sessions.middleware.SessionMiddleware",
     "django.middleware.common.CommonMiddleware",
     "django.middleware.csrf.CsrfViewMiddleware",
diff --git a/tests/test_project/urls.py b/tests/test_project/urls.py
index 32ddec0..34c81e5 100644
--- a/tests/test_project/urls.py
+++ b/tests/test_project/urls.py
@@ -7,6 +7,6 @@ urlpatterns = [
 
     url(r"^admin/", admin.site.urls),
 
-    url(r"^test-app/", include("test_app.urls", namespace="test")),
+    url(r"^test-app/", include("test_app.urls")),
 
 ]
diff --git a/tox.ini b/tox.ini
index b5a0629..bd6c472 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,7 +1,8 @@
 [tox]
 envlist =
     coverage-erase
-    test-{py27,py34,py35}-django{18,19,110,111}
+    test-{py27,py35,py36}-django{18,19,110,111}
+    test-{py35,py36}-djangomaster
     coverage-report
     flake8
     docs
@@ -14,12 +15,15 @@ deps =
     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>=mysqlclient
+    mysqlclient>=1.3.12
     coverage>=4.1
+ignore_outcome =
+    djangomaster: True
 commands =
     coverage-erase: coverage erase
-    test: coverage run tests/manage.py test tests
+    test: coverage run --append tests/manage.py test tests
     coverage-report: coverage report
 
 [testenv:flake8]

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/python-modules/packages/django-reversion.git



More information about the Python-modules-commits mailing list