Skip to content

Commit

Permalink
Merge branch 'release/1.2'
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
saxix committed Apr 5, 2016
2 parents 9d0ef2e + 42f149b commit f203ad7
Show file tree
Hide file tree
Showing 13 changed files with 175 additions and 24 deletions.
12 changes: 10 additions & 2 deletions CHANGES
@@ -1,3 +1,11 @@
Release 1.2 (05 Apr 2016)
-------------------------
* better support for django 1.9 ( ``TemplateDoesNotExist`` is now in ``django.template.exceptions``
* improved eror message in ``ConcurrencyListEditableMixin`` :issue:`63` :issue:`64`
* fixes :issue:`61`. Error in ``ConditionalVersionField`` (thanks ticosax)
* fixes ``skipif`` test in pypy


Release 1.1 (13 Feb 2016)
-------------------------
* drop support for django<1.7
Expand All @@ -9,14 +17,14 @@ Release 1.1 (13 Feb 2016)

Release 1.0.1
-------------
* fixes :issue:`56` (thanks oppianmatt).
* fixes :issue:`56` "Can't upgrade django-concurrency to 1.0" (thanks oppianmatt).


Release 1.0
-----------
* **BACKWARD INCOMPATIBLE**:: dropped support for Django prior 1.6
* code clean
* fixes :issue:`54` (thanks vmspike).
* fixes :issue:`54` "Incorrect default for IntegerVersionField" (thanks vmspike).
* fixes :issue:`53`. updates Documentation
* :ref:`disable_concurrency` can now disable concurrency in any model
* :ref:`disable_concurrency` is now also a decorator
Expand Down
2 changes: 1 addition & 1 deletion src/concurrency/__init__.py
Expand Up @@ -5,7 +5,7 @@
__author__ = 'sax'
default_app_config = 'concurrency.apps.ConcurrencyConfig'

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


Expand Down
23 changes: 12 additions & 11 deletions src/concurrency/admin.py
Expand Up @@ -224,18 +224,19 @@ def message_user(self, request, message, *args, **kwargs):
concurrency_errros = len(conflicts)
if m:
updated_record = int(m.group('num')) - concurrency_errros
if updated_record == 0:
message = _("No %(name)s were changed due conflict errors") % {'name': names[0]}

ids = ",".join(map(str, conflicts))
messages.error(request,
ungettext("Record with pk `{0}` has been modified and was not updated",
"Records `{0}` have been modified and were not updated",
concurrency_errros).format(ids))
if updated_record == 1:
name = force_text(opts.verbose_name)
else:
ids = ",".join(map(str, conflicts))
messages.error(request,
ungettext("Record with pk `{0}` has been modified and was not updated",
"Records `{0}` have been modified and were not updated",
concurrency_errros).format(ids))
if updated_record == 1:
name = force_text(opts.verbose_name)
else:
name = force_text(opts.verbose_name_plural)
name = force_text(opts.verbose_name_plural)

message = None
if updated_record > 0:
message = ungettext("%(count)s %(name)s was changed successfully.",
"%(count)s %(name)s were changed successfully.",
updated_record) % {'count': updated_record,
Expand Down
25 changes: 21 additions & 4 deletions src/concurrency/fields.py
@@ -1,6 +1,7 @@
from __future__ import absolute_import, unicode_literals

import copy
import functools
import hashlib
import logging
import time
Expand Down Expand Up @@ -44,8 +45,8 @@ def class_prepared_concurrency_handler(sender, **kwargs):

if hasattr(sender, 'ConcurrencyMeta'):
sender._concurrencymeta.enabled = getattr(sender.ConcurrencyMeta, 'enabled', True)
check_fields = getattr(sender.ConcurrencyMeta, 'check_fields', [])
ignore_fields = getattr(sender.ConcurrencyMeta, 'ignore_fields', [])
check_fields = getattr(sender.ConcurrencyMeta, 'check_fields', None)
ignore_fields = getattr(sender.ConcurrencyMeta, 'ignore_fields', None)
if check_fields and ignore_fields:
raise ValueError("Cannot set both 'check_fields' and 'ignore_fields'")

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


def filter_fields(instance, field):
if not field.concrete:
# reverse relation
return False
if field.is_relation and field.related_model is None:
# generic foreignkeys
return False
if field.many_to_many and instance.pk is None:
# can't load remote object yet
return False
return True


class ConditionalVersionField(AutoIncVersionField):
def contribute_to_class(self, cls, name, virtual_only=False):
super(ConditionalVersionField, self).contribute_to_class(cls, name, virtual_only)
Expand All @@ -333,8 +347,11 @@ def _get_hash(self, instance):
check_fields = instance._concurrencymeta.check_fields
ignore_fields = instance._concurrencymeta.ignore_fields

if check_fields is None:
fields = sorted([f.name for f in instance._meta.get_fields()
filter_ = functools.partial(filter_fields, instance)
if check_fields is None and ignore_fields is None:
fields = sorted([f.name for f in filter(filter_, instance._meta.get_fields())])
elif check_fields is None:
fields = sorted([f.name for f in filter(filter_, instance._meta.get_fields())
if f.name not in ignore_fields])
else:
fields = instance._concurrencymeta.check_fields
Expand Down
@@ -1,6 +1,5 @@
{% extends "admin/base_site.html" %}{% load concurrency %}
{% load i18n l10n %}
{% load url from future %}
{% load admin_urls %}

{% block breadcrumbs %}
Expand Down
@@ -0,0 +1,28 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals

from django.db import migrations, models
from django.conf import settings
import concurrency.fields


class Migration(migrations.Migration):

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('demo', '0001_initial'),
]

operations = [
migrations.CreateModel(
name='ConditionalVersionModelWithoutMeta',
fields=[
('id', models.AutoField(auto_created=True, verbose_name='ID', serialize=False, primary_key=True)),
('version', concurrency.fields.ConditionalVersionField(default=1, help_text='record revision number')),
('field1', models.CharField(unique=True, blank=True, max_length=30, null=True)),
('field2', models.CharField(unique=True, blank=True, max_length=30, null=True)),
('field3', models.CharField(unique=True, blank=True, max_length=30, null=True)),
('user', models.ForeignKey(to=settings.AUTH_USER_MODEL, null=True)),
],
),
]
27 changes: 27 additions & 0 deletions tests/demoapp/demo/migrations/0003_auto_20160224_0637.py
@@ -0,0 +1,27 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('demo', '0002_conditionalversionmodelwithoutmeta'),
]

operations = [
migrations.CreateModel(
name='Anything',
fields=[
('id', models.AutoField(primary_key=True, verbose_name='ID', serialize=False, auto_created=True)),
('name', models.CharField(max_length=10)),
('a_relation', models.ForeignKey(to='demo.ConditionalVersionModelWithoutMeta')),
],
),
migrations.AddField(
model_name='conditionalversionmodelwithoutmeta',
name='anythings',
field=models.ManyToManyField(to='demo.Anything'),
),
]
26 changes: 26 additions & 0 deletions tests/demoapp/demo/models.py
Expand Up @@ -11,6 +11,8 @@
__all__ = ['SimpleConcurrentModel', 'AutoIncConcurrentModel',
'ProxyModel', 'InheritedModel', 'CustomSaveModel',
'ConcreteModel', 'TriggerConcurrentModel',
'ConditionalVersionModelWithoutMeta',
'Anything',
]


Expand Down Expand Up @@ -210,3 +212,27 @@ class Meta:

class ConcurrencyMeta:
check_fields = ['field1', 'field2', 'user']


class Anything(models.Model):
"""
Will create a ManyToOneRel automatic field on
ConditionalVersionModelWithoutMeta instances.
"""
name = models.CharField(max_length=10)
a_relation = models.ForeignKey('demo.ConditionalVersionModelWithoutMeta')


class ConditionalVersionModelWithoutMeta(models.Model):
"""
This model doesn't have ConcurrencyMeta defined.
"""
version = ConditionalVersionField()
field1 = models.CharField(max_length=30, blank=True, null=True, unique=True)
field2 = models.CharField(max_length=30, blank=True, null=True, unique=True)
field3 = models.CharField(max_length=30, blank=True, null=True, unique=True)
user = models.ForeignKey(User, null=True)
anythings = models.ManyToManyField(Anything)

class Meta:
app_label = 'demo'
4 changes: 3 additions & 1 deletion tests/demoapp/demo/settings.py
Expand Up @@ -117,4 +117,6 @@
else:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3'}}
'ENGINE': 'django.db.backends.sqlite3',
'NAME': dbname,
}}
18 changes: 18 additions & 0 deletions tests/test_admin_actions.py
Expand Up @@ -88,3 +88,21 @@ def test_delete_not_allowed_if_updates(self):
sel.checked = True
res = form.submit().follow()
self.assertIn('One or more record were updated', res)


@pytest.mark.django_db
def test_deleteaction(self):
id = next(unique_id)

SimpleConcurrentModel.objects.get_or_create(pk=id)
response = self.app.get(django.core.urlresolvers.reverse('admin:demo_simpleconcurrentmodel_changelist'),
user='sax')
form = response.forms['changelist-form']
form.get('_selected_action', index=0).checked = True
form['action'] = 'delete_selected'
response = form.submit()
expected = 'All of the following objects and their related items will be deleted'
assert expected in response
response = response.form.submit().follow()
assert response.status_code == 200

3 changes: 1 addition & 2 deletions tests/test_admin_list_editable.py
Expand Up @@ -116,8 +116,7 @@ def test_message_user_no_changes(self):

messages = list(map(str, list(res.context['messages'])))

self.assertIn('No %s were changed due conflict errors' % force_text(self.TARGET._meta.verbose_name),
messages)
self.assertIn('Record with pk `%s` has been modified and was not updated' % id, messages)
self.assertEqual(len(messages), 1)

def test_log_change(self):
Expand Down
28 changes: 27 additions & 1 deletion tests/test_conditional.py
Expand Up @@ -8,7 +8,10 @@

from concurrency.exceptions import RecordModifiedError
from concurrency.utils import refetch
from demo.models import ConditionalVersionModel
from demo.models import (
ConditionalVersionModel,
ConditionalVersionModelWithoutMeta,
)

logger = logging.getLogger(__name__)

Expand All @@ -25,6 +28,15 @@ def instance(user):
field2='1', field3='1')[0]


@pytest.fixture
def instance_no_meta(user):
return ConditionalVersionModelWithoutMeta.objects.create(
field1='1',
user=user,
field2='1', field3='1'
)


@pytest.mark.django_db
def test_standard_save(instance):
# only increment if checked field
Expand Down Expand Up @@ -74,3 +86,17 @@ def test_save_allowed(instance):
batch_instance.field3 = 'aaaa'
batch_instance.save()
instance.save()


@pytest.mark.django_db(transaction=True)
def test_conflict_no_meta(instance_no_meta):
# Scenario: batch change any field,
# the user is NOT ALLOWED to save
batch_instance = instance_no_meta.__class__.objects.get(pk=instance_no_meta.pk)
assert batch_instance.version == instance_no_meta.version

batch_instance.field1 = 'aaaa'
batch_instance.save()

with pytest.raises(RecordModifiedError):
instance_no_meta.save()
2 changes: 1 addition & 1 deletion tests/test_threads.py
Expand Up @@ -11,7 +11,7 @@
from concurrency.utils import refetch


@pytest.mark.skypIf(hasattr(sys, "pypy_translation_info"), reason="skip if pypy")
@pytest.mark.skypif(hasattr(sys, "pypy_translation_info"), reason="skip if pypy")
@pytest.mark.django_db(transaction=True)
def test_threads():
if db.connection.vendor == 'sqlite':
Expand Down

0 comments on commit f203ad7

Please sign in to comment.