7 |
8 |
{% blocktrans %}Choose a date from the list below to revert to a previous version of this object.{% endblocktrans %}
9 |
10 |
11 | {% if action_list %}
12 |
13 |
14 |
15 | {% trans 'Date/time' %} |
16 | {% trans 'User' %} |
17 | {% trans 'Action' %} |
18 |
19 |
20 |
21 | {% for action in action_list %}
22 |
23 | {{action.revision.date_created|date:"DATETIME_FORMAT"}} |
24 |
25 | {% if action.revision.user %}
26 | {{action.revision.user.get_username}}
27 | {% if action.revision.user.get_full_name %} ({{action.revision.user.get_full_name}}){% endif %}
28 | {% else %}
29 | —
30 | {% endif %}
31 | |
32 | {{action.revision.get_comment|linebreaksbr|default:""}} |
33 |
34 | {% endfor %}
35 |
36 |
37 | {% else %}
38 |
{% trans "This object doesn't have a change history. It probably wasn't added via this admin site." %}
39 | {% endif %}
40 |
41 |
42 | {% endblock %}
43 |
--------------------------------------------------------------------------------
/reversion/templates/reversion/recover_form.html:
--------------------------------------------------------------------------------
1 | {% extends "reversion/revision_form.html" %}
2 | {% load i18n admin_urls %}
3 |
4 |
5 | {% block breadcrumbs %}
6 | {% blocktrans %}Press the save button below to recover this version of the object.{% endblocktrans %}
21 | {% endblock %}
22 |
23 |
24 | {% block submit_buttons_top %}{% with is_popup=1 %}{{block.super}}{% endwith %}{% endblock %}
25 | {% block submit_buttons_bottom %}{% with is_popup=1 %}{{block.super}}{% endwith %}{% endblock %}
26 |
--------------------------------------------------------------------------------
/reversion/templates/reversion/recover_list.html:
--------------------------------------------------------------------------------
1 | {% extends "admin/base_site.html" %}
2 | {% load i18n l10n admin_urls %}
3 |
4 |
5 | {% block breadcrumbs %}
6 |
17 |
{% blocktrans %}Choose a date from the list below to recover a deleted version of an object.{% endblocktrans %}
18 |
19 | {% if deleted %}
20 |
21 |
22 |
23 | {% trans 'Date/time' %} |
24 | {{opts.verbose_name|capfirst}} |
25 |
26 |
27 |
28 | {% for deletion in deleted %}
29 |
30 | {{deletion.revision.date_created}} |
31 | {{deletion.object_repr}} |
32 |
33 | {% endfor %}
34 |
35 |
36 | {% else %}
37 |
{% trans "There are no deleted objects to recover." %}
38 | {% endif %}
39 |
40 |
41 | {% endblock %}
42 |
--------------------------------------------------------------------------------
/reversion/templates/reversion/revision_form.html:
--------------------------------------------------------------------------------
1 | {% extends "admin/change_form.html" %}
2 | {% load i18n admin_urls %}
3 |
4 |
5 | {% block breadcrumbs %}
6 | {% blocktrans %}Press the save button below to revert to this version of the object.{% endblocktrans %}
22 | {% endblock %}
23 |
24 |
25 | {% block submit_buttons_top %}{% with is_popup=1 %}{{block.super}}{% endwith %}{% endblock %}
26 | {% block submit_buttons_bottom %}{% with is_popup=1 %}{{block.super}}{% endwith %}{% endblock %}
27 |
--------------------------------------------------------------------------------
/reversion/views.py:
--------------------------------------------------------------------------------
1 | from functools import wraps
2 |
3 | from reversion.revisions import create_revision as create_revision_base, set_user, get_user
4 |
5 |
6 | def _request_creates_revision(request):
7 | return request.method not in ("OPTIONS", "GET", "HEAD")
8 |
9 |
10 | def _set_user_from_request(request):
11 | if getattr(request, "user", None) and request.user.is_authenticated and get_user() is None:
12 | set_user(request.user)
13 |
14 |
15 | def create_revision(manage_manually=False, using=None, atomic=True, request_creates_revision=None):
16 | """
17 | View decorator that wraps the request in a revision.
18 |
19 | The revision will have it's user set from the request automatically.
20 | """
21 | request_creates_revision = request_creates_revision or _request_creates_revision
22 |
23 | def decorator(func):
24 | @wraps(func)
25 | def do_revision_view(request, *args, **kwargs):
26 | if request_creates_revision(request):
27 | with create_revision_base(manage_manually=manage_manually, using=using, atomic=atomic):
28 | response = func(request, *args, **kwargs)
29 | _set_user_from_request(request)
30 | return response
31 | return func(request, *args, **kwargs)
32 | return do_revision_view
33 | return decorator
34 |
35 |
36 | class RevisionMixin:
37 |
38 | """
39 | A class-based view mixin that wraps the request in a revision.
40 |
41 | The revision will have it's user set from the request automatically.
42 | """
43 |
44 | revision_manage_manually = False
45 |
46 | revision_using = None
47 |
48 | revision_atomic = True
49 |
50 | def __init__(self, *args, **kwargs):
51 | super().__init__(*args, **kwargs)
52 | self.dispatch = create_revision(
53 | manage_manually=self.revision_manage_manually,
54 | using=self.revision_using,
55 | atomic=self.revision_atomic,
56 | request_creates_revision=self.revision_request_creates_revision
57 | )(self.dispatch)
58 |
59 | def revision_request_creates_revision(self, request):
60 | return _request_creates_revision(request)
61 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [flake8]
2 | max-line-length=120
3 | exclude=venv,migrations
4 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import find_packages, setup
2 |
3 | from reversion import __version__
4 |
5 | # Load in babel support, if available.
6 | try:
7 | from babel.messages import frontend as babel
8 |
9 | cmdclass = {
10 | "compile_catalog": babel.compile_catalog,
11 | "extract_messages": babel.extract_messages,
12 | "init_catalog": babel.init_catalog,
13 | "update_catalog": babel.update_catalog,
14 | }
15 | except ImportError:
16 | cmdclass = {}
17 |
18 |
19 | def read(filepath):
20 | with open(filepath, encoding="utf-8") as f:
21 | return f.read()
22 |
23 |
24 | setup(
25 | name="django-reversion",
26 | version=".".join(str(x) for x in __version__),
27 | license="BSD",
28 | description="An extension to the Django web framework that provides version control for model instances.",
29 | long_description=read("README.rst"),
30 | author="Dave Hall",
31 | author_email="dave@etianen.com",
32 | url="https://github.com/etianen/django-reversion",
33 | zip_safe=False,
34 | packages=find_packages(),
35 | package_data={
36 | "reversion": ["locale/*/LC_MESSAGES/django.*", "templates/reversion/*.html"]
37 | },
38 | cmdclass=cmdclass,
39 | install_requires=[
40 | "django>=4.2",
41 | ],
42 | python_requires=">=3.8",
43 | classifiers=[
44 | "Development Status :: 5 - Production/Stable",
45 | "Environment :: Web Environment",
46 | "Intended Audience :: Developers",
47 | "License :: OSI Approved :: BSD License",
48 | "Operating System :: OS Independent",
49 | "Programming Language :: Python",
50 | "Programming Language :: Python :: 3.8",
51 | "Programming Language :: Python :: 3.9",
52 | "Programming Language :: Python :: 3.10",
53 | "Programming Language :: Python :: 3.11",
54 | "Programming Language :: Python :: 3.12",
55 | "Framework :: Django",
56 | ],
57 | )
58 |
--------------------------------------------------------------------------------
/tests/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import os
3 | import sys
4 |
5 | if __name__ == "__main__":
6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_project.settings")
7 | try:
8 | from django.core.management import execute_from_command_line
9 | except ImportError as exc:
10 | raise ImportError(
11 | "Couldn't import Django. Are you sure it's installed and "
12 | "available on your PYTHONPATH environment variable? Did you "
13 | "forget to activate a virtual environment?"
14 | ) from exc
15 | execute_from_command_line(sys.argv)
16 |
--------------------------------------------------------------------------------
/tests/test_app/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/etianen/django-reversion/9a7cd7419121e56d65247b050b7aa8a105aa000c/tests/test_app/__init__.py
--------------------------------------------------------------------------------
/tests/test_app/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 | from reversion.admin import VersionAdmin
3 | from test_app.models import TestModel, TestModelRelated
4 |
5 |
6 | class TestModelAdmin(VersionAdmin):
7 |
8 | filter_horizontal = ("related",)
9 |
10 |
11 | admin.site.register(TestModel, TestModelAdmin)
12 |
13 |
14 | admin.site.register(TestModelRelated, admin.ModelAdmin)
15 |
--------------------------------------------------------------------------------
/tests/test_app/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.1 on 2020-08-31 10:05
2 |
3 | from django.db import migrations, models
4 | import django.db.models.deletion
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | initial = True
10 |
11 | dependencies = [
12 | ('reversion', '0001_squashed_0004_auto_20160611_1202'),
13 | ('contenttypes', '0002_remove_content_type_name'),
14 | ]
15 |
16 | operations = [
17 | migrations.CreateModel(
18 | name='TestModel',
19 | fields=[
20 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
21 | ('name', models.CharField(default='v1', max_length=191)),
22 | ],
23 | ),
24 | migrations.CreateModel(
25 | name='TestModelEscapePK',
26 | fields=[
27 | ('name', models.CharField(max_length=191, primary_key=True, serialize=False)),
28 | ],
29 | ),
30 | migrations.CreateModel(
31 | name='TestModelInline',
32 | fields=[
33 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
34 | ('inline_name', models.CharField(default='v1', max_length=191)),
35 | ('test_model', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='test_app.testmodel')),
36 | ],
37 | ),
38 | migrations.CreateModel(
39 | name='TestModelRelated',
40 | fields=[
41 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
42 | ('name', models.CharField(default='v1', max_length=191)),
43 | ],
44 | ),
45 | migrations.CreateModel(
46 | name='TestModelWithNaturalKey',
47 | fields=[
48 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
49 | ('name', models.CharField(default='v1', max_length=191)),
50 | ],
51 | ),
52 | migrations.CreateModel(
53 | name='TestModelParent',
54 | fields=[
55 | ('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')),
56 | ('parent_name', models.CharField(default='parent v1', max_length=191)),
57 | ],
58 | bases=('test_app.testmodel',),
59 | ),
60 | migrations.CreateModel(
61 | name='TestModelThrough',
62 | fields=[
63 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
64 | ('name', models.CharField(default='v1', max_length=191)),
65 | ('test_model', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to='test_app.testmodel')),
66 | ('test_model_related', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to='test_app.testmodelrelated')),
67 | ],
68 | ),
69 | migrations.CreateModel(
70 | name='TestModelNestedInline',
71 | fields=[
72 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
73 | ('nested_inline_name', models.CharField(default='v1', max_length=191)),
74 | ('test_model_inline', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='test_app.testmodelinline')),
75 | ],
76 | ),
77 | migrations.CreateModel(
78 | name='TestModelInlineByNaturalKey',
79 | fields=[
80 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
81 | ('test_model', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='test_app.testmodelwithnaturalkey')),
82 | ],
83 | ),
84 | migrations.CreateModel(
85 | name='TestModelGenericInline',
86 | fields=[
87 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
88 | ('object_id', models.IntegerField()),
89 | ('inline_name', models.CharField(default='v1', max_length=191)),
90 | ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')),
91 | ],
92 | ),
93 | migrations.AddField(
94 | model_name='testmodel',
95 | name='related',
96 | field=models.ManyToManyField(blank=True, related_name='_testmodel_related_+', to='test_app.TestModelRelated'),
97 | ),
98 | migrations.AddField(
99 | model_name='testmodel',
100 | name='related_through',
101 | field=models.ManyToManyField(blank=True, related_name='_testmodel_related_through_+', through='test_app.TestModelThrough', to='test_app.TestModelRelated'),
102 | ),
103 | migrations.CreateModel(
104 | name='TestMeta',
105 | fields=[
106 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
107 | ('name', models.CharField(max_length=191)),
108 | ('revision', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='reversion.revision')),
109 | ],
110 | ),
111 | migrations.CreateModel(
112 | name='TestModelWithUniqueConstraint',
113 | fields=[
114 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
115 | ('name', models.CharField(max_length=191, unique=True)),
116 | ],
117 | ),
118 | ]
119 |
--------------------------------------------------------------------------------
/tests/test_app/migrations/0002_alter_testmodel_related_and_more.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 5.0.1 on 2024-01-30 19:07
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('test_app', '0001_initial'),
10 | ]
11 |
12 | operations = [
13 | migrations.AlterField(
14 | model_name='testmodel',
15 | name='related',
16 | field=models.ManyToManyField(blank=True, related_name='+', to='test_app.testmodelrelated'),
17 | ),
18 | migrations.AlterField(
19 | model_name='testmodel',
20 | name='related_through',
21 | field=models.ManyToManyField(blank=True, related_name='+', through='test_app.TestModelThrough', to='test_app.testmodelrelated'),
22 | ),
23 | ]
24 |
--------------------------------------------------------------------------------
/tests/test_app/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/etianen/django-reversion/9a7cd7419121e56d65247b050b7aa8a105aa000c/tests/test_app/migrations/__init__.py
--------------------------------------------------------------------------------
/tests/test_app/models.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 | from django.contrib.contenttypes.fields import GenericRelation
3 | from django.contrib.contenttypes.models import ContentType
4 | from reversion.models import Revision
5 |
6 |
7 | class TestModelGenericInline(models.Model):
8 |
9 | object_id = models.IntegerField()
10 |
11 | content_type = models.ForeignKey(
12 | ContentType,
13 | on_delete=models.CASCADE,
14 | )
15 |
16 | inline_name = models.CharField(
17 | max_length=191,
18 | default="v1",
19 | )
20 |
21 |
22 | class TestModel(models.Model):
23 |
24 | name = models.CharField(
25 | max_length=191,
26 | default="v1",
27 | )
28 |
29 | related = models.ManyToManyField(
30 | "TestModelRelated",
31 | blank=True,
32 | related_name="+",
33 | )
34 |
35 | related_through = models.ManyToManyField(
36 | "TestModelRelated",
37 | blank=True,
38 | through="TestModelThrough",
39 | related_name="+",
40 | )
41 |
42 | generic_inlines = GenericRelation(TestModelGenericInline)
43 |
44 |
45 | class TestModelEscapePK(models.Model):
46 |
47 | name = models.CharField(max_length=191, primary_key=True)
48 |
49 |
50 | class TestModelThrough(models.Model):
51 |
52 | test_model = models.ForeignKey(
53 | "TestModel",
54 | related_name="+",
55 | on_delete=models.CASCADE,
56 | )
57 |
58 | test_model_related = models.ForeignKey(
59 | "TestModelRelated",
60 | related_name="+",
61 | on_delete=models.CASCADE,
62 | )
63 |
64 | name = models.CharField(
65 | max_length=191,
66 | default="v1",
67 | )
68 |
69 |
70 | class TestModelRelated(models.Model):
71 |
72 | name = models.CharField(
73 | max_length=191,
74 | default="v1",
75 | )
76 |
77 |
78 | class TestModelParent(TestModel):
79 |
80 | parent_name = models.CharField(
81 | max_length=191,
82 | default="parent v1",
83 | )
84 |
85 |
86 | class TestModelInline(models.Model):
87 |
88 | test_model = models.ForeignKey(
89 | TestModel,
90 | on_delete=models.CASCADE,
91 | )
92 |
93 | inline_name = models.CharField(
94 | max_length=191,
95 | default="v1",
96 | )
97 |
98 |
99 | class TestModelNestedInline(models.Model):
100 | test_model_inline = models.ForeignKey(
101 | TestModelInline,
102 | on_delete=models.CASCADE,
103 | )
104 |
105 | nested_inline_name = models.CharField(
106 | max_length=191,
107 | default="v1",
108 | )
109 |
110 |
111 | class TestMeta(models.Model):
112 |
113 | revision = models.ForeignKey(
114 | Revision,
115 | on_delete=models.CASCADE,
116 | )
117 |
118 | name = models.CharField(
119 | max_length=191,
120 | )
121 |
122 |
123 | class TestModelWithNaturalKeyManager(models.Manager):
124 | def get_by_natural_key(self, name):
125 | return self.get(name=name)
126 |
127 |
128 | class TestModelWithNaturalKey(models.Model):
129 | name = models.CharField(
130 | max_length=191,
131 | default="v1",
132 | )
133 |
134 | objects = TestModelWithNaturalKeyManager()
135 |
136 | def natural_key(self):
137 | return (self.name,)
138 |
139 |
140 | class TestModelInlineByNaturalKey(models.Model):
141 | test_model = models.ForeignKey(
142 | TestModelWithNaturalKey,
143 | on_delete=models.CASCADE,
144 | )
145 |
146 |
147 | class TestModelWithUniqueConstraint(models.Model):
148 |
149 | name = models.CharField(
150 | max_length=191,
151 | unique=True,
152 | )
153 |
--------------------------------------------------------------------------------
/tests/test_app/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/etianen/django-reversion/9a7cd7419121e56d65247b050b7aa8a105aa000c/tests/test_app/tests/__init__.py
--------------------------------------------------------------------------------
/tests/test_app/tests/base.py:
--------------------------------------------------------------------------------
1 | from datetime import timedelta
2 | from importlib import import_module, reload
3 | from io import StringIO
4 |
5 | from django.conf import settings
6 | from django.contrib.auth.models import User
7 | from django.core.management import call_command
8 | from django.urls import clear_url_caches
9 | from django.test import TestCase, TransactionTestCase
10 | from django.test.utils import override_settings
11 | from django.utils import timezone
12 |
13 | import reversion
14 | from reversion.models import Revision, Version
15 | from test_app.models import TestModel, TestModelParent
16 |
17 |
18 | # Test helpers.
19 |
20 | class TestBaseMixin:
21 |
22 | databases = "__all__"
23 |
24 | def reloadUrls(self):
25 | reload(import_module(settings.ROOT_URLCONF))
26 | clear_url_caches()
27 |
28 | def setUp(self):
29 | super().setUp()
30 | for model in list(reversion.get_registered_models()):
31 | reversion.unregister(model)
32 |
33 | def tearDown(self):
34 | super().tearDown()
35 | for model in list(reversion.get_registered_models()):
36 | reversion.unregister(model)
37 |
38 | def callCommand(self, command, *args, **kwargs):
39 | kwargs.setdefault("stdout", StringIO())
40 | kwargs.setdefault("stderr", StringIO())
41 | kwargs.setdefault("verbosity", 2)
42 | return call_command(command, *args, **kwargs)
43 |
44 | def assertSingleRevision(self, objects, user=None, comment="", meta_names=(), date_created=None,
45 | using=None, model_db=None):
46 | revision = Version.objects.using(using).get_for_object(objects[0], model_db=model_db).get().revision
47 | self.assertEqual(revision.user, user)
48 | if hasattr(comment, 'pattern'):
49 | self.assertRegex(revision.get_comment(), comment)
50 | elif comment is not None: # Allow a wildcard comment.
51 | self.assertEqual(revision.get_comment(), comment)
52 | self.assertAlmostEqual(revision.date_created, date_created or timezone.now(), delta=timedelta(seconds=1))
53 | # Check meta.
54 | self.assertEqual(revision.testmeta_set.count(), len(meta_names))
55 | for meta_name in meta_names:
56 | self.assertTrue(revision.testmeta_set.filter(name=meta_name).exists())
57 | # Check objects.
58 | self.assertEqual(revision.version_set.count(), len(objects))
59 | for obj in objects:
60 | self.assertTrue(Version.objects.using(using).get_for_object(
61 | obj,
62 | model_db=model_db,
63 | ).filter(
64 | revision=revision,
65 | ).exists())
66 |
67 | def assertNoRevision(self, using=None):
68 | self.assertEqual(Revision.objects.using(using).all().count(), 0)
69 |
70 |
71 | class TestBase(TestBaseMixin, TestCase):
72 | pass
73 |
74 |
75 | class TestBaseTransaction(TestBaseMixin, TransactionTestCase):
76 | pass
77 |
78 |
79 | class TestModelMixin:
80 |
81 | def setUp(self):
82 | super().setUp()
83 | reversion.register(TestModel)
84 |
85 |
86 | class TestModelParentMixin(TestModelMixin):
87 |
88 | def setUp(self):
89 | super().setUp()
90 | reversion.register(TestModelParent, follow=("testmodel_ptr",))
91 |
92 |
93 | @override_settings(PASSWORD_HASHERS=["django.contrib.auth.hashers.MD5PasswordHasher"])
94 | class UserMixin(TestBase):
95 |
96 | def setUp(self):
97 | super().setUp()
98 | self.user = User(username="test", is_staff=True, is_superuser=True)
99 | self.user.set_password("password")
100 | self.user.save()
101 |
102 |
103 | class LoginMixin(UserMixin):
104 |
105 | def setUp(self):
106 | super().setUp()
107 | self.client.login(username="test", password="password")
108 |
--------------------------------------------------------------------------------
/tests/test_app/tests/test_admin.py:
--------------------------------------------------------------------------------
1 | import re
2 | from django.contrib import admin
3 | from django.contrib.contenttypes.admin import GenericTabularInline
4 | from django.shortcuts import resolve_url
5 | import reversion
6 | from reversion.admin import VersionAdmin
7 | from reversion.models import Version
8 | from test_app.models import TestModel, TestModelParent, TestModelInline, TestModelGenericInline, TestModelEscapePK
9 | from test_app.tests.base import TestBase, LoginMixin
10 |
11 |
12 | class AdminMixin(TestBase):
13 |
14 | def setUp(self):
15 | super().setUp()
16 | admin.site.register(TestModelParent, VersionAdmin)
17 | self.reloadUrls()
18 |
19 | def tearDown(self):
20 | super().tearDown()
21 | admin.site.unregister(TestModelParent)
22 | self.reloadUrls()
23 |
24 |
25 | class AdminRegisterTest(AdminMixin, TestBase):
26 |
27 | def setAutoRegister(self):
28 | self.assertTrue(reversion.is_registered(TestModelParent))
29 |
30 | def setAutoRegisterFollowsParent(self):
31 | self.assertTrue(reversion.is_registered(TestModel))
32 |
33 |
34 | class AdminAddViewTest(LoginMixin, AdminMixin, TestBase):
35 |
36 | def testAddView(self):
37 | self.client.post(resolve_url("admin:test_app_testmodelparent_add"), {
38 | "name": "v1",
39 | "parent_name": "parent_v1",
40 | })
41 | obj = TestModelParent.objects.get()
42 | self.assertSingleRevision(
43 | (obj, obj.testmodel_ptr), user=self.user, comment="Added."
44 | )
45 |
46 |
47 | class AdminUpdateViewTest(LoginMixin, AdminMixin, TestBase):
48 |
49 | def testUpdateView(self):
50 | obj = TestModelParent.objects.create()
51 | self.client.post(resolve_url("admin:test_app_testmodelparent_change", obj.pk), {
52 | "name": "v2",
53 | "parent_name": "parent v2",
54 | })
55 | self.assertSingleRevision(
56 | (obj, obj.testmodel_ptr), user=self.user,
57 | # Django 3.0 changed formatting a bit.
58 | comment=re.compile(r"Changed [nN]ame and [pP]arent[ _]name\.")
59 | )
60 |
61 |
62 | class AdminChangelistView(LoginMixin, AdminMixin, TestBase):
63 |
64 | def testChangelistView(self):
65 | obj = TestModelParent.objects.create()
66 | response = self.client.get(resolve_url("admin:test_app_testmodelparent_changelist"))
67 | self.assertContains(response, resolve_url("admin:test_app_testmodelparent_change", obj.pk))
68 |
69 |
70 | class AdminRevisionViewTest(LoginMixin, AdminMixin, TestBase):
71 |
72 | def setUp(self):
73 | super().setUp()
74 | with reversion.create_revision():
75 | self.obj = TestModelParent.objects.create()
76 | with reversion.create_revision():
77 | self.obj.name = "v2"
78 | self.obj.parent_name = "parent v2"
79 | self.obj.save()
80 |
81 | def testRevisionView(self):
82 | response = self.client.get(resolve_url(
83 | "admin:test_app_testmodelparent_revision",
84 | self.obj.pk,
85 | Version.objects.get_for_object(self.obj)[1].pk,
86 | ))
87 | self.assertContains(response, 'value="v1"')
88 | self.assertContains(response, 'value="parent v1"')
89 | # Test that the changes were rolled back.
90 | self.obj.refresh_from_db()
91 | self.assertEqual(self.obj.name, "v2")
92 | self.assertEqual(self.obj.parent_name, "parent v2")
93 | self.assertIn("revert", response.context)
94 | self.assertTrue(response.context["revert"])
95 |
96 | def testRevisionViewOldRevision(self):
97 | response = self.client.get(resolve_url(
98 | "admin:test_app_testmodelparent_revision",
99 | self.obj.pk,
100 | Version.objects.get_for_object(self.obj)[0].pk,
101 | ))
102 | self.assertContains(response, 'value="v2"')
103 | self.assertContains(response, 'value="parent v2"')
104 |
105 | def testRevisionViewRevertError(self):
106 | Version.objects.get_for_object(self.obj).update(format="boom")
107 | response = self.client.get(resolve_url(
108 | "admin:test_app_testmodelparent_revision",
109 | self.obj.pk,
110 | Version.objects.get_for_object(self.obj)[1].pk,
111 | ))
112 | self.assertEqual(
113 | response["Location"].replace("http://testserver", ""),
114 | resolve_url("admin:test_app_testmodelparent_changelist"),
115 | )
116 |
117 | def testRevisionViewRevert(self):
118 | self.client.post(resolve_url(
119 | "admin:test_app_testmodelparent_revision",
120 | self.obj.pk,
121 | Version.objects.get_for_object(self.obj)[1].pk,
122 | ), {
123 | "name": "v1",
124 | "parent_name": "parent v1",
125 | })
126 | self.obj.refresh_from_db()
127 | self.assertEqual(self.obj.name, "v1")
128 | self.assertEqual(self.obj.parent_name, "parent v1")
129 |
130 |
131 | class AdminRecoverViewTest(LoginMixin, AdminMixin, TestBase):
132 |
133 | def setUp(self):
134 | super().setUp()
135 | with reversion.create_revision():
136 | obj = TestModelParent.objects.create()
137 | obj.delete()
138 |
139 | def testRecoverView(self):
140 | response = self.client.get(resolve_url(
141 | "admin:test_app_testmodelparent_recover",
142 | Version.objects.get_for_model(TestModelParent).get().pk,
143 | ))
144 | self.assertContains(response, 'value="v1"')
145 | self.assertContains(response, 'value="parent v1"')
146 | self.assertIn("recover", response.context)
147 | self.assertTrue(response.context["recover"])
148 |
149 | def testRecoverViewRecover(self):
150 | self.client.post(resolve_url(
151 | "admin:test_app_testmodelparent_recover",
152 | Version.objects.get_for_model(TestModelParent).get().pk,
153 | ), {
154 | "name": "v1",
155 | "parent_name": "parent v1",
156 | })
157 | obj = TestModelParent.objects.get()
158 | self.assertEqual(obj.name, "v1")
159 | self.assertEqual(obj.parent_name, "parent v1")
160 |
161 |
162 | class AdminRecoverlistViewTest(LoginMixin, AdminMixin, TestBase):
163 |
164 | def testRecoverlistView(self):
165 | with reversion.create_revision():
166 | obj = TestModelParent.objects.create()
167 | obj.delete()
168 | response = self.client.get(resolve_url("admin:test_app_testmodelparent_recoverlist"))
169 | self.assertContains(response, resolve_url(
170 | "admin:test_app_testmodelparent_recover",
171 | Version.objects.get_for_model(TestModelParent).get().pk,
172 | ))
173 |
174 |
175 | class AdminHistoryViewTest(LoginMixin, AdminMixin, TestBase):
176 |
177 | def testHistorylistView(self):
178 | with reversion.create_revision():
179 | obj = TestModelParent.objects.create()
180 | response = self.client.get(resolve_url("admin:test_app_testmodelparent_history", obj.pk))
181 | self.assertContains(response, resolve_url(
182 | "admin:test_app_testmodelparent_revision",
183 | obj.pk,
184 | Version.objects.get_for_model(TestModelParent).get().pk,
185 | ))
186 |
187 |
188 | class AdminQuotingTest(LoginMixin, AdminMixin, TestBase):
189 |
190 | def setUp(self):
191 | super().setUp()
192 | admin.site.register(TestModelEscapePK, VersionAdmin)
193 | self.reloadUrls()
194 |
195 | def tearDown(self):
196 | super().tearDown()
197 | admin.site.unregister(TestModelEscapePK)
198 | self.reloadUrls()
199 |
200 | def testHistoryWithQuotedPrimaryKey(self):
201 | pk = 'ABC_123'
202 | quoted_pk = admin.utils.quote(pk)
203 | # test is invalid if quoting does not change anything
204 | assert quoted_pk != pk
205 |
206 | with reversion.create_revision():
207 | obj = TestModelEscapePK.objects.create(name=pk)
208 |
209 | revision_url = resolve_url(
210 | "admin:test_app_testmodelescapepk_revision",
211 | quoted_pk,
212 | Version.objects.get_for_object(obj).get().pk,
213 | )
214 | history_url = resolve_url(
215 | "admin:test_app_testmodelescapepk_history",
216 | quoted_pk
217 | )
218 | response = self.client.get(history_url)
219 | self.assertContains(response, revision_url)
220 | response = self.client.get(revision_url)
221 | self.assertContains(response, f'value="{pk}"')
222 |
223 |
224 | class TestModelInlineAdmin(admin.TabularInline):
225 |
226 | model = TestModelInline
227 |
228 |
229 | class TestModelGenericInlineAdmin(GenericTabularInline):
230 |
231 | model = TestModelGenericInline
232 |
233 |
234 | class TestModelParentAdmin(VersionAdmin):
235 |
236 | inlines = (TestModelInlineAdmin, TestModelGenericInlineAdmin)
237 |
238 |
239 | class AdminRegisterInlineTest(TestBase):
240 |
241 | def setUp(self):
242 | super().setUp()
243 | admin.site.register(TestModelParent, TestModelParentAdmin)
244 | self.reloadUrls()
245 |
246 | def tearDown(self):
247 | super().tearDown()
248 | admin.site.unregister(TestModelParent)
249 | self.reloadUrls()
250 |
251 | def testAutoRegisterInline(self):
252 | self.assertTrue(reversion.is_registered(TestModelInline))
253 |
254 | def testAutoRegisterGenericInline(self):
255 | self.assertTrue(reversion.is_registered(TestModelGenericInline))
256 |
--------------------------------------------------------------------------------
/tests/test_app/tests/test_api.py:
--------------------------------------------------------------------------------
1 | from datetime import timedelta
2 | from unittest.mock import MagicMock
3 |
4 | from django.contrib.auth.models import User
5 | from django.db import models
6 | from django.db.transaction import get_connection
7 | from django.utils import timezone
8 | import reversion
9 | from test_app.models import TestModel, TestModelRelated, TestModelThrough, TestModelParent, TestMeta
10 | from test_app.tests.base import TestBase, TestBaseTransaction, TestModelMixin, UserMixin
11 |
12 |
13 | class SaveTest(TestModelMixin, TestBase):
14 |
15 | def testModelSave(self):
16 | TestModel.objects.create()
17 | self.assertNoRevision()
18 |
19 |
20 | class IsRegisteredTest(TestModelMixin, TestBase):
21 |
22 | def testIsRegistered(self):
23 | self.assertTrue(reversion.is_registered(TestModel))
24 |
25 |
26 | class IsRegisterUnregisteredTest(TestBase):
27 |
28 | def testIsRegisteredFalse(self):
29 | self.assertFalse(reversion.is_registered(TestModel))
30 |
31 |
32 | class GetRegisteredModelsTest(TestModelMixin, TestBase):
33 |
34 | def testGetRegisteredModels(self):
35 | self.assertEqual(set(reversion.get_registered_models()), {TestModel})
36 |
37 |
38 | class RegisterTest(TestBase):
39 |
40 | def testRegister(self):
41 | reversion.register(TestModel)
42 | self.assertTrue(reversion.is_registered(TestModel))
43 |
44 | def testRegisterDecorator(self):
45 | @reversion.register()
46 | class TestModelDecorater(models.Model):
47 | pass
48 | self.assertTrue(reversion.is_registered(TestModelDecorater))
49 |
50 | def testRegisterAlreadyRegistered(self):
51 | reversion.register(TestModel)
52 | with self.assertRaises(reversion.RegistrationError):
53 | reversion.register(TestModel)
54 |
55 | def testRegisterM2MSThroughLazy(self):
56 | # When register is used as a decorator in models.py, lazy relations haven't had a chance to be resolved, so
57 | # will still be a string.
58 | @reversion.register()
59 | class TestModelLazy(models.Model):
60 | related = models.ManyToManyField(
61 | TestModelRelated,
62 | through="TestModelThroughLazy",
63 | )
64 |
65 | class TestModelThroughLazy(models.Model):
66 | pass
67 |
68 |
69 | class UnregisterTest(TestModelMixin, TestBase):
70 |
71 | def testUnregister(self):
72 | reversion.unregister(TestModel)
73 | self.assertFalse(reversion.is_registered(TestModel))
74 |
75 |
76 | class UnregisterUnregisteredTest(TestBase):
77 |
78 | def testUnregisterNotRegistered(self):
79 | with self.assertRaises(reversion.RegistrationError):
80 | reversion.unregister(User)
81 |
82 |
83 | class CreateRevisionTest(TestModelMixin, TestBase):
84 |
85 | def testCreateRevision(self):
86 | with reversion.create_revision():
87 | obj = TestModel.objects.create()
88 | self.assertSingleRevision((obj,))
89 |
90 | def testCreateRevisionNested(self):
91 | with reversion.create_revision():
92 | with reversion.create_revision():
93 | obj = TestModel.objects.create()
94 | self.assertSingleRevision((obj,))
95 |
96 | def testCreateRevisionEmpty(self):
97 | with reversion.create_revision():
98 | pass
99 | self.assertNoRevision()
100 |
101 | def testCreateRevisionException(self):
102 | try:
103 | with reversion.create_revision():
104 | TestModel.objects.create()
105 | raise Exception("Boom!")
106 | except Exception:
107 | pass
108 | self.assertNoRevision()
109 |
110 | def testCreateRevisionDecorator(self):
111 | obj = reversion.create_revision()(TestModel.objects.create)()
112 | self.assertSingleRevision((obj,))
113 |
114 | def testPreRevisionCommitSignal(self):
115 | _callback = MagicMock()
116 | reversion.signals.pre_revision_commit.connect(_callback)
117 |
118 | with reversion.create_revision():
119 | TestModel.objects.create()
120 | self.assertEqual(_callback.call_count, 1)
121 |
122 | def testPostRevisionCommitSignal(self):
123 | _callback = MagicMock()
124 | reversion.signals.post_revision_commit.connect(_callback)
125 |
126 | with reversion.create_revision():
127 | TestModel.objects.create()
128 | self.assertEqual(_callback.call_count, 1)
129 |
130 |
131 | class CreateRevisionAtomicTest(TestModelMixin, TestBaseTransaction):
132 | def testCreateRevisionAtomic(self):
133 | self.assertFalse(get_connection().in_atomic_block)
134 | with reversion.create_revision():
135 | self.assertTrue(get_connection().in_atomic_block)
136 |
137 | def testCreateRevisionNonAtomic(self):
138 | self.assertFalse(get_connection().in_atomic_block)
139 | with reversion.create_revision(atomic=False):
140 | self.assertFalse(get_connection().in_atomic_block)
141 |
142 | def testCreateRevisionInOnCommitHandler(self):
143 | from django.db import transaction
144 | from reversion.models import Revision
145 |
146 | self.assertEqual(Revision.objects.all().count(), 0)
147 |
148 | with reversion.create_revision(atomic=True):
149 | model = TestModel.objects.create()
150 |
151 | def on_commit():
152 | with reversion.create_revision(atomic=True):
153 | model.name = 'oncommit'
154 | model.save()
155 |
156 | transaction.on_commit(on_commit)
157 |
158 | self.assertEqual(Revision.objects.all().count(), 2)
159 |
160 |
161 | class CreateRevisionManageManuallyTest(TestModelMixin, TestBase):
162 |
163 | def testCreateRevisionManageManually(self):
164 | with reversion.create_revision(manage_manually=True):
165 | TestModel.objects.create()
166 | self.assertNoRevision()
167 |
168 | def testCreateRevisionManageManuallyNested(self):
169 | with reversion.create_revision():
170 | with reversion.create_revision(manage_manually=True):
171 | TestModel.objects.create()
172 | self.assertNoRevision()
173 |
174 |
175 | class CreateRevisionDbTest(TestModelMixin, TestBase):
176 | databases = {"default", "mysql", "postgres"}
177 |
178 | def testCreateRevisionMultiDb(self):
179 | with reversion.create_revision(using="mysql"), reversion.create_revision(using="postgres"):
180 | obj = TestModel.objects.create()
181 | self.assertNoRevision()
182 | self.assertSingleRevision((obj,), using="mysql")
183 | self.assertSingleRevision((obj,), using="postgres")
184 |
185 |
186 | class CreateRevisionFollowTest(TestBase):
187 |
188 | def testCreateRevisionFollow(self):
189 | reversion.register(TestModel, follow=("related",))
190 | reversion.register(TestModelRelated)
191 | obj_related = TestModelRelated.objects.create()
192 | with reversion.create_revision():
193 | obj = TestModel.objects.create()
194 | obj.related.add(obj_related)
195 | self.assertSingleRevision((obj, obj_related))
196 |
197 | def testCreateRevisionFollowThrough(self):
198 | reversion.register(TestModel, follow=("related_through",))
199 | reversion.register(TestModelThrough, follow=("test_model", "test_model_related",))
200 | reversion.register(TestModelRelated)
201 | obj_related = TestModelRelated.objects.create()
202 | with reversion.create_revision():
203 | obj = TestModel.objects.create()
204 | obj_through = TestModelThrough.objects.create(
205 | test_model=obj,
206 | test_model_related=obj_related,
207 | )
208 | self.assertSingleRevision((obj, obj_through, obj_related))
209 |
210 | def testCreateRevisionFollowInvalid(self):
211 | reversion.register(TestModel, follow=("name",))
212 | with reversion.create_revision():
213 | with self.assertRaises(reversion.RegistrationError):
214 | TestModel.objects.create()
215 |
216 |
217 | class CreateRevisionIgnoreDuplicatesTest(TestBase):
218 |
219 | def testCreateRevisionIgnoreDuplicates(self):
220 | reversion.register(TestModel, ignore_duplicates=True)
221 | with reversion.create_revision():
222 | obj = TestModel.objects.create()
223 | with reversion.create_revision():
224 | obj.save()
225 | self.assertSingleRevision((obj,))
226 |
227 |
228 | class CreateRevisionInheritanceTest(TestModelMixin, TestBase):
229 |
230 | def testCreateRevisionInheritance(self):
231 | reversion.register(TestModelParent, follow=("testmodel_ptr",))
232 | with reversion.create_revision():
233 | obj = TestModelParent.objects.create()
234 | self.assertSingleRevision((obj, obj.testmodel_ptr))
235 |
236 |
237 | class SetCommentTest(TestModelMixin, TestBase):
238 |
239 | def testSetComment(self):
240 | with reversion.create_revision():
241 | reversion.set_comment("comment v1")
242 | obj = TestModel.objects.create()
243 | self.assertSingleRevision((obj,), comment="comment v1")
244 |
245 | def testSetCommentNoBlock(self):
246 | with self.assertRaises(reversion.RevisionManagementError):
247 | reversion.set_comment("comment v1")
248 |
249 |
250 | class GetCommentTest(TestBase):
251 |
252 | def testGetComment(self):
253 | with reversion.create_revision():
254 | reversion.set_comment("comment v1")
255 | self.assertEqual(reversion.get_comment(), "comment v1")
256 |
257 | def testGetCommentDefault(self):
258 | with reversion.create_revision():
259 | self.assertEqual(reversion.get_comment(), "")
260 |
261 | def testGetCommentNoBlock(self):
262 | with self.assertRaises(reversion.RevisionManagementError):
263 | reversion.get_comment()
264 |
265 |
266 | class SetUserTest(UserMixin, TestModelMixin, TestBase):
267 |
268 | def testSetUser(self):
269 | with reversion.create_revision():
270 | reversion.set_user(self.user)
271 | obj = TestModel.objects.create()
272 | self.assertSingleRevision((obj,), user=self.user)
273 |
274 | def testSetUserNoBlock(self):
275 | with self.assertRaises(reversion.RevisionManagementError):
276 | reversion.set_user(self.user)
277 |
278 |
279 | class GetUserTest(UserMixin, TestBase):
280 |
281 | def testGetUser(self):
282 | with reversion.create_revision():
283 | reversion.set_user(self.user)
284 | self.assertEqual(reversion.get_user(), self.user)
285 |
286 | def testGetUserDefault(self):
287 | with reversion.create_revision():
288 | self.assertEqual(reversion.get_user(), None)
289 |
290 | def testGetUserNoBlock(self):
291 | with self.assertRaises(reversion.RevisionManagementError):
292 | reversion.get_user()
293 |
294 |
295 | class SetDateCreatedTest(TestModelMixin, TestBase):
296 |
297 | def testSetDateCreated(self):
298 | date_created = timezone.now() - timedelta(days=20)
299 | with reversion.create_revision():
300 | reversion.set_date_created(date_created)
301 | obj = TestModel.objects.create()
302 | self.assertSingleRevision((obj,), date_created=date_created)
303 |
304 | def testDateCreatedNoBlock(self):
305 | with self.assertRaises(reversion.RevisionManagementError):
306 | reversion.set_date_created(timezone.now())
307 |
308 |
309 | class GetDateCreatedTest(TestBase):
310 |
311 | def testGetDateCreated(self):
312 | date_created = timezone.now() - timedelta(days=20)
313 | with reversion.create_revision():
314 | reversion.set_date_created(date_created)
315 | self.assertEqual(reversion.get_date_created(), date_created)
316 |
317 | def testGetDateCreatedDefault(self):
318 | with reversion.create_revision():
319 | self.assertAlmostEqual(reversion.get_date_created(), timezone.now(), delta=timedelta(seconds=1))
320 |
321 | def testGetDateCreatedNoBlock(self):
322 | with self.assertRaises(reversion.RevisionManagementError):
323 | reversion.get_date_created()
324 |
325 |
326 | class AddMetaTest(TestModelMixin, TestBase):
327 | databases = {"default", "mysql", "postgres"}
328 |
329 | def testAddMeta(self):
330 | with reversion.create_revision():
331 | reversion.add_meta(TestMeta, name="meta v1")
332 | obj = TestModel.objects.create()
333 | self.assertSingleRevision((obj,), meta_names=("meta v1",))
334 |
335 | def testAddMetaNoBlock(self):
336 | with self.assertRaises(reversion.RevisionManagementError):
337 | reversion.add_meta(TestMeta, name="meta v1")
338 |
339 | def testAddMetaMultDb(self):
340 | with reversion.create_revision(using="mysql"), reversion.create_revision(using="postgres"):
341 | obj = TestModel.objects.create()
342 | reversion.add_meta(TestMeta, name="meta v1")
343 | self.assertNoRevision()
344 | self.assertSingleRevision((obj,), meta_names=("meta v1",), using="mysql")
345 | self.assertSingleRevision((obj,), meta_names=("meta v1",), using="postgres")
346 |
--------------------------------------------------------------------------------
/tests/test_app/tests/test_commands.py:
--------------------------------------------------------------------------------
1 | import json
2 | from datetime import timedelta
3 | from django.core.management import CommandError
4 | from django.utils import timezone
5 | import reversion
6 | from test_app.models import TestModel
7 | from test_app.tests.base import TestBase, TestModelMixin
8 |
9 |
10 | class CreateInitialRevisionsTest(TestModelMixin, TestBase):
11 |
12 | def testCreateInitialRevisions(self):
13 | obj = TestModel.objects.create()
14 | self.callCommand("createinitialrevisions")
15 | self.assertSingleRevision((obj,), comment="Initial version.")
16 |
17 | def testCreateInitialRevisionsAlreadyCreated(self):
18 | obj = TestModel.objects.create()
19 | self.callCommand("createinitialrevisions")
20 | self.callCommand("createinitialrevisions")
21 | self.assertSingleRevision((obj,), comment="Initial version.")
22 |
23 |
24 | class CreateInitialRevisionsAppLabelTest(TestModelMixin, TestBase):
25 |
26 | def testCreateInitialRevisionsAppLabel(self):
27 | obj = TestModel.objects.create()
28 | self.callCommand("createinitialrevisions", "test_app")
29 | self.assertSingleRevision((obj,), comment="Initial version.")
30 |
31 | def testCreateInitialRevisionsAppLabelMissing(self):
32 | with self.assertRaises(CommandError):
33 | self.callCommand("createinitialrevisions", "boom")
34 |
35 | def testCreateInitialRevisionsModel(self):
36 | obj = TestModel.objects.create()
37 | self.callCommand("createinitialrevisions", "test_app.TestModel")
38 | self.assertSingleRevision((obj,), comment="Initial version.")
39 |
40 | def testCreateInitialRevisionsModelMissing(self):
41 | with self.assertRaises(CommandError):
42 | self.callCommand("createinitialrevisions", "test_app.boom")
43 |
44 | def testCreateInitialRevisionsModelMissingApp(self):
45 | with self.assertRaises(CommandError):
46 | self.callCommand("createinitialrevisions", "boom.boom")
47 |
48 | def testCreateInitialRevisionsModelNotRegistered(self):
49 | TestModel.objects.create()
50 | self.callCommand("createinitialrevisions", "auth.User")
51 | self.assertNoRevision()
52 |
53 |
54 | class CreateInitialRevisionsDbTest(TestModelMixin, TestBase):
55 | databases = {"default", "mysql", "postgres"}
56 |
57 | def testCreateInitialRevisionsDb(self):
58 | obj = TestModel.objects.create()
59 | self.callCommand("createinitialrevisions", using="postgres")
60 | self.assertNoRevision()
61 | self.assertSingleRevision((obj,), comment="Initial version.", using="postgres")
62 |
63 | def testCreateInitialRevisionsDbMySql(self):
64 | obj = TestModel.objects.create()
65 | self.callCommand("createinitialrevisions", using="mysql")
66 | self.assertNoRevision()
67 | self.assertSingleRevision((obj,), comment="Initial version.", using="mysql")
68 |
69 |
70 | class CreateInitialRevisionsModelDbTest(TestModelMixin, TestBase):
71 | databases = {"default", "postgres"}
72 |
73 | def testCreateInitialRevisionsModelDb(self):
74 | obj = TestModel.objects.db_manager("postgres").create()
75 | self.callCommand("createinitialrevisions", model_db="postgres")
76 | self.assertSingleRevision((obj,), comment="Initial version.", model_db="postgres")
77 |
78 |
79 | class CreateInitialRevisionsCommentTest(TestModelMixin, TestBase):
80 |
81 | def testCreateInitialRevisionsComment(self):
82 | obj = TestModel.objects.create()
83 | self.callCommand("createinitialrevisions", comment="comment v1")
84 | self.assertSingleRevision((obj,), comment="comment v1")
85 |
86 |
87 | class CreateInitialRevisionsMetaTest(TestModelMixin, TestBase):
88 | def testCreateInitialRevisionsComment(self):
89 | obj = TestModel.objects.create()
90 | meta_name = "meta name"
91 | meta = json.dumps({"test_app.TestMeta": {"name": meta_name}})
92 | self.callCommand("createinitialrevisions", "--meta", meta)
93 | self.assertSingleRevision((obj,), meta_names=(meta_name, ), comment="Initial version.")
94 |
95 |
96 | class DeleteRevisionsTest(TestModelMixin, TestBase):
97 |
98 | def testDeleteRevisions(self):
99 | with reversion.create_revision():
100 | TestModel.objects.create()
101 | self.callCommand("deleterevisions")
102 | self.assertNoRevision()
103 |
104 |
105 | class DeleteRevisionsAppLabelTest(TestModelMixin, TestBase):
106 |
107 | def testDeleteRevisionsAppLabel(self):
108 | with reversion.create_revision():
109 | TestModel.objects.create()
110 | self.callCommand("deleterevisions", "test_app")
111 | self.assertNoRevision()
112 |
113 | def testDeleteRevisionsAppLabelMissing(self):
114 | with self.assertRaises(CommandError):
115 | self.callCommand("deleterevisions", "boom")
116 |
117 | def testDeleteRevisionsModel(self):
118 | with reversion.create_revision():
119 | TestModel.objects.create()
120 | self.callCommand("deleterevisions", "test_app.TestModel")
121 | self.assertNoRevision()
122 |
123 | def testDeleteRevisionsModelMissing(self):
124 | with self.assertRaises(CommandError):
125 | self.callCommand("deleterevisions", "test_app.boom")
126 |
127 | def testDeleteRevisionsModelMissingApp(self):
128 | with self.assertRaises(CommandError):
129 | self.callCommand("deleterevisions", "boom.boom")
130 |
131 | def testDeleteRevisionsModelNotRegistered(self):
132 | with reversion.create_revision():
133 | obj = TestModel.objects.create()
134 | self.callCommand("deleterevisions", "auth.User")
135 | self.assertSingleRevision((obj,))
136 |
137 |
138 | class DeleteRevisionsDbTest(TestModelMixin, TestBase):
139 | databases = {"default", "mysql", "postgres"}
140 |
141 | def testDeleteRevisionsDb(self):
142 | with reversion.create_revision(using="postgres"):
143 | TestModel.objects.create()
144 | self.callCommand("deleterevisions", using="postgres")
145 | self.assertNoRevision(using="postgres")
146 |
147 | def testDeleteRevisionsDbMySql(self):
148 | with reversion.create_revision(using="mysql"):
149 | TestModel.objects.create()
150 | self.callCommand("deleterevisions", using="mysql")
151 | self.assertNoRevision(using="mysql")
152 |
153 | def testDeleteRevisionsDbNoMatch(self):
154 | with reversion.create_revision():
155 | obj = TestModel.objects.create()
156 | self.callCommand("deleterevisions", using="postgres")
157 | self.assertSingleRevision((obj,))
158 |
159 |
160 | class DeleteRevisionsModelDbTest(TestModelMixin, TestBase):
161 | databases = {"default", "postgres"}
162 |
163 | def testDeleteRevisionsModelDb(self):
164 | with reversion.create_revision():
165 | TestModel.objects.db_manager("postgres").create()
166 | self.callCommand("deleterevisions", model_db="postgres")
167 | self.assertNoRevision(using="postgres")
168 |
169 |
170 | class DeleteRevisionsDaysTest(TestModelMixin, TestBase):
171 |
172 | def testDeleteRevisionsDays(self):
173 | date_created = timezone.now() - timedelta(days=20)
174 | with reversion.create_revision():
175 | TestModel.objects.create()
176 | reversion.set_date_created(date_created)
177 | self.callCommand("deleterevisions", days=19)
178 | self.assertNoRevision()
179 |
180 | def testDeleteRevisionsDaysNoMatch(self):
181 | date_created = timezone.now() - timedelta(days=20)
182 | with reversion.create_revision():
183 | obj = TestModel.objects.create()
184 | reversion.set_date_created(date_created)
185 | self.callCommand("deleterevisions", days=21)
186 | self.assertSingleRevision((obj,), date_created=date_created)
187 |
188 |
189 | class DeleteRevisionsKeepTest(TestModelMixin, TestBase):
190 |
191 | def testDeleteRevisionsKeep(self):
192 | with reversion.create_revision():
193 | obj_1 = TestModel.objects.create()
194 | reversion.set_comment("obj_1 v1")
195 | with reversion.create_revision():
196 | obj_1.save()
197 | reversion.set_comment("obj_1 v2")
198 | with reversion.create_revision():
199 | obj_2 = TestModel.objects.create()
200 | reversion.set_comment("obj_2 v1")
201 | with reversion.create_revision():
202 | obj_2.save()
203 | reversion.set_comment("obj_2 v2")
204 | with reversion.create_revision():
205 | obj_3 = TestModel.objects.create()
206 | self.callCommand("deleterevisions", keep=1)
207 | self.assertSingleRevision((obj_1,), comment="obj_1 v2")
208 | self.assertSingleRevision((obj_2,), comment="obj_2 v2")
209 | self.assertSingleRevision((obj_3,))
210 |
--------------------------------------------------------------------------------
/tests/test_app/tests/test_middleware.py:
--------------------------------------------------------------------------------
1 | from django.conf import settings
2 | from django.test.utils import override_settings
3 | from test_app.models import TestModel
4 | from test_app.tests.base import TestBase, TestModelMixin, LoginMixin
5 |
6 |
7 | use_middleware = override_settings(
8 | MIDDLEWARE=settings.MIDDLEWARE + ["reversion.middleware.RevisionMiddleware"],
9 | )
10 |
11 |
12 | @use_middleware
13 | class RevisionMiddlewareTest(TestModelMixin, TestBase):
14 |
15 | def testCreateRevision(self):
16 | response = self.client.post("/test-app/save-obj/")
17 | obj = TestModel.objects.get(pk=response.content)
18 | self.assertSingleRevision((obj,))
19 |
20 | def testCreateRevisionError(self):
21 | with self.assertRaises(Exception):
22 | self.client.post("/test-app/save-obj-error/")
23 | self.assertNoRevision()
24 |
25 | def testCreateRevisionGet(self):
26 | self.client.get("/test-app/create-revision/")
27 | self.assertNoRevision()
28 |
29 |
30 | @use_middleware
31 | class RevisionMiddlewareUserTest(TestModelMixin, LoginMixin, TestBase):
32 |
33 | def testCreateRevisionUser(self):
34 | response = self.client.post("/test-app/save-obj/")
35 | obj = TestModel.objects.get(pk=response.content)
36 | self.assertSingleRevision((obj,), user=self.user)
37 |
--------------------------------------------------------------------------------
/tests/test_app/tests/test_views.py:
--------------------------------------------------------------------------------
1 | from test_app.models import TestModel
2 | from test_app.tests.base import TestBase, TestModelMixin, LoginMixin
3 |
4 |
5 | class CreateRevisionTest(TestModelMixin, TestBase):
6 |
7 | def testCreateRevision(self):
8 | response = self.client.post("/test-app/create-revision/")
9 | obj = TestModel.objects.get(pk=response.content)
10 | self.assertSingleRevision((obj,))
11 |
12 | def testCreateRevisionGet(self):
13 | self.client.get("/test-app/create-revision/")
14 | self.assertNoRevision()
15 |
16 |
17 | class CreateRevisionUserTest(LoginMixin, TestModelMixin, TestBase):
18 |
19 | def testCreateRevisionUser(self):
20 | response = self.client.post("/test-app/create-revision/")
21 | obj = TestModel.objects.get(pk=response.content)
22 | self.assertSingleRevision((obj,), user=self.user)
23 |
24 |
25 | class RevisionMixinTest(TestModelMixin, TestBase):
26 |
27 | def testRevisionMixin(self):
28 | response = self.client.post("/test-app/revision-mixin/")
29 | obj = TestModel.objects.get(pk=response.content)
30 | self.assertSingleRevision((obj,))
31 |
32 | def testRevisionMixinGet(self):
33 | self.client.get("/test-app/revision-mixin/")
34 | self.assertNoRevision()
35 |
36 | def testRevisionMixinCustomPredicate(self):
37 | self.client.post("/test-app/revision-mixin/", HTTP_X_NOREVISION="true")
38 | self.assertNoRevision()
39 |
40 |
41 | class RevisionMixinUserTest(LoginMixin, TestModelMixin, TestBase):
42 |
43 | def testCreateRevisionUser(self):
44 | response = self.client.post("/test-app/revision-mixin/")
45 | obj = TestModel.objects.get(pk=response.content)
46 | self.assertSingleRevision((obj,), user=self.user)
47 |
--------------------------------------------------------------------------------
/tests/test_app/urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import path
2 | from test_app import views
3 |
4 |
5 | urlpatterns = [
6 | path("save-obj/", views.save_obj_view),
7 | path("save-obj-error/", views.save_obj_error_view),
8 | path("create-revision/", views.create_revision_view),
9 | path("revision-mixin/", views.RevisionMixinView.as_view()),
10 | ]
11 |
--------------------------------------------------------------------------------
/tests/test_app/views.py:
--------------------------------------------------------------------------------
1 | from django.db import transaction
2 | from django.http import HttpResponse
3 | from django.views.generic.base import View
4 | from reversion.views import create_revision, RevisionMixin
5 | from test_app.models import TestModel
6 |
7 |
8 | def save_obj_view(request):
9 | return HttpResponse(TestModel.objects.create().id)
10 |
11 |
12 | def save_obj_error_view(request):
13 | with transaction.atomic():
14 | TestModel.objects.create()
15 | raise Exception("Boom!")
16 |
17 |
18 | @create_revision()
19 | def create_revision_view(request):
20 | return save_obj_view(request)
21 |
22 |
23 | class RevisionMixinView(RevisionMixin, View):
24 |
25 | def revision_request_creates_revision(self, request):
26 | silent = request.headers.get('X-Norevision', "false") == "true"
27 | return super().revision_request_creates_revision(request) and not silent
28 |
29 | def dispatch(self, request):
30 | return save_obj_view(request)
31 |
--------------------------------------------------------------------------------
/tests/test_project/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/etianen/django-reversion/9a7cd7419121e56d65247b050b7aa8a105aa000c/tests/test_project/__init__.py
--------------------------------------------------------------------------------
/tests/test_project/settings.py:
--------------------------------------------------------------------------------
1 | """
2 | Django settings for test_project project.
3 |
4 | Generated by "django-admin startproject" using Django 1.10a1.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/dev/topics/settings/
8 |
9 | For the full list of settings and their values, see
10 | https://docs.djangoproject.com/en/dev/ref/settings/
11 | """
12 |
13 | import os
14 | import getpass
15 |
16 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
17 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
18 |
19 |
20 | # Quick-start development settings - unsuitable for production
21 | # See https://docs.djangoproject.com/en/dev/howto/deployment/checklist/
22 |
23 | # SECURITY WARNING: keep the secret key used in production secret!
24 | SECRET_KEY = "lzu78x^s$rit0p*vdt)$1e&hh*)4y=xv))=@zsx(am7t=7406a"
25 |
26 | # SECURITY WARNING: don"t run with debug turned on in production!
27 | DEBUG = True
28 |
29 | ALLOWED_HOSTS = []
30 |
31 |
32 | # Application definition
33 |
34 | INSTALLED_APPS = [
35 | "django.contrib.admin",
36 | "django.contrib.auth",
37 | "django.contrib.contenttypes",
38 | "django.contrib.sessions",
39 | "django.contrib.messages",
40 | "django.contrib.staticfiles",
41 | "reversion",
42 | "test_app",
43 | ]
44 |
45 | MIDDLEWARE = [
46 | "django.middleware.security.SecurityMiddleware",
47 | "django.contrib.sessions.middleware.SessionMiddleware",
48 | "django.middleware.common.CommonMiddleware",
49 | "django.middleware.csrf.CsrfViewMiddleware",
50 | "django.contrib.auth.middleware.AuthenticationMiddleware",
51 | "django.contrib.messages.middleware.MessageMiddleware",
52 | "django.middleware.clickjacking.XFrameOptionsMiddleware",
53 | ]
54 |
55 | ROOT_URLCONF = "test_project.urls"
56 |
57 | TEMPLATES = [
58 | {
59 | "BACKEND": "django.template.backends.django.DjangoTemplates",
60 | "DIRS": [],
61 | "APP_DIRS": True,
62 | "OPTIONS": {
63 | "context_processors": [
64 | "django.template.context_processors.debug",
65 | "django.template.context_processors.request",
66 | "django.contrib.auth.context_processors.auth",
67 | "django.contrib.messages.context_processors.messages",
68 | ],
69 | },
70 | },
71 | ]
72 |
73 | WSGI_APPLICATION = "test_project.wsgi.application"
74 |
75 |
76 | # Database
77 | # https://docs.djangoproject.com/en/dev/ref/settings/#databases
78 |
79 | DATABASES = {
80 | "default": {
81 | "ENGINE": "django.db.backends.sqlite3",
82 | "NAME": os.path.join(BASE_DIR, "db.sqlite3"),
83 | },
84 | "postgres": {
85 | "ENGINE": "django.db.backends.postgresql_psycopg2",
86 | "HOST": os.environ.get("DJANGO_DATABASE_HOST_POSTGRES", ""),
87 | "NAME": os.environ.get("DJANGO_DATABASE_NAME_POSTGRES", "test_project"),
88 | "USER": os.environ.get("DJANGO_DATABASE_USER_POSTGRES", getpass.getuser()),
89 | "PASSWORD": os.environ.get("DJANGO_DATABASE_PASSWORD_POSTGRES", ""),
90 | },
91 | "mysql": {
92 | "ENGINE": "django.db.backends.mysql",
93 | "HOST": os.environ.get("DJANGO_DATABASE_HOST_MYSQL", ""),
94 | "NAME": os.environ.get("DJANGO_DATABASE_NAME_MYSQL", "test_project"),
95 | "USER": os.environ.get("DJANGO_DATABASE_USER_MYSQL", getpass.getuser()),
96 | "PASSWORD": os.environ.get("DJANGO_DATABASE_PASSWORD_MYSQL", ""),
97 | },
98 | }
99 |
100 | DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
101 |
102 | # Password validation
103 | # https://docs.djangoproject.com/en/dev/ref/settings/#auth-password-validators
104 |
105 | AUTH_PASSWORD_VALIDATORS = [
106 | {
107 | "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
108 | },
109 | {
110 | "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
111 | },
112 | {
113 | "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
114 | },
115 | {
116 | "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
117 | },
118 | ]
119 |
120 |
121 | # Internationalization
122 | # https://docs.djangoproject.com/en/dev/topics/i18n/
123 |
124 | LANGUAGE_CODE = "en-us"
125 |
126 | TIME_ZONE = "UTC"
127 |
128 | USE_I18N = True
129 |
130 | USE_L10N = True
131 |
132 | USE_TZ = True
133 |
134 |
135 | # Static files (CSS, JavaScript, Images)
136 | # https://docs.djangoproject.com/en/dev/howto/static-files/
137 |
138 | STATIC_URL = "/static/"
139 |
--------------------------------------------------------------------------------
/tests/test_project/urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import include
2 | from django.urls import path
3 | from django.contrib import admin
4 |
5 | admin.autodiscover()
6 |
7 | urlpatterns = [
8 |
9 | path("admin/", admin.site.urls),
10 | path("test-app/", include("test_app.urls")),
11 |
12 | ]
13 |
--------------------------------------------------------------------------------
/tests/test_project/wsgi.py:
--------------------------------------------------------------------------------
1 | """
2 | WSGI config for test_project project.
3 |
4 | It exposes the WSGI callable as a module-level variable named ``application``.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/dev/howto/deployment/wsgi/
8 | """
9 |
10 | import os
11 |
12 | from django.core.wsgi import get_wsgi_application
13 |
14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_project.settings")
15 |
16 | application = get_wsgi_application()
17 |
--------------------------------------------------------------------------------