[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