Skip to content

Commit

Permalink
Merge tag '2.6' into develop
Browse files Browse the repository at this point in the history
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
  • Loading branch information
saxix committed Sep 28, 2024
2 parents 4d71784 + 2195839 commit b915a3c
Show file tree
Hide file tree
Showing 61 changed files with 1,395 additions and 1,205 deletions.
13 changes: 13 additions & 0 deletions .flake8
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[flake8]
max-complexity = 12
max-line-length = 120
exclude =
.*/
__pycache__
docs
~build
dist
*.md

per-file-ignores =
src/**/migrations/*.py:E501
4 changes: 2 additions & 2 deletions .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: [ "3.10", "3.11"]
django-version: [ "3.2", "4.2", "5.0"]
python-version: [ "3.11", "3.12"]
django-version: [ "4.2", "5.1"]
db-engine: ["pg", "mysql"]
env:
PY_VER: ${{ matrix.python-version}}
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
!.editorconfig
!.pre-commit-config.yaml
!.readthedocs.yaml
!.flake8
coverage.xml
notes.txt
build/
Expand Down
3 changes: 2 additions & 1 deletion CHANGES
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
(Dev)
Release 2.6
-----------
* add support do Django 5.x
* drop support python 3.9
* drop support django 3.x
* move to .pyproject.toml


Expand Down
48 changes: 32 additions & 16 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "django-concurrency"
version = "2.5"
version = "2.6.0"
description = "Optimistic lock implementation for Django. Prevents users from doing concurrent editing"
authors = [
{name = "sax", email = "[email protected]"},
Expand All @@ -10,6 +10,14 @@ requires-python = ">=3.10"
readme = "README.md"
license = {text = "MIT"}

[project.optional-dependencies]
dj4 = [
"django>=4.2,<5",
]
dj5 = [
"django>=5.1",
]

[tool.pdm]
[[tool.pdm.source]]
url = "https://pypi.org/simple"
Expand All @@ -18,18 +26,17 @@ name = "pypi"

[tool.pdm.dev-dependencies]
dev = [
"black",
"black>=24.8.0",
"bump2version>=1.0.1",
"check-manifest",
"django",
"django-reversion",
"django-webtest",
"flake8",
"isort",
"mock",
"pre-commit",
"psycopg2-binary",
"pytest",
"pytest>=8.3.3",
"pytest-cov",
"pytest-django",
"pytest-echo",
Expand All @@ -40,15 +47,24 @@ dev = [
]

[tool.isort]
combine_as_imports = true
default_section = "THIRDPARTY"
include_trailing_comma = true
known_tests = "pytest,unittest,factory"
known_demo = "demo"
known_django = "django"
sections = "FUTURE,STDLIB,DJANGO,THIRDPARTY,TESTS,FIRSTPARTY,DEMO,LOCALFOLDER"
known_first_party = "etools_validator"
multi_line_output = 3
line_length = 120
balanced_wrapping = true
order_by_type = false
profile = "black"

[tool.black]
line-length = 120
include = '\.pyi?$'
exclude = '''
/(
\.git
| \.hg
| \.mypy_cache
| \.tox
| \.venv
| venv
| _build
| buck-out
| build
| dist
| migrations
| snapshots
)/
'''
19 changes: 0 additions & 19 deletions setup.cfg

This file was deleted.

6 changes: 3 additions & 3 deletions src/concurrency/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
__author__ = 'sax'
__author__ = "sax"

VERSION = __version__ = "2.5"
NAME = 'django-concurrency'
VERSION = __version__ = "2.5.0"
NAME = "django-concurrency"
116 changes: 67 additions & 49 deletions src/concurrency/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,19 @@
from django.db import transaction
from django.db.models import Q
from django.forms import CheckboxInput
from django.forms.formsets import INITIAL_FORM_COUNT, ManagementForm, MAX_NUM_FORM_COUNT, TOTAL_FORM_COUNT
from django.forms.formsets import (
INITIAL_FORM_COUNT,
MAX_NUM_FORM_COUNT,
TOTAL_FORM_COUNT,
ManagementForm,
)
from django.forms.models import BaseModelFormSet
from django.http import HttpResponse, HttpResponseRedirect
from django.utils.encoding import force_str
from django.utils.html import format_html
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _, ngettext
from django.utils.translation import gettext_lazy as _
from django.utils.translation import ngettext

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

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

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

action_form = self.action_form(data, auto_id=None)
action_form.fields['action'].choices = self.get_action_choices(request)
action_form.fields["action"].choices = self.get_action_choices(request)

# If the form's valid we can handle the action.
if action_form.is_valid():
action = action_form.cleaned_data['action']
action = action_form.cleaned_data["action"]
func, name, description = self.get_actions(request)[action]

# Get the list of selected PKs. If nothing's selected, we can't
# perform an action on it, so bail.
if action_form.cleaned_data['select_across']:
if action_form.cleaned_data["select_across"]:
selected = ALL
else:
selected = request.POST.getlist(helpers.ACTION_CHECKBOX_NAME)
Expand All @@ -114,20 +120,27 @@ def response_action(self, request, queryset): # noqa
try:
pk, version = x.split(",")
except ValueError: # pragma: no cover
raise ImproperlyConfigured('`ConcurrencyActionMixin` error.'
'A tuple with `primary_key, version_number` '
'expected: `%s` found' % x)
filters.append(Q(**{'pk': pk,
revision_field.attname: version}))
raise ImproperlyConfigured(
"`ConcurrencyActionMixin` error."
"A tuple with `primary_key, version_number` "
"expected: `%s` found" % x
)
filters.append(Q(**{"pk": pk, revision_field.attname: version}))

queryset = queryset.filter(reduce(operator.or_, filters))
if len(selected) != queryset.count():
messages.error(request, 'One or more record were updated. '
'(Probably by other user) '
'The execution was aborted.')
messages.error(
request,
"One or more record were updated. "
"(Probably by other user) "
"The execution was aborted.",
)
return HttpResponseRedirect(".")
else:
messages.warning(request, 'Selecting all records, you will avoid the concurrency check')
messages.warning(
request,
"Selecting all records, you will avoid the concurrency check",
)

response = func(self, request, queryset)

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

class ConcurrentManagementForm(ManagementForm):
def __init__(self, *args, **kwargs):
self._versions = kwargs.pop('versions', [])
self._versions = kwargs.pop("versions", [])
super().__init__(*args, **kwargs)

def _get_concurrency_fields(self):
Expand Down Expand Up @@ -172,18 +185,20 @@ class ConcurrentBaseModelFormSet(BaseModelFormSet):
def _management_form(self):
"""Returns the ManagementForm instance for this FormSet."""
if self.is_bound:
form = ConcurrentManagementForm(self.data, auto_id=self.auto_id,
prefix=self.prefix)
form = ConcurrentManagementForm(self.data, auto_id=self.auto_id, prefix=self.prefix)
if not form.is_valid():
raise ValidationError('ManagementForm data is missing or has been tampered with')
raise ValidationError("ManagementForm data is missing or has been tampered with")
else:
form = ConcurrentManagementForm(auto_id=self.auto_id,
prefix=self.prefix,
initial={TOTAL_FORM_COUNT: self.total_form_count(),
INITIAL_FORM_COUNT: self.initial_form_count(),
MAX_NUM_FORM_COUNT: self.max_num},
versions=[(form.instance.pk, get_revision_of_object(form.instance)) for form
in self.initial_forms])
form = ConcurrentManagementForm(
auto_id=self.auto_id,
prefix=self.prefix,
initial={
TOTAL_FORM_COUNT: self.total_form_count(),
INITIAL_FORM_COUNT: self.initial_form_count(),
MAX_NUM_FORM_COUNT: self.max_num,
},
versions=[(form.instance.pk, get_revision_of_object(form.instance)) for form in self.initial_forms],
)
return form

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

def get_changelist_formset(self, request, **kwargs):
kwargs['formset'] = ConcurrentBaseModelFormSet
kwargs["formset"] = ConcurrentBaseModelFormSet
return super().get_changelist_formset(request, **kwargs)

def _add_conflict(self, request, obj):
if hasattr(request, '_concurrency_list_editable_errors'):
if hasattr(request, "_concurrency_list_editable_errors"):
request._concurrency_list_editable_errors.append(obj.pk)
else:
request._concurrency_list_editable_errors = [obj.pk]

def _get_conflicts(self, request):
if hasattr(request, '_concurrency_list_editable_errors'):
if hasattr(request, "_concurrency_list_editable_errors"):
return request._concurrency_list_editable_errors
else:
return []
Expand All @@ -212,7 +227,7 @@ def _get_conflicts(self, request):
def save_model(self, request, obj, form, change):
try:
if change:
version = request.POST.get(f'{concurrency_param_name}_{obj.pk}', None)
version = request.POST.get(f"{concurrency_param_name}_{obj.pk}", None)
if version:
core._set_version(obj, version)
super().save_model(request, obj, form, change)
Expand Down Expand Up @@ -248,33 +263,36 @@ def message_user(self, request, message, *args, **kwargs):
m = rex.match(message)
concurrency_errros = len(conflicts)
if m:
updated_record = int(m.group('num')) - concurrency_errros
updated_record = int(m.group("num")) - concurrency_errros

ids = ",".join(map(str, conflicts))
messages.error(request,
ngettext("Record with pk `{0}` has been modified and was not updated",
"Records `{0}` have been modified and were not updated",
concurrency_errros).format(ids))
messages.error(
request,
ngettext(
"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_str(opts.verbose_name)
else:
name = force_str(opts.verbose_name_plural)

message = None
if updated_record > 0:
message = ngettext("%(count)s %(name)s was changed successfully.",
"%(count)s %(name)s were changed successfully.",
updated_record) % {'count': updated_record,
'name': name}
message = ngettext(
"%(count)s %(name)s was changed successfully.",
"%(count)s %(name)s were changed successfully.",
updated_record,
) % {"count": updated_record, "name": name}

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


class ConcurrentModelAdmin(ConcurrencyActionMixin,
ConcurrencyListEditableMixin,
admin.ModelAdmin):
class ConcurrentModelAdmin(ConcurrencyActionMixin, ConcurrencyListEditableMixin, admin.ModelAdmin):
form = ConcurrentForm
formfield_overrides = {forms.VersionField: {'widget': VersionWidget}}
formfield_overrides = {forms.VersionField: {"widget": VersionWidget}}

def check(self, **kwargs):
errors = super().check(**kwargs)
Expand All @@ -283,23 +301,23 @@ def check(self, **kwargs):
if version_field.name not in self.fields:
errors.append(
Error(
'Missed version field in {} fields definition'.format(self),
"Missed version field in {} fields definition".format(self),
hint="Please add '{}' to the 'fields' attribute".format(version_field.name),
obj=None,
id='concurrency.A001',
id="concurrency.A001",
)
)
if self.fieldsets:
version_field = self.model._concurrencymeta.field
fields = flatten([v['fields'] for k, v in self.fieldsets])
fields = flatten([v["fields"] for k, v in self.fieldsets])

if version_field.name not in fields:
errors.append(
Error(
'Missed version field in {} fieldsets definition'.format(self),
"Missed version field in {} fieldsets definition".format(self),
hint="Please add '{}' to the 'fieldsets' attribute".format(version_field.name),
obj=None,
id='concurrency.A002',
id="concurrency.A002",
)
)
return errors
Loading

0 comments on commit b915a3c

Please sign in to comment.