Skip to content

Commit ffab58f

Browse files
committed
Merge branch 'release/1.3'
* release/1.3: bump v1.3 update CHANGES tox.ini: do not install postgis if not required update docs - Document new setting - Fix loaddata test to pass without disabling concurrency - Remove loaddata test that disable concurrency (now redundant) - fixes #36 add default value to ConcurrencyOptions.initial add explicit mention to MIT license open 1.3 alpha
2 parents f203ad7 + df5b743 commit ffab58f

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+236
-133
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ coverage.xml
99
.cache
1010
notes.txt
1111
docs/build/
12+
__pycache__
1213
/dist
1314
/build
1415
/MANIFEST

CHANGES

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
Release 1.3 (15 Jul 2016)
2+
-------------------------
3+
* drop support for Python < 3.3
4+
* add support Django>=1.10
5+
* change license
6+
* fixes :issue:`36`. (thanks claytondaley)
7+
* new :setting:`IGNORE_DEFAULT` to ignore default version number
8+
9+
110
Release 1.2 (05 Apr 2016)
211
-------------------------
312
* better support for django 1.9 ( ``TemplateDoesNotExist`` is now in ``django.template.exceptions``

LICENSE

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,24 @@
1-
* Copyright (c) 2010, Stefano Apostolico ([email protected])
2-
* Dual licensed under the MIT or GPL Version 2 licenses.
1+
MIT License
32

4-
Redistribution and use in source and binary forms, with or without modification,
5-
are permitted provided that the following conditions are met:
3+
Copyright (c) 2010-2016, Stefano Apostolico ([email protected])
64

7-
1. Redistributions of source code must retain the above copyright notice,
8-
this list of conditions and the following disclaimer.
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
911

10-
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
11-
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
12-
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
13-
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
14-
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
15-
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
16-
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
17-
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
18-
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
19-
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
2014

15+
Any use in a commercial product must be notified to the author by email
16+
indicating company name and product name
17+
18+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24+
SOFTWARE.

MANIFEST.in

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,11 @@ include *.py
1010
exclude Makefile
1111
exclude .editorconfig
1212
exclude .tx
13+
exclude .pyc
1314

1415
recursive-exclude .tx *
16+
recursive-exclude __pycache__ *
17+
recursive-exclude . *.pyc
1518

1619
recursive-include docs *
1720
recursive-include src *

docs/conf.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@
8787
version = ".".join(map(str, concurrency.VERSION[0:2]))
8888
# The full version, including alpha/beta/rc tags.
8989
release = concurrency.get_version()
90-
next_version = '1.0.1'
90+
next_version = '1.4'
9191

9292
# The language for content autogenerated by Sphinx. Refer to documentation
9393
# for a list of supported languages.

docs/settings.rst

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,21 @@ Default: ``CONCURRENCY_LIST_EDITABLE_POLICY_SILENT``
9292

9393
.. _list_editable_policies:
9494

95+
.. setting:: CONCURRENCY_IGNORE_DEFAULT
96+
97+
IGNORE_DEFAULT
98+
--------------
99+
.. versionadded:: >1.2
100+
101+
Default: ``True``
102+
103+
Determines whether a default version number is ignored or used in a concurrency check. While this
104+
configuration defaults to True for backwards compatibility, this setting can cause omitted version
105+
numbers to pass concurrency checks. New implementations are recommended to set this to ``False``.
106+
107+
.. note:: For security reasons, starting from version 1.5, default value will be ``False``.
108+
109+
95110
``CONCURRENCY_LIST_EDITABLE_POLICY_SILENT``
96111
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
97112
Used by admin's integrations to handle ``list_editable`` conflicts.

setup.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
#!/usr/bin/env python
2+
import imp
23
import os
34
import sys
4-
import imp
5-
from distutils import log
6-
from distutils.command.clean import clean as CleanCommand
7-
from distutils.dir_util import remove_tree
85

96
from setuptools import find_packages, setup
107
from setuptools.command.test import test as TestCommand
@@ -58,7 +55,7 @@ def run_tests(self):
5855
long_description=open('README.rst').read(),
5956
license='MIT License',
6057
keywords='django',
61-
setup_requires=['pytest-runner',],
58+
setup_requires=['pytest-runner', ],
6259
install_requires=install_requires,
6360
tests_require='django\n' + test_requires,
6461
extras_require={'test': test_requires,

src/concurrency/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
__author__ = 'sax'
66
default_app_config = 'concurrency.apps.ConcurrencyConfig'
77

8-
VERSION = __version__ = (1, 2, 0, 'final', 0)
8+
VERSION = __version__ = (1, 3, 0, 'final', 0)
99
NAME = 'django-concurrency'
1010

1111

src/concurrency/admin.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from django.http import HttpResponse, HttpResponseRedirect
1717
from django.utils.encoding import force_text
1818
from django.utils.safestring import mark_safe
19-
from django.utils.translation import ugettext as _, ungettext
19+
from django.utils.translation import ungettext
2020

2121
from concurrency import core, forms
2222
from concurrency.api import get_revision_of_object

src/concurrency/config.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,9 @@ class AppSettings(object):
5050
'FIELD_SIGNER': 'concurrency.forms.VersionFieldSigner',
5151
'POLICY': CONCURRENCY_LIST_EDITABLE_POLICY_SILENT,
5252
'CALLBACK': 'concurrency.views.callback',
53-
'HANDLER409': 'concurrency.views.conflict'}
53+
'HANDLER409': 'concurrency.views.conflict',
54+
'IGNORE_DEFAULT': True,
55+
}
5456

5557
def __init__(self, prefix):
5658
"""

src/concurrency/core.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,4 @@ class ConcurrencyOptions:
6161
ignore_fields = None
6262
skip = False
6363
increment = True
64+
initial = None

src/concurrency/fields.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -184,11 +184,23 @@ def _do_update(model_instance, base_qs, using, pk_val, values, update_fields, fo
184184
# else:
185185
# new_version = old_version
186186
break
187-
if values:
187+
188+
# This provides a default if either (1) no values were provided or (2) we reached this code as part of a
189+
# create. We don't need to worry about a race condition because a competing create should produce an
190+
# error anyway.
191+
updated = base_qs.filter(pk=pk_val).exists()
192+
193+
# This second situation can occur because `Model.save_base` calls `Model._save_parent` without relaying
194+
# the `force_insert` flag that marks the process as a create. Eventually, `Model._save_table` will call
195+
# this function as-if it were in the middle of an update. The update is expected to fail because there
196+
# is no object to update and the caller will fall back on the create logic instead. We need to ensure
197+
# the update fails (but does not raise an exception) under this circumstance by skipping the concurrency
198+
# logic.
199+
if values and updated:
188200
if (model_instance._concurrencymeta.enabled and
189201
conf.ENABLED and
190202
not getattr(model_instance, '_concurrency_disabled', False) and
191-
old_version):
203+
(old_version or not conf.IGNORE_DEFAULT)):
192204
filter_kwargs = {'pk': pk_val, version_field.attname: old_version}
193205
updated = base_qs.filter(**filter_kwargs)._update(values) >= 1
194206
if not updated:
@@ -197,8 +209,6 @@ def _do_update(model_instance, base_qs, using, pk_val, values, update_fields, fo
197209
else:
198210
filter_kwargs = {'pk': pk_val}
199211
updated = base_qs.filter(**filter_kwargs)._update(values) >= 1
200-
else:
201-
updated = base_qs.filter(pk=pk_val).exists()
202212

203213
return updated
204214

src/concurrency/management/commands/triggers.py

Lines changed: 34 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
# -*- coding: utf-8 -*-
2-
from optparse import make_option
3-
42
from django.core.exceptions import ImproperlyConfigured
53
from django.core.management.base import BaseCommand
64
from django.db import connections
@@ -12,20 +10,37 @@
1210
class Command(BaseCommand):
1311
args = ''
1412
help = 'register Report classes and create one ReportConfiguration per each'
15-
requires_model_validation = False
16-
# requires_system_checks = False
17-
18-
option_list = BaseCommand.option_list + (
19-
make_option('-d', '--database',
20-
action='store',
21-
dest='database',
22-
default=None,
23-
help='limit to this database'),
24-
make_option('-t', '--trigger',
25-
action='store',
26-
dest='trigger',
27-
default=None,
28-
help='limit to this trigger name'))
13+
14+
requires_system_checks = False
15+
16+
def add_arguments(self, parser):
17+
"""
18+
Entry point for subclassed commands to add custom arguments.
19+
"""
20+
subparsers = parser.add_subparsers(help='sub-command help',
21+
dest='command')
22+
23+
subparsers.add_parser('list',
24+
cmd=parser,
25+
help="command_a help")
26+
subparsers.add_parser('drop',
27+
cmd=parser,
28+
help="command_a help")
29+
subparsers.add_parser('create',
30+
cmd=parser,
31+
help="command_a help")
32+
33+
parser.add_argument('-d', '--database',
34+
action='store',
35+
dest='database',
36+
default=None,
37+
help='limit to this database')
38+
39+
parser.add_argument('-t', '--trigger',
40+
action='store',
41+
dest='trigger',
42+
default=None,
43+
help='limit to this trigger name')
2944

3045
def _list(self, databases):
3146
for alias, triggers in get_triggers(databases).items():
@@ -34,7 +49,9 @@ def _list(self, databases):
3449
self.stdout.write(" {}".format(trigger))
3550
self.stdout.write('')
3651

37-
def handle(self, cmd='list', *args, **options):
52+
def handle(self, *args, **options):
53+
54+
cmd = options['command']
3855
database = options['database']
3956
if database is None:
4057
databases = [alias for alias in connections]

tests/conftest.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,23 @@
1+
import os
2+
import platform
13
import sys
4+
25
import django
6+
37
import pytest
48

9+
py_impl = getattr(platform, 'python_implementation', lambda: None)
10+
PYPY = py_impl() == 'PyPy'
11+
PURE_PYTHON = os.environ.get('PURE_PYTHON', False)
12+
513
windows = pytest.mark.skipif(sys.platform != 'win32', reason="requires windows")
614

715
win32only = pytest.mark.skipif("sys.platform != 'win32'")
816

9-
skipIfDjangoVersion = lambda v: pytest.mark.skipif("django.VERSION[:2]>={}".format(v),
10-
reason="Skip if django>={}".format(v))
17+
skippypy = pytest.mark.skipif(PYPY, reason='skip on pypy')
18+
19+
# skipIfDjangoVersion = lambda v: pytest.mark.skipif("django.VERSION[:2]>={}".format(v),
20+
# reason="Skip if django>={}".format(v))
1121

1222

1323
def pytest_configure():

tests/demoapp/demo/admin.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,14 @@
33
from django.contrib import admin
44
from django.contrib.admin.sites import NotRegistered
55

6-
from concurrency.admin import ConcurrentModelAdmin
7-
from concurrency.api import disable_concurrency
8-
96
from demo.models import * # noqa
107
from demo.models import (
118
ListEditableConcurrentModel, NoActionsConcurrentModel, ReversionConcurrentModel
129
)
1310

11+
from concurrency.admin import ConcurrentModelAdmin
12+
from concurrency.api import disable_concurrency
13+
1414
try:
1515
from reversion import VersionAdmin
1616
except ImportError:

tests/demoapp/demo/base.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@
44
from django.contrib.auth.models import Group, User
55
from django.test import TransactionTestCase
66
from django.utils import timezone
7+
8+
from demo.admin import admin_register_models
79
from django_webtest import WebTestMixin
810

911
from concurrency.api import apply_concurrency_check
1012
from concurrency.fields import IntegerVersionField
1113

12-
from demo.admin import admin_register_models
13-
1414
SENTINEL = '**concurrent_update**'
1515

1616

tests/demoapp/demo/migrations/0001_initial.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@
22
# Generated by Django 1.9.1 on 2016-01-09 17:12
33
from __future__ import unicode_literals
44

5-
import concurrency.fields
5+
import django.db.models.deletion
66
from django.conf import settings
77
from django.db import migrations, models
8-
import django.db.models.deletion
8+
9+
import concurrency.fields
910

1011

1112
class Migration(migrations.Migration):

tests/demoapp/demo/migrations/0002_conditionalversionmodelwithoutmeta.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
# -*- coding: utf-8 -*-
22
from __future__ import unicode_literals
33

4-
from django.db import migrations, models
54
from django.conf import settings
5+
from django.db import migrations, models
6+
67
import concurrency.fields
78

89

tests/demoapp/demo/models.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@
44
from django.contrib.auth.models import Group, User
55
from django.db import models
66

7-
from concurrency.fields import (AutoIncVersionField, IntegerVersionField,
8-
TriggerVersionField,
9-
ConditionalVersionField)
7+
from concurrency.fields import (
8+
AutoIncVersionField, ConditionalVersionField, IntegerVersionField,
9+
TriggerVersionField
10+
)
1011

1112
__all__ = ['SimpleConcurrentModel', 'AutoIncConcurrentModel',
1213
'ProxyModel', 'InheritedModel', 'CustomSaveModel',
@@ -26,8 +27,11 @@ class Meta:
2627
verbose_name = "SimpleConcurrentModel"
2728
verbose_name_plural = "SimpleConcurrentModels"
2829

30+
def __str__(self):
31+
return "{0} #{1}".format(self.__class__.__name__, self.pk)
32+
2933
def __unicode__(self):
30-
return "{0.__class__.__name__} #{0.pk}".format(self)
34+
return "{0} #{1}".format(self.__class__.__name__, self.pk)
3135

3236

3337
class AutoIncConcurrentModel(models.Model):

0 commit comments

Comments
 (0)