Skip to content

Commit

Permalink
Merge branch 'release/1.3.2'
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
saxix committed Sep 13, 2016
2 parents 3b9a097 + 2fff161 commit aa1862a
Show file tree
Hide file tree
Showing 14 changed files with 276 additions and 127 deletions.
7 changes: 6 additions & 1 deletion CHANGES
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
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 pagckaging
* just packaging


Release 1.3 (15 Jul 2016)
Expand Down
3 changes: 0 additions & 3 deletions LICENSE
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,6 @@ furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

Any use in a commercial product must be notified to the author by email
indicating company name and product name

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
Expand Down
12 changes: 2 additions & 10 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,6 @@ develop:
@pip install -U pip setuptools
@sh -c "if [ '${DBENGINE}' = 'mysql' ]; then pip install MySQL-python; fi"
@sh -c "if [ '${DBENGINE}' = 'pg' ]; then pip install -q psycopg2; fi"
# @sh -c "if [ '${DJANGO}' = '1.4.x' ]; then pip install 'django>=1.4,<1.5'; fi"
# @sh -c "if [ '${DJANGO}' = '1.5.x' ]; then pip install 'django>=1.5,<1.6'; fi"
# @sh -c "if [ '${DJANGO}' = '1.6.x' ]; then pip install 'django>=1.6,<1.7'; fi"
# @sh -c "if [ '${DJANGO}' = '1.7.x' ]; then pip install 'django>=1.7,<1.8'; fi"
# @sh -c "if [ '${DJANGO}' = '1.8.x' ]; then pip install 'django>=1.8,<1.9'; fi"
# @sh -c "if [ '${DJANGO}' = '1.9.x' ]; then pip install 'django>=1.9,<1.10'; fi"
# @sh -c "if [ '${DJANGO}' = 'last' ]; then pip install django; fi"
# @sh -c "if [ '${DJANGO}' = 'dev' ]; then pip install git+git://github.com/django/django.git; fi"
@pip install -e .[dev]
$(MAKE) .init-db

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

test:
py.test -v
py.test -v --create-db

qa:
flake8 src/ tests/
Expand All @@ -41,7 +33,7 @@ qa:


clean:
rm -fr ${BUILDDIR} dist *.egg-info .coverage coverage.xml
rm -fr ${BUILDDIR} dist *.egg-info .coverage coverage.xml .eggs
find src -name __pycache__ -o -name "*.py?" -o -name "*.orig" -prune | xargs rm -rf
find tests -name __pycache__ -o -name "*.py?" -o -name "*.orig" -prune | xargs rm -rf
find src/concurrency/locale -name django.mo | xargs rm -f
Expand Down
2 changes: 1 addition & 1 deletion src/concurrency/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
__author__ = 'sax'
default_app_config = 'concurrency.apps.ConcurrencyConfig'

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


Expand Down
16 changes: 11 additions & 5 deletions src/concurrency/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from collections import OrderedDict
from functools import update_wrapper

from django.db import models
from django.db.models import signals
from django.db.models.fields import Field
from django.utils.encoding import force_text
Expand All @@ -17,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
from concurrency.utils import refetch, fqn

try:
from django.apps import apps
Expand Down Expand Up @@ -338,10 +339,12 @@ class ConditionalVersionField(AutoIncVersionField):
def contribute_to_class(self, cls, name, virtual_only=False):
super(ConditionalVersionField, self).contribute_to_class(cls, name, virtual_only)
signals.post_init.connect(self._load_model,
sender=cls, weak=False)
sender=cls,
dispatch_uid=fqn(cls))

signals.post_save.connect(self._save_model,
sender=cls, weak=False)
sender=cls,
dispatch_uid=fqn(cls))

def _load_model(self, *args, **kwargs):
instance = kwargs['instance']
Expand All @@ -365,11 +368,14 @@ def _get_hash(self, instance):
if f.name not in ignore_fields])
else:
fields = instance._concurrencymeta.check_fields

for field_name in fields:
# do not use getattr here. we do not need extra sql to retrieve
# FK. the raw value of the FK is enough
values[field_name] = opts.get_field(field_name).value_from_object(instance)
field = opts.get_field(field_name)
if isinstance(field, models.ManyToManyField):
values[field_name] = getattr(instance, field_name).values_list('pk', flat=True)
else:
values[field_name] = field.value_from_object(instance)
return hashlib.sha1(force_text(values).encode('utf-8')).hexdigest()

def _get_next_version(self, model_instance):
Expand Down
61 changes: 61 additions & 0 deletions src/concurrency/utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import, unicode_literals

import inspect
import logging
import warnings

Expand Down Expand Up @@ -121,3 +122,63 @@ def refetch(model_instance):
Reload model instance from the database
"""
return model_instance.__class__.objects.get(pk=model_instance.pk)


def get_classname(o):
""" Returns the classname of an object r a class
:param o:
:return:
"""
if inspect.isclass(o):
target = o
elif callable(o):
target = o
else:
target = o.__class__
try:
return target.__qualname__
except AttributeError:
return target.__name__


def fqn(o):
"""Returns the fully qualified class name of an object or a class
:param o: object or class
:return: class name
>>> fqn('str')
Traceback (most recent call last):
...
ValueError: Invalid argument `str`
>>> class A(object): pass
>>> fqn(A)
'wfp_commonlib.python.reflect.A'
>>> fqn(A())
'wfp_commonlib.python.reflect.A'
>>> from wfp_commonlib.python import RexList
>>> fqn(RexList.append)
'wfp_commonlib.python.structure.RexList.append'
"""
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__'):
parts.append(o.__module__)
parts.append(get_classname(o))
elif inspect.ismodule(o):
return o.__name__
if not parts:
raise ValueError("Invalid argument `%s`" % o)
return ".".join(parts)
109 changes: 64 additions & 45 deletions tests/demoapp/demo/auth_migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -1,77 +1,96 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.6 on 2016-09-09 15:22
from __future__ import unicode_literals

from django.core import validators
import django.contrib.auth.models
import django.core.validators
from django.db import migrations, models
from django.utils import timezone
import django.db.models.deletion
import django.utils.timezone


class Migration(migrations.Migration):

initial = True

dependencies = [
('contenttypes', '__first__'),
('contenttypes', '0002_remove_content_type_name'),
]

operations = [
migrations.CreateModel(
name='Permission',
name='User',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('name', models.CharField(max_length=50, verbose_name='name')),
('content_type', models.ForeignKey(to='contenttypes.ContentType', to_field='id')),
('codename', models.CharField(max_length=100, verbose_name='codename')),
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
('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')),
('first_name', models.CharField(blank=True, max_length=30, verbose_name='first name')),
('last_name', models.CharField(blank=True, max_length=30, verbose_name='last name')),
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
('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')),
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
],
options={
'ordering': ('content_type__app_label', 'content_type__model', 'codename'),
'unique_together': set([('content_type', 'codename')]),
'verbose_name': 'permission',
'verbose_name_plural': 'permissions',
'verbose_name_plural': 'users',
'abstract': False,
'swappable': 'AUTH_USER_MODEL',
'verbose_name': 'user',
},
managers=[
('objects', django.contrib.auth.models.UserManager()),
],
),
migrations.CreateModel(
name='Group',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('name', models.CharField(unique=True, max_length=80, verbose_name='name')),
('permissions', models.ManyToManyField(to='auth.Permission', verbose_name='permissions', blank=True)),
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=80, unique=True, verbose_name='name')),
],
options={
'verbose_name': 'group',
'verbose_name_plural': 'groups',
'verbose_name': 'group',
},
managers=[
('objects', django.contrib.auth.models.GroupManager()),
],
),
migrations.CreateModel(
name='User',
name='Permission',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login',
models.DateTimeField(default=timezone.now, verbose_name='last login', blank=True, null=True),),
('is_superuser', models.BooleanField(default=False,
help_text='Designates that this user has all permissions without explicitly assigning them.',
verbose_name='superuser status')),
('username',
models.CharField(help_text='Required. 30 characters or fewer. Letters, digits and @/./+/-/_ only.',
unique=True, max_length=30, verbose_name='username',
validators=[validators.RegexValidator('^[\\w.@+-]+$', 'Enter a valid username.',
'invalid')])),
('first_name', models.CharField(max_length=30, verbose_name='first name', blank=True)),
('last_name', models.CharField(max_length=30, verbose_name='last name', blank=True)),
('email', models.EmailField(max_length=75, verbose_name='email address', blank=True)),
('is_staff', models.BooleanField(default=False,
help_text='Designates whether the user can log into this admin site.',
verbose_name='staff status')),
('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')),
('date_joined', models.DateTimeField(default=timezone.now, verbose_name='date joined')),
('groups', models.ManyToManyField(to='auth.Group', verbose_name='groups', blank=True)),
('user_permissions',
models.ManyToManyField(to='auth.Permission', verbose_name='user permissions', blank=True)),
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255, verbose_name='name')),
('codename', models.CharField(max_length=100, verbose_name='codename')),
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType', verbose_name='content type')),
],
options={
'swappable': 'AUTH_USER_MODEL',
'verbose_name': 'user',
'verbose_name_plural': 'users',
'verbose_name_plural': 'permissions',
'ordering': ('content_type__app_label', 'content_type__model', 'codename'),
'verbose_name': 'permission',
},
managers=[
('objects', django.contrib.auth.models.PermissionManager()),
],
),
migrations.AddField(
model_name='group',
name='permissions',
field=models.ManyToManyField(blank=True, to='auth.Permission', verbose_name='permissions'),
),
migrations.AddField(
model_name='user',
name='groups',
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'),
),
migrations.AddField(
model_name='user',
name='user_permissions',
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'),
),
migrations.AlterUniqueTogether(
name='permission',
unique_together=set([('content_type', 'codename')]),
),
]
Loading

0 comments on commit aa1862a

Please sign in to comment.