Skip to content

Commit 7f276e8

Browse files
committed
new "callback" conflict management policy
1 parent eff21ed commit 7f276e8

File tree

14 files changed

+175
-59
lines changed

14 files changed

+175
-59
lines changed

concurrency/admin.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@
66
from django.db.models import Q
77
from django.forms.formsets import (ManagementForm, TOTAL_FORM_COUNT, INITIAL_FORM_COUNT,
88
MAX_NUM_FORM_COUNT)
9-
from django.forms.models import BaseModelFormSet, modelform_factory
9+
from django.forms.models import BaseModelFormSet
1010
from django.utils.safestring import mark_safe
1111
from django.contrib.admin import helpers
1212
from django.http import HttpResponse, HttpResponseRedirect
1313
from django.utils.translation import ugettext as _
1414
from concurrency import forms
15-
from concurrency.api import get_revision_of_object, get_version_fieldname
16-
from concurrency.config import conf, CONCURRENCY_POLICY_SILENT
15+
from concurrency.api import get_revision_of_object
16+
from concurrency.config import conf, CONCURRENCY_LIST_EDITABLE_POLICY_SILENT
1717
from concurrency.exceptions import RecordModifiedError
1818
from concurrency.forms import ConcurrentForm, VersionWidget
1919

@@ -148,7 +148,7 @@ def _management_form(self):
148148

149149

150150
class ConcurrencyListEditableMixin(object):
151-
list_editable_policy = conf.POLICY
151+
list_editable_policy = conf.POLICY & CONCURRENCY_LIST_EDITABLE_POLICY_SILENT
152152

153153
def get_changelist_formset(self, request, **kwargs):
154154
kwargs['formset'] = ConcurrentBaseModelFormSet
@@ -162,7 +162,7 @@ def save_model(self, request, obj, form, change):
162162
obj.version = int(version)
163163
super(ConcurrencyListEditableMixin, self).save_model(request, obj, form, change)
164164
except RecordModifiedError:
165-
if self.list_editable_policy == CONCURRENCY_POLICY_SILENT:
165+
if self.list_editable_policy == CONCURRENCY_LIST_EDITABLE_POLICY_SILENT:
166166
messages.error(request, _("Record with pk `{0.pk}` has been modified and was not updated").format(obj))
167167
else:
168168
raise
@@ -172,4 +172,4 @@ class ConcurrentModelAdmin(ConcurrencyActionMixin,
172172
ConcurrencyListEditableMixin,
173173
admin.ModelAdmin):
174174
form = ConcurrentForm
175-
formfield_overrides = {forms.VersionField: {'widget': VersionWidget},}
175+
formfield_overrides = {forms.VersionField: {'widget': VersionWidget}}

concurrency/config.py

Lines changed: 32 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,22 @@
11
from __future__ import absolute_import, unicode_literals
2-
from six import string_types
32
from django.core.exceptions import ImproperlyConfigured
3+
from django.core.urlresolvers import get_callable
4+
from django.utils import six
45
from django.test.signals import setting_changed
5-
from concurrency.utils import import_by_path
66

77

88
# List Editable Policy
99
# 0 do not save updated records, save others, show message to the user
1010
# 1 abort whole transaction
1111

12-
CONCURRENCY_POLICY_SILENT = 0
13-
CONCURRENCY_POLICY_ABORT_ALL = 1
14-
CONCURRENCY_POLICY_RAISE = 2
12+
CONCURRENCY_LIST_EDITABLE_POLICY_SILENT = 1
13+
CONCURRENCY_LIST_EDITABLE_POLICY_ABORT_ALL = 2
14+
CONCURRENCY_POLICY_RAISE = 4
15+
CONCURRENCY_POLICY_CALLBACK = 8
16+
17+
CONFLICTS_POLICIES = [CONCURRENCY_POLICY_RAISE, CONCURRENCY_POLICY_CALLBACK]
18+
LIST_EDITABLE_POLICIES = [CONCURRENCY_LIST_EDITABLE_POLICY_SILENT, CONCURRENCY_LIST_EDITABLE_POLICY_ABORT_ALL]
19+
1520

1621
class AppSettings(object):
1722
"""
@@ -43,7 +48,8 @@ class AppSettings(object):
4348
defaults = {
4449
'SANITY_CHECK': True,
4550
'FIELD_SIGNER': 'concurrency.forms.VersionFieldSigner',
46-
'POLICY': CONCURRENCY_POLICY_SILENT,
51+
'POLICY': CONCURRENCY_LIST_EDITABLE_POLICY_SILENT | CONCURRENCY_POLICY_RAISE,
52+
'CALLBACK': 'concurrency.views.callback',
4753
'HANDLER409': 'concurrency.views.conflict'}
4854

4955
def __init__(self, prefix):
@@ -63,8 +69,28 @@ def __init__(self, prefix):
6369

6470
setting_changed.connect(self._handler)
6571

72+
def _check_config(self):
73+
list_editable_policy = self.POLICY | sum(LIST_EDITABLE_POLICIES)
74+
if list_editable_policy == sum(LIST_EDITABLE_POLICIES):
75+
raise ImproperlyConfigured("Invalid value for `CONCURRENCY_POLICY`: "
76+
"Use only one of `CONCURRENCY_LIST_EDITABLE_*` flags")
77+
78+
conflict_policy = self.POLICY | sum(CONFLICTS_POLICIES)
79+
if conflict_policy == sum(CONFLICTS_POLICIES):
80+
raise ImproperlyConfigured("Invalid value for `CONCURRENCY_POLICY`: "
81+
"Use only one of `CONCURRENCY_POLICY_*` flags")
82+
6683
def _set_attr(self, prefix_name, value):
6784
name = prefix_name[len(self.prefix) + 1:]
85+
if name == 'CALLBACK':
86+
if isinstance(value, six.string_types):
87+
func = get_callable(value)
88+
elif callable(value):
89+
func = value
90+
else:
91+
raise ImproperlyConfigured("`CALLBACK` must be a callable or a fullpath to callable")
92+
self._callback = func
93+
6894
setattr(self, name, value)
6995

7096
def _handler(self, sender, setting, value, **kwargs):
@@ -76,19 +102,5 @@ def _handler(self, sender, setting, value, **kwargs):
76102
if setting.startswith(self.prefix):
77103
self._set_attr(setting, value)
78104

79-
def _import_by_path(self, attrname, value):
80-
processed = None
81-
if isinstance(value, (list, tuple)):
82-
processed = []
83-
for entry in value:
84-
processed.append(import_by_path(entry))
85-
elif isinstance(value, string_types):
86-
processed = import_by_path(value)
87-
88-
if processed is not None:
89-
setattr(self, attrname, processed)
90-
else:
91-
raise ImproperlyConfigured('Cannot import by path `%s`' % value)
92-
93105

94106
conf = AppSettings('CONCURRENCY')

concurrency/core.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from functools import update_wrapper
44
from django.db import connections, router
55
from django.utils.translation import ugettext as _
6-
from concurrency.config import conf
6+
from concurrency.config import conf, CONCURRENCY_POLICY_CALLBACK
77
from concurrency.exceptions import RecordModifiedError, InconsistencyError
88

99
# Set default logging handler to avoid "No handler found" warnings.
@@ -33,8 +33,11 @@ def _select_lock(model_instance, version_value=None):
3333
if not entry:
3434
logger.debug("Conflict detected on `{0}` pk:`{0.pk}`, "
3535
"version `{1}` not found".format(model_instance, value))
36-
raise RecordModifiedError(_('Record has been modified or no version value passed'),
37-
target=model_instance)
36+
if conf.POLICY & CONCURRENCY_POLICY_CALLBACK:
37+
conf._callback(model_instance)
38+
else:
39+
raise RecordModifiedError(_('Record has been modified or no version value passed'),
40+
target=model_instance)
3841

3942
elif is_versioned and conf.SANITY_CHECK and model_instance._revisionmetainfo.sanity_check:
4043
raise InconsistencyError(_('Version field is set (%s) but record has not `pk`.' % value))

concurrency/tests/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from concurrency.tests.middleware import ConcurrencyMiddlewareTest # NOQA
44
from concurrency.tests.conf import SettingsTest # NOQA
55
from concurrency.tests.api import ConcurrencyTestApi # NOQA
6+
from concurrency.tests.policy import TestPolicy # NOQA
67

78
from concurrency.tests.admin_edit import TestAdminEdit, TestConcurrentModelAdmin # NOQA
89
from concurrency.tests.admin_list_editable import TestListEditable, TestListEditableWithNoActions # NOQA

concurrency/tests/policy.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# -*- coding: utf-8 -*-
2+
from mock import Mock
3+
from concurrency.config import CONCURRENCY_POLICY_CALLBACK, CONCURRENCY_POLICY_RAISE
4+
from concurrency.exceptions import RecordModifiedError
5+
from concurrency.tests.models import TestModel0
6+
from concurrency.tests.base import AdminTestCase
7+
8+
9+
class TestPolicy(AdminTestCase):
10+
11+
def test_policy_callback(self):
12+
callback = Mock()
13+
14+
with self.settings(CONCURRENCY_POLICY=CONCURRENCY_POLICY_CALLBACK,
15+
CONCURRENCY_CALLBACK=callback):
16+
obj1, __ = TestModel0.objects.get_or_create(username='123')
17+
obj2 = TestModel0.objects.get(username='123')
18+
19+
obj2.save()
20+
self.assertEqual(callback.call_count, 0)
21+
22+
obj1.save()
23+
self.assertEqual(callback.call_count, 1)
24+
25+
def test_policy_raise(self):
26+
with self.settings(CONCURRENCY_POLICY=CONCURRENCY_POLICY_RAISE):
27+
28+
obj1, __ = TestModel0.objects.get_or_create(username='123')
29+
obj2 = TestModel0.objects.get(username='123')
30+
obj2.save()
31+
self.assertRaises(RecordModifiedError, obj1.save)

concurrency/views.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ class ConflictResponse(HttpResponse):
1111
handler409 = 'concurrency.views.conflict'
1212

1313

14+
def callback(target, *args, **kwargs):
15+
pass
16+
17+
1418
def conflict(request, target=None, template_name='409.html'):
1519
"""
1620
409 error handler.

demo/demoproject/demoapp/admin.py

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,28 @@
11
from concurrency.admin import ConcurrentModelAdmin
2-
try:
3-
from import_export.admin import ImportExportMixin
4-
except:
5-
class ImportExportMixin(object):
6-
pass
2+
from demoproject.demoapp.models import DemoModel, proxy_factory
73

8-
class DemoModelAdmin(ImportExportMixin, ConcurrentModelAdmin):
4+
5+
class DemoModelAdmin(ConcurrentModelAdmin):
96
# list_display = [f.name for f in DemoModel._meta.fields]
107
list_display = ('id', 'char', 'integer')
118
list_display_links = ('id', )
129
list_editable = ('char', 'integer')
1310
actions = None
11+
12+
13+
try:
14+
from import_export.admin import ImportExportMixin
15+
16+
class ImportExportDemoModelAdmin(ImportExportMixin, ConcurrentModelAdmin):
17+
# list_display = [f.name for f in DemoModel._meta.fields]
18+
list_display = ('id', 'char', 'integer')
19+
list_display_links = ('id', )
20+
list_editable = ('char', 'integer')
21+
actions = None
22+
23+
except:
24+
pass
25+
26+
def register(site):
27+
site.register(DemoModel, DemoModelAdmin)
28+
site.register(proxy_factory("ImportExport"), ImportExportDemoModelAdmin)

demo/demoproject/demoapp/models.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,14 @@ class DemoModel(models.Model):
99

1010
class Meta:
1111
app_label = 'demoapp'
12+
13+
class ProxyDemoModel(DemoModel):
14+
15+
class Meta:
16+
app_label = 'demoapp'
17+
proxy = True
18+
19+
def proxy_factory(name):
20+
return type(name, (ProxyDemoModel,), {'__module__':ProxyDemoModel.__module__,
21+
'Meta': type('Meta', (object,), {'proxy': True, 'app_label': 'demoapp'}, )
22+
})

demo/demoproject/urls.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,30 @@
11
from django.conf.urls import patterns, include
22
# from django.contrib.admin import ModelAdmin
3-
import django.contrib.admin.sites
3+
from django.contrib import admin
44
from django.contrib import admin
55
from django.contrib.auth.models import User, Group
6-
from demoproject.demoapp.admin import DemoModelAdmin
7-
from demoproject.demoapp.models import DemoModel
6+
import demoproject.demoapp.admin
7+
from demoproject.demoapp.admin import DemoModelAdmin, ImportExportDemoModelAdmin
8+
from demoproject.demoapp.models import DemoModel, proxy_factory
89

910
admin.autodiscover()
1011

1112

12-
class PublicAdminSite(django.contrib.admin.sites.AdminSite):
13+
class PublicAdminSite(admin.AdminSite):
1314
def has_permission(self, request):
1415
request.user = User.objects.get_or_create(username='sax')[0]
1516
return True
1617

1718
public_site = PublicAdminSite()
18-
django.contrib.admin.autodiscover()
19+
admin.autodiscover()
1920
public_site.register([User, Group])
21+
for e,v in admin.site._registry.items():
22+
public_site._registry[e] = v
23+
24+
# demoproject.demoapp.admin.site = public_site
25+
2026
public_site.register(DemoModel, DemoModelAdmin)
27+
public_site.register(proxy_factory("ImportExport"), ImportExportDemoModelAdmin)
2128

2229
urlpatterns = patterns('',
2330
(r'^admin/', include(include(public_site.urls))),

docs/admin.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ Handle ``list_editable``
1717

1818
Extend your ModelAdmin with :ref:`ConcurrencyListEditableMixin` or use :ref:`ConcurrentModelAdmin`
1919

20+
.. seealso:: :ref:`list_editable_policies`
21+
2022

2123
.. _admin_action:
2224

0 commit comments

Comments
 (0)