Skip to content

Commit f203ad7

Browse files
committed
Merge branch 'release/1.2'
* release/1.2: bump 1.2 fixes skipif/skipIf typo updates CHANGES Changed test for admin list editable Changed view errors for ConcurrencyListEditableMixin For ConditionalVersionField get_fields() returns too many fields Check that ConditionalVersionField can be used even if ConcurrencyMeta is missing compatibility 1.9 open v1.2
2 parents 9d0ef2e + 42f149b commit f203ad7

13 files changed

+175
-24
lines changed

CHANGES

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
Release 1.2 (05 Apr 2016)
2+
-------------------------
3+
* better support for django 1.9 ( ``TemplateDoesNotExist`` is now in ``django.template.exceptions``
4+
* improved eror message in ``ConcurrencyListEditableMixin`` :issue:`63` :issue:`64`
5+
* fixes :issue:`61`. Error in ``ConditionalVersionField`` (thanks ticosax)
6+
* fixes ``skipif`` test in pypy
7+
8+
19
Release 1.1 (13 Feb 2016)
210
-------------------------
311
* drop support for django<1.7
@@ -9,14 +17,14 @@ Release 1.1 (13 Feb 2016)
917

1018
Release 1.0.1
1119
-------------
12-
* fixes :issue:`56` (thanks oppianmatt).
20+
* fixes :issue:`56` "Can't upgrade django-concurrency to 1.0" (thanks oppianmatt).
1321

1422

1523
Release 1.0
1624
-----------
1725
* **BACKWARD INCOMPATIBLE**:: dropped support for Django prior 1.6
1826
* code clean
19-
* fixes :issue:`54` (thanks vmspike).
27+
* fixes :issue:`54` "Incorrect default for IntegerVersionField" (thanks vmspike).
2028
* fixes :issue:`53`. updates Documentation
2129
* :ref:`disable_concurrency` can now disable concurrency in any model
2230
* :ref:`disable_concurrency` is now also a decorator

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, 1, 0, 'final', 0)
8+
VERSION = __version__ = (1, 2, 0, 'final', 0)
99
NAME = 'django-concurrency'
1010

1111

src/concurrency/admin.py

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -224,18 +224,19 @@ def message_user(self, request, message, *args, **kwargs):
224224
concurrency_errros = len(conflicts)
225225
if m:
226226
updated_record = int(m.group('num')) - concurrency_errros
227-
if updated_record == 0:
228-
message = _("No %(name)s were changed due conflict errors") % {'name': names[0]}
227+
228+
ids = ",".join(map(str, conflicts))
229+
messages.error(request,
230+
ungettext("Record with pk `{0}` has been modified and was not updated",
231+
"Records `{0}` have been modified and were not updated",
232+
concurrency_errros).format(ids))
233+
if updated_record == 1:
234+
name = force_text(opts.verbose_name)
229235
else:
230-
ids = ",".join(map(str, conflicts))
231-
messages.error(request,
232-
ungettext("Record with pk `{0}` has been modified and was not updated",
233-
"Records `{0}` have been modified and were not updated",
234-
concurrency_errros).format(ids))
235-
if updated_record == 1:
236-
name = force_text(opts.verbose_name)
237-
else:
238-
name = force_text(opts.verbose_name_plural)
236+
name = force_text(opts.verbose_name_plural)
237+
238+
message = None
239+
if updated_record > 0:
239240
message = ungettext("%(count)s %(name)s was changed successfully.",
240241
"%(count)s %(name)s were changed successfully.",
241242
updated_record) % {'count': updated_record,

src/concurrency/fields.py

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import absolute_import, unicode_literals
22

33
import copy
4+
import functools
45
import hashlib
56
import logging
67
import time
@@ -44,8 +45,8 @@ def class_prepared_concurrency_handler(sender, **kwargs):
4445

4546
if hasattr(sender, 'ConcurrencyMeta'):
4647
sender._concurrencymeta.enabled = getattr(sender.ConcurrencyMeta, 'enabled', True)
47-
check_fields = getattr(sender.ConcurrencyMeta, 'check_fields', [])
48-
ignore_fields = getattr(sender.ConcurrencyMeta, 'ignore_fields', [])
48+
check_fields = getattr(sender.ConcurrencyMeta, 'check_fields', None)
49+
ignore_fields = getattr(sender.ConcurrencyMeta, 'ignore_fields', None)
4950
if check_fields and ignore_fields:
5051
raise ValueError("Cannot set both 'check_fields' and 'ignore_fields'")
5152

@@ -310,6 +311,19 @@ def inner(self, force_insert=False, force_update=False, using=None, **kwargs):
310311
return update_wrapper(inner, func)
311312

312313

314+
def filter_fields(instance, field):
315+
if not field.concrete:
316+
# reverse relation
317+
return False
318+
if field.is_relation and field.related_model is None:
319+
# generic foreignkeys
320+
return False
321+
if field.many_to_many and instance.pk is None:
322+
# can't load remote object yet
323+
return False
324+
return True
325+
326+
313327
class ConditionalVersionField(AutoIncVersionField):
314328
def contribute_to_class(self, cls, name, virtual_only=False):
315329
super(ConditionalVersionField, self).contribute_to_class(cls, name, virtual_only)
@@ -333,8 +347,11 @@ def _get_hash(self, instance):
333347
check_fields = instance._concurrencymeta.check_fields
334348
ignore_fields = instance._concurrencymeta.ignore_fields
335349

336-
if check_fields is None:
337-
fields = sorted([f.name for f in instance._meta.get_fields()
350+
filter_ = functools.partial(filter_fields, instance)
351+
if check_fields is None and ignore_fields is None:
352+
fields = sorted([f.name for f in filter(filter_, instance._meta.get_fields())])
353+
elif check_fields is None:
354+
fields = sorted([f.name for f in filter(filter_, instance._meta.get_fields())
338355
if f.name not in ignore_fields])
339356
else:
340357
fields = instance._concurrencymeta.check_fields

src/concurrency/templates/concurrency/delete_selected_confirmation.html

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
{% extends "admin/base_site.html" %}{% load concurrency %}
22
{% load i18n l10n %}
3-
{% load url from future %}
43
{% load admin_urls %}
54

65
{% block breadcrumbs %}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# -*- coding: utf-8 -*-
2+
from __future__ import unicode_literals
3+
4+
from django.db import migrations, models
5+
from django.conf import settings
6+
import concurrency.fields
7+
8+
9+
class Migration(migrations.Migration):
10+
11+
dependencies = [
12+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
13+
('demo', '0001_initial'),
14+
]
15+
16+
operations = [
17+
migrations.CreateModel(
18+
name='ConditionalVersionModelWithoutMeta',
19+
fields=[
20+
('id', models.AutoField(auto_created=True, verbose_name='ID', serialize=False, primary_key=True)),
21+
('version', concurrency.fields.ConditionalVersionField(default=1, help_text='record revision number')),
22+
('field1', models.CharField(unique=True, blank=True, max_length=30, null=True)),
23+
('field2', models.CharField(unique=True, blank=True, max_length=30, null=True)),
24+
('field3', models.CharField(unique=True, blank=True, max_length=30, null=True)),
25+
('user', models.ForeignKey(to=settings.AUTH_USER_MODEL, null=True)),
26+
],
27+
),
28+
]
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# -*- coding: utf-8 -*-
2+
from __future__ import unicode_literals
3+
4+
from django.db import migrations, models
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
('demo', '0002_conditionalversionmodelwithoutmeta'),
11+
]
12+
13+
operations = [
14+
migrations.CreateModel(
15+
name='Anything',
16+
fields=[
17+
('id', models.AutoField(primary_key=True, verbose_name='ID', serialize=False, auto_created=True)),
18+
('name', models.CharField(max_length=10)),
19+
('a_relation', models.ForeignKey(to='demo.ConditionalVersionModelWithoutMeta')),
20+
],
21+
),
22+
migrations.AddField(
23+
model_name='conditionalversionmodelwithoutmeta',
24+
name='anythings',
25+
field=models.ManyToManyField(to='demo.Anything'),
26+
),
27+
]

tests/demoapp/demo/models.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
__all__ = ['SimpleConcurrentModel', 'AutoIncConcurrentModel',
1212
'ProxyModel', 'InheritedModel', 'CustomSaveModel',
1313
'ConcreteModel', 'TriggerConcurrentModel',
14+
'ConditionalVersionModelWithoutMeta',
15+
'Anything',
1416
]
1517

1618

@@ -210,3 +212,27 @@ class Meta:
210212

211213
class ConcurrencyMeta:
212214
check_fields = ['field1', 'field2', 'user']
215+
216+
217+
class Anything(models.Model):
218+
"""
219+
Will create a ManyToOneRel automatic field on
220+
ConditionalVersionModelWithoutMeta instances.
221+
"""
222+
name = models.CharField(max_length=10)
223+
a_relation = models.ForeignKey('demo.ConditionalVersionModelWithoutMeta')
224+
225+
226+
class ConditionalVersionModelWithoutMeta(models.Model):
227+
"""
228+
This model doesn't have ConcurrencyMeta defined.
229+
"""
230+
version = ConditionalVersionField()
231+
field1 = models.CharField(max_length=30, blank=True, null=True, unique=True)
232+
field2 = models.CharField(max_length=30, blank=True, null=True, unique=True)
233+
field3 = models.CharField(max_length=30, blank=True, null=True, unique=True)
234+
user = models.ForeignKey(User, null=True)
235+
anythings = models.ManyToManyField(Anything)
236+
237+
class Meta:
238+
app_label = 'demo'

tests/demoapp/demo/settings.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,4 +117,6 @@
117117
else:
118118
DATABASES = {
119119
'default': {
120-
'ENGINE': 'django.db.backends.sqlite3'}}
120+
'ENGINE': 'django.db.backends.sqlite3',
121+
'NAME': dbname,
122+
}}

tests/test_admin_actions.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,3 +88,21 @@ def test_delete_not_allowed_if_updates(self):
8888
sel.checked = True
8989
res = form.submit().follow()
9090
self.assertIn('One or more record were updated', res)
91+
92+
93+
@pytest.mark.django_db
94+
def test_deleteaction(self):
95+
id = next(unique_id)
96+
97+
SimpleConcurrentModel.objects.get_or_create(pk=id)
98+
response = self.app.get(django.core.urlresolvers.reverse('admin:demo_simpleconcurrentmodel_changelist'),
99+
user='sax')
100+
form = response.forms['changelist-form']
101+
form.get('_selected_action', index=0).checked = True
102+
form['action'] = 'delete_selected'
103+
response = form.submit()
104+
expected = 'All of the following objects and their related items will be deleted'
105+
assert expected in response
106+
response = response.form.submit().follow()
107+
assert response.status_code == 200
108+

0 commit comments

Comments
 (0)