Skip to content

Commit b915a3c

Browse files
committed
Merge tag '2.6' into develop
v2.6 * tag '2.6': lint updats tox matrix fixes tox matrix updats tox updates tox config add flake8 config updates CI lint updates CI lint preparing release
2 parents 4d71784 + 2195839 commit b915a3c

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

61 files changed

+1395
-1205
lines changed

.flake8

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
[flake8]
2+
max-complexity = 12
3+
max-line-length = 120
4+
exclude =
5+
.*/
6+
__pycache__
7+
docs
8+
~build
9+
dist
10+
*.md
11+
12+
per-file-ignores =
13+
src/**/migrations/*.py:E501

.github/workflows/tests.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ jobs:
2626
strategy:
2727
fail-fast: false
2828
matrix:
29-
python-version: [ "3.10", "3.11"]
30-
django-version: [ "3.2", "4.2", "5.0"]
29+
python-version: [ "3.11", "3.12"]
30+
django-version: [ "4.2", "5.1"]
3131
db-engine: ["pg", "mysql"]
3232
env:
3333
PY_VER: ${{ matrix.python-version}}

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
!.editorconfig
66
!.pre-commit-config.yaml
77
!.readthedocs.yaml
8+
!.flake8
89
coverage.xml
910
notes.txt
1011
build/

CHANGES

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
(Dev)
1+
Release 2.6
22
-----------
33
* add support do Django 5.x
44
* drop support python 3.9
5+
* drop support django 3.x
56
* move to .pyproject.toml
67

78

pyproject.toml

Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "django-concurrency"
3-
version = "2.5"
3+
version = "2.6.0"
44
description = "Optimistic lock implementation for Django. Prevents users from doing concurrent editing"
55
authors = [
66
{name = "sax", email = "[email protected]"},
@@ -10,6 +10,14 @@ requires-python = ">=3.10"
1010
readme = "README.md"
1111
license = {text = "MIT"}
1212

13+
[project.optional-dependencies]
14+
dj4 = [
15+
"django>=4.2,<5",
16+
]
17+
dj5 = [
18+
"django>=5.1",
19+
]
20+
1321
[tool.pdm]
1422
[[tool.pdm.source]]
1523
url = "https://pypi.org/simple"
@@ -18,18 +26,17 @@ name = "pypi"
1826

1927
[tool.pdm.dev-dependencies]
2028
dev = [
21-
"black",
29+
"black>=24.8.0",
2230
"bump2version>=1.0.1",
2331
"check-manifest",
24-
"django",
2532
"django-reversion",
2633
"django-webtest",
2734
"flake8",
2835
"isort",
2936
"mock",
3037
"pre-commit",
3138
"psycopg2-binary",
32-
"pytest",
39+
"pytest>=8.3.3",
3340
"pytest-cov",
3441
"pytest-django",
3542
"pytest-echo",
@@ -40,15 +47,24 @@ dev = [
4047
]
4148

4249
[tool.isort]
43-
combine_as_imports = true
44-
default_section = "THIRDPARTY"
45-
include_trailing_comma = true
46-
known_tests = "pytest,unittest,factory"
47-
known_demo = "demo"
48-
known_django = "django"
49-
sections = "FUTURE,STDLIB,DJANGO,THIRDPARTY,TESTS,FIRSTPARTY,DEMO,LOCALFOLDER"
50-
known_first_party = "etools_validator"
51-
multi_line_output = 3
52-
line_length = 120
53-
balanced_wrapping = true
54-
order_by_type = false
50+
profile = "black"
51+
52+
[tool.black]
53+
line-length = 120
54+
include = '\.pyi?$'
55+
exclude = '''
56+
/(
57+
\.git
58+
| \.hg
59+
| \.mypy_cache
60+
| \.tox
61+
| \.venv
62+
| venv
63+
| _build
64+
| buck-out
65+
| build
66+
| dist
67+
| migrations
68+
| snapshots
69+
)/
70+
'''

setup.cfg

Lines changed: 0 additions & 19 deletions
This file was deleted.

src/concurrency/__init__.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
__author__ = 'sax'
1+
__author__ = "sax"
22

3-
VERSION = __version__ = "2.5"
4-
NAME = 'django-concurrency'
3+
VERSION = __version__ = "2.5.0"
4+
NAME = "django-concurrency"

src/concurrency/admin.py

Lines changed: 67 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,19 @@
1010
from django.db import transaction
1111
from django.db.models import Q
1212
from django.forms import CheckboxInput
13-
from django.forms.formsets import INITIAL_FORM_COUNT, ManagementForm, MAX_NUM_FORM_COUNT, TOTAL_FORM_COUNT
13+
from django.forms.formsets import (
14+
INITIAL_FORM_COUNT,
15+
MAX_NUM_FORM_COUNT,
16+
TOTAL_FORM_COUNT,
17+
ManagementForm,
18+
)
1419
from django.forms.models import BaseModelFormSet
1520
from django.http import HttpResponse, HttpResponseRedirect
1621
from django.utils.encoding import force_str
1722
from django.utils.html import format_html
1823
from django.utils.safestring import mark_safe
19-
from django.utils.translation import gettext_lazy as _, ngettext
24+
from django.utils.translation import gettext_lazy as _
25+
from django.utils.translation import ngettext
2026

2127
from concurrency import core, forms
2228
from concurrency.api import get_revision_of_object
@@ -66,7 +72,7 @@ def response_action(self, request, queryset): # noqa
6672
# and bottom of the change list, for example). Get the action
6773
# whose button was pushed.
6874
try:
69-
action_index = int(request.POST.get('index', 0))
75+
action_index = int(request.POST.get("index", 0))
7076
except ValueError: # pragma: no cover
7177
action_index = 0
7278

@@ -77,24 +83,24 @@ def response_action(self, request, queryset): # noqa
7783

7884
# Use the action whose button was pushed
7985
try:
80-
data.update({'action': data.getlist('action')[action_index]})
86+
data.update({"action": data.getlist("action")[action_index]})
8187
except IndexError: # pragma: no cover
8288
# If we didn't get an action from the chosen form that's invalid
8389
# POST data, so by deleting action it'll fail the validation check
8490
# below. So no need to do anything here
8591
pass
8692

8793
action_form = self.action_form(data, auto_id=None)
88-
action_form.fields['action'].choices = self.get_action_choices(request)
94+
action_form.fields["action"].choices = self.get_action_choices(request)
8995

9096
# If the form's valid we can handle the action.
9197
if action_form.is_valid():
92-
action = action_form.cleaned_data['action']
98+
action = action_form.cleaned_data["action"]
9399
func, name, description = self.get_actions(request)[action]
94100

95101
# Get the list of selected PKs. If nothing's selected, we can't
96102
# perform an action on it, so bail.
97-
if action_form.cleaned_data['select_across']:
103+
if action_form.cleaned_data["select_across"]:
98104
selected = ALL
99105
else:
100106
selected = request.POST.getlist(helpers.ACTION_CHECKBOX_NAME)
@@ -114,20 +120,27 @@ def response_action(self, request, queryset): # noqa
114120
try:
115121
pk, version = x.split(",")
116122
except ValueError: # pragma: no cover
117-
raise ImproperlyConfigured('`ConcurrencyActionMixin` error.'
118-
'A tuple with `primary_key, version_number` '
119-
'expected: `%s` found' % x)
120-
filters.append(Q(**{'pk': pk,
121-
revision_field.attname: version}))
123+
raise ImproperlyConfigured(
124+
"`ConcurrencyActionMixin` error."
125+
"A tuple with `primary_key, version_number` "
126+
"expected: `%s` found" % x
127+
)
128+
filters.append(Q(**{"pk": pk, revision_field.attname: version}))
122129

123130
queryset = queryset.filter(reduce(operator.or_, filters))
124131
if len(selected) != queryset.count():
125-
messages.error(request, 'One or more record were updated. '
126-
'(Probably by other user) '
127-
'The execution was aborted.')
132+
messages.error(
133+
request,
134+
"One or more record were updated. "
135+
"(Probably by other user) "
136+
"The execution was aborted.",
137+
)
128138
return HttpResponseRedirect(".")
129139
else:
130-
messages.warning(request, 'Selecting all records, you will avoid the concurrency check')
140+
messages.warning(
141+
request,
142+
"Selecting all records, you will avoid the concurrency check",
143+
)
131144

132145
response = func(self, request, queryset)
133146

@@ -142,7 +155,7 @@ def response_action(self, request, queryset): # noqa
142155

143156
class ConcurrentManagementForm(ManagementForm):
144157
def __init__(self, *args, **kwargs):
145-
self._versions = kwargs.pop('versions', [])
158+
self._versions = kwargs.pop("versions", [])
146159
super().__init__(*args, **kwargs)
147160

148161
def _get_concurrency_fields(self):
@@ -172,18 +185,20 @@ class ConcurrentBaseModelFormSet(BaseModelFormSet):
172185
def _management_form(self):
173186
"""Returns the ManagementForm instance for this FormSet."""
174187
if self.is_bound:
175-
form = ConcurrentManagementForm(self.data, auto_id=self.auto_id,
176-
prefix=self.prefix)
188+
form = ConcurrentManagementForm(self.data, auto_id=self.auto_id, prefix=self.prefix)
177189
if not form.is_valid():
178-
raise ValidationError('ManagementForm data is missing or has been tampered with')
190+
raise ValidationError("ManagementForm data is missing or has been tampered with")
179191
else:
180-
form = ConcurrentManagementForm(auto_id=self.auto_id,
181-
prefix=self.prefix,
182-
initial={TOTAL_FORM_COUNT: self.total_form_count(),
183-
INITIAL_FORM_COUNT: self.initial_form_count(),
184-
MAX_NUM_FORM_COUNT: self.max_num},
185-
versions=[(form.instance.pk, get_revision_of_object(form.instance)) for form
186-
in self.initial_forms])
192+
form = ConcurrentManagementForm(
193+
auto_id=self.auto_id,
194+
prefix=self.prefix,
195+
initial={
196+
TOTAL_FORM_COUNT: self.total_form_count(),
197+
INITIAL_FORM_COUNT: self.initial_form_count(),
198+
MAX_NUM_FORM_COUNT: self.max_num,
199+
},
200+
versions=[(form.instance.pk, get_revision_of_object(form.instance)) for form in self.initial_forms],
201+
)
187202
return form
188203

189204
management_form = property(_management_form)
@@ -193,17 +208,17 @@ class ConcurrencyListEditableMixin:
193208
list_editable_policy = conf.POLICY
194209

195210
def get_changelist_formset(self, request, **kwargs):
196-
kwargs['formset'] = ConcurrentBaseModelFormSet
211+
kwargs["formset"] = ConcurrentBaseModelFormSet
197212
return super().get_changelist_formset(request, **kwargs)
198213

199214
def _add_conflict(self, request, obj):
200-
if hasattr(request, '_concurrency_list_editable_errors'):
215+
if hasattr(request, "_concurrency_list_editable_errors"):
201216
request._concurrency_list_editable_errors.append(obj.pk)
202217
else:
203218
request._concurrency_list_editable_errors = [obj.pk]
204219

205220
def _get_conflicts(self, request):
206-
if hasattr(request, '_concurrency_list_editable_errors'):
221+
if hasattr(request, "_concurrency_list_editable_errors"):
207222
return request._concurrency_list_editable_errors
208223
else:
209224
return []
@@ -212,7 +227,7 @@ def _get_conflicts(self, request):
212227
def save_model(self, request, obj, form, change):
213228
try:
214229
if change:
215-
version = request.POST.get(f'{concurrency_param_name}_{obj.pk}', None)
230+
version = request.POST.get(f"{concurrency_param_name}_{obj.pk}", None)
216231
if version:
217232
core._set_version(obj, version)
218233
super().save_model(request, obj, form, change)
@@ -248,33 +263,36 @@ def message_user(self, request, message, *args, **kwargs):
248263
m = rex.match(message)
249264
concurrency_errros = len(conflicts)
250265
if m:
251-
updated_record = int(m.group('num')) - concurrency_errros
266+
updated_record = int(m.group("num")) - concurrency_errros
252267

253268
ids = ",".join(map(str, conflicts))
254-
messages.error(request,
255-
ngettext("Record with pk `{0}` has been modified and was not updated",
256-
"Records `{0}` have been modified and were not updated",
257-
concurrency_errros).format(ids))
269+
messages.error(
270+
request,
271+
ngettext(
272+
"Record with pk `{0}` has been modified and was not updated",
273+
"Records `{0}` have been modified and were not updated",
274+
concurrency_errros,
275+
).format(ids),
276+
)
258277
if updated_record == 1:
259278
name = force_str(opts.verbose_name)
260279
else:
261280
name = force_str(opts.verbose_name_plural)
262281

263282
message = None
264283
if updated_record > 0:
265-
message = ngettext("%(count)s %(name)s was changed successfully.",
266-
"%(count)s %(name)s were changed successfully.",
267-
updated_record) % {'count': updated_record,
268-
'name': name}
284+
message = ngettext(
285+
"%(count)s %(name)s was changed successfully.",
286+
"%(count)s %(name)s were changed successfully.",
287+
updated_record,
288+
) % {"count": updated_record, "name": name}
269289

270290
return super().message_user(request, message, *args, **kwargs)
271291

272292

273-
class ConcurrentModelAdmin(ConcurrencyActionMixin,
274-
ConcurrencyListEditableMixin,
275-
admin.ModelAdmin):
293+
class ConcurrentModelAdmin(ConcurrencyActionMixin, ConcurrencyListEditableMixin, admin.ModelAdmin):
276294
form = ConcurrentForm
277-
formfield_overrides = {forms.VersionField: {'widget': VersionWidget}}
295+
formfield_overrides = {forms.VersionField: {"widget": VersionWidget}}
278296

279297
def check(self, **kwargs):
280298
errors = super().check(**kwargs)
@@ -283,23 +301,23 @@ def check(self, **kwargs):
283301
if version_field.name not in self.fields:
284302
errors.append(
285303
Error(
286-
'Missed version field in {} fields definition'.format(self),
304+
"Missed version field in {} fields definition".format(self),
287305
hint="Please add '{}' to the 'fields' attribute".format(version_field.name),
288306
obj=None,
289-
id='concurrency.A001',
307+
id="concurrency.A001",
290308
)
291309
)
292310
if self.fieldsets:
293311
version_field = self.model._concurrencymeta.field
294-
fields = flatten([v['fields'] for k, v in self.fieldsets])
312+
fields = flatten([v["fields"] for k, v in self.fieldsets])
295313

296314
if version_field.name not in fields:
297315
errors.append(
298316
Error(
299-
'Missed version field in {} fieldsets definition'.format(self),
317+
"Missed version field in {} fieldsets definition".format(self),
300318
hint="Please add '{}' to the 'fieldsets' attribute".format(version_field.name),
301319
obj=None,
302-
id='concurrency.A002',
320+
id="concurrency.A002",
303321
)
304322
)
305323
return errors

0 commit comments

Comments
 (0)