Skip to content

Commit aa1862a

Browse files
committed
Merge branch 'release/1.3.2'
* release/1.3.2: bump 1.3.2 fixes bug in ConditionalVersionField that produced 'maximum recursion error' when a model had a ManyToManyField with a field to same model (self-relation) change LICENSE fixes typo in CHANGES open 1.4
2 parents 3b9a097 + 2fff161 commit aa1862a

File tree

14 files changed

+276
-127
lines changed

14 files changed

+276
-127
lines changed

CHANGES

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
1+
Release 1.3.2 (10 Sep 2016)
2+
-------------------------
3+
* fixes bug in ConditionalVersionField that produced 'maximum recursion error' when a model had a ManyToManyField with a field to same model (self-relation)
4+
5+
16
Release 1.3.1 (15 Jul 2016)
27
-------------------------
3-
* just pagckaging
8+
* just packaging
49

510

611
Release 1.3 (15 Jul 2016)

LICENSE

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,6 @@ furnished to do so, subject to the following conditions:
1212
The above copyright notice and this permission notice shall be included in all
1313
copies or substantial portions of the Software.
1414

15-
Any use in a commercial product must be notified to the author by email
16-
indicating company name and product name
17-
1815
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
1916
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
2017
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE

Makefile

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,6 @@ develop:
1212
@pip install -U pip setuptools
1313
@sh -c "if [ '${DBENGINE}' = 'mysql' ]; then pip install MySQL-python; fi"
1414
@sh -c "if [ '${DBENGINE}' = 'pg' ]; then pip install -q psycopg2; fi"
15-
# @sh -c "if [ '${DJANGO}' = '1.4.x' ]; then pip install 'django>=1.4,<1.5'; fi"
16-
# @sh -c "if [ '${DJANGO}' = '1.5.x' ]; then pip install 'django>=1.5,<1.6'; fi"
17-
# @sh -c "if [ '${DJANGO}' = '1.6.x' ]; then pip install 'django>=1.6,<1.7'; fi"
18-
# @sh -c "if [ '${DJANGO}' = '1.7.x' ]; then pip install 'django>=1.7,<1.8'; fi"
19-
# @sh -c "if [ '${DJANGO}' = '1.8.x' ]; then pip install 'django>=1.8,<1.9'; fi"
20-
# @sh -c "if [ '${DJANGO}' = '1.9.x' ]; then pip install 'django>=1.9,<1.10'; fi"
21-
# @sh -c "if [ '${DJANGO}' = 'last' ]; then pip install django; fi"
22-
# @sh -c "if [ '${DJANGO}' = 'dev' ]; then pip install git+git://github.com/django/django.git; fi"
2315
@pip install -e .[dev]
2416
$(MAKE) .init-db
2517

@@ -32,7 +24,7 @@ develop:
3224
@sh -c "if [ '${DBENGINE}' = 'pg' ]; then psql -c 'CREATE DATABASE concurrency;' -U postgres; fi"
3325

3426
test:
35-
py.test -v
27+
py.test -v --create-db
3628

3729
qa:
3830
flake8 src/ tests/
@@ -41,7 +33,7 @@ qa:
4133

4234

4335
clean:
44-
rm -fr ${BUILDDIR} dist *.egg-info .coverage coverage.xml
36+
rm -fr ${BUILDDIR} dist *.egg-info .coverage coverage.xml .eggs
4537
find src -name __pycache__ -o -name "*.py?" -o -name "*.orig" -prune | xargs rm -rf
4638
find tests -name __pycache__ -o -name "*.py?" -o -name "*.orig" -prune | xargs rm -rf
4739
find src/concurrency/locale -name django.mo | xargs rm -f

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

1111

src/concurrency/fields.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from collections import OrderedDict
99
from functools import update_wrapper
1010

11+
from django.db import models
1112
from django.db.models import signals
1213
from django.db.models.fields import Field
1314
from django.utils.encoding import force_text
@@ -17,7 +18,7 @@
1718
from concurrency.api import get_revision_of_object
1819
from concurrency.config import conf
1920
from concurrency.core import ConcurrencyOptions
20-
from concurrency.utils import refetch
21+
from concurrency.utils import refetch, fqn
2122

2223
try:
2324
from django.apps import apps
@@ -338,10 +339,12 @@ class ConditionalVersionField(AutoIncVersionField):
338339
def contribute_to_class(self, cls, name, virtual_only=False):
339340
super(ConditionalVersionField, self).contribute_to_class(cls, name, virtual_only)
340341
signals.post_init.connect(self._load_model,
341-
sender=cls, weak=False)
342+
sender=cls,
343+
dispatch_uid=fqn(cls))
342344

343345
signals.post_save.connect(self._save_model,
344-
sender=cls, weak=False)
346+
sender=cls,
347+
dispatch_uid=fqn(cls))
345348

346349
def _load_model(self, *args, **kwargs):
347350
instance = kwargs['instance']
@@ -365,11 +368,14 @@ def _get_hash(self, instance):
365368
if f.name not in ignore_fields])
366369
else:
367370
fields = instance._concurrencymeta.check_fields
368-
369371
for field_name in fields:
370372
# do not use getattr here. we do not need extra sql to retrieve
371373
# FK. the raw value of the FK is enough
372-
values[field_name] = opts.get_field(field_name).value_from_object(instance)
374+
field = opts.get_field(field_name)
375+
if isinstance(field, models.ManyToManyField):
376+
values[field_name] = getattr(instance, field_name).values_list('pk', flat=True)
377+
else:
378+
values[field_name] = field.value_from_object(instance)
373379
return hashlib.sha1(force_text(values).encode('utf-8')).hexdigest()
374380

375381
def _get_next_version(self, model_instance):

src/concurrency/utils.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# -*- coding: utf-8 -*-
22
from __future__ import absolute_import, unicode_literals
33

4+
import inspect
45
import logging
56
import warnings
67

@@ -121,3 +122,63 @@ def refetch(model_instance):
121122
Reload model instance from the database
122123
"""
123124
return model_instance.__class__.objects.get(pk=model_instance.pk)
125+
126+
127+
def get_classname(o):
128+
""" Returns the classname of an object r a class
129+
130+
:param o:
131+
:return:
132+
"""
133+
if inspect.isclass(o):
134+
target = o
135+
elif callable(o):
136+
target = o
137+
else:
138+
target = o.__class__
139+
try:
140+
return target.__qualname__
141+
except AttributeError:
142+
return target.__name__
143+
144+
145+
def fqn(o):
146+
"""Returns the fully qualified class name of an object or a class
147+
148+
:param o: object or class
149+
:return: class name
150+
151+
>>> fqn('str')
152+
Traceback (most recent call last):
153+
...
154+
ValueError: Invalid argument `str`
155+
>>> class A(object): pass
156+
>>> fqn(A)
157+
'wfp_commonlib.python.reflect.A'
158+
159+
>>> fqn(A())
160+
'wfp_commonlib.python.reflect.A'
161+
162+
>>> from wfp_commonlib.python import RexList
163+
>>> fqn(RexList.append)
164+
'wfp_commonlib.python.structure.RexList.append'
165+
"""
166+
parts = []
167+
168+
if inspect.ismethod(o):
169+
try:
170+
cls = o.im_class
171+
except AttributeError:
172+
# Python 3 eliminates im_class, substitutes __module__ and
173+
# __qualname__ to provide similar information.
174+
parts = (o.__module__, o.__qualname__)
175+
else:
176+
parts = (fqn(cls), get_classname(o))
177+
elif hasattr(o, '__module__'):
178+
parts.append(o.__module__)
179+
parts.append(get_classname(o))
180+
elif inspect.ismodule(o):
181+
return o.__name__
182+
if not parts:
183+
raise ValueError("Invalid argument `%s`" % o)
184+
return ".".join(parts)
Lines changed: 64 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,77 +1,96 @@
11
# -*- coding: utf-8 -*-
2+
# Generated by Django 1.9.6 on 2016-09-09 15:22
23
from __future__ import unicode_literals
34

4-
from django.core import validators
5+
import django.contrib.auth.models
6+
import django.core.validators
57
from django.db import migrations, models
6-
from django.utils import timezone
8+
import django.db.models.deletion
9+
import django.utils.timezone
710

811

912
class Migration(migrations.Migration):
13+
14+
initial = True
15+
1016
dependencies = [
11-
('contenttypes', '__first__'),
17+
('contenttypes', '0002_remove_content_type_name'),
1218
]
1319

1420
operations = [
1521
migrations.CreateModel(
16-
name='Permission',
22+
name='User',
1723
fields=[
18-
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
19-
('name', models.CharField(max_length=50, verbose_name='name')),
20-
('content_type', models.ForeignKey(to='contenttypes.ContentType', to_field='id')),
21-
('codename', models.CharField(max_length=100, verbose_name='codename')),
24+
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
25+
('password', models.CharField(max_length=128, verbose_name='password')),
26+
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
27+
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
28+
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 30 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=30, unique=True, validators=[django.core.validators.RegexValidator('^[\\w.@+-]+$', 'Enter a valid username. This value may contain only letters, numbers and @/./+/-/_ characters.')], verbose_name='username')),
29+
('first_name', models.CharField(blank=True, max_length=30, verbose_name='first name')),
30+
('last_name', models.CharField(blank=True, max_length=30, verbose_name='last name')),
31+
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
32+
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
33+
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
34+
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
2235
],
2336
options={
24-
'ordering': ('content_type__app_label', 'content_type__model', 'codename'),
25-
'unique_together': set([('content_type', 'codename')]),
26-
'verbose_name': 'permission',
27-
'verbose_name_plural': 'permissions',
37+
'verbose_name_plural': 'users',
38+
'abstract': False,
39+
'swappable': 'AUTH_USER_MODEL',
40+
'verbose_name': 'user',
2841
},
42+
managers=[
43+
('objects', django.contrib.auth.models.UserManager()),
44+
],
2945
),
3046
migrations.CreateModel(
3147
name='Group',
3248
fields=[
33-
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
34-
('name', models.CharField(unique=True, max_length=80, verbose_name='name')),
35-
('permissions', models.ManyToManyField(to='auth.Permission', verbose_name='permissions', blank=True)),
49+
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
50+
('name', models.CharField(max_length=80, unique=True, verbose_name='name')),
3651
],
3752
options={
38-
'verbose_name': 'group',
3953
'verbose_name_plural': 'groups',
54+
'verbose_name': 'group',
4055
},
56+
managers=[
57+
('objects', django.contrib.auth.models.GroupManager()),
58+
],
4159
),
4260
migrations.CreateModel(
43-
name='User',
61+
name='Permission',
4462
fields=[
45-
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
46-
('password', models.CharField(max_length=128, verbose_name='password')),
47-
('last_login',
48-
models.DateTimeField(default=timezone.now, verbose_name='last login', blank=True, null=True),),
49-
('is_superuser', models.BooleanField(default=False,
50-
help_text='Designates that this user has all permissions without explicitly assigning them.',
51-
verbose_name='superuser status')),
52-
('username',
53-
models.CharField(help_text='Required. 30 characters or fewer. Letters, digits and @/./+/-/_ only.',
54-
unique=True, max_length=30, verbose_name='username',
55-
validators=[validators.RegexValidator('^[\\w.@+-]+$', 'Enter a valid username.',
56-
'invalid')])),
57-
('first_name', models.CharField(max_length=30, verbose_name='first name', blank=True)),
58-
('last_name', models.CharField(max_length=30, verbose_name='last name', blank=True)),
59-
('email', models.EmailField(max_length=75, verbose_name='email address', blank=True)),
60-
('is_staff', models.BooleanField(default=False,
61-
help_text='Designates whether the user can log into this admin site.',
62-
verbose_name='staff status')),
63-
('is_active', models.BooleanField(default=True,
64-
help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.',
65-
verbose_name='active')),
66-
('date_joined', models.DateTimeField(default=timezone.now, verbose_name='date joined')),
67-
('groups', models.ManyToManyField(to='auth.Group', verbose_name='groups', blank=True)),
68-
('user_permissions',
69-
models.ManyToManyField(to='auth.Permission', verbose_name='user permissions', blank=True)),
63+
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
64+
('name', models.CharField(max_length=255, verbose_name='name')),
65+
('codename', models.CharField(max_length=100, verbose_name='codename')),
66+
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType', verbose_name='content type')),
7067
],
7168
options={
72-
'swappable': 'AUTH_USER_MODEL',
73-
'verbose_name': 'user',
74-
'verbose_name_plural': 'users',
69+
'verbose_name_plural': 'permissions',
70+
'ordering': ('content_type__app_label', 'content_type__model', 'codename'),
71+
'verbose_name': 'permission',
7572
},
73+
managers=[
74+
('objects', django.contrib.auth.models.PermissionManager()),
75+
],
76+
),
77+
migrations.AddField(
78+
model_name='group',
79+
name='permissions',
80+
field=models.ManyToManyField(blank=True, to='auth.Permission', verbose_name='permissions'),
81+
),
82+
migrations.AddField(
83+
model_name='user',
84+
name='groups',
85+
field=models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups'),
86+
),
87+
migrations.AddField(
88+
model_name='user',
89+
name='user_permissions',
90+
field=models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions'),
91+
),
92+
migrations.AlterUniqueTogether(
93+
name='permission',
94+
unique_together=set([('content_type', 'codename')]),
7695
),
7796
]

0 commit comments

Comments
 (0)