From 0d41e49bbb7cb23b8d2aaaff5206afaa95833edb Mon Sep 17 00:00:00 2001 From: sax Date: Fri, 5 May 2017 08:42:40 +0200 Subject: [PATCH 01/24] travis config --- .travis.yml | 66 +++++++++++++++++++---------------------------- tests/.coveragerc | 2 +- tox.ini | 6 ++--- 3 files changed, 30 insertions(+), 44 deletions(-) diff --git a/.travis.yml b/.travis.yml index 467d96a..4c079ef 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,11 @@ language: python sudo: false python: + - 2.7 + - 3.4 - 3.5 + - 3.6 + cache: directories: @@ -12,51 +16,33 @@ services: - PostgreSQL env: - - TOXENV=py27-d18-pg - - TOXENV=py27-d18-sqlite - - TOXENV=py27-d18-mysql - - TOXENV=py27-d19-pg - - TOXENV=py27-d19-sqlite - - TOXENV=py27-d19-mysql - - TOXENV=py27-d110-pg - - TOXENV=py27-d110-sqlite - - TOXENV=py27-d110-mysql - - TOXENV=py27-d111-pg - - TOXENV=py27-d111-sqlite - - TOXENV=py27-d111-mysql - - - TOXENV=py33-d18-pg - - TOXENV=py33-d18-sqlite - - - TOXENV=py34-d18-pg - - TOXENV=py34-d18-sqlite - - TOXENV=py34-d19-pg - - TOXENV=py34-d19-sqlite - - - TOXENV=py35-d18-pg - - TOXENV=py35-d18-sqlite - - TOXENV=py35-d19-pg - - TOXENV=py35-d19-sqlite - - TOXENV=py35-d110-pg - - TOXENV=py35-d110-sqlite - - TOXENV=py35-d111-pg - - TOXENV=py35-d111-sqlite - - - TOXENV=pypy-d18-pg - - TOXENV=pypy-d18-sqlite - - TOXENV=pypy-d19-pg - - TOXENV=pypy-d19-sqlite - - - TOXENV=pypy-d110-pg - - TOXENV=pypy-d110-sqlite - - + - DJANGO=1.8 + - DJANGO=1.9 + - DJANGO=1.10 + - DJANGO=1.11 + - DB=pg + - DB=mysql + +matrix: + exclude: + - python: 2.7 + env: DJANGO=2.0 + + - python: 3.4 + env: DJANGO=2.0 + + - python: 3.6 + env: DJANGO=1.8 + - python: 3.6 + env: DJANGO=1.9 + - python: 3.6 + env: DJANGO=1.10 install: - pip install tox "coverage<=4.0" python-coveralls>=2.5 coveralls>=0.5 codecov script: - - tox -e $TOXENV -- py.test tests -v --capture=no --cov=concurrency --cov-report=xml --cov-config=tests/.coveragerc + - tox -e "py${TRAVIS_PYTHON_VERSION//.}-d${DJANGO//.}-${DB}" -- py.test tests -v --capture=no --cov=concurrency --cov-report=xml --cov-config=tests/.coveragerc before_success: - coverage erase diff --git a/tests/.coveragerc b/tests/.coveragerc index 8dd1041..93d0539 100644 --- a/tests/.coveragerc +++ b/tests/.coveragerc @@ -25,4 +25,4 @@ exclude_lines = ignore_errors = True [html] -directory = build/coverage +directory = ~build/coverage diff --git a/tox.ini b/tox.ini index 16c8615..c5713cf 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] envlist = py{27}-d{18,19,110,111}-{pg,sqlite,mysql}, - py{34,35}-d{18,19,110,111}-{pg,sqlite} + py{34,35,36}-d{18,19,110,111}-{pg,sqlite, mysql} pypy-d{18,19,110,111}-{pg,sqlite} [pytest] @@ -38,8 +38,8 @@ setenv = sqlite: DBENGINE = sqlite deps= - py{27,33,34,35}-{pg}: psycopg2>=2.6.1 - pypy-d{18,19,110}-{pg}: psycopg2cffi + py{27,33,34,35,36}-{pg}: psycopg2>=2.6.1 + pypy-d{18,19,110,111}-{pg}: psycopg2cffi mysql: mysqlclient From 734f0ab07ae8a8f75a5e1d6ac20f169ec6b65715 Mon Sep 17 00:00:00 2001 From: sax Date: Fri, 5 May 2017 08:44:15 +0200 Subject: [PATCH 02/24] travis config --- .travis.yml | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index 4c079ef..8542ec2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,12 +16,16 @@ services: - PostgreSQL env: - - DJANGO=1.8 - - DJANGO=1.9 - - DJANGO=1.10 - - DJANGO=1.11 - - DB=pg - - DB=mysql + - DJANGO=1.8 DB=pg + - DJANGO=1.9 DB=pg + - DJANGO=1.10 DB=pg + - DJANGO=1.11 DB=pg + + - DJANGO=1.8 DB=mysql + - DJANGO=1.9 DB=mysql + - DJANGO=1.10 DB=mysql + - DJANGO=1.11 DB=mysql + matrix: exclude: From 07b07a2f92a15fdd52fc2d961f6733233a542df3 Mon Sep 17 00:00:00 2001 From: sax Date: Mon, 22 May 2017 12:38:43 +0200 Subject: [PATCH 03/24] add some triggerversionfield tests --- src/requirements/testing.pip | 1 + tests/test_triggerversionfield.py | 26 +++++++++++++++++++++++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/requirements/testing.pip b/src/requirements/testing.pip index 56559bf..ecabe5d 100644 --- a/src/requirements/testing.pip +++ b/src/requirements/testing.pip @@ -1,5 +1,6 @@ check-manifest==0.30 django-webtest>=1.9.1 +django-reversion mock>=1.0.1 pytest-cache>=1.0 pytest-cov>=1.6 diff --git a/tests/test_triggerversionfield.py b/tests/test_triggerversionfield.py index 8c62eb5..dfbf410 100644 --- a/tests/test_triggerversionfield.py +++ b/tests/test_triggerversionfield.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- from django.core.signals import request_started -from django.db import IntegrityError, connections +from django.db import IntegrityError, connections, connection import mock import pytest @@ -54,6 +54,30 @@ def __exit__(self, exc_type, exc_value, traceback): self.final_queries = len(self.connection.queries) +@pytest.mark.django_db +def test_trigger_external_update(): + instance = TriggerConcurrentModel() + assert instance.pk is None + assert instance.version == 0 + + instance.save() + assert instance.version == 1 + with connection.cursor() as c: + c.execute("UPDATE {} SET username='aaa' WHERE id='{}'".format(instance._meta.db_table, instance.pk)) + obj = refetch(instance) + assert obj.version == 2 + + +@pytest.mark.django_db +def test_trigger_external_create(): + with connection.cursor() as c: + c.execute("INSERT INTO {} (username, count, cm_version_id) VALUES ('abc', 1, -1)".format( + TriggerConcurrentModel._meta.db_table)) + instance = TriggerConcurrentModel.objects.get(username='abc') + obj = refetch(instance) + assert obj.version == -1 + + @pytest.mark.django_db def test_trigger(): instance = TriggerConcurrentModel() From abdbfee0e11218a8788f5f95de71eb17ab4f140a Mon Sep 17 00:00:00 2001 From: sax Date: Mon, 22 May 2017 12:40:22 +0200 Subject: [PATCH 04/24] add some triggerversionfield tests --- tests/test_triggerversionfield.py | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/tests/test_triggerversionfield.py b/tests/test_triggerversionfield.py index 8c62eb5..c93b4dd 100644 --- a/tests/test_triggerversionfield.py +++ b/tests/test_triggerversionfield.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- from django.core.signals import request_started -from django.db import IntegrityError, connections +from django.db import IntegrityError, connections, connection import mock import pytest @@ -54,6 +54,32 @@ def __exit__(self, exc_type, exc_value, traceback): self.final_queries = len(self.connection.queries) + +@pytest.mark.django_db +def test_trigger_external_update(): + instance = TriggerConcurrentModel() + assert instance.pk is None + assert instance.version == 0 + + instance.save() + assert instance.version == 1 + with connection.cursor() as c: + c.execute("UPDATE {} SET username='aaa' WHERE id='{}'".format(instance._meta.db_table, instance.pk)) + obj = refetch(instance) + assert obj.version == 2 + + +@pytest.mark.django_db +def test_trigger_external_create(): + with connection.cursor() as c: + c.execute("INSERT INTO {} (username, count, cm_version_id) VALUES ('abc', 1, -1)".format( + TriggerConcurrentModel._meta.db_table)) + instance = TriggerConcurrentModel.objects.get(username='abc') + obj = refetch(instance) + assert obj.version == -1 + + + @pytest.mark.django_db def test_trigger(): instance = TriggerConcurrentModel() From 0f50cdf1c3f880ba535287db09a250bd7d54265e Mon Sep 17 00:00:00 2001 From: sax Date: Mon, 22 May 2017 17:25:56 +0200 Subject: [PATCH 05/24] pep8 and minor updates --- .travis.yml | 9 +++++++++ src/concurrency/config.py | 5 +++-- src/concurrency/fields.py | 2 +- src/concurrency/middleware.py | 1 - tests/conftest.py | 1 - tests/demoapp/demo/admin.py | 4 ++-- tests/demoapp/demo/auth_migrations/0001_initial.py | 2 +- tests/demoapp/demo/migrations/0001_initial.py | 5 +++-- tests/demoapp/demo/migrations/0002_auto_20160909_1544.py | 2 +- tests/demoapp/demo/settings.py | 2 +- tests/demoapp/demo/urls.py | 2 +- tests/demoapp/demo/util.py | 2 +- tests/test_admin_actions.py | 2 +- tests/test_admin_list_editable.py | 3 +-- tests/test_api.py | 2 +- tests/test_checks.py | 2 +- tests/test_command.py | 3 +-- tests/test_conditional.py | 9 +++++---- tests/test_enable_disable.py | 2 +- tests/test_forms.py | 2 +- tests/test_issues.py | 3 +-- tests/test_loaddata_dumpdata.py | 5 +---- tests/test_manager.py | 1 + tests/test_middleware.py | 5 ++--- tests/test_templatetags.py | 1 + tests/test_threads.py | 6 ++---- tests/test_triggers.py | 2 +- tests/test_triggerversionfield.py | 8 +++----- tests/test_utils.py | 2 +- tox.ini | 6 +++--- 30 files changed, 51 insertions(+), 50 deletions(-) diff --git a/.travis.yml b/.travis.yml index 467d96a..c1e1b26 100644 --- a/.travis.yml +++ b/.travis.yml @@ -42,6 +42,15 @@ env: - TOXENV=py35-d111-pg - TOXENV=py35-d111-sqlite + - TOXENV=py36-d18-pg + - TOXENV=py36-d18-sqlite + - TOXENV=py36-d19-pg + - TOXENV=py36-d19-sqlite + - TOXENV=py36-d110-pg + - TOXENV=py36-d110-sqlite + - TOXENV=py36-d111-pg + - TOXENV=py36-d111-sqlite + - TOXENV=pypy-d18-pg - TOXENV=pypy-d18-sqlite - TOXENV=pypy-d19-pg diff --git a/src/concurrency/config.py b/src/concurrency/config.py index 6691cec..b7baafd 100644 --- a/src/concurrency/config.py +++ b/src/concurrency/config.py @@ -1,12 +1,13 @@ from __future__ import absolute_import, unicode_literals from django.core.exceptions import ImproperlyConfigured +from django.test.signals import setting_changed +from django.utils import six + try: from django.core.urlresolvers import get_callable except ImportError: from django.urls.utils import get_callable -from django.test.signals import setting_changed -from django.utils import six # List Editable Policy # 0 do not save updated records, save others, show message to the user diff --git a/src/concurrency/fields.py b/src/concurrency/fields.py index 2fb8ca3..7016e06 100755 --- a/src/concurrency/fields.py +++ b/src/concurrency/fields.py @@ -18,7 +18,7 @@ from concurrency.api import get_revision_of_object from concurrency.config import conf from concurrency.core import ConcurrencyOptions -from concurrency.utils import refetch, fqn +from concurrency.utils import fqn, refetch try: from django.apps import apps diff --git a/src/concurrency/middleware.py b/src/concurrency/middleware.py index ca26742..e963c81 100644 --- a/src/concurrency/middleware.py +++ b/src/concurrency/middleware.py @@ -12,7 +12,6 @@ from django.urls.utils import get_callable - class ConcurrencyMiddleware(object): """ Intercept :ref:`RecordModifiedError` and invoke a callable defined in :setting:`CONCURRECY_HANDLER409` passing the request and the object. diff --git a/tests/conftest.py b/tests/conftest.py index f2d2fec..0f4fabe 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,7 +3,6 @@ import sys import django - import pytest py_impl = getattr(platform, 'python_implementation', lambda: None) diff --git a/tests/demoapp/demo/admin.py b/tests/demoapp/demo/admin.py index c5848ce..770908f 100644 --- a/tests/demoapp/demo/admin.py +++ b/tests/demoapp/demo/admin.py @@ -3,9 +3,9 @@ from django.contrib import admin from django.contrib.admin.sites import NotRegistered -from demo.models import * # noqa from demo.models import ( - ListEditableConcurrentModel, NoActionsConcurrentModel, ReversionConcurrentModel + InheritedModel, ListEditableConcurrentModel, NoActionsConcurrentModel, ProxyModel, + ReversionConcurrentModel, SimpleConcurrentModel ) from concurrency.admin import ConcurrentModelAdmin diff --git a/tests/demoapp/demo/auth_migrations/0001_initial.py b/tests/demoapp/demo/auth_migrations/0001_initial.py index 05e7ff5..ae4b48e 100644 --- a/tests/demoapp/demo/auth_migrations/0001_initial.py +++ b/tests/demoapp/demo/auth_migrations/0001_initial.py @@ -4,9 +4,9 @@ import django.contrib.auth.models import django.core.validators -from django.db import migrations, models import django.db.models.deletion import django.utils.timezone +from django.db import migrations, models class Migration(migrations.Migration): diff --git a/tests/demoapp/demo/migrations/0001_initial.py b/tests/demoapp/demo/migrations/0001_initial.py index 1fe9761..a36747e 100644 --- a/tests/demoapp/demo/migrations/0001_initial.py +++ b/tests/demoapp/demo/migrations/0001_initial.py @@ -2,10 +2,11 @@ # Generated by Django 1.9.6 on 2016-09-09 15:41 from __future__ import unicode_literals -import concurrency.fields +import django.db.models.deletion from django.conf import settings from django.db import migrations, models -import django.db.models.deletion + +import concurrency.fields class Migration(migrations.Migration): diff --git a/tests/demoapp/demo/migrations/0002_auto_20160909_1544.py b/tests/demoapp/demo/migrations/0002_auto_20160909_1544.py index e959cc2..2240448 100644 --- a/tests/demoapp/demo/migrations/0002_auto_20160909_1544.py +++ b/tests/demoapp/demo/migrations/0002_auto_20160909_1544.py @@ -2,8 +2,8 @@ # Generated by Django 1.9.6 on 2016-09-09 15:44 from __future__ import unicode_literals -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): diff --git a/tests/demoapp/demo/settings.py b/tests/demoapp/demo/settings.py index 6d6d799..f48762f 100644 --- a/tests/demoapp/demo/settings.py +++ b/tests/demoapp/demo/settings.py @@ -36,7 +36,7 @@ 'auth': 'demo.auth_migrations', } -if django.VERSION[0] == 2 or django.VERSION[1] >= 10: +if django.VERSION[0] == 2 or django.VERSION[1] >= 10: MIDDLEWARE_CLASSES = [] MIDDLEWARE = [ # 'concurrency.middleware.ConcurrencyMiddleware', diff --git a/tests/demoapp/demo/urls.py b/tests/demoapp/demo/urls.py index e3aa377..e8d0943 100644 --- a/tests/demoapp/demo/urls.py +++ b/tests/demoapp/demo/urls.py @@ -1,4 +1,4 @@ -from django.conf.urls import include, url +from django.conf.urls import url from django.contrib import admin from django.views.generic.edit import UpdateView diff --git a/tests/demoapp/demo/util.py b/tests/demoapp/demo/util.py index 2d704f9..3842cd9 100644 --- a/tests/demoapp/demo/util.py +++ b/tests/demoapp/demo/util.py @@ -3,9 +3,9 @@ from functools import partial, update_wrapper from itertools import count +import pytest from django import db -import pytest from demo.models import ( AutoIncConcurrentModel, ConcreteModel, CustomSaveModel, InheritedModel, ProxyModel, SimpleConcurrentModel, TriggerConcurrentModel diff --git a/tests/test_admin_actions.py b/tests/test_admin_actions.py index c68bbd7..5ab26b4 100644 --- a/tests/test_admin_actions.py +++ b/tests/test_admin_actions.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- import django - import pytest + from demo.base import SENTINEL, AdminTestCase from demo.models import SimpleConcurrentModel from demo.util import unique_id diff --git a/tests/test_admin_list_editable.py b/tests/test_admin_list_editable.py index 0c3df53..8b72f94 100644 --- a/tests/test_admin_list_editable.py +++ b/tests/test_admin_list_editable.py @@ -1,14 +1,13 @@ # -*- coding: utf-8 -*- from __future__ import absolute_import, unicode_literals +import pytest from django.contrib.admin.models import LogEntry from django.contrib.admin.sites import site from django.contrib.contenttypes.models import ContentType from django.db import transaction -from django.test import modify_settings from django.utils.encoding import force_text -import pytest from demo.base import SENTINEL, AdminTestCase from demo.models import ListEditableConcurrentModel from demo.util import attributes, unique_id diff --git a/tests/test_api.py b/tests/test_api.py index 1187183..e1287de 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1,6 +1,6 @@ +import pytest from django.contrib.auth.models import Group -import pytest from demo.models import SimpleConcurrentModel from demo.util import nextgroup, nextname diff --git a/tests/test_checks.py b/tests/test_checks.py index 9cc4541..66a6af1 100644 --- a/tests/test_checks.py +++ b/tests/test_checks.py @@ -4,8 +4,8 @@ import logging import django - import pytest + from demo.models import TriggerConcurrentModel logger = logging.getLogger(__name__) diff --git a/tests/test_command.py b/tests/test_command.py index 5717f77..5237377 100644 --- a/tests/test_command.py +++ b/tests/test_command.py @@ -1,10 +1,9 @@ # -*- coding: utf-8 -*- import logging +import pytest import six from django.core.management import call_command - -import pytest from mock import Mock import concurrency.management.commands.triggers as command diff --git a/tests/test_conditional.py b/tests/test_conditional.py index 89aaa18..a8bee9f 100644 --- a/tests/test_conditional.py +++ b/tests/test_conditional.py @@ -3,12 +3,13 @@ import logging +import pytest from django.contrib.auth.models import User -import pytest -from demo.models import ConditionalVersionModel, ConditionalVersionModelWithoutMeta, \ - ConditionalVersionModelSelfRelation, \ - ThroughRelation +from demo.models import ( + ConditionalVersionModel, ConditionalVersionModelSelfRelation, + ConditionalVersionModelWithoutMeta, ThroughRelation +) from concurrency.exceptions import RecordModifiedError from concurrency.utils import refetch diff --git a/tests/test_enable_disable.py b/tests/test_enable_disable.py index 66156bf..0df3aed 100644 --- a/tests/test_enable_disable.py +++ b/tests/test_enable_disable.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- +import pytest from django.test.utils import override_settings -import pytest from demo.models import AutoIncConcurrentModel, SimpleConcurrentModel from demo.util import nextname diff --git a/tests/test_forms.py b/tests/test_forms.py index c0b4ac7..5e41d9d 100644 --- a/tests/test_forms.py +++ b/tests/test_forms.py @@ -1,3 +1,4 @@ +import pytest from django.core.exceptions import ImproperlyConfigured, SuspiciousOperation from django.forms.models import modelform_factory from django.forms.widgets import HiddenInput, TextInput @@ -6,7 +7,6 @@ from django.utils.encoding import smart_str from django.utils.translation import ugettext as _ -import pytest from demo.models import Issue3TestModel, SimpleConcurrentModel from concurrency.exceptions import VersionError diff --git a/tests/test_issues.py b/tests/test_issues.py index 8f0d05e..2e24dde 100644 --- a/tests/test_issues.py +++ b/tests/test_issues.py @@ -2,15 +2,14 @@ import re import django +import pytest from django.contrib.admin.sites import site from django.contrib.auth.models import User -from django.core.management import call_command from django.http import QueryDict from django.test.client import RequestFactory from django.test.testcases import SimpleTestCase from django.utils.encoding import force_text -import pytest from demo.admin import ActionsModelAdmin, admin_register from demo.base import AdminTestCase from demo.models import ListEditableConcurrentModel, ReversionConcurrentModel diff --git a/tests/test_loaddata_dumpdata.py b/tests/test_loaddata_dumpdata.py index 1131136..70fae8e 100644 --- a/tests/test_loaddata_dumpdata.py +++ b/tests/test_loaddata_dumpdata.py @@ -5,15 +5,12 @@ import logging import os +import pytest from django.core.management import call_command from six import StringIO -import pytest from demo.models import SimpleConcurrentModel -from concurrency.api import disable_concurrency -from concurrency.exceptions import RecordModifiedError - logger = logging.getLogger(__name__) diff --git a/tests/test_manager.py b/tests/test_manager.py index 35b196c..f450971 100644 --- a/tests/test_manager.py +++ b/tests/test_manager.py @@ -1,4 +1,5 @@ import pytest + from demo.models import ( AutoIncConcurrentModel, ConcreteModel, CustomSaveModel, InheritedModel, ProxyModel, SimpleConcurrentModel diff --git a/tests/test_middleware.py b/tests/test_middleware.py index f1355e5..4179925 100644 --- a/tests/test_middleware.py +++ b/tests/test_middleware.py @@ -1,10 +1,10 @@ # -*- coding: utf-8 -*- +import mock from django.conf import settings from django.contrib.admin.sites import site from django.http import HttpRequest from django.test.utils import override_settings -import mock from demo.base import AdminTestCase from demo.models import SimpleConcurrentModel from demo.util import DELETE_ATTRIBUTE, attributes, unique_id @@ -19,6 +19,7 @@ except ImportError: from django.urls import reverse + def _get_request(path): request = HttpRequest() request.META = { @@ -64,7 +65,6 @@ def test_process_exception(self): class ConcurrencyMiddlewareTest2(AdminTestCase): - @property def settings_middleware(self): return getattr(settings, self.middleware_setting_name) + ['concurrency.middleware.ConcurrencyMiddleware'] @@ -79,7 +79,6 @@ def test_in_admin(self): with attributes((model_admin.__class__, 'list_editable_policy', CONCURRENCY_LIST_EDITABLE_POLICY_ABORT_ALL), (ConcurrentModelAdmin, 'form', DELETE_ATTRIBUTE)): - saved, __ = SimpleConcurrentModel.objects.get_or_create(pk=id) url = reverse('admin:demo_simpleconcurrentmodel_change', args=[saved.pk]) diff --git a/tests/test_templatetags.py b/tests/test_templatetags.py index f88ecbf..1297345 100644 --- a/tests/test_templatetags.py +++ b/tests/test_templatetags.py @@ -4,6 +4,7 @@ import logging import pytest + from demo.models import SimpleConcurrentModel from concurrency.templatetags.concurrency import identity, is_version, version diff --git a/tests/test_threads.py b/tests/test_threads.py index 4118f9f..4e51161 100644 --- a/tests/test_threads.py +++ b/tests/test_threads.py @@ -1,15 +1,13 @@ -import sys - +import pytest from django import db from django.db import transaction -import pytest +from conftest import skippypy from demo.models import TriggerConcurrentModel from demo.util import concurrently from concurrency.exceptions import RecordModifiedError from concurrency.utils import refetch -from conftest import skippypy @skippypy diff --git a/tests/test_triggers.py b/tests/test_triggers.py index 85bb36e..033b014 100644 --- a/tests/test_triggers.py +++ b/tests/test_triggers.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- import logging +import pytest from django.db import connections -import pytest from demo.models import DropTriggerConcurrentModel, TriggerConcurrentModel # noqa from concurrency.triggers import drop_triggers, factory diff --git a/tests/test_triggerversionfield.py b/tests/test_triggerversionfield.py index c93b4dd..e139e9d 100644 --- a/tests/test_triggerversionfield.py +++ b/tests/test_triggerversionfield.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- -from django.core.signals import request_started -from django.db import IntegrityError, connections, connection - import mock import pytest +from django.core.signals import request_started +from django.db import IntegrityError, connection, connections + from demo.models import TriggerConcurrentModel # Register an event to reset saved queries when a Django request is started. from demo.util import nextname @@ -54,7 +54,6 @@ def __exit__(self, exc_type, exc_value, traceback): self.final_queries = len(self.connection.queries) - @pytest.mark.django_db def test_trigger_external_update(): instance = TriggerConcurrentModel() @@ -79,7 +78,6 @@ def test_trigger_external_create(): assert obj.version == -1 - @pytest.mark.django_db def test_trigger(): instance = TriggerConcurrentModel() diff --git a/tests/test_utils.py b/tests/test_utils.py index 9c04ec6..7b2dade 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -3,9 +3,9 @@ import logging +import pytest from django.test import TestCase -import pytest from demo.models import SimpleConcurrentModel from concurrency.utils import ConcurrencyTestMixin diff --git a/tox.ini b/tox.ini index 16c8615..8855816 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] envlist = py{27}-d{18,19,110,111}-{pg,sqlite,mysql}, - py{34,35}-d{18,19,110,111}-{pg,sqlite} + py{34,35,36}-d{18,19,110,111}-{pg,sqlite} pypy-d{18,19,110,111}-{pg,sqlite} [pytest] @@ -38,8 +38,8 @@ setenv = sqlite: DBENGINE = sqlite deps= - py{27,33,34,35}-{pg}: psycopg2>=2.6.1 - pypy-d{18,19,110}-{pg}: psycopg2cffi + py{27,33,34,35,36}-{pg}: psycopg2>=2.6.1 + pypy-d{18,19,110,111}-{pg}: psycopg2cffi mysql: mysqlclient From 55decb08387857be7bd0af0ae1f22125cb6ffee1 Mon Sep 17 00:00:00 2001 From: sax Date: Tue, 23 May 2017 07:16:39 +0200 Subject: [PATCH 06/24] removes double requirement in testing --- src/requirements/testing.pip | 1 - 1 file changed, 1 deletion(-) diff --git a/src/requirements/testing.pip b/src/requirements/testing.pip index ecabe5d..56559bf 100644 --- a/src/requirements/testing.pip +++ b/src/requirements/testing.pip @@ -1,6 +1,5 @@ check-manifest==0.30 django-webtest>=1.9.1 -django-reversion mock>=1.0.1 pytest-cache>=1.0 pytest-cov>=1.6 From a133205fe1915fd57247bda93da8793f703c61dc Mon Sep 17 00:00:00 2001 From: sax Date: Tue, 23 May 2017 07:38:47 +0200 Subject: [PATCH 07/24] removes unused tests requirements --- src/requirements/testing.pip | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/requirements/testing.pip b/src/requirements/testing.pip index 56559bf..1738873 100644 --- a/src/requirements/testing.pip +++ b/src/requirements/testing.pip @@ -1,14 +1,11 @@ check-manifest==0.30 django-webtest>=1.9.1 mock>=1.0.1 -pytest-cache>=1.0 pytest-cov>=1.6 pytest-django>=3.0.0 pytest-echo>=1.3 pytest-pythonpath>=0.7.1 -pytest>=3.0.3 -pytest-watch -pytest-testmon +pytest==3.1 pdbpp readline tox>=2.4.1 From a40eee31fb25b16ff30013713230be36877d3649 Mon Sep 17 00:00:00 2001 From: sax Date: Tue, 23 May 2017 08:07:49 +0200 Subject: [PATCH 08/24] updates tests --- .travis.yml | 2 +- tests/test_loaddata_dumpdata.py | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index 8542ec2..920f4c5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -46,7 +46,7 @@ install: - pip install tox "coverage<=4.0" python-coveralls>=2.5 coveralls>=0.5 codecov script: - - tox -e "py${TRAVIS_PYTHON_VERSION//.}-d${DJANGO//.}-${DB}" -- py.test tests -v --capture=no --cov=concurrency --cov-report=xml --cov-config=tests/.coveragerc + - tox -e "py${TRAVIS_PYTHON_VERSION//.}-d${DJANGO//.}-${DB}" -- py.test tests -v -W ignore::DeprecationWarning --capture=no --cov=concurrency --cov-report=xml --cov-config=tests/.coveragerc before_success: - coverage erase diff --git a/tests/test_loaddata_dumpdata.py b/tests/test_loaddata_dumpdata.py index 1131136..83ec8ba 100644 --- a/tests/test_loaddata_dumpdata.py +++ b/tests/test_loaddata_dumpdata.py @@ -11,9 +11,6 @@ import pytest from demo.models import SimpleConcurrentModel -from concurrency.api import disable_concurrency -from concurrency.exceptions import RecordModifiedError - logger = logging.getLogger(__name__) @@ -29,7 +26,8 @@ def test_dumpdata(): @pytest.mark.django_db(transaction=True) def test_loaddata_fail(): datafile = os.path.join(os.path.dirname(__file__), 'dumpdata.json') - data = json.load(open(datafile, 'r')) + with open(datafile, 'r') as f: + data = json.load(f) pk = data[0]['pk'] call_command('loaddata', datafile, stdout=StringIO()) From 77d062192d2ee0436786f097f8263b39fe23da4e Mon Sep 17 00:00:00 2001 From: sax Date: Tue, 23 May 2017 09:07:38 +0200 Subject: [PATCH 09/24] updates docs --- .travis.yml | 1 - CHANGES | 10 +++++----- README.rst | 10 +++++----- docs/admin.rst | 4 ++-- docs/api.rst | 4 +--- docs/conf.py | 9 +-------- docs/faq.rst | 2 +- docs/fields.rst | 8 ++------ docs/settings.rst | 7 ++----- 9 files changed, 19 insertions(+), 36 deletions(-) diff --git a/.travis.yml b/.travis.yml index 920f4c5..4961062 100644 --- a/.travis.yml +++ b/.travis.yml @@ -53,7 +53,6 @@ before_success: after_success: - coverage combine - - coveralls - codecov diff --git a/CHANGES b/CHANGES index 36878bb..15500b5 100644 --- a/CHANGES +++ b/CHANGES @@ -4,12 +4,12 @@ Release 1.4 (dev) * some minor support for Django 2.0 Release 1.3.2 (10 Sep 2016) -------------------------- +--------------------------- * fixes bug in ConditionalVersionField that produced 'maximum recursion error' when a model had a ManyToManyField with a field to same model (self-relation) Release 1.3.1 (15 Jul 2016) -------------------------- +--------------------------- * just packaging @@ -117,7 +117,7 @@ Release 0.5.0 Release 0.4.0 ---------------------------------- +------------- * start deprecation of ``concurrency.core.VersionChangedError``, ``concurrency.core.RecordModifiedError``, ``concurrency.core.InconsistencyError``,moved in ``concurrency.exceptions`` * start deprecation of ``concurrency.core.apply_concurrency_check``, ``concurrency.core.concurrency_check`` moved in ``concurrency.api`` @@ -127,13 +127,13 @@ Release 0.4.0 * changed way to add concurrency to existing models (:ref:`apply_concurrency_check`) * fixed :issue:`4` (thanks FrankBie) * removed RandomVersionField -* new :ref:`concurrency_check` +* new `concurrency_check` * added :ref:`concurrentform` to mitigate some concurrency conflict * select_for_update now executed with ``nowait=True`` * removed some internal methods, to avoid unlikely but possible name clashes Release 0.3.2 ---------------------------------- +------------- * fixed :issue:`3` (thanks pombredanne) * fixed :issue:`1` (thanks mbrochh) diff --git a/README.rst b/README.rst index 8a8bf2b..e9ff114 100644 --- a/README.rst +++ b/README.rst @@ -69,15 +69,15 @@ Links .. |master-build| image:: https://secure.travis-ci.org/saxix/django-concurrency.png?branch=master :target: http://travis-ci.org/saxix/django-concurrency/ -.. |master-cov| image:: https://coveralls.io/repos/saxix/django-concurrency/badge.svg?branch=master&service=github - :target: https://coveralls.io/github/saxix/django-concurrency?branch=master - +.. |master-cov| image:: https://codecov.io/gh/saxix/django-concurrency/branch/master/graph/badge.svg + :target: https://codecov.io/gh/saxix/django-concurrency .. |dev-build| image:: https://secure.travis-ci.org/saxix/django-concurrency.png?branch=develop :target: http://travis-ci.org/saxix/django-concurrency/ -.. |dev-cov| image:: https://coveralls.io/repos/saxix/django-concurrency/badge.svg?branch=develop&service=github - :target: https://coveralls.io/github/saxix/django-concurrency?branch=develop +.. |dev-cov| image:: https://codecov.io/gh/saxix/django-concurrency/branch/develop/graph/badge.svg + :target: https://codecov.io/gh/saxix/django-concurrency + .. |wheel| image:: https://pypip.in/wheel/blackhole/badge.png diff --git a/docs/admin.rst b/docs/admin.rst index fb11ed9..942f9db 100644 --- a/docs/admin.rst +++ b/docs/admin.rst @@ -1,9 +1,9 @@ .. include:: globals.txt .. _admin: -================== +================= Admin Integration -================== +================= .. contents:: :local: diff --git a/docs/api.rst b/docs/api.rst index 6f1a3d5..b52cb0d 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -115,7 +115,6 @@ concurrency.views.conflict Helpers ------- - .. _apply_concurrency_check: `apply_concurrency_check()` @@ -132,8 +131,7 @@ Add concurrency check to existing classes. you need to use a migration to add a `VersionField` to the desired Model. -.. note:: See ``demo.auth_migrations`` for a example how to add -:class:`IntegerVersionField ` to :class:`auth.Group` ) +.. note:: See ``demo.auth_migrations`` for a example how to add :class:`IntegerVersionField ` to :class:`auth.Group` ) .. code-block:: python diff --git a/docs/conf.py b/docs/conf.py index afc0ec6..3fab117 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -128,14 +128,7 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. - -if os.environ.get('READTHEDOCS', None) == 'True': - html_theme = "sphinx_rtd_theme" -else: - import sphinx_rtd_theme - - html_theme = "sphinx_rtd_theme" - html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] +html_theme = "default" # # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the diff --git a/docs/faq.rst b/docs/faq.rst index d2a12f4..523b775 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -9,7 +9,7 @@ FAQ .. contents:: :local: -.. _south: +.. _south_support: South support ? --------------- diff --git a/docs/fields.rst b/docs/fields.rst index a5f438f..13a2784 100644 --- a/docs/fields.rst +++ b/docs/fields.rst @@ -36,13 +36,11 @@ TriggerVersionField This field use a database trigger to update the version field. Using this you can control external updates (ie using tools like phpMyAdmin, pgAdmin, SQLDeveloper). The trigger is automatically created during ``syncdb()`` -or you can use the :ref:`triggers` management command. +or you can use the `triggers`_ management command. .. versionchanged:: 1.0 -.. warning:: Before |concurrency| 1.0 two triggers per field were created, -if you are upgrading you must manually remove old triggers and recreate them -using :ref:`triggers`_ management command +.. warning:: Before |concurrency| 1.0 two triggers per field were created, if you are upgrading you must manually remove old triggers and recreate them using `triggers`_ management command `trigger_name` ~~~~~~~~~~~~~~ @@ -67,8 +65,6 @@ Otherwise for each `TriggerVersionField` will be created two triggers named: `triggers` management command ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. command:: triggers - Helper command to work with triggers: * ``list`` : list existing triggers for each database diff --git a/docs/settings.rst b/docs/settings.rst index 7fd7787..3aa0c62 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -116,10 +116,7 @@ show a message to the user ``CONCURRENCY_LIST_EDITABLE_POLICY_ABORT_ALL`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Used by admin's integations to handle ``list_editable``. -Stop at the first conflict and raise -:class:`RecordModifiedError `. -Note that if you want to use -:class:`ConcurrencyMiddleware ` -based conflict management you must set this flag. +Stop at the first conflict and raise :class:`RecordModifiedError `. +Note that if you want to use :class:`ConcurrencyMiddleware ` based conflict management you must set this flag. .. seealso:: :ref:`list_editable`, :ref:`middleware` From d445ea6fe51db95f271d58ad83b9222dc9f92fc2 Mon Sep 17 00:00:00 2001 From: Richard Eames Date: Wed, 7 Jun 2017 13:56:44 -0600 Subject: [PATCH 10/24] Fix warnings on django 1.11 with Python 3.6 -Wall - On django 1.11, django.core.urlresolvers still exists, but issues a warning, so import directly from the new location first - Widget._format_value was renamed --- src/concurrency/config.py | 4 ++-- src/concurrency/forms.py | 4 +++- src/concurrency/middleware.py | 4 ++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/concurrency/config.py b/src/concurrency/config.py index b7baafd..791e7a8 100644 --- a/src/concurrency/config.py +++ b/src/concurrency/config.py @@ -5,9 +5,9 @@ from django.utils import six try: - from django.core.urlresolvers import get_callable -except ImportError: from django.urls.utils import get_callable +except ImportError: + from django.core.urlresolvers import get_callable # List Editable Policy # 0 do not save updated records, save others, show message to the user diff --git a/src/concurrency/forms.py b/src/concurrency/forms.py index 212d39a..cc591fe 100644 --- a/src/concurrency/forms.py +++ b/src/concurrency/forms.py @@ -41,11 +41,13 @@ class VersionWidget(HiddenInput): any value, you should use this widget to display the current revision number """ - def _format_value(self, value): + def format_value(self, value): if value: value = str(value) return value + _format_value = format_value + def render(self, name, value, attrs=None): ret = super(VersionWidget, self).render(name, value, attrs) label = '' diff --git a/src/concurrency/middleware.py b/src/concurrency/middleware.py index e963c81..21355f6 100644 --- a/src/concurrency/middleware.py +++ b/src/concurrency/middleware.py @@ -7,9 +7,9 @@ from concurrency.exceptions import RecordModifiedError try: - from django.core.urlresolvers import get_callable -except ImportError: from django.urls.utils import get_callable +except ImportError: + from django.core.urlresolvers import get_callable class ConcurrencyMiddleware(object): From 90c8710f998a7edd2cf779384dee939e0944780a Mon Sep 17 00:00:00 2001 From: sax Date: Wed, 14 Jun 2017 11:19:05 +0200 Subject: [PATCH 11/24] fixes issue #80 --- src/concurrency/core.py | 1 + src/concurrency/fields.py | 8 ++------ src/concurrency/triggers.py | 17 ++++++++++------- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/concurrency/core.py b/src/concurrency/core.py index a17e853..e85d9b8 100644 --- a/src/concurrency/core.py +++ b/src/concurrency/core.py @@ -62,3 +62,4 @@ class ConcurrencyOptions: skip = False increment = True initial = None + triggers = [] diff --git a/src/concurrency/fields.py b/src/concurrency/fields.py index 7016e06..87ef550 100755 --- a/src/concurrency/fields.py +++ b/src/concurrency/fields.py @@ -74,18 +74,13 @@ def post_syncdb_concurrency_handler(sender, **kwargs): class TriggerRegistry(object): - # FIXME: this is very bad. it seems required only by tests - # see - # https://github.com/pytest-dev/pytest-django/issues/75 - # https://code.djangoproject.com/ticket/22280#comment:20 - _fields = [] def append(self, field): self._fields.append([field.model._meta.app_label, field.model.__name__]) def __iter__(self): - return iter([get_model(*i)._concurrencymeta.field for i in self._fields]) + return iter(self._fields) def __contains__(self, field): target = [field.model._meta.app_label, field.model.__name__] @@ -143,6 +138,7 @@ def contribute_to_class(self, cls, name, virtual_only=False): setattr(cls, '_concurrencymeta', ConcurrencyOptions()) cls._concurrencymeta.field = self cls._concurrencymeta.base = cls + cls._concurrencymeta.triggers = [] def _set_version_value(self, model_instance, value): setattr(model_instance, self.attname, int(value)) diff --git a/src/concurrency/triggers.py b/src/concurrency/triggers.py index 90ade7c..938fcfc 100644 --- a/src/concurrency/triggers.py +++ b/src/concurrency/triggers.py @@ -6,7 +6,7 @@ from django.db import connections, router from django.db.utils import DatabaseError -from concurrency.fields import _TRIGGERS # noqa +from concurrency.fields import _TRIGGERS, get_model # noqa def get_trigger_name(field): @@ -38,8 +38,9 @@ def get_triggers(databases=None): def drop_triggers(*databases): global _TRIGGERS ret = defaultdict(lambda: []) - for field in set(_TRIGGERS): - model = field.model + for app_label, model_name in _TRIGGERS: + model = get_model(app_label, model_name) + field = model._concurrencymeta.field alias = router.db_for_write(model) if alias in databases: connection = connections[alias] @@ -54,12 +55,14 @@ def create_triggers(databases): global _TRIGGERS ret = defaultdict(lambda: []) - for field in set(_TRIGGERS): - model = field.model + for app_label, model_name in _TRIGGERS: + model = get_model(app_label, model_name) + field = model._concurrencymeta.field + storage = model._concurrencymeta.triggers alias = router.db_for_write(model) if alias in databases: - if not field._trigger_exists: - field._trigger_exists = True + if field not in storage: + storage.append(field) connection = connections[alias] f = factory(connection) f.create(field) From 075cf9f94a69227021913ebff977fca20101c6f7 Mon Sep 17 00:00:00 2001 From: sax Date: Thu, 15 Jun 2017 10:52:55 +0200 Subject: [PATCH 12/24] updates CHANGES --- CHANGES | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES b/CHANGES index 15500b5..09436c7 100644 --- a/CHANGES +++ b/CHANGES @@ -1,8 +1,10 @@ Release 1.4 (dev) ----------------- +* fixes :issue:`80`. (thanks Naddiseo for the useful support) * Django 1.11 compatibility * some minor support for Django 2.0 + Release 1.3.2 (10 Sep 2016) --------------------------- * fixes bug in ConditionalVersionField that produced 'maximum recursion error' when a model had a ManyToManyField with a field to same model (self-relation) From 49d892e27a45dfe7e4b236eeba891b87690334ba Mon Sep 17 00:00:00 2001 From: sax Date: Sun, 25 Jun 2017 17:58:58 +0200 Subject: [PATCH 13/24] add test for issue #54 (just asa reference) --- tests/test_issues.py | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/tests/test_issues.py b/tests/test_issues.py index 2e24dde..8804bc0 100644 --- a/tests/test_issues.py +++ b/tests/test_issues.py @@ -6,13 +6,15 @@ from django.contrib.admin.sites import site from django.contrib.auth.models import User from django.http import QueryDict +from django.test import override_settings from django.test.client import RequestFactory from django.test.testcases import SimpleTestCase from django.utils.encoding import force_text +from concurrency.exceptions import RecordModifiedError from demo.admin import ActionsModelAdmin, admin_register from demo.base import AdminTestCase -from demo.models import ListEditableConcurrentModel, ReversionConcurrentModel +from demo.models import ListEditableConcurrentModel, ReversionConcurrentModel, SimpleConcurrentModel from demo.util import attributes, unique_id from concurrency.admin import ConcurrentModelAdmin @@ -85,3 +87,27 @@ def test_issue_53(admin_client): admin_client.post('/admin/demo/reversionconcurrentmodel/recover/{}/'.format(deleted_pk), {'username': 'aaaa'}) assert ReversionConcurrentModel.objects.filter(id=pk).exists() + + +@pytest.mark.django_db() +def test_issue_54(): + m = SimpleConcurrentModel(version=0) + m.save() + SimpleConcurrentModel.objects.update(version=0) + m1 = SimpleConcurrentModel.objects.get(pk=m.pk) + m2 = SimpleConcurrentModel.objects.get(pk=m.pk) + assert m1.version == m2.version == 0 + m1.save() + m2.save() + + with override_settings(CONCURRENCY_IGNORE_DEFAULT=False): + m = SimpleConcurrentModel(version=0) + m.save() + SimpleConcurrentModel.objects.update(version=0) + m1 = SimpleConcurrentModel.objects.get(pk=m.pk) + m2 = SimpleConcurrentModel.objects.get(pk=m.pk) + assert m1.version == m2.version == 0 + m1.save() + + with pytest.raises(RecordModifiedError): + m2.save() From 72372b2fe144f6192583fc21effb43521328e7db Mon Sep 17 00:00:00 2001 From: sax Date: Sun, 25 Jun 2017 18:01:14 +0200 Subject: [PATCH 14/24] fixes docs pagination --- docs/settings.rst | 35 ++++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/docs/settings.rst b/docs/settings.rst index 3aa0c62..8b544c2 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -5,9 +5,6 @@ Settings ======== -Available settings -================== - Here's a full list of all available settings, in alphabetical order, and their default values. @@ -69,6 +66,24 @@ while passing into the context the object that is going to be saved (``target``) .. seealso:: :ref:`middleware` + +.. setting:: CONCURRENCY_IGNORE_DEFAULT + +IGNORE_DEFAULT +-------------- +.. versionadded:: 1.2 + +Default: ``True`` + +Determines whether a default version number is ignored or used in a concurrency check. While this +configuration defaults to True for backwards compatibility, this setting can cause omitted version +numbers to pass concurrency checks. New implementations are recommended to set this to ``False``. + +.. note:: For security reasons, starting from version 1.5, default value will be ``False``. + + + + .. setting:: CONCURRECY_MANUAL_TRIGGERS MANUAL_TRIGGERS @@ -82,6 +97,7 @@ If false do not automatically create triggers, you can create them using :ref:`t + .. setting:: CONCURRENCY_POLICY POLICY @@ -94,18 +110,6 @@ Default: ``CONCURRENCY_LIST_EDITABLE_POLICY_SILENT`` .. setting:: CONCURRENCY_IGNORE_DEFAULT -IGNORE_DEFAULT --------------- -.. versionadded:: >1.2 - -Default: ``True`` - -Determines whether a default version number is ignored or used in a concurrency check. While this -configuration defaults to True for backwards compatibility, this setting can cause omitted version -numbers to pass concurrency checks. New implementations are recommended to set this to ``False``. - -.. note:: For security reasons, starting from version 1.5, default value will be ``False``. - ``CONCURRENCY_LIST_EDITABLE_POLICY_SILENT`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -113,6 +117,7 @@ Used by admin's integrations to handle ``list_editable`` conflicts. Do not save conflicting records, continue and save all non-conflicting records, show a message to the user + ``CONCURRENCY_LIST_EDITABLE_POLICY_ABORT_ALL`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Used by admin's integations to handle ``list_editable``. From 60d4c3c3bb7dc318210b2a68267ff12f950d2ea2 Mon Sep 17 00:00:00 2001 From: sax Date: Sun, 25 Jun 2017 18:14:39 +0200 Subject: [PATCH 15/24] updates docs --- docs/faq.rst | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/docs/faq.rst b/docs/faq.rst index 523b775..8028b48 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -9,6 +9,22 @@ FAQ .. contents:: :local: + + +I use Django-Rest-Framework and |concurrency| seems do not work +--------------------------------------------------------------- +Use :setting:`CONCURRENCY_IGNORE_DEFAULT` accordingly or be sure +that serializer does not set `0` as default value + + + +Just added |concurrency| to existing project and it does not work +----------------------------------------------------------------- + +Check that your records do not have `0` as version number +and use :setting:`CONCURRENCY_IGNORE_DEFAULT` accordingly + + .. _south_support: South support ? From 0cfa6f1d5005b1d78ed03018d3950b6978eabede Mon Sep 17 00:00:00 2001 From: sax Date: Sun, 25 Jun 2017 18:18:09 +0200 Subject: [PATCH 16/24] updates docs --- docs/faq.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/faq.rst b/docs/faq.rst index 8028b48..258d680 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -14,7 +14,7 @@ FAQ I use Django-Rest-Framework and |concurrency| seems do not work --------------------------------------------------------------- Use :setting:`CONCURRENCY_IGNORE_DEFAULT` accordingly or be sure -that serializer does not set `0` as default value +that serializer does not set `0` as initial value From 04072f939f3c16f100a45d8cde7a64ec5a4343eb Mon Sep 17 00:00:00 2001 From: sax Date: Mon, 26 Jun 2017 08:47:31 +0200 Subject: [PATCH 17/24] some code clean, removes old compatibility related code --- src/concurrency/compat.py | 8 ++--- src/concurrency/config.py | 7 ++-- src/concurrency/core.py | 1 + src/concurrency/fields.py | 34 +++++-------------- src/concurrency/forms.py | 1 + .../management/commands/triggers.py | 2 +- src/concurrency/triggers.py | 7 ++-- tox.ini | 2 +- 8 files changed, 20 insertions(+), 42 deletions(-) diff --git a/src/concurrency/compat.py b/src/concurrency/compat.py index cbdc2fd..188f63f 100644 --- a/src/concurrency/compat.py +++ b/src/concurrency/compat.py @@ -1,12 +1,10 @@ # -*- coding: utf-8 -*- from __future__ import absolute_import, print_function, unicode_literals +# from django.core.urlresolvers import get_callable +# from django.db.transaction import atomic + try: from django.template.base import TemplateDoesNotExist except ImportError: from django.template.exceptions import TemplateDoesNotExist # noqa - django 1.9 - -try: - from django.db.transaction import atomic -except ImportError: - from django.db.transaction import commit_on_success as atomic # noqa diff --git a/src/concurrency/config.py b/src/concurrency/config.py index 791e7a8..b06891c 100644 --- a/src/concurrency/config.py +++ b/src/concurrency/config.py @@ -1,14 +1,11 @@ +# coding=utf-8 from __future__ import absolute_import, unicode_literals from django.core.exceptions import ImproperlyConfigured from django.test.signals import setting_changed +from django.urls import get_callable from django.utils import six -try: - from django.urls.utils import get_callable -except ImportError: - from django.core.urlresolvers import get_callable - # List Editable Policy # 0 do not save updated records, save others, show message to the user # 1 abort whole transaction diff --git a/src/concurrency/core.py b/src/concurrency/core.py index e85d9b8..23718e6 100644 --- a/src/concurrency/core.py +++ b/src/concurrency/core.py @@ -1,3 +1,4 @@ +# coding=utf-8 from __future__ import absolute_import, unicode_literals import logging diff --git a/src/concurrency/fields.py b/src/concurrency/fields.py index 87ef550..697bb7a 100755 --- a/src/concurrency/fields.py +++ b/src/concurrency/fields.py @@ -1,3 +1,4 @@ +# coding=utf-8 from __future__ import absolute_import, unicode_literals import copy @@ -11,6 +12,7 @@ from django.db import models from django.db.models import signals from django.db.models.fields import Field +from django.db.models.signals import class_prepared, post_migrate from django.utils.encoding import force_text from django.utils.translation import ugettext_lazy as _ @@ -20,18 +22,6 @@ from concurrency.core import ConcurrencyOptions from concurrency.utils import fqn, refetch -try: - from django.apps import apps - - get_model = apps.get_model -except ImportError: - from django.db.models.loading import get_model - -try: - from django.db.models.signals import class_prepared, post_migrate -except: - from django.db.models.signals import class_prepared, post_syncdb as post_migrate - logger = logging.getLogger(__name__) OFFSET = int(time.mktime((2000, 1, 1, 0, 0, 0, 0, 0, 0))) @@ -109,14 +99,6 @@ def __init__(self, *args, **kwargs): db_tablespace=db_tablespace, db_column=db_column) - def deconstruct(self): - name, path, args, kwargs = super(VersionField, self).deconstruct() - kwargs['default'] = 1 - return name, path, args, kwargs - - def get_default(self): - return 0 - def get_internal_type(self): return "BigIntegerField" @@ -131,8 +113,8 @@ def formfield(self, **kwargs): kwargs['widget'] = forms.VersionField.widget return super(VersionField, self).formfield(**kwargs) - def contribute_to_class(self, cls, name, virtual_only=False): - super(VersionField, self).contribute_to_class(cls, name, virtual_only) + def contribute_to_class(self, cls, *args, **kwargs): + super(VersionField, self).contribute_to_class(cls, *args, **kwargs) if hasattr(cls, '_concurrencymeta') or cls._meta.abstract: return setattr(cls, '_concurrencymeta', ConcurrencyOptions()) @@ -250,8 +232,8 @@ def __init__(self, *args, **kwargs): self._trigger_exists = False super(TriggerVersionField, self).__init__(*args, **kwargs) - def contribute_to_class(self, cls, name, virtual_only=False): - super(TriggerVersionField, self).contribute_to_class(cls, name) + def contribute_to_class(self, cls, *args, **kwargs): + super(TriggerVersionField, self).contribute_to_class(cls, *args, **kwargs) if not cls._meta.abstract or cls._meta.proxy: if self not in _TRIGGERS: _TRIGGERS.append(self) @@ -332,8 +314,8 @@ def filter_fields(instance, field): class ConditionalVersionField(AutoIncVersionField): - def contribute_to_class(self, cls, name, virtual_only=False): - super(ConditionalVersionField, self).contribute_to_class(cls, name, virtual_only) + def contribute_to_class(self, cls, *args, **kwargs): + super(ConditionalVersionField, self).contribute_to_class(cls, *args, **kwargs) signals.post_init.connect(self._load_model, sender=cls, dispatch_uid=fqn(cls)) diff --git a/src/concurrency/forms.py b/src/concurrency/forms.py index cc591fe..29afe49 100644 --- a/src/concurrency/forms.py +++ b/src/concurrency/forms.py @@ -1,3 +1,4 @@ +# coding=utf-8 from __future__ import absolute_import, unicode_literals from importlib import import_module diff --git a/src/concurrency/management/commands/triggers.py b/src/concurrency/management/commands/triggers.py index 760fc34..c635ceb 100644 --- a/src/concurrency/management/commands/triggers.py +++ b/src/concurrency/management/commands/triggers.py @@ -2,8 +2,8 @@ from django.core.exceptions import ImproperlyConfigured from django.core.management.base import BaseCommand from django.db import connections +from django.db.transaction import atomic -from concurrency.compat import atomic from concurrency.triggers import create_triggers, drop_triggers, get_triggers diff --git a/src/concurrency/triggers.py b/src/concurrency/triggers.py index 938fcfc..2a260f2 100644 --- a/src/concurrency/triggers.py +++ b/src/concurrency/triggers.py @@ -3,11 +3,10 @@ from collections import defaultdict +from django.apps import apps from django.db import connections, router from django.db.utils import DatabaseError -from concurrency.fields import _TRIGGERS, get_model # noqa - def get_trigger_name(field): """ @@ -39,7 +38,7 @@ def drop_triggers(*databases): global _TRIGGERS ret = defaultdict(lambda: []) for app_label, model_name in _TRIGGERS: - model = get_model(app_label, model_name) + model = apps.get_model(app_label, model_name) field = model._concurrencymeta.field alias = router.db_for_write(model) if alias in databases: @@ -56,7 +55,7 @@ def create_triggers(databases): ret = defaultdict(lambda: []) for app_label, model_name in _TRIGGERS: - model = get_model(app_label, model_name) + model = apps.get_model(app_label, model_name) field = model._concurrencymeta.field storage = model._concurrencymeta.triggers alias = router.db_for_write(model) diff --git a/tox.ini b/tox.ini index c5713cf..7a456cb 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] envlist = py{27}-d{18,19,110,111}-{pg,sqlite,mysql}, - py{34,35,36}-d{18,19,110,111}-{pg,sqlite, mysql} + py{34,35,36}-d{18,19,110,111}-{pg,sqlite,mysql} pypy-d{18,19,110,111}-{pg,sqlite} [pytest] From 8358c9ea4ab5483f8406a94963705a779cf3674f Mon Sep 17 00:00:00 2001 From: sax Date: Mon, 26 Jun 2017 12:22:34 +0200 Subject: [PATCH 18/24] fixes wrong import --- src/concurrency/compat.py | 14 +++++++++++--- src/concurrency/config.py | 3 ++- src/concurrency/triggers.py | 1 + 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/concurrency/compat.py b/src/concurrency/compat.py index 188f63f..76939f6 100644 --- a/src/concurrency/compat.py +++ b/src/concurrency/compat.py @@ -1,10 +1,18 @@ # -*- coding: utf-8 -*- from __future__ import absolute_import, print_function, unicode_literals -# from django.core.urlresolvers import get_callable -# from django.db.transaction import atomic - try: from django.template.base import TemplateDoesNotExist except ImportError: from django.template.exceptions import TemplateDoesNotExist # noqa - django 1.9 + +# try: +# from django.db.transaction import atomic +# except ImportError: +# from django.db.transaction import commit_on_success as atomic # noqa +# + +try: + from django.urls.utils import get_callable +except ImportError: + from django.core.urlresolvers import get_callable diff --git a/src/concurrency/config.py b/src/concurrency/config.py index b06891c..0d86dec 100644 --- a/src/concurrency/config.py +++ b/src/concurrency/config.py @@ -3,9 +3,10 @@ from django.core.exceptions import ImproperlyConfigured from django.test.signals import setting_changed -from django.urls import get_callable from django.utils import six +from .compat import get_callable + # List Editable Policy # 0 do not save updated records, save others, show message to the user # 1 abort whole transaction diff --git a/src/concurrency/triggers.py b/src/concurrency/triggers.py index 2a260f2..c8ee745 100644 --- a/src/concurrency/triggers.py +++ b/src/concurrency/triggers.py @@ -7,6 +7,7 @@ from django.db import connections, router from django.db.utils import DatabaseError +from .fields import _TRIGGERS # noqa def get_trigger_name(field): """ From 1c3bdc1d5a1ef6c917fcdc471291f8101d5b536c Mon Sep 17 00:00:00 2001 From: sax Date: Mon, 26 Jun 2017 16:58:24 +0200 Subject: [PATCH 19/24] minor tests cleanup --- .travis.yml | 2 +- src/concurrency/admin.py | 7 ++- src/concurrency/api.py | 6 +- src/concurrency/compat.py | 2 +- src/concurrency/config.py | 22 ++++---- .../management/commands/triggers.py | 2 +- src/concurrency/triggers.py | 22 +++++--- src/concurrency/utils.py | 43 ++++++++------- src/concurrency/views.py | 4 +- tests/test_api.py | 6 ++ tests/test_enable_disable.py | 55 ++++++++++++++++++- tests/test_forms.py | 16 +++++- tox.ini | 7 +++ 13 files changed, 142 insertions(+), 52 deletions(-) diff --git a/.travis.yml b/.travis.yml index 4961062..71682ed 100644 --- a/.travis.yml +++ b/.travis.yml @@ -46,7 +46,7 @@ install: - pip install tox "coverage<=4.0" python-coveralls>=2.5 coveralls>=0.5 codecov script: - - tox -e "py${TRAVIS_PYTHON_VERSION//.}-d${DJANGO//.}-${DB}" -- py.test tests -v -W ignore::DeprecationWarning --capture=no --cov=concurrency --cov-report=xml --cov-config=tests/.coveragerc + - tox -e "py${TRAVIS_PYTHON_VERSION//.}-d${DJANGO//.}-${DB}" -- py.test tests src/concurrency -v before_success: - coverage erase diff --git a/src/concurrency/admin.py b/src/concurrency/admin.py index 8796abb..2d9676b 100644 --- a/src/concurrency/admin.py +++ b/src/concurrency/admin.py @@ -38,7 +38,7 @@ def action_checkbox(self, obj): return helpers.checkbox.render(helpers.ACTION_CHECKBOX_NAME, force_text("%s,%s" % (obj.pk, get_revision_of_object(obj)))) - else: + else: # pragma: no cover return super(ConcurrencyActionMixin, self).action_checkbox(obj) action_checkbox.short_description = mark_safe('') @@ -69,7 +69,7 @@ def response_action(self, request, queryset): # noqa # Use the action whose button was pushed try: data.update({'action': data.getlist('action')[action_index]}) - except IndexError: + except IndexError: # pragma: no cover # If we didn't get an action from the chosen form that's invalid # POST data, so by deleting action it'll fail the validation check # below. So no need to do anything here @@ -90,10 +90,11 @@ def response_action(self, request, queryset): # noqa else: selected = request.POST.getlist(helpers.ACTION_CHECKBOX_NAME) - revision_field = self.model._concurrencymeta.field if not selected: return None + revision_field = self.model._concurrencymeta.field + if self.check_concurrent_action: self.delete_selected_confirmation_template = self.get_confirmation_template() diff --git a/src/concurrency/api.py b/src/concurrency/api.py index 5715095..31865b3 100644 --- a/src/concurrency/api.py +++ b/src/concurrency/api.py @@ -8,6 +8,7 @@ from concurrency.config import conf from concurrency.core import _select_lock, get_version_fieldname # _wrap_model_save from concurrency.exceptions import RecordModifiedError +from concurrency.utils import deprecated __all__ = ['apply_concurrency_check', 'concurrency_check', 'get_revision_of_object', 'RecordModifiedError', 'disable_concurrency', @@ -66,10 +67,11 @@ def apply_concurrency_check(model, fieldname, versionclass): class_prepared_concurrency_handler(model) - if not model._concurrencymeta.versioned_save: - versionclass._wrap_model_save(model) + # if not model._concurrencymeta.versioned_save: + # versionclass._wrap_model_save(model) +@deprecated(version="1.5") def concurrency_check(model_instance, force_insert=False, force_update=False, using=None, **kwargs): if not force_insert: _select_lock(model_instance) diff --git a/src/concurrency/compat.py b/src/concurrency/compat.py index 76939f6..163233b 100644 --- a/src/concurrency/compat.py +++ b/src/concurrency/compat.py @@ -15,4 +15,4 @@ try: from django.urls.utils import get_callable except ImportError: - from django.core.urlresolvers import get_callable + from django.core.urlresolvers import get_callable # noqa diff --git a/src/concurrency/config.py b/src/concurrency/config.py index 0d86dec..f3e49f6 100644 --- a/src/concurrency/config.py +++ b/src/concurrency/config.py @@ -23,27 +23,29 @@ class AppSettings(object): """ Class to manage application related settings How to use: - + >>> import pytest >>> from django.conf import settings + >>> from concurrency.utils import fqn >>> settings.APP_OVERRIDE = 'overridden' - >>> settings.MYAPP_CALLBACK = 100 >>> class MySettings(AppSettings): - ... defaults = {'ENTRY1': 'abc', 'ENTRY2': 123, 'OVERRIDE': None, 'CALLBACK':10} - ... def set_CALLBACK(self, value): - ... setattr(self, 'CALLBACK', value*2) + ... defaults = {'ENTRY1': 'abc', 'ENTRY2': 123, 'OVERRIDE': None, 'CALLBACK': fqn(fqn)} >>> conf = MySettings("APP") - >>> conf.ENTRY1, settings.APP_ENTRY1 + >>> str(conf.ENTRY1), str(settings.APP_ENTRY1) ('abc', 'abc') - >>> conf.OVERRIDE, settings.APP_OVERRIDE + >>> str(conf.OVERRIDE), str(settings.APP_OVERRIDE) ('overridden', 'overridden') >>> conf = MySettings("MYAPP") >>> conf.ENTRY2, settings.MYAPP_ENTRY2 (123, 123) + >>> settings.MYAPP_CALLBACK = fqn >>> conf = MySettings("MYAPP") - >>> conf.CALLBACK - 200 + >>> conf.CALLBACK == fqn + True + >>> with pytest.raises(ImproperlyConfigured): + ... settings.OTHER_CALLBACK = 222 + ... conf = MySettings("OTHER") """ defaults = { @@ -81,7 +83,7 @@ def _set_attr(self, prefix_name, value): elif callable(value): func = value else: - raise ImproperlyConfigured("`CALLBACK` must be a callable or a fullpath to callable") + raise ImproperlyConfigured("{} is not a valid value for `CALLBACK`. It must be a callable or a fullpath to callable. ".format(value)) self._callback = func setattr(self, name, value) diff --git a/src/concurrency/management/commands/triggers.py b/src/concurrency/management/commands/triggers.py index c635ceb..bc9e460 100644 --- a/src/concurrency/management/commands/triggers.py +++ b/src/concurrency/management/commands/triggers.py @@ -76,5 +76,5 @@ def handle(self, *args, **options): self.stdout.write('') else: raise Exception() - except ImproperlyConfigured as e: + except ImproperlyConfigured as e: # pragma: no cover self.stdout.write(self.style.ERROR(e)) diff --git a/src/concurrency/triggers.py b/src/concurrency/triggers.py index c8ee745..8077621 100644 --- a/src/concurrency/triggers.py +++ b/src/concurrency/triggers.py @@ -9,6 +9,7 @@ from .fields import _TRIGGERS # noqa + def get_trigger_name(field): """ @@ -48,6 +49,8 @@ def drop_triggers(*databases): f.drop(field) field._trigger_exists = False ret[alias].append([model, field, field.trigger_name]) + else: # pragma: no cover + pass return ret @@ -60,14 +63,15 @@ def create_triggers(databases): field = model._concurrencymeta.field storage = model._concurrencymeta.triggers alias = router.db_for_write(model) - if alias in databases: - if field not in storage: - storage.append(field) - connection = connections[alias] - f = factory(connection) - f.create(field) - ret[alias].append([model, field, field.trigger_name]) - # _TRIGGERS = [] + if (alias in databases) and field not in storage: + storage.append(field) + connection = connections[alias] + f = factory(connection) + f.create(field) + ret[alias].append([model, field, field.trigger_name]) + else: # pragma: no cover + pass + return ret @@ -168,5 +172,5 @@ def factory(conn): 'sqlite3': Sqlite3, 'sqlite': Sqlite3, }[conn.vendor](conn) - except KeyError: + except KeyError: # pragma: no cover raise ValueError('{} is not supported by TriggerVersionField'.format(conn)) diff --git a/src/concurrency/utils.py b/src/concurrency/utils.py index 093e82e..f6f19b7 100644 --- a/src/concurrency/utils.py +++ b/src/concurrency/utils.py @@ -14,32 +14,26 @@ def deprecated(replacement=None, version=None): """A decorator which can be used to mark functions as deprecated. replacement is a callable that will be called with the same args as the decorated function. - + >>> import pytest >>> @deprecated() - ... def foo(x): + ... def foo1(x): ... return x ... - >>> ret = foo(1) - DeprecationWarning: foo is deprecated - >>> ret + >>> pytest.warns(DeprecationWarning, foo1, 1) 1 - >>> - >>> >>> def newfun(x): ... return 0 ... - >>> @deprecated(newfun) - ... def foo(x): + >>> @deprecated(newfun, '1.1') + ... def foo2(x): ... return x ... - >>> ret = foo(1) - DeprecationWarning: foo is deprecated; use newfun instead - >>> ret + >>> pytest.warns(DeprecationWarning, foo2, 1) 0 >>> """ - def outer(oldfun): # pragma: no cover + def outer(oldfun): def inner(*args, **kwargs): msg = "%s is deprecated" % oldfun.__name__ if version is not None: @@ -148,20 +142,27 @@ def fqn(o): :param o: object or class :return: class name + >>> import concurrency.fields >>> fqn('str') Traceback (most recent call last): ... ValueError: Invalid argument `str` - >>> class A(object): pass - >>> fqn(A) - 'wfp_commonlib.python.reflect.A' + >>> class A(object): + ... def method(self): + ... pass + >>> str(fqn(A)) + 'concurrency.utils.A' + + >>> str(fqn(A())) + 'concurrency.utils.A' + + >>> str(fqn(concurrency.fields)) + 'concurrency.fields' + + >>> str(fqn(A.method)) + 'concurrency.utils.A.method' - >>> fqn(A()) - 'wfp_commonlib.python.reflect.A' - >>> from wfp_commonlib.python import RexList - >>> fqn(RexList.append) - 'wfp_commonlib.python.structure.RexList.append' """ parts = [] diff --git a/src/concurrency/views.py b/src/concurrency/views.py index f053b2c..59cb47b 100644 --- a/src/concurrency/views.py +++ b/src/concurrency/views.py @@ -30,14 +30,14 @@ def conflict(request, target=None, template_name='409.html'): """ try: template = loader.get_template(template_name) - except TemplateDoesNotExist: + except TemplateDoesNotExist: # pragma: no cover template = Template( '

Conflict

' '

The request was unsuccessful due to a conflict. ' 'The object changed during the transaction.

') try: saved = target.__class__._default_manager.get(pk=target.pk) - except target.__class__.DoesNotExist: + except target.__class__.DoesNotExist: # pragma: no cover saved = None ctx = {'target': target, 'saved': saved, diff --git a/tests/test_api.py b/tests/test_api.py index e1287de..2829598 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -51,3 +51,9 @@ def test_apply_concurrency_check(): with pytest.raises(RecordModifiedError): instance.save() + + +@pytest.mark.django_db(transaction=False) +def test_apply_concurrency_check_ignore_multiple_call(): + apply_concurrency_check(Group, 'version', IntegerVersionField) + apply_concurrency_check(Group, 'version', IntegerVersionField) diff --git a/tests/test_enable_disable.py b/tests/test_enable_disable.py index 0df3aed..4d5d03c 100644 --- a/tests/test_enable_disable.py +++ b/tests/test_enable_disable.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- import pytest +from django.contrib.auth.models import User from django.test.utils import override_settings from demo.models import AutoIncConcurrentModel, SimpleConcurrentModel @@ -36,6 +37,26 @@ def test_disable_concurrency_global(): copy2.save() +@pytest.mark.django_db(transaction=False) +def test_disable_concurrency_not_managed(): + u = User(username='u1') + with disable_concurrency(u): + u.save() + + +@pytest.mark.django_db(transaction=False) +def test_disable_concurrency_decorator(): + + @disable_concurrency(SimpleConcurrentModel) + def test1(): + instance = SimpleConcurrentModel(username=next(nextname)) + instance.save() + copy = refetch(instance) + copy.save() + instance.save() + test1() + + @pytest.mark.django_db(transaction=False) def test_disable_concurrency_class(model_class=SimpleConcurrentModel): instance = model_class(username=next(nextname)) @@ -65,7 +86,7 @@ def test_disable_concurrency_instance(model_class=SimpleConcurrentModel): @pytest.mark.django_db(transaction=False) -def test_disable_increment(): +def test_concurrency_disable_increment(): instance1 = AutoIncConcurrentModel(username=next(nextname)) assert instance1.version == 0 instance1.save() @@ -76,3 +97,35 @@ def test_disable_increment(): assert instance1.version == 1 instance1.save() assert instance1.version == 2 + + +@pytest.mark.django_db(transaction=False) +def test_concurrency_disable_increment_on_class(): + instance1 = AutoIncConcurrentModel(username=next(nextname)) + assert instance1.version == 0 + instance1.save() + assert instance1.version == 1 + with concurrency_disable_increment(AutoIncConcurrentModel): + instance1.save() + instance1.save() + assert instance1.version == 1 + instance1.save() + assert instance1.version == 2 + + +@pytest.mark.django_db(transaction=False) +def test_concurrency_disable_increment_as_decorator(): + instance1 = AutoIncConcurrentModel(username=next(nextname)) + + @concurrency_disable_increment(instance1) + def test(): + assert instance1.version == 0 + instance1.save() + assert instance1.version == 1 + instance1.save() + instance1.save() + assert instance1.version == 1 + + test() + instance1.save() + assert instance1.version == 2 diff --git a/tests/test_forms.py b/tests/test_forms.py index 5e41d9d..bbc6783 100644 --- a/tests/test_forms.py +++ b/tests/test_forms.py @@ -2,7 +2,7 @@ from django.core.exceptions import ImproperlyConfigured, SuspiciousOperation from django.forms.models import modelform_factory from django.forms.widgets import HiddenInput, TextInput -from django.test import TestCase +from django.test import TestCase, override_settings from django.test.testcases import SimpleTestCase from django.utils.encoding import smart_str from django.utils.translation import ugettext as _ @@ -152,3 +152,17 @@ def test_is_valid(self): form = Form(data, instance=obj) obj.save() # save again simulate concurrent editing self.assertRaises(ValueError, form.save) + + +def test_disabled(db, settings): + obj, __ = SimpleConcurrentModel.objects.get_or_create(username='aaa') + Form = modelform_factory(SimpleConcurrentModel, ConcurrentForm, + fields=('username', 'id', 'version')) + data = {'username': 'aaa', + 'id': 1, + 'version': VersionFieldSigner().sign(obj.version)} + form = Form(data, instance=obj) + obj.save() # save again simulate concurrent editing + with override_settings(CONCURRENCY_ENABLED=False): + obj2 = form.save() + assert obj2.version == obj.version diff --git a/tox.ini b/tox.ini index 7a456cb..a111288 100644 --- a/tox.ini +++ b/tox.ini @@ -10,12 +10,19 @@ DJANGO_SETTINGS_MODULE=demo.settings norecursedirs = .tox docs ./demoapp/ python_files=tests/test_*.py addopts = + -q + -p no:warnings --tb=short --capture=no --echo-version django --echo-attr django.conf.settings.DATABASES.default.ENGINE + --cov=concurrency + --doctest-modules + --cov-report=html + --cov-config=tests/.coveragerc +;py.test tests -v -W ignore::DeprecationWarning --capture=no pep8ignore = * ALL markers = functional: mark a test as functional From af394e03ccd50cc3ba39684329d01cb70f5a4e86 Mon Sep 17 00:00:00 2001 From: sax Date: Mon, 26 Jun 2017 19:54:07 +0200 Subject: [PATCH 20/24] removes some doctests --- .travis.yml | 2 +- src/concurrency/config.py | 28 ---------------- src/concurrency/core.py | 5 ++- .../management/commands/triggers.py | 2 +- src/concurrency/middleware.py | 2 ++ src/concurrency/triggers.py | 2 ++ src/concurrency/utils.py | 22 ++++++------- tests/test_command.py | 15 +++++++++ tests/test_config.py | 30 +++++++++++++++++ tests/test_core.py | 23 +++++++++++++ tests/test_triggers.py | 10 +++++- tests/test_utils.py | 33 ++++++++++++++++++- tox.ini | 6 ++-- 13 files changed, 132 insertions(+), 48 deletions(-) create mode 100644 tests/test_config.py create mode 100644 tests/test_core.py diff --git a/.travis.yml b/.travis.yml index 71682ed..f1ac7e0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -46,7 +46,7 @@ install: - pip install tox "coverage<=4.0" python-coveralls>=2.5 coveralls>=0.5 codecov script: - - tox -e "py${TRAVIS_PYTHON_VERSION//.}-d${DJANGO//.}-${DB}" -- py.test tests src/concurrency -v + - tox -e "py${TRAVIS_PYTHON_VERSION//.}-d${DJANGO//.}-${DB}" -- py.test tests -v before_success: - coverage erase diff --git a/src/concurrency/config.py b/src/concurrency/config.py index f3e49f6..ead7c45 100644 --- a/src/concurrency/config.py +++ b/src/concurrency/config.py @@ -20,34 +20,6 @@ class AppSettings(object): - """ - Class to manage application related settings - How to use: - >>> import pytest - >>> from django.conf import settings - >>> from concurrency.utils import fqn - >>> settings.APP_OVERRIDE = 'overridden' - >>> class MySettings(AppSettings): - ... defaults = {'ENTRY1': 'abc', 'ENTRY2': 123, 'OVERRIDE': None, 'CALLBACK': fqn(fqn)} - - >>> conf = MySettings("APP") - >>> str(conf.ENTRY1), str(settings.APP_ENTRY1) - ('abc', 'abc') - >>> str(conf.OVERRIDE), str(settings.APP_OVERRIDE) - ('overridden', 'overridden') - - >>> conf = MySettings("MYAPP") - >>> conf.ENTRY2, settings.MYAPP_ENTRY2 - (123, 123) - >>> settings.MYAPP_CALLBACK = fqn - >>> conf = MySettings("MYAPP") - >>> conf.CALLBACK == fqn - True - >>> with pytest.raises(ImproperlyConfigured): - ... settings.OTHER_CALLBACK = 222 - ... conf = MySettings("OTHER") - - """ defaults = { 'ENABLED': True, 'MANUAL_TRIGGERS': False, diff --git a/src/concurrency/core.py b/src/concurrency/core.py index 23718e6..1e41e53 100644 --- a/src/concurrency/core.py +++ b/src/concurrency/core.py @@ -50,7 +50,10 @@ def _select_lock(model_instance, version_value=None): logger.debug("Conflict detected on `{0}` pk:`{0.pk}`, " "version `{1}` not found".format(model_instance, value)) conf._callback(model_instance) - + else: # pragma: no cover + pass + else: # pragma: no cover + pass class ConcurrencyOptions: field = None diff --git a/src/concurrency/management/commands/triggers.py b/src/concurrency/management/commands/triggers.py index bc9e460..cb81fdd 100644 --- a/src/concurrency/management/commands/triggers.py +++ b/src/concurrency/management/commands/triggers.py @@ -74,7 +74,7 @@ def handle(self, *args, **options): for trigger in triggers: self.stdout.write(" Dropped {0[2]}".format(trigger)) self.stdout.write('') - else: + else: # pragma: no cover raise Exception() except ImproperlyConfigured as e: # pragma: no cover self.stdout.write(self.style.ERROR(e)) diff --git a/src/concurrency/middleware.py b/src/concurrency/middleware.py index 21355f6..a8f5d4d 100644 --- a/src/concurrency/middleware.py +++ b/src/concurrency/middleware.py @@ -30,3 +30,5 @@ def process_exception(self, request, exception): got_request_exception.send(sender=self, request=request) callback = get_callable(conf.HANDLER409) return callback(request, target=exception.target) + else: # pragma: no cover + pass diff --git a/src/concurrency/triggers.py b/src/concurrency/triggers.py index 8077621..cd15011 100644 --- a/src/concurrency/triggers.py +++ b/src/concurrency/triggers.py @@ -99,6 +99,8 @@ def create(self, field): raise DatabaseError("""Error executing: {1} {0}""".format(exc, stm)) + else: # pragma: no cover + pass field._trigger_exists = True def drop(self, field): diff --git a/src/concurrency/utils.py b/src/concurrency/utils.py index f6f19b7..5ee33b2 100644 --- a/src/concurrency/utils.py +++ b/src/concurrency/utils.py @@ -114,7 +114,7 @@ class ConcurrencyAdminTestMixin(object): def refetch(model_instance): """ Reload model instance from the database - """ + # """ return model_instance.__class__.objects.get(pk=model_instance.pk) @@ -166,16 +166,16 @@ def fqn(o): """ parts = [] - if inspect.ismethod(o): - try: - cls = o.im_class - except AttributeError: - # Python 3 eliminates im_class, substitutes __module__ and - # __qualname__ to provide similar information. - parts = (o.__module__, o.__qualname__) - else: - parts = (fqn(cls), get_classname(o)) - elif hasattr(o, '__module__'): + # if inspect.ismethod(o): + # try: + # cls = o.im_class + # except AttributeError: + # # Python 3 eliminates im_class, substitutes __module__ and + # # __qualname__ to provide similar information. + # parts = (o.__module__, o.__qualname__) + # else: + # parts = (fqn(cls), get_classname(o)) + if hasattr(o, '__module__'): parts.append(o.__module__) parts.append(get_classname(o)) elif inspect.ismodule(o): diff --git a/tests/test_command.py b/tests/test_command.py index 5237377..8c53fb8 100644 --- a/tests/test_command.py +++ b/tests/test_command.py @@ -26,6 +26,21 @@ def test_command_create(monkeypatch): assert mock_create.call_count == 1 +@pytest.mark.django_db +def test_command_create_db(monkeypatch): + out = six.StringIO() + mock_create = Mock() + mock_create.return_value = {'default': [['model', 'field', 'trigger']]} + + monkeypatch.setattr(command, 'create_triggers', mock_create) + call_command('triggers', 'create', database='default', stdout=out) + + out.seek(0) + output = out.read() + assert output.find('Created trigger for field') > 0 + assert mock_create.call_count == 1 + + @pytest.mark.django_db def test_command_list(): out = six.StringIO() diff --git a/tests/test_config.py b/tests/test_config.py new file mode 100644 index 0000000..f29ac3b --- /dev/null +++ b/tests/test_config.py @@ -0,0 +1,30 @@ +import pytest +from django.core.exceptions import ImproperlyConfigured + +from concurrency.config import AppSettings +from concurrency.utils import fqn + + +def test_config(settings): + settings.APP_OVERRIDE = 'overridden' + class MySettings(AppSettings): + defaults = {'ENTRY1': 'abc', + 'ENTRY2': 123, + 'OVERRIDE': None, + 'CALLBACK': fqn(fqn)} + + conf = MySettings("APP") + assert str(conf.ENTRY1) == str(settings.APP_ENTRY1) + + assert str(conf.OVERRIDE) == str(settings.APP_OVERRIDE) + + conf = MySettings("MYAPP") + assert conf.ENTRY2 == settings.MYAPP_ENTRY2 + + settings.MYAPP_CALLBACK = fqn + conf = MySettings("MYAPP") + assert conf.CALLBACK == fqn + + with pytest.raises(ImproperlyConfigured): + settings.OTHER_CALLBACK = 222 + conf = MySettings("OTHER") diff --git a/tests/test_core.py b/tests/test_core.py new file mode 100644 index 0000000..59dcd27 --- /dev/null +++ b/tests/test_core.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, unicode_literals + +import pytest + +from concurrency.core import _select_lock +from concurrency.exceptions import RecordModifiedError +from concurrency.utils import refetch +from demo.models import SimpleConcurrentModel + + +@pytest.mark.django_db +def test_select_lock(settings): + s1 = SimpleConcurrentModel.objects.create() + s2 = refetch(s1) + assert s1.version == s2.version + s2.save() + with pytest.raises(RecordModifiedError): + _select_lock(s1) + + settings.CONCURRENCY_ENABLED = False + _select_lock(s1) + diff --git a/tests/test_triggers.py b/tests/test_triggers.py index 033b014..98361f8 100644 --- a/tests/test_triggers.py +++ b/tests/test_triggers.py @@ -6,7 +6,7 @@ from demo.models import DropTriggerConcurrentModel, TriggerConcurrentModel # noqa -from concurrency.triggers import drop_triggers, factory +from concurrency.triggers import drop_triggers, factory, get_triggers logger = logging.getLogger(__name__) @@ -20,6 +20,14 @@ def test_list_triggers(): u'concurrency_demo_triggerconcurrentmodel_version'] +@pytest.mark.django_db +def test_get_triggers(): + assert get_triggers(['default']) == {'default': [u'concurrency_demo_droptriggerconcurrentmodel_version', + u'concurrency_demo_triggerconcurrentmodel_version']} + assert get_triggers() == {'default': [u'concurrency_demo_droptriggerconcurrentmodel_version', + u'concurrency_demo_triggerconcurrentmodel_version']} + + @pytest.mark.django_db def test_get_trigger(monkeypatch): conn = connections['default'] diff --git a/tests/test_utils.py b/tests/test_utils.py index 7b2dade..77b3ae1 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -8,7 +8,8 @@ from demo.models import SimpleConcurrentModel -from concurrency.utils import ConcurrencyTestMixin +import concurrency.fields +from concurrency.utils import ConcurrencyTestMixin, fqn, deprecated logger = logging.getLogger(__name__) @@ -16,3 +17,33 @@ @pytest.mark.django_db class TestConcurrencyTestMixin(ConcurrencyTestMixin, TestCase): concurrency_model = SimpleConcurrentModel + + +def test_fqn(): + + with pytest.raises(ValueError): + fqn('str') + + assert fqn(SimpleConcurrentModel) == 'demo.models.SimpleConcurrentModel' + assert fqn(SimpleConcurrentModel()) == 'demo.models.SimpleConcurrentModel' + assert fqn(concurrency.fields) == 'concurrency.fields' + + +def test_deprecated(): + + @deprecated() + def foo1(x): + return x + + with pytest.warns(DeprecationWarning): + assert foo1(12) == 12 + + def newfun(x): + return 0 + + @deprecated(newfun, '1.1') + def foo2(x): + return x + + with pytest.warns(DeprecationWarning): + assert foo2(10) == 0 diff --git a/tox.ini b/tox.ini index a111288..e020137 100644 --- a/tox.ini +++ b/tox.ini @@ -10,7 +10,6 @@ DJANGO_SETTINGS_MODULE=demo.settings norecursedirs = .tox docs ./demoapp/ python_files=tests/test_*.py addopts = - -q -p no:warnings --tb=short @@ -18,11 +17,9 @@ addopts = --echo-version django --echo-attr django.conf.settings.DATABASES.default.ENGINE --cov=concurrency - --doctest-modules --cov-report=html --cov-config=tests/.coveragerc -;py.test tests -v -W ignore::DeprecationWarning --capture=no pep8ignore = * ALL markers = functional: mark a test as functional @@ -44,6 +41,7 @@ setenv = mysql: DBENGINE = mysql sqlite: DBENGINE = sqlite + deps= py{27,33,34,35,36}-{pg}: psycopg2>=2.6.1 pypy-d{18,19,110,111}-{pg}: psycopg2cffi @@ -68,9 +66,9 @@ deps= trunk: django-reversion>=2.0.8 trunk: git+git://github.com/django/django.git#egg=django - -rsrc/requirements/testing.pip + commands = mysql: - mysql -u root -e 'CREATE DATABASE IF NOT EXISTS concurrency;' pg: - psql -c 'DROP DATABASE "concurrency";' -U postgres From a4e54601dc07a418a6b2ac89565c56078e72b2cb Mon Sep 17 00:00:00 2001 From: sax Date: Thu, 6 Jul 2017 19:58:01 +0200 Subject: [PATCH 21/24] closes issue #81 --- CHANGES | 1 + docs/api.rst | 4 ++++ docs/conf.py | 2 ++ src/concurrency/admin.py | 30 ++++++++++++++++++++++++++++++ src/concurrency/utils.py | 31 ++++++++++++++++++++++++++++++- tests/test_issues.py | 23 +++++++++++++++++++++++ 6 files changed, 90 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 09436c7..e4f9b54 100644 --- a/CHANGES +++ b/CHANGES @@ -1,5 +1,6 @@ Release 1.4 (dev) ----------------- +* closes :issue:`81`. Add docs and check. * fixes :issue:`80`. (thanks Naddiseo for the useful support) * Django 1.11 compatibility * some minor support for Django 2.0 diff --git a/docs/api.rst b/docs/api.rst index b52cb0d..a80281d 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -75,6 +75,10 @@ ConcurrentModelAdmin -------------------- .. autoclass:: concurrency.admin.ConcurrentModelAdmin +.. warning:: If you customize ``fields`` or ``fieldsets`` remember to add version field to the list. (See issue :ghissue:`81`) + + + .. _ConcurrencyActionMixin: ConcurrencyActionMixin diff --git a/docs/conf.py b/docs/conf.py index 3fab117..498d0d6 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -58,6 +58,8 @@ } +github_project_url = 'https://github.com/saxix/django-concurrency' + todo_include_todos = True # Add any paths that contain templates here, relative to this directory. diff --git a/src/concurrency/admin.py b/src/concurrency/admin.py index 2d9676b..5c4e9c5 100644 --- a/src/concurrency/admin.py +++ b/src/concurrency/admin.py @@ -7,6 +7,7 @@ from django.contrib import admin, messages from django.contrib.admin import helpers +from django.core.checks import Error from django.core.exceptions import ImproperlyConfigured, ValidationError from django.db.models import Q from django.forms.formsets import ( @@ -23,6 +24,7 @@ from concurrency.config import CONCURRENCY_LIST_EDITABLE_POLICY_ABORT_ALL, conf from concurrency.exceptions import RecordModifiedError from concurrency.forms import ConcurrentForm, VersionWidget +from concurrency.utils import flatten ALL = object() @@ -251,3 +253,31 @@ class ConcurrentModelAdmin(ConcurrencyActionMixin, admin.ModelAdmin): form = ConcurrentForm formfield_overrides = {forms.VersionField: {'widget': VersionWidget}} + + def check(self, **kwargs): + errors = [] + if self.fields: + version_field = self.model._concurrencymeta.field + if version_field.name not in self.fields: + errors.append( + Error( + 'Missed version field in {} fields definition'.format(self), + hint="Please add '{}' to the 'fields' attribute".format(version_field.name), + obj=None, + id='concurrency.A001', + ) + ) + if self.fieldsets: + version_field = self.model._concurrencymeta.field + fields = flatten([v['fields'] for k, v in self.fieldsets]) + + if version_field.name not in fields: + errors.append( + Error( + 'Missed version field in {} fieldsets definition'.format(self), + hint="Please add '{}' to the 'fieldsets' attribute".format(version_field.name), + obj=None, + id='concurrency.A002', + ) + ) + return errors diff --git a/src/concurrency/utils.py b/src/concurrency/utils.py index 5ee33b2..99bb24c 100644 --- a/src/concurrency/utils.py +++ b/src/concurrency/utils.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- from __future__ import absolute_import, unicode_literals - +import six import inspect import logging import warnings @@ -183,3 +183,32 @@ def fqn(o): if not parts: raise ValueError("Invalid argument `%s`" % o) return ".".join(parts) + + +def flatten(iterable): + """ + flatten(sequence) -> list + + Returns a single, flat list which contains all elements retrieved + from the sequence and all recursively contained sub-sequences + (iterables). + + :param sequence: any object that implements iterable protocol (see: :ref:`typeiter`) + :return: list + + Examples: + + >>> from adminactions.utils import flatten + >>> [1, 2, [3,4], (5,6)] + [1, 2, [3, 4], (5, 6)] + + >>> flatten([[[1,2,3], (42,None)], [4,5], [6], 7, (8,9,10)]) + [1, 2, 3, 42, None, 4, 5, 6, 7, 8, 9, 10]""" + + result = list() + for el in iterable: + if hasattr(el, "__iter__") and not isinstance(el, six.string_types): + result.extend(flatten(el)) + else: + result.append(el) + return list(result) diff --git a/tests/test_issues.py b/tests/test_issues.py index 8804bc0..929735b 100644 --- a/tests/test_issues.py +++ b/tests/test_issues.py @@ -5,6 +5,8 @@ import pytest from django.contrib.admin.sites import site from django.contrib.auth.models import User +from django.core.management import call_command +from django.core.management.base import SystemCheckError from django.http import QueryDict from django.test import override_settings from django.test.client import RequestFactory @@ -111,3 +113,24 @@ def test_issue_54(): with pytest.raises(RecordModifiedError): m2.save() + + +@pytest.mark.django_db() +def test_issue_81a(monkeypatch): + monkeypatch.setattr('demo.admin.ActionsModelAdmin.fields', ('id',)) + with pytest.raises(SystemCheckError) as e: + call_command('check') + assert 'concurrency.A001' in e.value.message + + +@pytest.mark.django_db() +def test_issue_81b(monkeypatch): + fieldsets = ( + ('Standard info', { + 'fields': ('id',) + }), + ) + monkeypatch.setattr('demo.admin.ActionsModelAdmin.fieldsets', fieldsets) + with pytest.raises(SystemCheckError) as e: + call_command('check') + assert 'concurrency.A002' in e.value.message From 1beb67f6c758c97e822153d5b7d6a564cd1ccb7b Mon Sep 17 00:00:00 2001 From: sax Date: Thu, 6 Jul 2017 21:36:38 +0200 Subject: [PATCH 22/24] enable ConcurrentModelAdmin.check only on django>=1.11 --- src/concurrency/admin.py | 52 ++++++++++++++++++++------------------- src/concurrency/compat.py | 4 +++ tests/conftest.py | 4 +-- tests/test_issues.py | 3 +++ 4 files changed, 36 insertions(+), 27 deletions(-) diff --git a/src/concurrency/admin.py b/src/concurrency/admin.py index 5c4e9c5..887a3c9 100644 --- a/src/concurrency/admin.py +++ b/src/concurrency/admin.py @@ -21,6 +21,7 @@ from concurrency import core, forms from concurrency.api import get_revision_of_object +from concurrency.compat import DJANGO_11 from concurrency.config import CONCURRENCY_LIST_EDITABLE_POLICY_ABORT_ALL, conf from concurrency.exceptions import RecordModifiedError from concurrency.forms import ConcurrentForm, VersionWidget @@ -254,30 +255,31 @@ class ConcurrentModelAdmin(ConcurrencyActionMixin, form = ConcurrentForm formfield_overrides = {forms.VersionField: {'widget': VersionWidget}} - def check(self, **kwargs): - errors = [] - if self.fields: - version_field = self.model._concurrencymeta.field - if version_field.name not in self.fields: - errors.append( - Error( - 'Missed version field in {} fields definition'.format(self), - hint="Please add '{}' to the 'fields' attribute".format(version_field.name), - obj=None, - id='concurrency.A001', + if DJANGO_11: + def check(self, **kwargs): + errors = [] + if self.fields: + version_field = self.model._concurrencymeta.field + if version_field.name not in self.fields: + errors.append( + Error( + 'Missed version field in {} fields definition'.format(self), + hint="Please add '{}' to the 'fields' attribute".format(version_field.name), + obj=None, + id='concurrency.A001', + ) ) - ) - if self.fieldsets: - version_field = self.model._concurrencymeta.field - fields = flatten([v['fields'] for k, v in self.fieldsets]) - - if version_field.name not in fields: - errors.append( - Error( - 'Missed version field in {} fieldsets definition'.format(self), - hint="Please add '{}' to the 'fieldsets' attribute".format(version_field.name), - obj=None, - id='concurrency.A002', + if self.fieldsets: + version_field = self.model._concurrencymeta.field + fields = flatten([v['fields'] for k, v in self.fieldsets]) + + if version_field.name not in fields: + errors.append( + Error( + 'Missed version field in {} fieldsets definition'.format(self), + hint="Please add '{}' to the 'fieldsets' attribute".format(version_field.name), + obj=None, + id='concurrency.A002', + ) ) - ) - return errors + return errors diff --git a/src/concurrency/compat.py b/src/concurrency/compat.py index 163233b..1f0a221 100644 --- a/src/concurrency/compat.py +++ b/src/concurrency/compat.py @@ -1,6 +1,8 @@ # -*- coding: utf-8 -*- from __future__ import absolute_import, print_function, unicode_literals +import django + try: from django.template.base import TemplateDoesNotExist except ImportError: @@ -16,3 +18,5 @@ from django.urls.utils import get_callable except ImportError: from django.core.urlresolvers import get_callable # noqa + +DJANGO_11 = django.VERSION[:2] == (1,11) diff --git a/tests/conftest.py b/tests/conftest.py index 0f4fabe..2967c8c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -15,8 +15,8 @@ skippypy = pytest.mark.skipif(PYPY, reason='skip on pypy') -# skipIfDjangoVersion = lambda v: pytest.mark.skipif("django.VERSION[:2]>={}".format(v), -# reason="Skip if django>={}".format(v)) +skipIfDjangoVersion = lambda v: pytest.mark.skipif("django.VERSION[:2]{}".format(v), + reason="Skip if django{}".format(v)) def pytest_configure(): diff --git a/tests/test_issues.py b/tests/test_issues.py index 929735b..bf365ab 100644 --- a/tests/test_issues.py +++ b/tests/test_issues.py @@ -14,6 +14,7 @@ from django.utils.encoding import force_text from concurrency.exceptions import RecordModifiedError +from conftest import skipIfDjangoVersion from demo.admin import ActionsModelAdmin, admin_register from demo.base import AdminTestCase from demo.models import ListEditableConcurrentModel, ReversionConcurrentModel, SimpleConcurrentModel @@ -115,6 +116,7 @@ def test_issue_54(): m2.save() +@skipIfDjangoVersion("<(1,11)") @pytest.mark.django_db() def test_issue_81a(monkeypatch): monkeypatch.setattr('demo.admin.ActionsModelAdmin.fields', ('id',)) @@ -123,6 +125,7 @@ def test_issue_81a(monkeypatch): assert 'concurrency.A001' in e.value.message +@skipIfDjangoVersion("<(1,11)") @pytest.mark.django_db() def test_issue_81b(monkeypatch): fieldsets = ( From 16ca52028872d86d408ad59b7a2d3253b37bd681 Mon Sep 17 00:00:00 2001 From: sax Date: Thu, 6 Jul 2017 21:49:02 +0200 Subject: [PATCH 23/24] fixes py3.4 tests --- tests/test_issues.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_issues.py b/tests/test_issues.py index bf365ab..4efe5fb 100644 --- a/tests/test_issues.py +++ b/tests/test_issues.py @@ -122,7 +122,7 @@ def test_issue_81a(monkeypatch): monkeypatch.setattr('demo.admin.ActionsModelAdmin.fields', ('id',)) with pytest.raises(SystemCheckError) as e: call_command('check') - assert 'concurrency.A001' in e.value.message + assert 'concurrency.A001' in str(e.value) @skipIfDjangoVersion("<(1,11)") @@ -136,4 +136,4 @@ def test_issue_81b(monkeypatch): monkeypatch.setattr('demo.admin.ActionsModelAdmin.fieldsets', fieldsets) with pytest.raises(SystemCheckError) as e: call_command('check') - assert 'concurrency.A002' in e.value.message + assert 'concurrency.A002' in str(e.value) From 38102cb9e1248238789e0d535f2730458ebfbef1 Mon Sep 17 00:00:00 2001 From: sax Date: Sun, 9 Jul 2017 07:52:36 +0200 Subject: [PATCH 24/24] updates CHANGES --- CHANGES | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES b/CHANGES index e4f9b54..434c757 100644 --- a/CHANGES +++ b/CHANGES @@ -1,5 +1,5 @@ -Release 1.4 (dev) ------------------ +Release 1.4 +----------- * closes :issue:`81`. Add docs and check. * fixes :issue:`80`. (thanks Naddiseo for the useful support) * Django 1.11 compatibility