diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..c3fe134 --- /dev/null +++ b/.flake8 @@ -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 diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 99d42aa..d7b2f6a 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -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}} diff --git a/.gitignore b/.gitignore index 589d1a5..97717ee 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ !.editorconfig !.pre-commit-config.yaml !.readthedocs.yaml +!.flake8 coverage.xml notes.txt build/ diff --git a/CHANGES b/CHANGES index 3a02519..668d1c9 100644 --- a/CHANGES +++ b/CHANGES @@ -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 diff --git a/pyproject.toml b/pyproject.toml index 481498d..b5e0a2d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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 = "s.apostolico@gmail.com"}, @@ -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" @@ -18,10 +26,9 @@ name = "pypi" [tool.pdm.dev-dependencies] dev = [ - "black", + "black>=24.8.0", "bump2version>=1.0.1", "check-manifest", - "django", "django-reversion", "django-webtest", "flake8", @@ -29,7 +36,7 @@ dev = [ "mock", "pre-commit", "psycopg2-binary", - "pytest", + "pytest>=8.3.3", "pytest-cov", "pytest-django", "pytest-echo", @@ -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 +)/ +''' diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 4fcde23..0000000 --- a/setup.cfg +++ /dev/null @@ -1,19 +0,0 @@ -[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 - -[flake8] -max-complexity = 12 -max-line-length = 160 -exclude = .tox,migrations,.git,docs,diff_match_patch.py, deploy/**,settings -ignore = E501,E401,W391,E128,E261,E731,W504 diff --git a/src/concurrency/__init__.py b/src/concurrency/__init__.py index 1d4a7f4..1add51f 100755 --- a/src/concurrency/__init__.py +++ b/src/concurrency/__init__.py @@ -1,4 +1,4 @@ -__author__ = 'sax' +__author__ = "sax" -VERSION = __version__ = "2.5" -NAME = 'django-concurrency' +VERSION = __version__ = "2.5.0" +NAME = "django-concurrency" diff --git a/src/concurrency/admin.py b/src/concurrency/admin.py index 4ae558f..d1c1f7d 100644 --- a/src/concurrency/admin.py +++ b/src/concurrency/admin.py @@ -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 @@ -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 @@ -77,7 +83,7 @@ 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 @@ -85,16 +91,16 @@ def response_action(self, request, queryset): # noqa 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) @@ -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) @@ -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): @@ -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) @@ -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 [] @@ -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) @@ -248,13 +263,17 @@ 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: @@ -262,19 +281,18 @@ def message_user(self, request, message, *args, **kwargs): 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) @@ -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 diff --git a/src/concurrency/api.py b/src/concurrency/api.py index ede4863..6d79b60 100644 --- a/src/concurrency/api.py +++ b/src/concurrency/api.py @@ -6,9 +6,17 @@ from concurrency.core import get_version_fieldname # _wrap_model_save from concurrency.exceptions import RecordModifiedError -__all__ = ['apply_concurrency_check', 'get_revision_of_object', - 'RecordModifiedError', 'disable_concurrency', - 'get_version', 'is_changed', 'get_version_fieldname'] +__all__ = [ + "apply_concurrency_check", + "get_revision_of_object", + "get_version_fieldname", + "RecordModifiedError", + "disable_concurrency", + "concurrency_disable_increment", + "get_version", + "is_changed", + "get_version_fieldname", +] logger = logging.getLogger(__name__) @@ -31,8 +39,7 @@ def is_changed(obj): """ revision_field = get_version_fieldname(obj) version = get_revision_of_object(obj) - return not obj.__class__.objects.filter(**{obj._meta.pk.name: obj.pk, - revision_field: version}).exists() + return not obj.__class__.objects.filter(**{obj._meta.pk.name: obj.pk, revision_field: version}).exists() def get_version(model_instance, version): @@ -44,15 +51,15 @@ def get_version(model_instance, version): :return: """ version_field = get_version_fieldname(model_instance) - kwargs = {'pk': model_instance.pk, version_field: version} + kwargs = {"pk": model_instance.pk, version_field: version} return model_instance.__class__.objects.get(**kwargs) def apply_concurrency_check(model, fieldname, versionclass): - if hasattr(model, '_concurrencymeta'): + if hasattr(model, "_concurrencymeta"): return - logger.debug('Applying concurrency check to %s' % model) + logger.debug("Applying concurrency check to %s" % model) ver = versionclass() @@ -74,10 +81,16 @@ def __init__(self, model): def __enter__(self): if isinstance(self.model, Model): - self.old_value, self.model._concurrency_disable_increment = getattr(self.model, '_concurrency_disable_increment', False), True + self.old_value, self.model._concurrency_disable_increment = ( + getattr(self.model, "_concurrency_disable_increment", False), + True, + ) self.model._concurrency_disabled = True else: - self.old_value, self.model._concurrencymeta.increment = self.model._concurrencymeta.increment, False + self.old_value, self.model._concurrencymeta.increment = ( + self.model._concurrencymeta.increment, + False, + ) def __exit__(self, *args, **kwds): if isinstance(self.model, Model): @@ -106,7 +119,7 @@ class disable_concurrency: def __init__(self, model=None): self.model = model self.old_value = conf.ENABLED - self.concurrency_managed = (model is None) or hasattr(model, '_concurrencymeta') + self.concurrency_managed = (model is None) or hasattr(model, "_concurrencymeta") def start(self): if not self.concurrency_managed: @@ -114,10 +127,16 @@ def start(self): if self.model is None: self.old_value, conf.ENABLED = conf.ENABLED, False elif isinstance(self.model, Model): - self.old_value, self.model._concurrency_disabled = getattr(self.model, '_concurrency_disabled', False), True + self.old_value, self.model._concurrency_disabled = ( + getattr(self.model, "_concurrency_disabled", False), + True, + ) self.model._concurrency_disabled = True else: - self.old_value, self.model._concurrencymeta.enabled = self.model._concurrencymeta.enabled, False + self.old_value, self.model._concurrencymeta.enabled = ( + self.model._concurrencymeta.enabled, + False, + ) def finish(self): if not self.concurrency_managed: diff --git a/src/concurrency/apps.py b/src/concurrency/apps.py index 13b6db1..8ac89c3 100644 --- a/src/concurrency/apps.py +++ b/src/concurrency/apps.py @@ -2,5 +2,5 @@ class ConcurrencyConfig(AppConfig): - name = 'concurrency' - verbose = 'Django Concurrency' + name = "concurrency" + verbose = "Django Concurrency" diff --git a/src/concurrency/compat.py b/src/concurrency/compat.py index 33274aa..bcf57bb 100644 --- a/src/concurrency/compat.py +++ b/src/concurrency/compat.py @@ -1,8 +1,4 @@ -import django from django.template.exceptions import TemplateDoesNotExist # noqa from django.urls.utils import get_callable # noqa -if django.VERSION[:2] >= (4, 0): - concurrency_param_name = 'form-_concurrency_version' -else: - concurrency_param_name = '_concurrency_version' +concurrency_param_name = "form-_concurrency_version" diff --git a/src/concurrency/config.py b/src/concurrency/config.py index ec2a279..19affc9 100644 --- a/src/concurrency/config.py +++ b/src/concurrency/config.py @@ -12,24 +12,27 @@ CONCURRENCY_POLICY_RAISE = 4 CONCURRENCY_POLICY_CALLBACK = 8 -LIST_EDITABLE_POLICIES = [CONCURRENCY_LIST_EDITABLE_POLICY_SILENT, CONCURRENCY_LIST_EDITABLE_POLICY_ABORT_ALL] +LIST_EDITABLE_POLICIES = [ + CONCURRENCY_LIST_EDITABLE_POLICY_SILENT, + CONCURRENCY_LIST_EDITABLE_POLICY_ABORT_ALL, +] class AppSettings: defaults = { - 'ENABLED': True, - 'AUTO_CREATE_TRIGGERS': True, - 'FIELD_SIGNER': 'concurrency.forms.VersionFieldSigner', - 'POLICY': CONCURRENCY_LIST_EDITABLE_POLICY_SILENT, - 'CALLBACK': 'concurrency.views.callback', - 'HANDLER409': 'concurrency.views.conflict', - 'VERSION_FIELD_REQUIRED': True, - 'TRIGGERS_FACTORY': { - 'postgresql': "concurrency.triggers.PostgreSQL", - 'mysql': "concurrency.triggers.MySQL", - 'sqlite3': "concurrency.triggers.Sqlite3", - 'sqlite': "concurrency.triggers.Sqlite3", - } + "ENABLED": True, + "AUTO_CREATE_TRIGGERS": True, + "FIELD_SIGNER": "concurrency.forms.VersionFieldSigner", + "POLICY": CONCURRENCY_LIST_EDITABLE_POLICY_SILENT, + "CALLBACK": "concurrency.views.callback", + "HANDLER409": "concurrency.views.conflict", + "VERSION_FIELD_REQUIRED": True, + "TRIGGERS_FACTORY": { + "postgresql": "concurrency.triggers.PostgreSQL", + "mysql": "concurrency.triggers.MySQL", + "sqlite3": "concurrency.triggers.Sqlite3", + "sqlite": "concurrency.triggers.Sqlite3", + }, } def __init__(self, prefix): @@ -41,7 +44,7 @@ def __init__(self, prefix): from django.conf import settings for name, default in self.defaults.items(): - prefix_name = (self.prefix + '_' + name).upper() + prefix_name = (self.prefix + "_" + name).upper() value = getattr(settings, prefix_name, default) self._set_attr(prefix_name, value) setattr(settings, prefix_name, value) @@ -50,11 +53,12 @@ def __init__(self, prefix): setting_changed.connect(self._handler) def _set_attr(self, prefix_name, value): - name = prefix_name[len(self.prefix) + 1:] - if name == 'IGNORE_DEFAULT': - raise ImproperlyConfigured('IGNORE_DEFAULT has been removed in django-concurrency 1.5. ' - 'Use VERSION_FIELD_REQUIRED instead') - elif name == 'CALLBACK': + name = prefix_name[1 + len(self.prefix):] + if name == "IGNORE_DEFAULT": + raise ImproperlyConfigured( + "IGNORE_DEFAULT has been removed in django-concurrency 1.5. " "Use VERSION_FIELD_REQUIRED instead" + ) + elif name == "CALLBACK": if isinstance(value, str): func = get_callable(value) elif callable(value): @@ -62,7 +66,9 @@ def _set_attr(self, prefix_name, value): else: raise ImproperlyConfigured( "{} is not a valid value for `CALLBACK`. It must be a callable or a fullpath to callable. ".format( - value)) + value + ) + ) self._callback = func elif name == "TRIGGERS_FACTORY": original = dict(value) @@ -84,4 +90,4 @@ def _handler(self, sender, setting, value, **kwargs): self._set_attr(setting, value) -conf = AppSettings('CONCURRENCY') +conf = AppSettings("CONCURRENCY") diff --git a/src/concurrency/core.py b/src/concurrency/core.py index b0919f7..daf7890 100644 --- a/src/concurrency/core.py +++ b/src/concurrency/core.py @@ -3,7 +3,7 @@ from concurrency.config import conf -logging.getLogger('concurrency').addHandler(NullHandler()) +logging.getLogger("concurrency").addHandler(NullHandler()) logger = logging.getLogger(__name__) @@ -25,7 +25,7 @@ def _set_version(obj, version): def _select_lock(model_instance, version_value=None): - if (not conf.ENABLED): + if not conf.ENABLED: return version_field = model_instance._concurrencymeta.field @@ -33,12 +33,13 @@ def _select_lock(model_instance, version_value=None): is_versioned = value != version_field.get_default() if model_instance.pk is not None and is_versioned: - kwargs = {'pk': model_instance.pk, version_field.name: value} + kwargs = {"pk": model_instance.pk, version_field.name: value} entry = model_instance.__class__._base_manager.filter(**kwargs) if not entry: - logger.debug("Conflict detected on `{0}` pk:`{0.pk}`, " - "version `{1}` not found".format(model_instance, value)) + logger.debug( + "Conflict detected on `{0}` pk:`{0.pk}`, " "version `{1}` not found".format(model_instance, value) + ) conf._callback(model_instance) else: # pragma: no cover pass diff --git a/src/concurrency/exceptions.py b/src/concurrency/exceptions.py index dbb683f..661e9c0 100644 --- a/src/concurrency/exceptions.py +++ b/src/concurrency/exceptions.py @@ -9,7 +9,7 @@ class VersionChangedError(ValidationError): class RecordModifiedError(DatabaseError): def __init__(self, *args, **kwargs): - self.target = kwargs.pop('target') + self.target = kwargs.pop("target") super().__init__(*args, **kwargs) diff --git a/src/concurrency/fields.py b/src/concurrency/fields.py index 73d9b11..71c8320 100755 --- a/src/concurrency/fields.py +++ b/src/concurrency/fields.py @@ -27,28 +27,28 @@ def class_prepared_concurrency_handler(sender, **kwargs): - if hasattr(sender, '_concurrencymeta'): + if hasattr(sender, "_concurrencymeta"): if sender != sender._concurrencymeta.base: - origin = getattr(sender._concurrencymeta.base, '_concurrencymeta') + origin = getattr(sender._concurrencymeta.base, "_concurrencymeta") local = copy.deepcopy(origin) - setattr(sender, '_concurrencymeta', local) + setattr(sender, "_concurrencymeta", local) - if hasattr(sender, 'ConcurrencyMeta'): - sender._concurrencymeta.enabled = getattr(sender.ConcurrencyMeta, 'enabled', True) - check_fields = getattr(sender.ConcurrencyMeta, 'check_fields', None) - ignore_fields = getattr(sender.ConcurrencyMeta, 'ignore_fields', None) + if hasattr(sender, "ConcurrencyMeta"): + sender._concurrencymeta.enabled = getattr(sender.ConcurrencyMeta, "enabled", True) + 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'") sender._concurrencymeta.check_fields = check_fields sender._concurrencymeta.ignore_fields = ignore_fields - sender._concurrencymeta.increment = getattr(sender.ConcurrencyMeta, 'increment', True) + sender._concurrencymeta.increment = getattr(sender.ConcurrencyMeta, "increment", True) sender._concurrencymeta.skip = False if not (sender._concurrencymeta.manually): sender._concurrencymeta.field.wrap_model(sender) - setattr(sender, 'get_concurrency_version', get_revision_of_object) + setattr(sender, "get_concurrency_version", get_revision_of_object) def post_syncdb_concurrency_handler(sender, **kwargs): @@ -60,30 +60,34 @@ def post_syncdb_concurrency_handler(sender, **kwargs): create_triggers(databases) -class_prepared.connect(class_prepared_concurrency_handler, - dispatch_uid='class_prepared_concurrency_handler') +class_prepared.connect( + class_prepared_concurrency_handler, + dispatch_uid="class_prepared_concurrency_handler", +) if conf.AUTO_CREATE_TRIGGERS: - post_migrate.connect(post_syncdb_concurrency_handler, - dispatch_uid='post_syncdb_concurrency_handler') + post_migrate.connect(post_syncdb_concurrency_handler, dispatch_uid="post_syncdb_concurrency_handler") class VersionField(Field): - """ Base class """ + """Base class""" def __init__(self, *args, **kwargs): - verbose_name = kwargs.get('verbose_name', None) - name = kwargs.get('name', None) - db_tablespace = kwargs.get('db_tablespace', None) - db_column = kwargs.get('db_column', None) - help_text = kwargs.get('help_text', _('record revision number')) - - super().__init__(verbose_name, name, - help_text=help_text, - default=0, - db_tablespace=db_tablespace, - db_column=db_column) + verbose_name = kwargs.get("verbose_name", None) + name = kwargs.get("name", None) + db_tablespace = kwargs.get("db_tablespace", None) + db_column = kwargs.get("db_column", None) + help_text = kwargs.get("help_text", _("record revision number")) + + super().__init__( + verbose_name, + name, + help_text=help_text, + default=0, + db_tablespace=db_tablespace, + db_column=db_column, + ) def get_internal_type(self): return "BigIntegerField" @@ -95,15 +99,15 @@ def validate(self, value, model_instance): pass def formfield(self, **kwargs): - kwargs['form_class'] = self.form_class - kwargs['widget'] = forms.VersionField.widget + kwargs["form_class"] = self.form_class + kwargs["widget"] = forms.VersionField.widget return super().formfield(**kwargs) def contribute_to_class(self, cls, *args, **kwargs): super().contribute_to_class(cls, *args, **kwargs) - if hasattr(cls, '_concurrencymeta') or cls._meta.abstract: + if hasattr(cls, "_concurrencymeta") or cls._meta.abstract: return - setattr(cls, '_concurrencymeta', ConcurrencyOptions()) + setattr(cls, "_concurrencymeta", ConcurrencyOptions()) cls._concurrencymeta.field = self cls._concurrencymeta.base = cls cls._concurrencymeta.triggers = [] @@ -126,8 +130,12 @@ def wrap_model(cls, model, force=False): @staticmethod def _wrap_model_methods(model): - old_do_update = getattr(model, '_do_update') - setattr(model, '_do_update', model._concurrencymeta.field._wrap_do_update(old_do_update)) + old_do_update = getattr(model, "_do_update") + setattr( + model, + "_do_update", + model._concurrencymeta.field._wrap_do_update(old_do_update), + ) def _wrap_do_update(self, func): @@ -136,12 +144,21 @@ def _do_update(model_instance, base_qs, using, pk_val, values, update_fields, fo old_version = get_revision_of_object(model_instance) if not version_field.model._meta.abstract: if version_field.model is not base_qs.model: - return func(model_instance, base_qs, using, pk_val, values, update_fields, forced_update) + return func( + model_instance, + base_qs, + using, + pk_val, + values, + update_fields, + forced_update, + ) for i, (field, _1, value) in enumerate(values): if field == version_field: - if (model_instance._concurrencymeta.increment and not - getattr(model_instance, '_concurrency_disable_increment', False)): + if model_instance._concurrencymeta.increment and not getattr( + model_instance, "_concurrency_disable_increment", False + ): new_version = field._get_next_version(model_instance) values[i] = (field, _1, new_version) field._set_version_value(model_instance, new_version) @@ -161,17 +178,19 @@ def _do_update(model_instance, base_qs, using, pk_val, values, update_fields, fo # the update fails (but does not raise an exception) under this circumstance by skipping the concurrency # logic. if values and updated: - if (model_instance._concurrencymeta.enabled and - conf.ENABLED and - not getattr(model_instance, '_concurrency_disabled', False) and - (old_version or conf.VERSION_FIELD_REQUIRED)): - filter_kwargs = {'pk': pk_val, version_field.attname: old_version} + if ( + model_instance._concurrencymeta.enabled + and conf.ENABLED + and not getattr(model_instance, "_concurrency_disabled", False) + and (old_version or conf.VERSION_FIELD_REQUIRED) + ): + filter_kwargs = {"pk": pk_val, version_field.attname: old_version} updated = base_qs.filter(**filter_kwargs)._update(values) >= 1 if not updated: version_field._set_version_value(model_instance, old_version) updated = conf._callback(model_instance) else: - filter_kwargs = {'pk': pk_val} + filter_kwargs = {"pk": pk_val} updated = base_qs.filter(**filter_kwargs)._update(values) >= 1 return updated @@ -181,12 +200,13 @@ def _do_update(model_instance, base_qs, using, pk_val, values, update_fields, fo class IntegerVersionField(VersionField): """ - Version Field that returns a "unique" version number for the record. + Version Field that returns a "unique" version number for the record. - The version number is produced using time.time() * 1000000, to get the benefits - of microsecond if the system clock provides them. + The version number is produced using time.time() * 1000000, to get the benefits + of microsecond if the system clock provides them. """ + form_class = forms.VersionField def _get_next_version(self, model_instance): @@ -196,9 +216,10 @@ def _get_next_version(self, model_instance): class AutoIncVersionField(VersionField): """ - Version Field increment the revision number each commit + Version Field increment the revision number each commit """ + form_class = forms.VersionField def _get_next_version(self, model_instance): @@ -207,13 +228,14 @@ def _get_next_version(self, model_instance): class TriggerVersionField(VersionField): """ - Version Field increment the revision number each commit + Version Field increment the revision number each commit """ + form_class = forms.VersionField def __init__(self, *args, **kwargs): - self._trigger_name = kwargs.pop('trigger_name', None) + self._trigger_name = kwargs.pop("trigger_name", None) self._trigger_exists = False super().__init__(*args, **kwargs) @@ -237,10 +259,10 @@ def check(self, **kwargs): if not f.get_trigger(self): errors.append( Warning( - 'Missed trigger for field {}'.format(self), + "Missed trigger for field {}".format(self), hint=None, obj=None, - id='concurrency.W001', + id="concurrency.W001", ) ) return errors @@ -267,20 +289,22 @@ def _increment_version_number(obj): @staticmethod def _wrap_model_methods(model): super(TriggerVersionField, TriggerVersionField)._wrap_model_methods(model) - old_save = getattr(model, 'save') - setattr(model, 'save', model._concurrencymeta.field._wrap_save(old_save)) + old_save = getattr(model, "save") + setattr(model, "save", model._concurrencymeta.field._wrap_save(old_save)) @staticmethod def _wrap_save(func): def inner(self, force_insert=False, force_update=False, using=None, **kwargs): - reload = kwargs.pop('refetch', False) + reload = kwargs.pop("refetch", False) ret = func(self, force_insert, force_update, using, **kwargs) TriggerVersionField._increment_version_number(self) if reload: ret = refetch(self) - setattr(self, - self._concurrencymeta.field.attname, - get_revision_of_object(ret)) + setattr( + self, + self._concurrencymeta.field.attname, + get_revision_of_object(ret), + ) return ret return update_wrapper(inner, func) @@ -302,20 +326,16 @@ def filter_fields(instance, field): class ConditionalVersionField(AutoIncVersionField): def contribute_to_class(self, cls, *args, **kwargs): super().contribute_to_class(cls, *args, **kwargs) - signals.post_init.connect(self._load_model, - sender=cls, - dispatch_uid=fqn(cls)) + signals.post_init.connect(self._load_model, sender=cls, dispatch_uid=fqn(cls)) - signals.post_save.connect(self._save_model, - sender=cls, - dispatch_uid=fqn(cls)) + signals.post_save.connect(self._save_model, sender=cls, dispatch_uid=fqn(cls)) def _load_model(self, *args, **kwargs): - instance = kwargs['instance'] + instance = kwargs["instance"] instance._concurrencymeta.initial = self._get_hash(instance) def _save_model(self, *args, **kwargs): - instance = kwargs['instance'] + instance = kwargs["instance"] instance._concurrencymeta.initial = self._get_hash(instance) def _get_hash(self, instance): @@ -328,8 +348,9 @@ def _get_hash(self, 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]) + 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 for field_name in fields: @@ -337,10 +358,10 @@ def _get_hash(self, instance): # FK. the raw value of the FK is enough field = opts.get_field(field_name) if isinstance(field, models.ManyToManyField): - values[field_name] = getattr(instance, field_name).values_list('pk', flat=True) + 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_str(values).encode('utf-8')).hexdigest() + return hashlib.sha1(force_str(values).encode("utf-8")).hexdigest() def _get_next_version(self, model_instance): if not model_instance.pk: diff --git a/src/concurrency/forms.py b/src/concurrency/forms.py index ba68584..31d735c 100644 --- a/src/concurrency/forms.py +++ b/src/concurrency/forms.py @@ -1,7 +1,11 @@ from importlib import import_module from django import forms -from django.core.exceptions import ImproperlyConfigured, NON_FIELD_ERRORS, ValidationError +from django.core.exceptions import ( + NON_FIELD_ERRORS, + ImproperlyConfigured, + ValidationError, +) from django.core.signing import BadSignature, Signer from django.forms import HiddenInput, ModelForm from django.utils.safestring import mark_safe @@ -13,19 +17,22 @@ class ConcurrentForm(ModelForm): - """ Simple wrapper to ModelForm that try to mitigate some concurrency error. - Note that is always possible have a RecordModifiedError in model.save(). - Statistically form.clean() should catch most of the concurrent editing, but - is good to catch RecordModifiedError in the view too. + """Simple wrapper to ModelForm that try to mitigate some concurrency error. + Note that is always possible have a RecordModifiedError in model.save(). + Statistically form.clean() should catch most of the concurrent editing, but + is good to catch RecordModifiedError in the view too. """ def clean(self): try: if self.instance.pk: - _select_lock(self.instance, self.cleaned_data[self.instance._concurrencymeta.field.name]) + _select_lock( + self.instance, + self.cleaned_data[self.instance._concurrencymeta.field.name], + ) except RecordModifiedError: - self._update_errors(ValidationError({NON_FIELD_ERRORS: self.error_class([_('Record Modified')])})) + self._update_errors(ValidationError({NON_FIELD_ERRORS: self.error_class([_("Record Modified")])})) return super().clean() @@ -48,9 +55,9 @@ def format_value(self, value): def render(self, name, value, attrs=None): ret = super().render(name, value, attrs) - label = '' + label = "" if isinstance(value, SignedValue): - label = str(value).split(':')[0] + label = str(value).split(":")[0] elif value is not None: label = str(value) @@ -66,7 +73,7 @@ def sign(self, value): def get_signer(): path = conf.FIELD_SIGNER - i = path.rfind('.') + i = path.rfind(".") module, attr = path[:i], path[i + 1:] try: mod = import_module(module) @@ -87,7 +94,7 @@ def __repr__(self): if self.value: return str(self.value) else: - return '' + return "" class VersionField(forms.IntegerField): @@ -95,12 +102,12 @@ class VersionField(forms.IntegerField): hidden_widget = HiddenInput # Default widget to use when rendering this as "hidden". def __init__(self, *args, **kwargs): - self._signer = kwargs.pop('signer', get_signer()) - kwargs.pop('min_value', None) - kwargs.pop('max_value', None) - kwargs['required'] = True - kwargs['initial'] = None - kwargs.setdefault('widget', HiddenInput) + self._signer = kwargs.pop("signer", get_signer()) + kwargs.pop("min_value", None) + kwargs.pop("max_value", None) + kwargs["required"] = True + kwargs["initial"] = None + kwargs.setdefault("widget", HiddenInput) super().__init__(*args, **kwargs) def bound_data(self, data, initial): @@ -110,12 +117,12 @@ def prepare_value(self, value): if isinstance(value, SignedValue): return value elif value is None: - return '' + return "" return SignedValue(self._signer.sign(value)) def to_python(self, value): try: - if value not in (None, '', 'None'): + if value not in (None, "", "None"): return int(self._signer.unsign(str(value))) return 0 except (BadSignature, ValueError): diff --git a/src/concurrency/management/commands/triggers.py b/src/concurrency/management/commands/triggers.py index 1e50545..78165f3 100644 --- a/src/concurrency/management/commands/triggers.py +++ b/src/concurrency/management/commands/triggers.py @@ -11,17 +11,14 @@ def _add_subparser(subparsers, parser, name, help): if django.VERSION >= (2, 1): - subparsers.add_parser(name, - help=help) + subparsers.add_parser(name, help=help) else: - subparsers.add_parser(name, - cmd=parser, - help=help) + subparsers.add_parser(name, cmd=parser, help=help) class Command(BaseCommand): - args = '' - help = 'register Report classes and create one ReportConfiguration per each' + args = "" + help = "register Report classes and create one ReportConfiguration per each" requires_system_checks = [] @@ -29,38 +26,43 @@ def add_arguments(self, parser): """ Entry point for subclassed commands to add custom arguments. """ - subparsers = parser.add_subparsers(help='sub-command help', - dest='command') + subparsers = parser.add_subparsers(help="sub-command help", dest="command") add_parser = partial(_add_subparser, subparsers, parser) - add_parser('list', help="list concurrency triggers") - add_parser('drop', help="drop concurrency triggers") - add_parser('create', help="create concurrency triggers") - - parser.add_argument('-d', '--database', - action='store', - dest='database', - default=None, - help='limit to this database') - - parser.add_argument('-t', '--trigger', - action='store', - dest='trigger', - default=None, - help='limit to this trigger name') + add_parser("list", help="list concurrency triggers") + add_parser("drop", help="drop concurrency triggers") + add_parser("create", help="create concurrency triggers") + + parser.add_argument( + "-d", + "--database", + action="store", + dest="database", + default=None, + help="limit to this database", + ) + + parser.add_argument( + "-t", + "--trigger", + action="store", + dest="trigger", + default=None, + help="limit to this trigger name", + ) def _list(self, databases): for alias, triggers in get_triggers(databases).items(): self.stdout.write("Database: {}".format(alias)) for trigger in triggers: self.stdout.write(" {}".format(trigger)) - self.stdout.write('') + self.stdout.write("") def handle(self, *args, **options): - cmd = options['command'] - database = options['database'] + cmd = options["command"] + database = options["database"] if database is None: databases = [alias for alias in connections] else: @@ -68,20 +70,20 @@ def handle(self, *args, **options): with atomic(): try: - if cmd == 'list': + if cmd == "list": self._list(databases) - elif cmd == 'create': + elif cmd == "create": for alias, triggers in create_triggers(databases).items(): self.stdout.write("Database: {}".format(alias)) for trigger in triggers: self.stdout.write(" Created {0[2]} for {0[1]}".format(trigger)) - self.stdout.write('') - elif cmd == 'drop': + self.stdout.write("") + elif cmd == "drop": for alias, triggers in drop_triggers(*databases).items(): self.stdout.write("Database: {}".format(alias)) for trigger in triggers: self.stdout.write(" Dropped {0[2]}".format(trigger)) - self.stdout.write('') + self.stdout.write("") else: # pragma: no cover raise Exception() except ImproperlyConfigured as e: # pragma: no cover diff --git a/src/concurrency/middleware.py b/src/concurrency/middleware.py index 67e4011..b099a81 100644 --- a/src/concurrency/middleware.py +++ b/src/concurrency/middleware.py @@ -6,7 +6,7 @@ class ConcurrencyMiddleware: - """ Intercept :ref:`RecordModifiedError` and invoke a callable defined in + """Intercept :ref:`RecordModifiedError` and invoke a callable defined in :setting:`CONCURRECY_HANDLER409` passing the request and the object. """ diff --git a/src/concurrency/templatetags/concurrency.py b/src/concurrency/templatetags/concurrency.py index cffa8d2..0701ce7 100644 --- a/src/concurrency/templatetags/concurrency.py +++ b/src/concurrency/templatetags/concurrency.py @@ -13,9 +13,8 @@ def identity(obj): """ returns a string representing "," of the passed object """ - if hasattr(obj, '_concurrencymeta'): - return mark_safe("{0},{1}".format(unlocalize(obj.pk), - get_revision_of_object(obj))) + if hasattr(obj, "_concurrencymeta"): + return mark_safe("{0},{1}".format(unlocalize(obj.pk), get_revision_of_object(obj))) else: return mark_safe(unlocalize(obj.pk)) diff --git a/src/concurrency/triggers.py b/src/concurrency/triggers.py index 0573a55..f2009da 100644 --- a/src/concurrency/triggers.py +++ b/src/concurrency/triggers.py @@ -33,8 +33,8 @@ def get_trigger_name(field): if field._trigger_name: name = field._trigger_name else: - name = '{1.db_table}_{0.name}'.format(field, field.model._meta) - return 'concurrency_{}'.format(name) + name = "{1.db_table}_{0.name}".format(field, field.model._meta) + return "concurrency_{}".format(name) def get_triggers(databases=None): @@ -112,6 +112,7 @@ class TriggerFactory: \"\"\" """ + update_clause = "" drop_clause = "" list_clause = "" @@ -126,15 +127,17 @@ def get_trigger(self, field): def create(self, field): if field.trigger_name not in self.get_list(): - stm = self.update_clause.format(trigger_name=field.trigger_name, - opts=field.model._meta, - field=field) + stm = self.update_clause.format(trigger_name=field.trigger_name, opts=field.model._meta, field=field) try: self.connection.cursor().execute(stm) except BaseException as exc: # pragma: no cover - raise DatabaseError("""Error executing: + raise DatabaseError( + """Error executing: {1} -{0}""".format(exc, stm)) +{0}""".format( + exc, stm + ) + ) else: # pragma: no cover pass field._trigger_exists = True @@ -142,9 +145,7 @@ def create(self, field): def drop(self, field): opts = field.model._meta ret = [] - stm = self.drop_clause.format(trigger_name=field.trigger_name, - opts=opts, - field=field) + stm = self.drop_clause.format(trigger_name=field.trigger_name, opts=opts, field=field) self.connection.cursor().execute(stm) ret.append(field.trigger_name) return ret @@ -202,8 +203,9 @@ class MySQL(TriggerFactory): def factory(conn): from concurrency.config import conf + mapping = conf.TRIGGERS_FACTORY try: return mapping[conn.vendor](conn) except KeyError: # pragma: no cover - raise ValueError('{} is not supported by TriggerVersionField'.format(conn)) + raise ValueError("{} is not supported by TriggerVersionField".format(conn)) diff --git a/src/concurrency/utils.py b/src/concurrency/utils.py index b2b4ecd..06e8e29 100644 --- a/src/concurrency/utils.py +++ b/src/concurrency/utils.py @@ -95,13 +95,17 @@ def test_concurrency_safety(self): def test_concurrency_management(self): target = self.concurrency_model - self.assertTrue(hasattr(target, '_concurrencymeta'), - "%s is not under concurrency management" % self.concurrency_model) + self.assertTrue( + hasattr(target, "_concurrencymeta"), + "%s is not under concurrency management" % self.concurrency_model, + ) revision_field = target._concurrencymeta.field - self.assertTrue(revision_field in target._meta.fields, - "%s: version field not in meta.fields" % self.concurrency_model) + self.assertTrue( + revision_field in target._meta.fields, + "%s: version field not in meta.fields" % self.concurrency_model, + ) class ConcurrencyAdminTestMixin: @@ -111,12 +115,12 @@ class ConcurrencyAdminTestMixin: 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 + """Returns the classname of an object r a class :param o: :return: @@ -172,7 +176,7 @@ def fqn(o): # parts = (o.__module__, o.__qualname__) # else: # parts = (fqn(cls), get_classname(o)) - if hasattr(o, '__module__'): + if hasattr(o, "__module__"): parts.append(o.__module__) parts.append(get_classname(o)) elif inspect.ismodule(o): diff --git a/src/concurrency/views.py b/src/concurrency/views.py index cf7491e..64d228f 100644 --- a/src/concurrency/views.py +++ b/src/concurrency/views.py @@ -12,10 +12,10 @@ class ConflictResponse(HttpResponse): def callback(target, *args, **kwargs): - raise RecordModifiedError(_('Record has been modified'), target=target) + raise RecordModifiedError(_("Record has been modified"), target=target) -def conflict(request, target=None, template_name='409.html'): +def conflict(request, target=None, template_name="409.html"): """409 error handler. :param request: Request @@ -29,15 +29,14 @@ def conflict(request, target=None, template_name='409.html'): template = loader.get_template(template_name) except TemplateDoesNotExist: # pragma: no cover template = Template( - '

Conflict

' - '

The request was unsuccessful due to a conflict. ' - 'The object changed during the transaction.

') + "

Conflict

" + "

The request was unsuccessful due to a conflict. " + "The object changed during the transaction.

" + ) try: saved = target.__class__._default_manager.get(pk=target.pk) except target.__class__.DoesNotExist: # pragma: no cover saved = None - ctx = {'target': target, - 'saved': saved, - 'request_path': request.path} + ctx = {"target": target, "saved": saved, "request_path": request.path} return ConflictResponse(template.render(ctx)) diff --git a/tests/conftest.py b/tests/conftest.py index ccd4603..8ef1594 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,29 +5,29 @@ import pytest -py_impl = getattr(platform, 'python_implementation', lambda: None) -PYPY = py_impl() == 'PyPy' -PURE_PYTHON = os.environ.get('PURE_PYTHON', False) +py_impl = getattr(platform, "python_implementation", lambda: None) +PYPY = py_impl() == "PyPy" +PURE_PYTHON = os.environ.get("PURE_PYTHON", False) -windows = pytest.mark.skipif(sys.platform != 'win32', reason="requires windows") +windows = pytest.mark.skipif(sys.platform != "win32", reason="requires windows") win32only = pytest.mark.skipif("sys.platform != 'win32'") -skippypy = pytest.mark.skipif(PYPY, reason='skip on pypy') +skippypy = pytest.mark.skipif(PYPY, reason="skip on pypy") here = Path(__file__).parent -sys.path.insert(0, str(here / 'tests' / "demoapp")) +sys.path.insert(0, str(here / "tests" / "demoapp")) def pytest_configure(): from django.conf import settings - settings.SILENCED_SYSTEM_CHECKS = ['concurrency.W001'] + settings.SILENCED_SYSTEM_CHECKS = ["concurrency.W001"] settings.CONCURRENCY_VERSION_FIELD_REQUIRED = False settings.CONCURRENCY_AUTO_CREATE_TRIGGERS = True -@pytest.fixture(scope='session') +@pytest.fixture(scope="session") def client(request): import django_webtest diff --git a/tests/demoapp/demo/admin.py b/tests/demoapp/demo/admin.py index a92a423..50ac0f5 100644 --- a/tests/demoapp/demo/admin.py +++ b/tests/demoapp/demo/admin.py @@ -1,9 +1,3 @@ -from django.contrib import admin -from django.contrib.admin.sites import NotRegistered - -from concurrency.admin import ConcurrentModelAdmin -from concurrency.api import disable_concurrency - from demo.models import ( InheritedModel, ListEditableConcurrentModel, @@ -12,31 +6,37 @@ ReversionConcurrentModel, SimpleConcurrentModel, ) +from django.contrib import admin +from django.contrib.admin.sites import NotRegistered + +from concurrency.admin import ConcurrentModelAdmin +from concurrency.api import disable_concurrency try: from reversion.admin import VersionAdmin except ImportError: + class VersionAdmin: pass class ListEditableModelAdmin(ConcurrentModelAdmin): - list_display = ('__unicode__', 'version', 'username') - list_editable = ('username',) - ordering = ('id',) + list_display = ("__unicode__", "version", "username") + list_editable = ("username",) + ordering = ("id",) class NoActionsModelAdmin(ConcurrentModelAdmin): - list_display = ('__unicode__', 'version', 'username') - list_editable = ('username',) - ordering = ('id',) + list_display = ("__unicode__", "version", "username") + list_editable = ("username",) + ordering = ("id",) actions = None class ReversionConcurrentModelAdmin(VersionAdmin, ConcurrentModelAdmin): - list_display = ('__unicode__', 'version', 'username') - list_editable = ('username',) - ordering = ('id',) + list_display = ("__unicode__", "version", "username") + list_editable = ("username",) + ordering = ("id",) actions = None @disable_concurrency() @@ -45,13 +45,13 @@ def recover_view(self, request, version_id, extra_context=None): class ActionsModelAdmin(ConcurrentModelAdmin): - list_display = ('__unicode__', 'version', 'username') - actions = ['dummy_action'] - ordering = ('id',) + list_display = ("__unicode__", "version", "username") + actions = ["dummy_action"] + ordering = ("id",) def dummy_action(self, request, queryset): for el in queryset: - el.username = '**action_update**' + el.username = "**action_update**" el.save() diff --git a/tests/demoapp/demo/apps.py b/tests/demoapp/demo/apps.py index ae6688f..841e3df 100644 --- a/tests/demoapp/demo/apps.py +++ b/tests/demoapp/demo/apps.py @@ -2,6 +2,6 @@ class ConcurrencyTestConfig(AppConfig): - name = 'demo' - label = 'demo' - verbose_name = 'Concurrency Tests' + name = "demo" + label = "demo" + verbose_name = "Concurrency Tests" diff --git a/tests/demoapp/demo/auth_migrations/0001_initial.py b/tests/demoapp/demo/auth_migrations/0001_initial.py index d80313c..c54ad6b 100644 --- a/tests/demoapp/demo/auth_migrations/0001_initial.py +++ b/tests/demoapp/demo/auth_migrations/0001_initial.py @@ -11,83 +11,143 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('contenttypes', '0002_remove_content_type_name'), + ("contenttypes", "0002_remove_content_type_name"), ] operations = [ migrations.CreateModel( - name='User', + name="User", fields=[ - ('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')), + ("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={ - 'verbose_name_plural': 'users', - 'abstract': False, - 'swappable': 'AUTH_USER_MODEL', - 'verbose_name': 'user', + "verbose_name_plural": "users", + "abstract": False, + "swappable": "AUTH_USER_MODEL", + "verbose_name": "user", }, managers=[ - ('objects', django.contrib.auth.models.UserManager()), + ("objects", django.contrib.auth.models.UserManager()), ], ), migrations.CreateModel( - name='Group', + name="Group", fields=[ - ('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')), + ("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_plural': 'groups', - 'verbose_name': 'group', + "verbose_name_plural": "groups", + "verbose_name": "group", }, managers=[ - ('objects', django.contrib.auth.models.GroupManager()), + ("objects", django.contrib.auth.models.GroupManager()), ], ), migrations.CreateModel( - name='Permission', + name="Permission", fields=[ - ('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')), + ("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={ - 'verbose_name_plural': 'permissions', - 'ordering': ('content_type__app_label', 'content_type__model', 'codename'), - 'verbose_name': 'permission', + "verbose_name_plural": "permissions", + "ordering": ("content_type__app_label", "content_type__model", "codename"), + "verbose_name": "permission", }, managers=[ - ('objects', django.contrib.auth.models.PermissionManager()), + ("objects", django.contrib.auth.models.PermissionManager()), ], ), migrations.AddField( - model_name='group', - name='permissions', - field=models.ManyToManyField(blank=True, to='auth.Permission', verbose_name='permissions'), + 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'), + 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'), + 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')]), + name="permission", + unique_together=set([("content_type", "codename")]), ), ] diff --git a/tests/demoapp/demo/auth_migrations/0002_concurrency_add_version_to_group.py b/tests/demoapp/demo/auth_migrations/0002_concurrency_add_version_to_group.py index 6077010..bb3c600 100644 --- a/tests/demoapp/demo/auth_migrations/0002_concurrency_add_version_to_group.py +++ b/tests/demoapp/demo/auth_migrations/0002_concurrency_add_version_to_group.py @@ -14,15 +14,15 @@ class Migration(migrations.Migration): } """ + dependencies = [ - ('auth', '0001_initial'), + ("auth", "0001_initial"), ] operations = [ migrations.AddField( - model_name='Group', - name='version', - field=IntegerVersionField(help_text=b'Version', default=1), - + model_name="Group", + name="version", + field=IntegerVersionField(help_text=b"Version", default=1), ), ] diff --git a/tests/demoapp/demo/auth_migrations5/0001_initial.py b/tests/demoapp/demo/auth_migrations5/0001_initial.py index e6ecf13..be4765f 100644 --- a/tests/demoapp/demo/auth_migrations5/0001_initial.py +++ b/tests/demoapp/demo/auth_migrations5/0001_initial.py @@ -71,9 +71,7 @@ class Migration(migrations.Migration): ), ( "permissions", - models.ManyToManyField( - blank=True, to="auth.permission", verbose_name="permissions" - ), + models.ManyToManyField(blank=True, to="auth.permission", verbose_name="permissions"), ), ], options={ @@ -99,9 +97,7 @@ class Migration(migrations.Migration): ("password", models.CharField(max_length=128, verbose_name="password")), ( "last_login", - models.DateTimeField( - blank=True, null=True, verbose_name="last login" - ), + models.DateTimeField(blank=True, null=True, verbose_name="last login"), ), ( "is_superuser", @@ -114,35 +110,25 @@ class Migration(migrations.Migration): ( "username", models.CharField( - error_messages={ - "unique": "A user with that username already exists." - }, + error_messages={"unique": "A user with that username already exists."}, help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.", max_length=150, unique=True, - validators=[ - django.contrib.auth.validators.UnicodeUsernameValidator() - ], + validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name="username", ), ), ( "first_name", - models.CharField( - blank=True, max_length=150, verbose_name="first name" - ), + models.CharField(blank=True, max_length=150, verbose_name="first name"), ), ( "last_name", - models.CharField( - blank=True, max_length=150, verbose_name="last name" - ), + models.CharField(blank=True, max_length=150, verbose_name="last name"), ), ( "email", - models.EmailField( - blank=True, max_length=254, verbose_name="email address" - ), + models.EmailField(blank=True, max_length=254, verbose_name="email address"), ), ( "is_staff", @@ -156,21 +142,21 @@ class Migration(migrations.Migration): "is_active", models.BooleanField( default=True, - help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.", + 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" - ), + models.DateTimeField(default=django.utils.timezone.now, verbose_name="date joined"), ), ( "groups", models.ManyToManyField( blank=True, - help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.", + 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", diff --git a/tests/demoapp/demo/auth_migrations5/0002_concurrency_add_version_to_group.py b/tests/demoapp/demo/auth_migrations5/0002_concurrency_add_version_to_group.py index 6077010..bb3c600 100644 --- a/tests/demoapp/demo/auth_migrations5/0002_concurrency_add_version_to_group.py +++ b/tests/demoapp/demo/auth_migrations5/0002_concurrency_add_version_to_group.py @@ -14,15 +14,15 @@ class Migration(migrations.Migration): } """ + dependencies = [ - ('auth', '0001_initial'), + ("auth", "0001_initial"), ] operations = [ migrations.AddField( - model_name='Group', - name='version', - field=IntegerVersionField(help_text=b'Version', default=1), - + model_name="Group", + name="version", + field=IntegerVersionField(help_text=b"Version", default=1), ), ] diff --git a/tests/demoapp/demo/base.py b/tests/demoapp/demo/base.py index 490bccd..9b5ba74 100644 --- a/tests/demoapp/demo/base.py +++ b/tests/demoapp/demo/base.py @@ -1,30 +1,30 @@ +from demo.admin import admin_register_models from django.contrib.auth.models import Group, User from django.test import TransactionTestCase from django.utils import timezone - from django_webtest import WebTestMixin from concurrency.api import apply_concurrency_check from concurrency.fields import IntegerVersionField -from demo.admin import admin_register_models - -SENTINEL = '**concurrent_update**' +SENTINEL = "**concurrent_update**" -apply_concurrency_check(Group, 'version', IntegerVersionField) +apply_concurrency_check(Group, "version", IntegerVersionField) class AdminTestCase(WebTestMixin, TransactionTestCase): - urls = 'demo.urls' + urls = "demo.urls" def setUp(self): super().setUp() - self.user, __ = User.objects.get_or_create(is_superuser=True, - is_staff=True, - is_active=True, - last_login=timezone.now(), - email='sax@example.com', - username='sax') + self.user, __ = User.objects.get_or_create( + is_superuser=True, + is_staff=True, + is_active=True, + last_login=timezone.now(), + email="sax@example.com", + username="sax", + ) admin_register_models() diff --git a/tests/demoapp/demo/migrations/0001_initial.py b/tests/demoapp/demo/migrations/0001_initial.py index 0520297..b7215be 100644 --- a/tests/demoapp/demo/migrations/0001_initial.py +++ b/tests/demoapp/demo/migrations/0001_initial.py @@ -11,212 +11,312 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('auth', '0002_concurrency_add_version_to_group'), + ("auth", "0002_concurrency_add_version_to_group"), migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ migrations.CreateModel( - name='Anything', + name="Anything", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=10)), + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("name", models.CharField(max_length=10)), ], ), migrations.CreateModel( - name='AutoIncConcurrentModel', + name="AutoIncConcurrentModel", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('version', concurrency.fields.AutoIncVersionField(db_column='cm_version_id', default=1, help_text='record revision number')), - ('username', models.CharField(blank=True, max_length=30, null=True)), - ('date_field', models.DateField(blank=True, null=True)), + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ( + "version", + concurrency.fields.AutoIncVersionField( + db_column="cm_version_id", default=1, help_text="record revision number" + ), + ), + ("username", models.CharField(blank=True, max_length=30, null=True)), + ("date_field", models.DateField(blank=True, null=True)), ], options={ - 'verbose_name_plural': 'AutoIncConcurrentModel', - 'verbose_name': 'AutoIncConcurrentModel', + "verbose_name_plural": "AutoIncConcurrentModel", + "verbose_name": "AutoIncConcurrentModel", }, ), migrations.CreateModel( - name='ConcreteModel', + name="ConcreteModel", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('version', concurrency.fields.IntegerVersionField(db_column='cm_version_id', default=1, help_text='record revision number')), - ('username', models.CharField(blank=True, max_length=30, null=True, unique=True)), + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ( + "version", + concurrency.fields.IntegerVersionField( + db_column="cm_version_id", default=1, help_text="record revision number" + ), + ), + ("username", models.CharField(blank=True, max_length=30, null=True, unique=True)), ], ), migrations.CreateModel( - name='ConditionalVersionModel', + name="ConditionalVersionModel", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('version', concurrency.fields.ConditionalVersionField(default=1, help_text='record revision number')), - ('field1', models.CharField(blank=True, max_length=30, null=True, unique=True)), - ('field2', models.CharField(blank=True, max_length=30, null=True, unique=True)), - ('field3', models.CharField(blank=True, max_length=30, null=True, unique=True)), - ('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("version", concurrency.fields.ConditionalVersionField(default=1, help_text="record revision number")), + ("field1", models.CharField(blank=True, max_length=30, null=True, unique=True)), + ("field2", models.CharField(blank=True, max_length=30, null=True, unique=True)), + ("field3", models.CharField(blank=True, max_length=30, null=True, unique=True)), + ( + "user", + models.ForeignKey( + null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL + ), + ), ], ), migrations.CreateModel( - name='ConditionalVersionModelSelfRelation', + name="ConditionalVersionModelSelfRelation", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('version', concurrency.fields.ConditionalVersionField(default=1, help_text='record revision number')), - ('name', models.CharField(max_length=10)), + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("version", concurrency.fields.ConditionalVersionField(default=1, help_text="record revision number")), + ("name", models.CharField(max_length=10)), ], ), migrations.CreateModel( - name='ConditionalVersionModelWithoutMeta', + name="ConditionalVersionModelWithoutMeta", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('version', concurrency.fields.ConditionalVersionField(default=1, help_text='record revision number')), - ('field1', models.CharField(blank=True, max_length=30, null=True, unique=True)), - ('field2', models.CharField(blank=True, max_length=30, null=True, unique=True)), - ('field3', models.CharField(blank=True, max_length=30, null=True, unique=True)), - ('anythings', models.ManyToManyField(to='demo.Anything')), - ('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("version", concurrency.fields.ConditionalVersionField(default=1, help_text="record revision number")), + ("field1", models.CharField(blank=True, max_length=30, null=True, unique=True)), + ("field2", models.CharField(blank=True, max_length=30, null=True, unique=True)), + ("field3", models.CharField(blank=True, max_length=30, null=True, unique=True)), + ("anythings", models.ManyToManyField(to="demo.Anything")), + ( + "user", + models.ForeignKey( + null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL + ), + ), ], ), migrations.CreateModel( - name='DropTriggerConcurrentModel', + name="DropTriggerConcurrentModel", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('version', concurrency.fields.TriggerVersionField(db_column='cm_version_id', default=1, help_text='record revision number')), - ('username', models.CharField(blank=True, max_length=30, null=True)), - ('count', models.IntegerField(default=0)), + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ( + "version", + concurrency.fields.TriggerVersionField( + db_column="cm_version_id", default=1, help_text="record revision number" + ), + ), + ("username", models.CharField(blank=True, max_length=30, null=True)), + ("count", models.IntegerField(default=0)), ], ), migrations.CreateModel( - name='GroupTestModel', + name="GroupTestModel", fields=[ - ('group_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='auth.Group')), - ('username', models.CharField(max_length=50, verbose_name='username')), + ( + "group_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="auth.Group", + ), + ), + ("username", models.CharField(max_length=50, verbose_name="username")), ], - bases=('auth.group',), + bases=("auth.group",), ), migrations.CreateModel( - name='Issue3TestModel', + name="Issue3TestModel", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('username', models.CharField(blank=True, max_length=30, null=True)), - ('last_name', models.CharField(blank=True, max_length=30, null=True)), - ('char_field', models.CharField(blank=True, max_length=30, null=True)), - ('date_field', models.DateField(blank=True, null=True)), - ('version', models.CharField(blank=True, default='abc', max_length=10, null=True)), - ('revision', concurrency.fields.IntegerVersionField(db_column='cm_version_id', default=1, help_text='record revision number')), + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("username", models.CharField(blank=True, max_length=30, null=True)), + ("last_name", models.CharField(blank=True, max_length=30, null=True)), + ("char_field", models.CharField(blank=True, max_length=30, null=True)), + ("date_field", models.DateField(blank=True, null=True)), + ("version", models.CharField(blank=True, default="abc", max_length=10, null=True)), + ( + "revision", + concurrency.fields.IntegerVersionField( + db_column="cm_version_id", default=1, help_text="record revision number" + ), + ), ], ), migrations.CreateModel( - name='ReversionConcurrentModel', + name="ReversionConcurrentModel", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('version', concurrency.fields.IntegerVersionField(db_column='cm_version_id', default=1, help_text='record revision number')), - ('username', models.CharField(blank=True, max_length=30, null=True, unique=True)), - ('date_field', models.DateField(blank=True, null=True)), + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ( + "version", + concurrency.fields.IntegerVersionField( + db_column="cm_version_id", default=1, help_text="record revision number" + ), + ), + ("username", models.CharField(blank=True, max_length=30, null=True, unique=True)), + ("date_field", models.DateField(blank=True, null=True)), ], options={ - 'verbose_name_plural': 'Reversion-ConcurrentModels', - 'verbose_name': 'Reversion-ConcurrentModel', + "verbose_name_plural": "Reversion-ConcurrentModels", + "verbose_name": "Reversion-ConcurrentModel", }, ), migrations.CreateModel( - name='SimpleConcurrentModel', + name="SimpleConcurrentModel", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('version', concurrency.fields.IntegerVersionField(db_column='cm_version_id', default=1, help_text='record revision number')), - ('username', models.CharField(blank=True, max_length=30, null=True, unique=True)), - ('date_field', models.DateField(blank=True, null=True)), + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ( + "version", + concurrency.fields.IntegerVersionField( + db_column="cm_version_id", default=1, help_text="record revision number" + ), + ), + ("username", models.CharField(blank=True, max_length=30, null=True, unique=True)), + ("date_field", models.DateField(blank=True, null=True)), ], options={ - 'verbose_name_plural': 'SimpleConcurrentModels', - 'verbose_name': 'SimpleConcurrentModel', + "verbose_name_plural": "SimpleConcurrentModels", + "verbose_name": "SimpleConcurrentModel", }, ), migrations.CreateModel( - name='ThroughRelation', + name="ThroughRelation", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('version', concurrency.fields.ConditionalVersionField(default=1, help_text='record revision number')), - ('left', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to='demo.ConditionalVersionModelSelfRelation')), - ('right', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to='demo.ConditionalVersionModelSelfRelation')), + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("version", concurrency.fields.ConditionalVersionField(default=1, help_text="record revision number")), + ( + "left", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="+", + to="demo.ConditionalVersionModelSelfRelation", + ), + ), + ( + "right", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="+", + to="demo.ConditionalVersionModelSelfRelation", + ), + ), ], ), migrations.CreateModel( - name='TriggerConcurrentModel', + name="TriggerConcurrentModel", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('version', concurrency.fields.TriggerVersionField(db_column='cm_version_id', default=1, help_text='record revision number')), - ('username', models.CharField(blank=True, max_length=30, null=True)), - ('count', models.IntegerField(default=0)), + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ( + "version", + concurrency.fields.TriggerVersionField( + db_column="cm_version_id", default=1, help_text="record revision number" + ), + ), + ("username", models.CharField(blank=True, max_length=30, null=True)), + ("count", models.IntegerField(default=0)), ], options={ - 'verbose_name_plural': 'TriggerConcurrentModels', - 'verbose_name': 'TriggerConcurrentModel', + "verbose_name_plural": "TriggerConcurrentModels", + "verbose_name": "TriggerConcurrentModel", }, ), migrations.CreateModel( - name='ConcurrencyDisabledModel', + name="ConcurrencyDisabledModel", fields=[ - ('simpleconcurrentmodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='demo.SimpleConcurrentModel')), - ('dummy_char', models.CharField(blank=True, max_length=30, null=True)), + ( + "simpleconcurrentmodel_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="demo.SimpleConcurrentModel", + ), + ), + ("dummy_char", models.CharField(blank=True, max_length=30, null=True)), ], - bases=('demo.simpleconcurrentmodel',), + bases=("demo.simpleconcurrentmodel",), ), migrations.CreateModel( - name='CustomSaveModel', + name="CustomSaveModel", fields=[ - ('simpleconcurrentmodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='demo.SimpleConcurrentModel')), - ('extra_field', models.CharField(blank=True, max_length=30, null=True, unique=True)), + ( + "simpleconcurrentmodel_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="demo.SimpleConcurrentModel", + ), + ), + ("extra_field", models.CharField(blank=True, max_length=30, null=True, unique=True)), ], - bases=('demo.simpleconcurrentmodel',), + bases=("demo.simpleconcurrentmodel",), ), migrations.CreateModel( - name='InheritedModel', + name="InheritedModel", fields=[ - ('simpleconcurrentmodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='demo.SimpleConcurrentModel')), - ('extra_field', models.CharField(blank=True, max_length=30, null=True, unique=True)), + ( + "simpleconcurrentmodel_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="demo.SimpleConcurrentModel", + ), + ), + ("extra_field", models.CharField(blank=True, max_length=30, null=True, unique=True)), ], - bases=('demo.simpleconcurrentmodel',), + bases=("demo.simpleconcurrentmodel",), ), migrations.AddField( - model_name='conditionalversionmodelselfrelation', - name='relations', - field=models.ManyToManyField(blank=True, through='demo.ThroughRelation', to='demo.ConditionalVersionModelSelfRelation'), + model_name="conditionalversionmodelselfrelation", + name="relations", + field=models.ManyToManyField( + blank=True, through="demo.ThroughRelation", to="demo.ConditionalVersionModelSelfRelation" + ), ), migrations.AddField( - model_name='anything', - name='a_relation', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='demo.ConditionalVersionModelWithoutMeta'), + model_name="anything", + name="a_relation", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="demo.ConditionalVersionModelWithoutMeta" + ), ), migrations.CreateModel( - name='ListEditableConcurrentModel', - fields=[ - ], + name="ListEditableConcurrentModel", + fields=[], options={ - 'verbose_name_plural': 'ListEditableConcurrentModels', - 'verbose_name': 'ListEditableConcurrentModel', - 'proxy': True, + "verbose_name_plural": "ListEditableConcurrentModels", + "verbose_name": "ListEditableConcurrentModel", + "proxy": True, }, - bases=('demo.simpleconcurrentmodel',), + bases=("demo.simpleconcurrentmodel",), ), migrations.CreateModel( - name='NoActionsConcurrentModel', - fields=[ - ], + name="NoActionsConcurrentModel", + fields=[], options={ - 'verbose_name_plural': 'NoActions-ConcurrentModels', - 'verbose_name': 'NoActions-ConcurrentModel', - 'proxy': True, + "verbose_name_plural": "NoActions-ConcurrentModels", + "verbose_name": "NoActions-ConcurrentModel", + "proxy": True, }, - bases=('demo.simpleconcurrentmodel',), + bases=("demo.simpleconcurrentmodel",), ), migrations.CreateModel( - name='ProxyModel', - fields=[ - ], + name="ProxyModel", + fields=[], options={ - 'verbose_name_plural': 'ProxyModels', - 'verbose_name': 'ProxyModel', - 'proxy': True, + "verbose_name_plural": "ProxyModels", + "verbose_name": "ProxyModel", + "proxy": True, }, - bases=('demo.simpleconcurrentmodel',), + bases=("demo.simpleconcurrentmodel",), ), ] diff --git a/tests/demoapp/demo/migrations/0002_auto_20160909_1544.py b/tests/demoapp/demo/migrations/0002_auto_20160909_1544.py index 902b167..d87aba0 100644 --- a/tests/demoapp/demo/migrations/0002_auto_20160909_1544.py +++ b/tests/demoapp/demo/migrations/0002_auto_20160909_1544.py @@ -6,17 +6,22 @@ class Migration(migrations.Migration): dependencies = [ - ('demo', '0001_initial'), + ("demo", "0001_initial"), ] operations = [ migrations.RemoveField( - model_name='conditionalversionmodelselfrelation', - name='relations', + model_name="conditionalversionmodelselfrelation", + name="relations", ), migrations.AddField( - model_name='conditionalversionmodelselfrelation', - name='parent', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='demo.ConditionalVersionModelSelfRelation'), + model_name="conditionalversionmodelselfrelation", + name="parent", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="demo.ConditionalVersionModelSelfRelation", + ), ), ] diff --git a/tests/demoapp/demo/migrations/0003_auto_20171207_1254.py b/tests/demoapp/demo/migrations/0003_auto_20171207_1254.py index a819d98..a638256 100644 --- a/tests/demoapp/demo/migrations/0003_auto_20171207_1254.py +++ b/tests/demoapp/demo/migrations/0003_auto_20171207_1254.py @@ -4,17 +4,19 @@ class Migration(migrations.Migration): dependencies = [ - ('demo', '0002_auto_20160909_1544'), + ("demo", "0002_auto_20160909_1544"), ] operations = [ migrations.RemoveField( - model_name='conditionalversionmodelselfrelation', - name='parent', + model_name="conditionalversionmodelselfrelation", + name="parent", ), migrations.AddField( - model_name='conditionalversionmodelselfrelation', - name='relations', - field=models.ManyToManyField(to='demo.ConditionalVersionModelSelfRelation', null=True, through='demo.ThroughRelation', blank=True), + model_name="conditionalversionmodelselfrelation", + name="relations", + field=models.ManyToManyField( + to="demo.ConditionalVersionModelSelfRelation", null=True, through="demo.ThroughRelation", blank=True + ), ), ] diff --git a/tests/demoapp/demo/migrations5/0001_initial.py b/tests/demoapp/demo/migrations5/0001_initial.py index 345e60b..0938979 100644 --- a/tests/demoapp/demo/migrations5/0001_initial.py +++ b/tests/demoapp/demo/migrations5/0001_initial.py @@ -132,9 +132,7 @@ class Migration(migrations.Migration): ), ( "version", - concurrency.fields.ConditionalVersionField( - default=0, help_text="record revision number" - ), + concurrency.fields.ConditionalVersionField(default=0, help_text="record revision number"), ), ("name", models.CharField(max_length=10)), ], @@ -202,9 +200,7 @@ class Migration(migrations.Migration): ("date_field", models.DateField(blank=True, null=True)), ( "version", - models.CharField( - blank=True, default="abc", max_length=10, null=True - ), + models.CharField(blank=True, default="abc", max_length=10, null=True), ), ( "revision", @@ -385,9 +381,7 @@ class Migration(migrations.Migration): ), ( "version", - concurrency.fields.ConditionalVersionField( - default=0, help_text="record revision number" - ), + concurrency.fields.ConditionalVersionField(default=0, help_text="record revision number"), ), ( "field1", @@ -425,9 +419,7 @@ class Migration(migrations.Migration): ), ( "version", - concurrency.fields.ConditionalVersionField( - default=0, help_text="record revision number" - ), + concurrency.fields.ConditionalVersionField(default=0, help_text="record revision number"), ), ( "field1", @@ -474,9 +466,7 @@ class Migration(migrations.Migration): ), ( "version", - concurrency.fields.ConditionalVersionField( - default=0, help_text="record revision number" - ), + concurrency.fields.ConditionalVersionField(default=0, help_text="record revision number"), ), ( "left", diff --git a/tests/demoapp/demo/models.py b/tests/demoapp/demo/models.py index 1522de3..f6566e3 100644 --- a/tests/demoapp/demo/models.py +++ b/tests/demoapp/demo/models.py @@ -1,23 +1,33 @@ from django.contrib.auth.models import Group, User from django.db import models -from concurrency.fields import AutoIncVersionField, ConditionalVersionField, IntegerVersionField, TriggerVersionField - -__all__ = ['SimpleConcurrentModel', 'AutoIncConcurrentModel', - 'ProxyModel', 'InheritedModel', 'CustomSaveModel', - 'ConcreteModel', 'TriggerConcurrentModel', - 'ConditionalVersionModelWithoutMeta', - 'Anything', - ] +from concurrency.fields import ( + AutoIncVersionField, + ConditionalVersionField, + IntegerVersionField, + TriggerVersionField, +) + +__all__ = [ + "SimpleConcurrentModel", + "AutoIncConcurrentModel", + "ProxyModel", + "InheritedModel", + "CustomSaveModel", + "ConcreteModel", + "TriggerConcurrentModel", + "ConditionalVersionModelWithoutMeta", + "Anything", +] class SimpleConcurrentModel(models.Model): - version = IntegerVersionField(db_column='cm_version_id') + version = IntegerVersionField(db_column="cm_version_id") username = models.CharField(max_length=30, blank=True, null=True, unique=True) date_field = models.DateField(blank=True, null=True) class Meta: - app_label = 'demo' + app_label = "demo" verbose_name = "SimpleConcurrentModel" verbose_name_plural = "SimpleConcurrentModels" @@ -29,12 +39,12 @@ def __unicode__(self): class AutoIncConcurrentModel(models.Model): - version = AutoIncVersionField(db_column='cm_version_id') + version = AutoIncVersionField(db_column="cm_version_id") username = models.CharField(max_length=30, blank=True, null=True) date_field = models.DateField(blank=True, null=True) class Meta: - app_label = 'demo' + app_label = "demo" verbose_name = "AutoIncConcurrentModel" verbose_name_plural = "AutoIncConcurrentModel" @@ -43,12 +53,12 @@ def __unicode__(self): class TriggerConcurrentModel(models.Model): - version = TriggerVersionField(db_column='cm_version_id') + version = TriggerVersionField(db_column="cm_version_id") username = models.CharField(max_length=30, blank=True, null=True) count = models.IntegerField(default=0) class Meta: - app_label = 'demo' + app_label = "demo" verbose_name = "TriggerConcurrentModel" verbose_name_plural = "TriggerConcurrentModels" @@ -58,17 +68,17 @@ def __unicode__(self): class DropTriggerConcurrentModel(models.Model): # only used to test drop triggers - version = TriggerVersionField(db_column='cm_version_id') + version = TriggerVersionField(db_column="cm_version_id") username = models.CharField(max_length=30, blank=True, null=True) count = models.IntegerField(default=0) class Meta: - app_label = 'demo' + app_label = "demo" class ProxyModel(SimpleConcurrentModel): class Meta: - app_label = 'demo' + app_label = "demo" proxy = True verbose_name = "ProxyModel" verbose_name_plural = "ProxyModels" @@ -78,7 +88,7 @@ class InheritedModel(SimpleConcurrentModel): extra_field = models.CharField(max_length=30, blank=True, null=True, unique=True) class Meta: - app_label = 'demo' + app_label = "demo" class CustomSaveModel(SimpleConcurrentModel): @@ -88,15 +98,15 @@ def save(self, *args, **kwargs): super().save(*args, **kwargs) class Meta: - app_label = 'demo' + app_label = "demo" class AbstractModel(models.Model): - version = IntegerVersionField(db_column='cm_version_id') + version = IntegerVersionField(db_column="cm_version_id") username = models.CharField(max_length=30, blank=True, null=True, unique=True) class Meta: - app_label = 'demo' + app_label = "demo" abstract = True @@ -104,7 +114,7 @@ class ConcreteModel(AbstractModel): pass class Meta: - app_label = 'demo' + app_label = "demo" # class TestCustomUser(User): @@ -122,10 +132,10 @@ class GroupTestModel(Group): # and we need a 'fresh' model to check for on-the-fly addition # of version field. (added in tests 0.3.0) - username = models.CharField('username', max_length=50) + username = models.CharField("username", max_length=50) class Meta: - app_label = 'demo' + app_label = "demo" # class TestModelGroupWithCustomSave(TestModelGroup): @@ -143,44 +153,44 @@ class Issue3TestModel(models.Model): char_field = models.CharField(max_length=30, blank=True, null=True) date_field = models.DateField(blank=True, null=True) - version = models.CharField(default='abc', max_length=10, blank=True, null=True) - revision = IntegerVersionField(db_column='cm_version_id') + version = models.CharField(default="abc", max_length=10, blank=True, null=True) + revision = IntegerVersionField(db_column="cm_version_id") class Meta: - app_label = 'demo' + app_label = "demo" class ListEditableConcurrentModel(SimpleConcurrentModel): - """ Proxy model used by admin related test. + """Proxy model used by admin related test. This allow to use multiple ModelAdmin configuration with the same 'real' model """ class Meta: - app_label = 'demo' + app_label = "demo" proxy = True verbose_name = "ListEditableConcurrentModel" verbose_name_plural = "ListEditableConcurrentModels" class NoActionsConcurrentModel(SimpleConcurrentModel): - """ Proxy model used by admin related test. + """Proxy model used by admin related test. This allow to use multiple ModelAdmin configuration with the same 'real' model """ class Meta: - app_label = 'demo' + app_label = "demo" proxy = True verbose_name = "NoActions-ConcurrentModel" verbose_name_plural = "NoActions-ConcurrentModels" class ReversionConcurrentModel(models.Model): - version = IntegerVersionField(db_column='cm_version_id') + version = IntegerVersionField(db_column="cm_version_id") username = models.CharField(max_length=30, blank=True, null=True, unique=True) date_field = models.DateField(blank=True, null=True) class Meta: - app_label = 'demo' + app_label = "demo" verbose_name = "Reversion-ConcurrentModel" verbose_name_plural = "Reversion-ConcurrentModels" @@ -192,7 +202,7 @@ class ConcurrencyDisabledModel(SimpleConcurrentModel): dummy_char = models.CharField(max_length=30, blank=True, null=True) class Meta: - app_label = 'demo' + app_label = "demo" class ConcurrencyMeta: enabled = False @@ -206,10 +216,10 @@ class ConditionalVersionModel(models.Model): user = models.ForeignKey(User, null=True, on_delete=models.CASCADE) class Meta: - app_label = 'demo' + app_label = "demo" class ConcurrencyMeta: - check_fields = ['field1', 'field2', 'user'] + check_fields = ["field1", "field2", "user"] class Anything(models.Model): @@ -217,18 +227,19 @@ 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', - on_delete=models.CASCADE) + a_relation = models.ForeignKey("demo.ConditionalVersionModelWithoutMeta", on_delete=models.CASCADE) class Meta: - app_label = 'demo' + app_label = "demo" 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) @@ -237,32 +248,26 @@ class ConditionalVersionModelWithoutMeta(models.Model): anythings = models.ManyToManyField(Anything) class Meta: - app_label = 'demo' + app_label = "demo" class ThroughRelation(models.Model): version = ConditionalVersionField() - left = models.ForeignKey('demo.ConditionalVersionModelSelfRelation', - on_delete=models.CASCADE, - related_name='+') - right = models.ForeignKey('demo.ConditionalVersionModelSelfRelation', - on_delete=models.CASCADE, - related_name='+') + left = models.ForeignKey("demo.ConditionalVersionModelSelfRelation", on_delete=models.CASCADE, related_name="+") + right = models.ForeignKey("demo.ConditionalVersionModelSelfRelation", on_delete=models.CASCADE, related_name="+") class Meta: - app_label = 'demo' + app_label = "demo" class ConditionalVersionModelSelfRelation(models.Model): """ This model doesn't have ConcurrencyMeta defined. """ + version = ConditionalVersionField() name = models.CharField(max_length=10) - relations = models.ManyToManyField('self', - through='demo.ThroughRelation', - symmetrical=False, - blank=True) + relations = models.ManyToManyField("self", through="demo.ThroughRelation", symmetrical=False, blank=True) class Meta: - app_label = 'demo' + app_label = "demo" diff --git a/tests/demoapp/demo/settings.py b/tests/demoapp/demo/settings.py index 3c9bfab..ccc4637 100644 --- a/tests/demoapp/demo/settings.py +++ b/tests/demoapp/demo/settings.py @@ -11,63 +11,64 @@ pass DEBUG = True -STATIC_URL = '/static/' +STATIC_URL = "/static/" SITE_ID = 1 -ROOT_URLCONF = 'demo.urls' -SECRET_KEY = 'abc' -STATIC_ROOT = mktemp('static') -MEDIA_ROOT = mktemp('media') +ROOT_URLCONF = "demo.urls" +SECRET_KEY = "abc" +STATIC_ROOT = mktemp("static") +MEDIA_ROOT = mktemp("media") -INSTALLED_APPS = ['django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.sites', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'django.contrib.admin', - 'concurrency', - 'reversion', - 'demo' - ] +INSTALLED_APPS = [ + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.sites", + "django.contrib.messages", + "django.contrib.staticfiles", + "django.contrib.admin", + "concurrency", + "reversion", + "demo", +] IS_DJANGO_5 = django.VERSION[0] == 5 if IS_DJANGO_5: MIGRATION_MODULES = { - 'demo': 'demo.migrations5', - 'auth': 'demo.auth_migrations5', + "demo": "demo.migrations5", + "auth": "demo.auth_migrations5", } else: MIGRATION_MODULES = { - 'demo': 'demo.migrations', - 'auth': 'demo.auth_migrations', + "demo": "demo.migrations", + "auth": "demo.auth_migrations", } MIDDLEWARE_CLASSES = [] MIDDLEWARE = [ # 'concurrency.middleware.ConcurrencyMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", ] TEMPLATES = [ { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.contrib.auth.context_processors.auth', - 'django.template.context_processors.debug', - 'django.template.context_processors.i18n', - 'django.template.context_processors.media', - 'django.template.context_processors.static', - 'django.template.context_processors.tz', - 'django.contrib.messages.context_processors.messages', - 'django.template.context_processors.request', + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.contrib.auth.context_processors.auth", + "django.template.context_processors.debug", + "django.template.context_processors.i18n", + "django.template.context_processors.media", + "django.template.context_processors.static", + "django.template.context_processors.tz", + "django.contrib.messages.context_processors.messages", + "django.template.context_processors.request", ], # ... some options here ... }, @@ -75,77 +76,57 @@ ] LOGGING = { - 'version': 1, - 'disable_existing_loggers': False, - 'formatters': { - 'full': { - 'format': '%(levelname)-8s: %(asctime)s %(module)s %(process)d %(thread)d %(message)s' - }, - 'verbose': { - 'format': '%(levelname)-8s: %(asctime)s %(name)-25s %(message)s' - }, - 'simple': { - 'format': '%(levelname)-8s %(asctime)s %(name)-25s %(funcName)s %(message)s' - }, - 'debug': { - 'format': '%(levelno)s:%(levelname)-8s %(name)s %(funcName)s:%(lineno)s:: %(message)s' - } + "version": 1, + "disable_existing_loggers": False, + "formatters": { + "full": {"format": "%(levelname)-8s: %(asctime)s %(module)s %(process)d %(thread)d %(message)s"}, + "verbose": {"format": "%(levelname)-8s: %(asctime)s %(name)-25s %(message)s"}, + "simple": {"format": "%(levelname)-8s %(asctime)s %(name)-25s %(funcName)s %(message)s"}, + "debug": {"format": "%(levelno)s:%(levelname)-8s %(name)s %(funcName)s:%(lineno)s:: %(message)s"}, }, - 'filters': { - 'require_debug_false': { - '()': 'django.utils.log.RequireDebugFalse' - } - }, - 'handlers': { - 'null': { - 'level': 'DEBUG', - 'class': 'logging.NullHandler' - }, - 'console': { - 'level': 'DEBUG', - 'class': 'logging.StreamHandler', - 'formatter': 'debug' - } + "filters": {"require_debug_false": {"()": "django.utils.log.RequireDebugFalse"}}, + "handlers": { + "null": {"level": "DEBUG", "class": "logging.NullHandler"}, + "console": {"level": "DEBUG", "class": "logging.StreamHandler", "formatter": "debug"}, }, - 'loggers': { - 'concurrency': { - 'handlers': ['null'], - 'propagate': False, - 'level': 'DEBUG' - } - } + "loggers": {"concurrency": {"handlers": ["null"], "propagate": False, "level": "DEBUG"}}, } -db = os.environ.get('DBENGINE', 'pg') -dbname = os.environ.get('DBNAME', 'concurrency') -if db == 'pg': +db = os.environ.get("DBENGINE", "pg") +dbname = os.environ.get("DBNAME", "concurrency") +if db == "pg": DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.postgresql_psycopg2', - 'NAME': dbname, - 'HOST': os.environ.get('PGHOST', '127.0.0.1'), - 'PORT': os.environ.get('PGPORT', '5432'), - 'USER': 'postgres', - 'PASSWORD': 'postgres'}} -elif db == 'mysql': + "default": { + "ENGINE": "django.db.backends.postgresql_psycopg2", + "NAME": dbname, + "HOST": os.environ.get("PGHOST", "127.0.0.1"), + "PORT": os.environ.get("PGPORT", "5432"), + "USER": "postgres", + "PASSWORD": "postgres", + } + } +elif db == "mysql": DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.mysql', - 'NAME': dbname, - 'HOST': '127.0.0.1', - 'PORT': '', - 'USER': 'root', - 'PASSWORD': 'root', - 'CHARSET': 'utf8', - 'COLLATION': 'utf8_general_ci'}} + "default": { + "ENGINE": "django.db.backends.mysql", + "NAME": dbname, + "HOST": "127.0.0.1", + "PORT": "", + "USER": "root", + "PASSWORD": "root", + "CHARSET": "utf8", + "COLLATION": "utf8_general_ci", + } + } else: DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': dbname, - }} + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": dbname, + } + } -DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" AUTHENTICATION_BACKENDS = ( "demo.backends.AnyUserAuthBackend", diff --git a/tests/demoapp/demo/urls.py b/tests/demoapp/demo/urls.py index 53c4a7c..323970b 100644 --- a/tests/demoapp/demo/urls.py +++ b/tests/demoapp/demo/urls.py @@ -1,12 +1,11 @@ +from demo.models import SimpleConcurrentModel from django.contrib import admin from django.urls import re_path from django.views.generic.edit import UpdateView -from demo.models import SimpleConcurrentModel - admin.autodiscover() -urlpatterns = (re_path(r'cm/(?P\d+)/', - UpdateView.as_view(model=SimpleConcurrentModel), - name='concurrent-edit'), - re_path(r'^admin/', admin.site.urls)) +urlpatterns = ( + re_path(r"cm/(?P\d+)/", UpdateView.as_view(model=SimpleConcurrentModel), name="concurrent-edit"), + re_path(r"^admin/", admin.site.urls), +) diff --git a/tests/demoapp/demo/util.py b/tests/demoapp/demo/util.py index 00b5483..c9a7f53 100644 --- a/tests/demoapp/demo/util.py +++ b/tests/demoapp/demo/util.py @@ -3,12 +3,7 @@ from functools import partial, update_wrapper from itertools import count -from django import db - import pytest - -from concurrency.config import conf - from demo.models import ( AutoIncConcurrentModel, ConcreteModel, @@ -18,6 +13,9 @@ SimpleConcurrentModel, TriggerConcurrentModel, ) +from django import db + +from concurrency.config import conf def sequence(prefix): @@ -26,8 +24,8 @@ def sequence(prefix): yield "{0}-{1}".format(prefix, next(infinite)) -nextname = sequence('username') -nextgroup = sequence('group') +nextname = sequence("username") +nextgroup = sequence("group") unique_id = count(1) @@ -53,26 +51,34 @@ def clone_instance(model_instance): def with_models(*models, **kwargs): - ignore = kwargs.pop('ignore', []) + ignore = kwargs.pop("ignore", []) if ignore: models = filter(models, lambda x: x not in ignore) ids = [m.__name__ for m in models] - return pytest.mark.parametrize(('model_class,'), - models, - False, - ids, - None) + return pytest.mark.parametrize(("model_class,"), models, False, ids, None) -MODEL_CLASSES = [SimpleConcurrentModel, AutoIncConcurrentModel, - InheritedModel, CustomSaveModel, - ConcreteModel, ProxyModel, TriggerConcurrentModel] +MODEL_CLASSES = [ + SimpleConcurrentModel, + AutoIncConcurrentModel, + InheritedModel, + CustomSaveModel, + ConcreteModel, + ProxyModel, + TriggerConcurrentModel, +] -with_std_models = partial(with_models, SimpleConcurrentModel, AutoIncConcurrentModel, - InheritedModel, CustomSaveModel, - ConcreteModel, ProxyModel)() +with_std_models = partial( + with_models, + SimpleConcurrentModel, + AutoIncConcurrentModel, + InheritedModel, + CustomSaveModel, + ConcreteModel, + ProxyModel, +)() with_all_models = partial(with_models, *MODEL_CLASSES)() # with_all_models = partial(models_parametrize, ConcreteModel)() @@ -124,12 +130,12 @@ def _set(target, name, value): def concurrently(times=1): # from: http://www.caktusgroup.com/blog/2009/05/26/testing-django-views-for-concurrency-issues/ """ -Add this decorator to small pieces of code that you want to test -concurrently to make sure they don't raise exceptions when run at the -same time. E.g., some Django views that do a SELECT and then a subsequent -INSERT might fail when the INSERT assumes that the data has not changed -since the SELECT. -""" + Add this decorator to small pieces of code that you want to test + concurrently to make sure they don't raise exceptions when run at the + same time. E.g., some Django views that do a SELECT and then a subsequent + INSERT might fail when the INSERT assumes that the data has not changed + since the SELECT. + """ def concurrently_decorator(test_func): def wrapper(*args, **kwargs): @@ -153,9 +159,7 @@ def call_test_func(): for t in threads: t.join() if exceptions: - raise Exception( - 'test_concurrently intercepted %s exceptions: %s' % - (len(exceptions), exceptions)) + raise Exception("test_concurrently intercepted %s exceptions: %s" % (len(exceptions), exceptions)) return update_wrapper(wrapper, test_func) diff --git a/tests/test_admin_actions.py b/tests/test_admin_actions.py index 5d77b2d..af2bf27 100644 --- a/tests/test_admin_actions.py +++ b/tests/test_admin_actions.py @@ -1,10 +1,8 @@ -from django.urls import reverse - import pytest - -from demo.base import AdminTestCase, SENTINEL +from demo.base import SENTINEL, AdminTestCase from demo.models import SimpleConcurrentModel from demo.util import unique_id +from django.urls import reverse class TestAdminActions(AdminTestCase): @@ -16,92 +14,91 @@ def _create_conflict(self, pk): def test_dummy_action(self): id = next(unique_id) SimpleConcurrentModel.objects.get_or_create(pk=id) - res = self.app.get('/admin/', user='sax') + res = self.app.get("/admin/", user="sax") - res = res.click('^SimpleConcurrentModels') - assert 'SimpleConcurrentModel #%s' % id in res # sanity check + res = res.click("^SimpleConcurrentModels") + assert "SimpleConcurrentModel #%s" % id in res # sanity check self._create_conflict(id) - form = res.forms['changelist-form'] - form['action'].value = 'dummy_action' - sel = form.get('_selected_action', index=0) + form = res.forms["changelist-form"] + form["action"].value = "dummy_action" + sel = form.get("_selected_action", index=0) sel.checked = True res = form.submit().follow() - self.assertIn('SimpleConcurrentModel #%s' % id, res) - self.assertIn('**concurrent_update**', res) - self.assertNotIn('**action_update**', res) + self.assertIn("SimpleConcurrentModel #%s" % id, res) + self.assertIn("**concurrent_update**", res) + self.assertNotIn("**action_update**", res) def test_dummy_action_select_across(self): id = next(unique_id) SimpleConcurrentModel.objects.get_or_create(pk=id) - res = self.app.get('/admin/', user='sax') + res = self.app.get("/admin/", user="sax") - res = res.click('^SimpleConcurrentModels') - assert 'SimpleConcurrentModel #%s' % id in res # sanity check + res = res.click("^SimpleConcurrentModels") + assert "SimpleConcurrentModel #%s" % id in res # sanity check self._create_conflict(id) - form = res.forms['changelist-form'] - form['action'].value = 'dummy_action' - form['select_across'] = 'True' - sel = form.get('_selected_action', index=0) # needed + form = res.forms["changelist-form"] + form["action"].value = "dummy_action" + form["select_across"] = "True" + sel = form.get("_selected_action", index=0) # needed sel.checked = True # needed res = form.submit() res = res.follow() - self.assertIn('Selecting all records, you will avoid the concurrency check', res) + self.assertIn("Selecting all records, you will avoid the concurrency check", res) def test_delete_allowed_if_no_updates(self): id = next(unique_id) SimpleConcurrentModel.objects.get_or_create(pk=id) - res = self.app.get('/admin/', user='sax') - res = res.click('^SimpleConcurrentModels') - assert 'SimpleConcurrentModel #%s' % id in res # sanity check + res = self.app.get("/admin/", user="sax") + res = res.click("^SimpleConcurrentModels") + assert "SimpleConcurrentModel #%s" % id in res # sanity check - form = res.forms['changelist-form'] - form['action'].value = 'delete_selected' - sel = form.get('_selected_action', index=0) + form = res.forms["changelist-form"] + form["action"].value = "delete_selected" + sel = form.get("_selected_action", index=0) sel.checked = True res = form.submit() - assert 'Are you sure?' in res - assert 'SimpleConcurrentModel #%s' % id in res + assert "Are you sure?" in res + assert "SimpleConcurrentModel #%s" % id in res res = res.forms[0].submit() - assert 'SimpleConcurrentModel #%s' % id not in res + assert "SimpleConcurrentModel #%s" % id not in res def test_delete_not_allowed_if_updates(self): id = next(unique_id) SimpleConcurrentModel.objects.get_or_create(pk=id) - res = self.app.get('/admin/', user='sax') + res = self.app.get("/admin/", user="sax") - res = res.click('^SimpleConcurrentModels') - assert 'SimpleConcurrentModel #%s' % id in res # sanity check + res = res.click("^SimpleConcurrentModels") + assert "SimpleConcurrentModel #%s" % id in res # sanity check self._create_conflict(id) - form = res.forms['changelist-form'] - form['action'].value = 'delete_selected' - sel = form.get('_selected_action', index=0) + form = res.forms["changelist-form"] + form["action"].value = "delete_selected" + sel = form.get("_selected_action", index=0) sel.checked = True res = form.submit().follow() - self.assertIn('One or more record were updated', res) + 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(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 = self.app.get(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' + expected = "All of the following objects and their related items will be deleted" assert expected in response form = response.forms[1] if len(response.forms) > 1 else response.form # dj41 response = form.submit().follow() diff --git a/tests/test_admin_edit.py b/tests/test_admin_edit.py index 458ef68..0b7ce23 100644 --- a/tests/test_admin_edit.py +++ b/tests/test_admin_edit.py @@ -1,40 +1,37 @@ +import pytest +from demo.base import SENTINEL, AdminTestCase +from demo.models import SimpleConcurrentModel +from demo.util import nextname from django.urls import reverse from django.utils.translation import gettext as _ -import pytest - from concurrency.forms import VersionFieldSigner -from demo.base import AdminTestCase, SENTINEL -from demo.models import SimpleConcurrentModel -from demo.util import nextname - @pytest.mark.django_db @pytest.mark.admin def test_creation(admin_user, client): - url = reverse('admin:demo_simpleconcurrentmodel_add') + url = reverse("admin:demo_simpleconcurrentmodel_add") res = client.get(url, user=admin_user.username) - form = res.forms['simpleconcurrentmodel_form'] - form['username'] = 'CHAR' + form = res.forms["simpleconcurrentmodel_form"] + form["username"] = "CHAR" res = form.submit().follow() - assert SimpleConcurrentModel.objects.filter(username='CHAR').exists() - assert SimpleConcurrentModel.objects.get(username='CHAR').version > 0 + assert SimpleConcurrentModel.objects.filter(username="CHAR").exists() + assert SimpleConcurrentModel.objects.get(username="CHAR").version > 0 @pytest.mark.django_db @pytest.mark.functional def test_standard_update(admin_user, client): concurrentmodel = SimpleConcurrentModel.objects.create() - url = reverse('admin:demo_simpleconcurrentmodel_change', - args=[concurrentmodel.pk]) + url = reverse("admin:demo_simpleconcurrentmodel_change", args=[concurrentmodel.pk]) res = client.get(url, user=admin_user.username) - target = res.context['original'] + target = res.context["original"] old_version = target.version - form = res.forms['simpleconcurrentmodel_form'] - form['username'] = 'UPDATED' + form = res.forms["simpleconcurrentmodel_form"] + form["username"] = "UPDATED" res = form.submit().follow() target = SimpleConcurrentModel.objects.get(pk=target.pk) new_version = target.version @@ -46,58 +43,56 @@ def test_standard_update(admin_user, client): @pytest.mark.functional def test_conflict(admin_user, client): concurrentmodel = SimpleConcurrentModel.objects.create() - url = reverse('admin:demo_simpleconcurrentmodel_change', - args=[concurrentmodel.pk]) + url = reverse("admin:demo_simpleconcurrentmodel_change", args=[concurrentmodel.pk]) res = client.get(url, user=admin_user.username) - form = res.forms['simpleconcurrentmodel_form'] + form = res.forms["simpleconcurrentmodel_form"] concurrentmodel.save() # create conflict here res = form.submit() - assert 'original' in res.context - assert res.context['adminform'].form.errors - assert _('Record Modified') in str(res.context['adminform'].form.errors) + assert "original" in res.context + assert res.context["adminform"].form.errors + assert _("Record Modified") in str(res.context["adminform"].form.errors) class TestConcurrentModelAdmin(AdminTestCase): def test_standard_update(self): - target, __ = SimpleConcurrentModel.objects.get_or_create(username='aaa') - url = reverse('admin:demo_simpleconcurrentmodel_change', args=[target.pk]) - res = self.app.get(url, user='sax') - target = res.context['original'] + target, __ = SimpleConcurrentModel.objects.get_or_create(username="aaa") + url = reverse("admin:demo_simpleconcurrentmodel_change", args=[target.pk]) + res = self.app.get(url, user="sax") + target = res.context["original"] old_version = target.version - form = res.forms['simpleconcurrentmodel_form'] - form['username'] = 'UPDATED' + form = res.forms["simpleconcurrentmodel_form"] + form["username"] = "UPDATED" res = form.submit().follow() target = SimpleConcurrentModel.objects.get(pk=target.pk) new_version = target.version self.assertGreater(new_version, old_version) def test_creation(self): - url = reverse('admin:demo_simpleconcurrentmodel_add') - res = self.app.get(url, user='sax') - form = res.forms['simpleconcurrentmodel_form'] - form['username'] = 'CHAR' + url = reverse("admin:demo_simpleconcurrentmodel_add") + res = self.app.get(url, user="sax") + form = res.forms["simpleconcurrentmodel_form"] + form["username"] = "CHAR" res = form.submit().follow() - self.assertTrue(SimpleConcurrentModel.objects.filter(username='CHAR').exists()) - self.assertGreater(SimpleConcurrentModel.objects.get(username='CHAR').version, 0) + self.assertTrue(SimpleConcurrentModel.objects.filter(username="CHAR").exists()) + self.assertGreater(SimpleConcurrentModel.objects.get(username="CHAR").version, 0) def test_conflict(self): - target, __ = SimpleConcurrentModel.objects.get_or_create(username='aaa') - url = reverse('admin:demo_simpleconcurrentmodel_change', args=[target.pk]) - res = self.app.get(url, user='sax') + target, __ = SimpleConcurrentModel.objects.get_or_create(username="aaa") + url = reverse("admin:demo_simpleconcurrentmodel_change", args=[target.pk]) + res = self.app.get(url, user="sax") - form = res.forms['simpleconcurrentmodel_form'] + form = res.forms["simpleconcurrentmodel_form"] target.save() # create conflict here res = form.submit() - self.assertIn('original', res.context) - self.assertTrue(res.context['adminform'].form.errors, - res.context['adminform'].form.errors) - self.assertIn(_('Record Modified'), - str(res.context['adminform'].form.errors), - res.context['adminform'].form.errors) + self.assertIn("original", res.context) + self.assertTrue(res.context["adminform"].form.errors, res.context["adminform"].form.errors) + self.assertIn( + _("Record Modified"), str(res.context["adminform"].form.errors), res.context["adminform"].form.errors + ) class TestAdminEdit(AdminTestCase): @@ -107,20 +102,20 @@ def _create_conflict(self, pk): u.save() def test_creation(self): - url = reverse('admin:demo_simpleconcurrentmodel_add') - res = self.app.get(url, user='sax') - form = res.forms['simpleconcurrentmodel_form'] - form['username'] = 'CHAR' + url = reverse("admin:demo_simpleconcurrentmodel_add") + res = self.app.get(url, user="sax") + form = res.forms["simpleconcurrentmodel_form"] + form["username"] = "CHAR" res = form.submit().follow() - self.assertTrue(SimpleConcurrentModel.objects.filter(username='CHAR').exists()) - self.assertGreater(SimpleConcurrentModel.objects.get(username='CHAR').version, 0) + self.assertTrue(SimpleConcurrentModel.objects.filter(username="CHAR").exists()) + self.assertGreater(SimpleConcurrentModel.objects.get(username="CHAR").version, 0) def test_creation_with_customform(self): - url = reverse('admin:demo_simpleconcurrentmodel_add') - res = self.app.get(url, user='sax') - form = res.forms['simpleconcurrentmodel_form'] + url = reverse("admin:demo_simpleconcurrentmodel_add") + res = self.app.get(url, user="sax") + form = res.forms["simpleconcurrentmodel_form"] username = next(nextname) - form['username'] = username + form["username"] = username res = form.submit().follow() self.assertTrue(SimpleConcurrentModel.objects.filter(username=username).exists()) self.assertGreater(SimpleConcurrentModel.objects.get(username=username).version, 0) @@ -131,46 +126,44 @@ def test_creation_with_customform(self): self.assertContains(res, "SimpleConcurrentModel with this Username already exists.") def test_standard_update(self): - target, __ = SimpleConcurrentModel.objects.get_or_create(username='aaa') - url = reverse('admin:demo_simpleconcurrentmodel_change', args=[target.pk]) - res = self.app.get(url, user='sax') - target = res.context['original'] + target, __ = SimpleConcurrentModel.objects.get_or_create(username="aaa") + url = reverse("admin:demo_simpleconcurrentmodel_change", args=[target.pk]) + res = self.app.get(url, user="sax") + target = res.context["original"] old_version = target.version - form = res.forms['simpleconcurrentmodel_form'] - form['username'] = 'UPDATED' + form = res.forms["simpleconcurrentmodel_form"] + form["username"] = "UPDATED" res = form.submit().follow() target = SimpleConcurrentModel.objects.get(pk=target.pk) new_version = target.version self.assertGreater(new_version, old_version) def test_conflict(self): - target, __ = SimpleConcurrentModel.objects.get_or_create(username='aaa') + target, __ = SimpleConcurrentModel.objects.get_or_create(username="aaa") assert target.version - url = reverse('admin:demo_simpleconcurrentmodel_change', args=[target.pk]) - res = self.app.get(url, user='sax') - form = res.forms['simpleconcurrentmodel_form'] + url = reverse("admin:demo_simpleconcurrentmodel_change", args=[target.pk]) + res = self.app.get(url, user="sax") + form = res.forms["simpleconcurrentmodel_form"] target.save() # create conflict here res = form.submit() - self.assertIn('original', res.context) - self.assertTrue(res.context['adminform'].form.errors, - res.context['adminform'].form.errors) - self.assertIn(_('Record Modified'), - str(res.context['adminform'].form.errors), - res.context['adminform'].form.errors) + self.assertIn("original", res.context) + self.assertTrue(res.context["adminform"].form.errors, res.context["adminform"].form.errors) + self.assertIn( + _("Record Modified"), str(res.context["adminform"].form.errors), res.context["adminform"].form.errors + ) def test_sanity_signer(self): - target, __ = SimpleConcurrentModel.objects.get_or_create(username='aaa') - url = reverse('admin:demo_simpleconcurrentmodel_change', args=[target.pk]) - res = self.app.get(url, user='sax') - form = res.forms['simpleconcurrentmodel_form'] - version1 = int(str(form['version'].value).split(":")[0]) - form['version'] = VersionFieldSigner().sign(version1) - form['date_field'] = 'esss2010-09-01' + target, __ = SimpleConcurrentModel.objects.get_or_create(username="aaa") + url = reverse("admin:demo_simpleconcurrentmodel_change", args=[target.pk]) + res = self.app.get(url, user="sax") + form = res.forms["simpleconcurrentmodel_form"] + version1 = int(str(form["version"].value).split(":")[0]) + form["version"] = VersionFieldSigner().sign(version1) + form["date_field"] = "esss2010-09-01" response = form.submit() - self.assertIn('original', response.context) - self.assertTrue(response.context['adminform'].form.errors, - response.context['adminform'].form.errors) - form = response.context['adminform'].form - version2 = int(str(form['version'].value()).split(":")[0]) + self.assertIn("original", response.context) + self.assertTrue(response.context["adminform"].form.errors, response.context["adminform"].form.errors) + form = response.context["adminform"].form + version2 = int(str(form["version"].value()).split(":")[0]) self.assertEqual(version1, version2) diff --git a/tests/test_admin_list_editable.py b/tests/test_admin_list_editable.py index c14a0bd..1663a7f 100644 --- a/tests/test_admin_list_editable.py +++ b/tests/test_admin_list_editable.py @@ -1,19 +1,20 @@ +import pytest +from demo.base import SENTINEL, AdminTestCase +from demo.models import ListEditableConcurrentModel +from demo.util import attributes, unique_id from django.contrib.admin.models import LogEntry from django.contrib.admin.sites import site from django.contrib.contenttypes.models import ContentType from django.db import transaction from django.utils.encoding import force_str -import pytest - from concurrency.compat import concurrency_param_name -from concurrency.config import CONCURRENCY_LIST_EDITABLE_POLICY_ABORT_ALL, CONCURRENCY_LIST_EDITABLE_POLICY_SILENT +from concurrency.config import ( + CONCURRENCY_LIST_EDITABLE_POLICY_ABORT_ALL, + CONCURRENCY_LIST_EDITABLE_POLICY_SILENT, +) from concurrency.exceptions import RecordModifiedError -from demo.base import AdminTestCase, SENTINEL -from demo.models import ListEditableConcurrentModel -from demo.util import attributes, unique_id - class TestListEditable(AdminTestCase): TARGET = ListEditableConcurrentModel @@ -24,52 +25,52 @@ def _create_conflict(self, pk): u.save() def test_normal_add(self): - res = self.app.get('/admin/', user='sax') + res = self.app.get("/admin/", user="sax") # file:///admin/demo/listeditableconcurrentmodel/add/ res = res.click(self.TARGET._meta.verbose_name_plural) - res = res.click('Add', href=f'/admin/demo/{self.TARGET._meta.model_name}/add/', index=0) - form = res.forms['listeditableconcurrentmodel_form'] - form['username'] = 'CHAR' + res = res.click("Add", href=f"/admin/demo/{self.TARGET._meta.model_name}/add/", index=0) + form = res.forms["listeditableconcurrentmodel_form"] + form["username"] = "CHAR" form.submit().follow() def test_normal_update(self): self.TARGET.objects.get_or_create(pk=next(unique_id)) - res = self.app.get('/admin/', user='sax') + res = self.app.get("/admin/", user="sax") res = res.click(self.TARGET._meta.verbose_name_plural) - form = res.forms['changelist-form'] - form['form-0-username'] = 'CHAR' - form.submit('_save').follow() - self.assertTrue(self.TARGET.objects.filter(username='CHAR').exists()) + form = res.forms["changelist-form"] + form["form-0-username"] = "CHAR" + form.submit("_save").follow() + self.assertTrue(self.TARGET.objects.filter(username="CHAR").exists()) def test_concurrency_policy_abort(self): id = next(unique_id) self.TARGET.objects.get_or_create(pk=id) model_admin = site._registry[self.TARGET] - with attributes((model_admin.__class__, 'list_editable_policy', CONCURRENCY_LIST_EDITABLE_POLICY_ABORT_ALL)): - res = self.app.get('/admin/', user='sax') + with attributes((model_admin.__class__, "list_editable_policy", CONCURRENCY_LIST_EDITABLE_POLICY_ABORT_ALL)): + res = self.app.get("/admin/", user="sax") res = res.click(self.TARGET._meta.verbose_name_plural) self._create_conflict(id) - form = res.forms['changelist-form'] - form['form-0-username'] = 'CHAR' + form = res.forms["changelist-form"] + form["form-0-username"] = "CHAR" with pytest.raises(RecordModifiedError): - res = form.submit('_save') + res = form.submit("_save") self.assertTrue(self.TARGET.objects.filter(username=SENTINEL).exists()) - self.assertFalse(self.TARGET.objects.filter(username='CHAR').exists()) + self.assertFalse(self.TARGET.objects.filter(username="CHAR").exists()) def test_concurrency_policy_silent(self): id = next(unique_id) self.TARGET.objects.get_or_create(pk=id) model_admin = site._registry[self.TARGET] - with attributes((model_admin.__class__, 'list_editable_policy', CONCURRENCY_LIST_EDITABLE_POLICY_SILENT)): - res = self.app.get('/admin/', user='sax') + with attributes((model_admin.__class__, "list_editable_policy", CONCURRENCY_LIST_EDITABLE_POLICY_SILENT)): + res = self.app.get("/admin/", user="sax") res = res.click(self.TARGET._meta.verbose_name_plural) self._create_conflict(id) - form = res.forms['changelist-form'] - form['form-0-username'] = 'CHAR' - version = int(form[f'{concurrency_param_name}_{id}'].value) - res = form.submit('_save').follow() + form = res.forms["changelist-form"] + form["form-0-username"] = "CHAR" + version = int(form[f"{concurrency_param_name}_{id}"].value) + res = form.submit("_save").follow() changed = self.TARGET.objects.filter(username=SENTINEL).first() self.assertTrue(changed) self.assertGreater(changed.version, version) @@ -79,57 +80,54 @@ def test_message_user(self): id2 = next(unique_id) self.TARGET.objects.get_or_create(pk=id1) self.TARGET.objects.get_or_create(pk=id2) - res = self.app.get('/admin/', user='sax') + res = self.app.get("/admin/", user="sax") res = res.click(self.TARGET._meta.verbose_name_plural) self._create_conflict(id1) - form = res.forms['changelist-form'] - form['form-0-username'] = 'CHAR1' - form['form-1-username'] = 'CHAR2' - res = form.submit('_save').follow() + form = res.forms["changelist-form"] + form["form-0-username"] = "CHAR1" + form["form-1-username"] = "CHAR2" + res = form.submit("_save").follow() - messages = map(str, list(res.context['messages'])) + messages = map(str, list(res.context["messages"])) - self.assertIn('Record with pk `%s` has been modified and was not updated' % id1, - messages) - self.assertIn('1 %s was changed successfully.' % force_str(self.TARGET._meta.verbose_name), - messages) + self.assertIn("Record with pk `%s` has been modified and was not updated" % id1, messages) + self.assertIn("1 %s was changed successfully." % force_str(self.TARGET._meta.verbose_name), messages) def test_message_user_no_changes(self): id = next(unique_id) self.TARGET.objects.get_or_create(pk=id) - res = self.app.get('/admin/', user='sax') + res = self.app.get("/admin/", user="sax") res = res.click(self.TARGET._meta.verbose_name_plural) self._create_conflict(id) - form = res.forms['changelist-form'] - form['form-0-username'] = 'CHAR1' - res = form.submit('_save').follow() + form = res.forms["changelist-form"] + form["form-0-username"] = "CHAR1" + res = form.submit("_save").follow() - messages = list(map(str, list(res.context['messages']))) + messages = list(map(str, list(res.context["messages"]))) - self.assertIn('Record with pk `%s` has been modified and was not updated' % id, set(messages)) + self.assertIn("Record with pk `%s` has been modified and was not updated" % id, set(messages)) self.assertEqual(len(set(messages)), 1) def test_log_change(self): id = next(unique_id) self.TARGET.objects.get_or_create(pk=id) - res = self.app.get('/admin/', user='sax') + res = self.app.get("/admin/", user="sax") res = res.click(self.TARGET._meta.verbose_name_plural) - log_filter = dict(user__username='sax', - content_type=ContentType.objects.get_for_model(self.TARGET)) + log_filter = dict(user__username="sax", content_type=ContentType.objects.get_for_model(self.TARGET)) - logs = list(LogEntry.objects.filter(**log_filter).values_list('pk', flat=True)) + logs = list(LogEntry.objects.filter(**log_filter).values_list("pk", flat=True)) self._create_conflict(id) - form = res.forms['changelist-form'] - form['form-0-username'] = 'CHAR1' - res = form.submit('_save').follow() + form = res.forms["changelist-form"] + form["form-0-username"] = "CHAR1" + res = form.submit("_save").follow() new_logs = LogEntry.objects.filter(**log_filter).exclude(id__in=logs).exists() self.assertFalse(new_logs, "LogEntry created even if conflict error") transaction.rollback() diff --git a/tests/test_api.py b/tests/test_api.py index 9ae44c1..b475a12 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1,15 +1,18 @@ -from django.contrib.auth.models import Group - import pytest +from demo.models import SimpleConcurrentModel +from demo.util import nextgroup, nextname +from django.contrib.auth.models import Group -from concurrency.api import apply_concurrency_check, get_revision_of_object, get_version, is_changed +from concurrency.api import ( + apply_concurrency_check, + get_revision_of_object, + get_version, + is_changed, +) from concurrency.exceptions import RecordModifiedError from concurrency.fields import IntegerVersionField from concurrency.utils import refetch -from demo.models import SimpleConcurrentModel -from demo.util import nextgroup, nextname - @pytest.mark.django_db(transaction=False) @pytest.mark.skipif('os.environ.get("DBENGINE", "")=="pg"') @@ -40,7 +43,7 @@ def test_get_version(model_class=SimpleConcurrentModel): @pytest.mark.django_db(transaction=False) def test_apply_concurrency_check(): - apply_concurrency_check(Group, 'version', IntegerVersionField) + apply_concurrency_check(Group, "version", IntegerVersionField) instance, __ = Group.objects.get_or_create(name=next(nextgroup)) instance.save() @@ -54,5 +57,5 @@ def test_apply_concurrency_check(): @pytest.mark.django_db(transaction=False) def test_apply_concurrency_check_ignore_multiple_call(): - apply_concurrency_check(Group, 'version', IntegerVersionField) - apply_concurrency_check(Group, 'version', IntegerVersionField) + apply_concurrency_check(Group, "version", IntegerVersionField) + apply_concurrency_check(Group, "version", IntegerVersionField) diff --git a/tests/test_base.py b/tests/test_base.py index 2f03cfc..7778448 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -1,13 +1,11 @@ -from django.test import override_settings - import pytest +from demo.util import concurrent_model, unique_id, with_all_models, with_std_models +from django.test import override_settings from concurrency.core import _set_version from concurrency.exceptions import RecordModifiedError from concurrency.utils import refetch -from demo.util import concurrent_model, unique_id, with_all_models, with_std_models - pytest.mark.django_db(transaction=False) @@ -62,7 +60,8 @@ def test_do_not_check_if_no_version(model_class): @pytest.mark.django_db(transaction=True) @with_std_models def test_conflict_no_version_and_no_skip_flag(model_class): - """When VERSION_FIELD_REQUIRED is enabled, attempting to update a record with a default version number should fail.""" + """When VERSION_FIELD_REQUIRED is enabled, + attempting to update a record with a default version number should fail.""" with override_settings(CONCURRENCY_VERSION_FIELD_REQUIRED=True): id = next(unique_id) instance, __ = model_class.objects.get_or_create(pk=id) @@ -83,16 +82,16 @@ def test_update_fields(model_class): the version. """ - instance = model_class.objects.create(username='abc') + instance = model_class.objects.create(username="abc") copy = refetch(instance) # do not update version - instance.save(update_fields=['username']) + instance.save(update_fields=["username"]) # copy can be saved - copy.username = 'def' + copy.username = "def" copy.save() - assert refetch(instance).username, 'def' + assert refetch(instance).username, "def" assert refetch(instance).version == copy.version @@ -103,10 +102,10 @@ def test_update_fields_still_checks(model_class): Excluding the VersionField from update_fields should still check for conflicts. """ - instance = model_class.objects.create(username='abc') + instance = model_class.objects.create(username="abc") copy = refetch(instance) instance.save() - copy.name = 'def' + copy.name = "def" with pytest.raises(RecordModifiedError): - copy.save(update_fields=['username']) + copy.save(update_fields=["username"]) diff --git a/tests/test_checks.py b/tests/test_checks.py index d4f9ac1..2f06291 100644 --- a/tests/test_checks.py +++ b/tests/test_checks.py @@ -1,7 +1,6 @@ import logging import pytest - from demo.models import TriggerConcurrentModel logger = logging.getLogger(__name__) @@ -15,6 +14,7 @@ def obj(): @pytest.mark.django_db def test_check(obj, monkeypatch): from django.core.checks import Warning - monkeypatch.setattr(obj._concurrencymeta.field, '_trigger_name', 'test') + + monkeypatch.setattr(obj._concurrencymeta.field, "_trigger_name", "test") assert isinstance(obj._concurrencymeta.field.check()[0], Warning) diff --git a/tests/test_command.py b/tests/test_command.py index 2a44ee9..d4fc622 100644 --- a/tests/test_command.py +++ b/tests/test_command.py @@ -1,12 +1,10 @@ import io import logging +import pytest from django.core.management import call_command - from mock import Mock -import pytest - import concurrency.management.commands.triggers as command logger = logging.getLogger(__name__) @@ -16,14 +14,14 @@ def test_command_create(monkeypatch): out = io.StringIO() mock_create = Mock() - mock_create.return_value = {'default': [['model', 'field', 'trigger']]} + mock_create.return_value = {"default": [["model", "field", "trigger"]]} - monkeypatch.setattr(command, 'create_triggers', mock_create) - call_command('triggers', 'create', stdout=out) + monkeypatch.setattr(command, "create_triggers", mock_create) + call_command("triggers", "create", stdout=out) out.seek(0) output = out.read() - assert output.find('Created trigger for field') > 0 + assert output.find("Created trigger for field") > 0 assert mock_create.call_count == 1 @@ -31,37 +29,37 @@ def test_command_create(monkeypatch): def test_command_create_db(monkeypatch): out = io.StringIO() mock_create = Mock() - mock_create.return_value = {'default': [['model', 'field', 'trigger']]} + mock_create.return_value = {"default": [["model", "field", "trigger"]]} - monkeypatch.setattr(command, 'create_triggers', mock_create) - call_command('triggers', 'create', database='default', stdout=out) + monkeypatch.setattr(command, "create_triggers", mock_create) + call_command("triggers", "create", database="default", stdout=out) out.seek(0) output = out.read() - assert output.find('Created trigger for field') > 0 + assert output.find("Created trigger for field") > 0 assert mock_create.call_count == 1 @pytest.mark.django_db def test_command_list(): out = io.StringIO() - call_command('triggers', 'list', stdout=out) + call_command("triggers", "list", stdout=out) out.seek(0) output = out.read() - assert output.find('concurrency_demo_triggerconcurrentmodel_i') - assert output.find('concurrency_demo_triggerconcurrentmodel_u') + assert output.find("concurrency_demo_triggerconcurrentmodel_i") + assert output.find("concurrency_demo_triggerconcurrentmodel_u") @pytest.mark.django_db def test_command_drop(monkeypatch): out = io.StringIO() mock_drop = Mock() - mock_drop.return_value = {'default': [['model', 'field', 'trigger']]} + mock_drop.return_value = {"default": [["model", "field", "trigger"]]} - monkeypatch.setattr(command, 'drop_triggers', mock_drop) - call_command('triggers', 'drop', stdout=out) + monkeypatch.setattr(command, "drop_triggers", mock_drop) + call_command("triggers", "drop", stdout=out) out.seek(0) output = out.read() - assert output.find('Dropped trigger') > 0 + assert output.find("Dropped trigger") > 0 assert mock_drop.call_count == 1 diff --git a/tests/test_concurrencymetainfo.py b/tests/test_concurrencymetainfo.py index 6cb5f1f..3a8a65e 100644 --- a/tests/test_concurrencymetainfo.py +++ b/tests/test_concurrencymetainfo.py @@ -1,13 +1,12 @@ +from demo.models import ConcurrencyDisabledModel, SimpleConcurrentModel from django.test import TransactionTestCase from concurrency.exceptions import RecordModifiedError -from demo.models import ConcurrencyDisabledModel, SimpleConcurrentModel - class TestCustomConcurrencyMeta(TransactionTestCase): concurrency_model = ConcurrencyDisabledModel - concurrency_kwargs = {'username': 'test'} + concurrency_kwargs = {"username": "test"} def setUp(self): super().setUp() @@ -27,8 +26,9 @@ def test_meta_inheritance(self): # TestModelWithCustomOptions extends ConcurrentModel # but we disabled concurrency only in TestModelWithCustomOptions import concurrency.api as api - concurrency_enabled1 = SimpleConcurrentModel.objects.get_or_create(**{'username': 'test'})[0] - concurrency_enabled2 = SimpleConcurrentModel.objects.get_or_create(**{'username': 'test'})[0] + + concurrency_enabled1 = SimpleConcurrentModel.objects.get_or_create(**{"username": "test"})[0] + concurrency_enabled2 = SimpleConcurrentModel.objects.get_or_create(**{"username": "test"})[0] v1 = api.get_revision_of_object(concurrency_enabled1) v2 = api.get_revision_of_object(concurrency_enabled2) assert v1 == v2, "got same row with different version (%s/%s)" % (v1, v2) @@ -36,8 +36,8 @@ def test_meta_inheritance(self): assert concurrency_enabled1.pk is not None # sanity check self.assertRaises(RecordModifiedError, concurrency_enabled2.save) - concurrency_disabled1 = ConcurrencyDisabledModel.objects.get_or_create(**{'username': 'test'})[0] - concurrency_disabled2 = ConcurrencyDisabledModel.objects.get_or_create(**{'username': 'test'})[0] + concurrency_disabled1 = ConcurrencyDisabledModel.objects.get_or_create(**{"username": "test"})[0] + concurrency_disabled2 = ConcurrencyDisabledModel.objects.get_or_create(**{"username": "test"})[0] v1 = api.get_revision_of_object(concurrency_disabled1) v2 = api.get_revision_of_object(concurrency_disabled2) assert v1 == v2, "got same row with different version (%s/%s)" % (v1, v2) diff --git a/tests/test_conditional.py b/tests/test_conditional.py index 65d48f3..3615e62 100644 --- a/tests/test_conditional.py +++ b/tests/test_conditional.py @@ -1,41 +1,33 @@ import logging -from django.contrib.auth.models import User - import pytest - -from concurrency.exceptions import RecordModifiedError -from concurrency.utils import refetch - from demo.models import ( ConditionalVersionModel, ConditionalVersionModelSelfRelation, ConditionalVersionModelWithoutMeta, ThroughRelation, ) +from django.contrib.auth.models import User + +from concurrency.exceptions import RecordModifiedError +from concurrency.utils import refetch logger = logging.getLogger(__name__) @pytest.fixture def user(): - return User.objects.get_or_create(username='username')[0] + return User.objects.get_or_create(username="username")[0] @pytest.fixture def instance(user): - return ConditionalVersionModel.objects.get_or_create(field1='1', - user=user, - field2='1', field3='1')[0] + return ConditionalVersionModel.objects.get_or_create(field1="1", user=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' - ) + return ConditionalVersionModelWithoutMeta.objects.create(field1="1", user=user, field2="1", field3="1") @pytest.fixture @@ -53,12 +45,12 @@ def test_standard_save(instance): version1 = instance.get_concurrency_version() assert version1 == 1 # version2 > version1 - instance.field1 = '2' + instance.field1 = "2" instance.save() version2 = instance.get_concurrency_version() assert version2 == 2 # version2 > version1 - instance.field3 = '3' + instance.field3 = "3" instance.save() version3 = instance.get_concurrency_version() assert version3 == 2 # version3 == version2 @@ -76,7 +68,7 @@ def test_conflict(instance): batch_instance = instance.__class__.objects.get(pk=instance.pk) assert batch_instance.version == instance.version - batch_instance.field1 = 'aaaa' + batch_instance.field1 = "aaaa" batch_instance.save() with pytest.raises(RecordModifiedError): @@ -92,7 +84,7 @@ def test_save_allowed(instance): instance = refetch(instance) batch_instance = refetch(instance) - batch_instance.field3 = 'aaaa' + batch_instance.field3 = "aaaa" batch_instance.save() instance.save() @@ -104,7 +96,7 @@ def test_conflict_no_meta(instance_no_meta): 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.field1 = "aaaa" batch_instance.save() with pytest.raises(RecordModifiedError): @@ -113,13 +105,12 @@ def test_conflict_no_meta(instance_no_meta): @pytest.mark.django_db() def test_self_relations(): - a = ConditionalVersionModelSelfRelation.objects.create(name='a') - ConditionalVersionModelSelfRelation.objects.create(name='b') + a = ConditionalVersionModelSelfRelation.objects.create(name="a") + ConditionalVersionModelSelfRelation.objects.create(name="b") - r = ThroughRelation.objects.create(left=a, - right=a) + r = ThroughRelation.objects.create(left=a, right=a) r.save() a1 = ConditionalVersionModelSelfRelation.objects.get(pk=a.pk) - a1.name = 'a' + a1.name = "a" a1.save() diff --git a/tests/test_config.py b/tests/test_config.py index 1b0b1c0..40bd887 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1,19 +1,15 @@ -from django.core.exceptions import ImproperlyConfigured - import pytest +from django.core.exceptions import ImproperlyConfigured from concurrency.config import AppSettings from concurrency.utils import fqn def test_config(settings): - settings.APP_OVERRIDE = 'overridden' + settings.APP_OVERRIDE = "overridden" class MySettings(AppSettings): - defaults = {'ENTRY1': 'abc', - 'ENTRY2': 123, - 'OVERRIDE': None, - 'CALLBACK': fqn(fqn)} + defaults = {"ENTRY1": "abc", "ENTRY2": 123, "OVERRIDE": None, "CALLBACK": fqn(fqn)} conf = MySettings("APP") assert str(conf.ENTRY1) == str(settings.APP_ENTRY1) @@ -36,5 +32,7 @@ def test_IGNORE_DEFAULT(settings): with pytest.raises(ImproperlyConfigured) as excinfo: settings.CONCURRENCY_IGNORE_DEFAULT = False AppSettings("") - assert str( - excinfo.value) == "IGNORE_DEFAULT has been removed in django-concurrency 1.5. Use VERSION_FIELD_REQUIRED instead" + assert ( + str(excinfo.value) + == "IGNORE_DEFAULT has been removed in django-concurrency 1.5. Use VERSION_FIELD_REQUIRED instead" + ) diff --git a/tests/test_core.py b/tests/test_core.py index 6117266..864dff1 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -1,11 +1,10 @@ import pytest +from demo.models import SimpleConcurrentModel from concurrency.core import _select_lock from concurrency.exceptions import RecordModifiedError from concurrency.utils import refetch -from demo.models import SimpleConcurrentModel - @pytest.mark.django_db def test_select_lock(settings): diff --git a/tests/test_enable_disable.py b/tests/test_enable_disable.py index cb2a4f2..623c732 100644 --- a/tests/test_enable_disable.py +++ b/tests/test_enable_disable.py @@ -1,15 +1,13 @@ +import pytest +from demo.models import AutoIncConcurrentModel, SimpleConcurrentModel +from demo.util import nextname from django.contrib.auth.models import User from django.test.utils import override_settings -import pytest - from concurrency.api import concurrency_disable_increment, disable_concurrency from concurrency.exceptions import RecordModifiedError from concurrency.utils import refetch -from demo.models import AutoIncConcurrentModel, SimpleConcurrentModel -from demo.util import nextname - @pytest.mark.django_db(transaction=False) def test_disable_concurrency_settings(settings): @@ -39,7 +37,7 @@ def test_disable_concurrency_global(): @pytest.mark.django_db(transaction=False) def test_disable_concurrency_not_managed(): - u = User(username='u1') + u = User(username="u1") with disable_concurrency(u): u.save() @@ -54,6 +52,7 @@ def test1(): copy = refetch(instance) copy.save() instance.save() + test1() @@ -90,10 +89,12 @@ def test_concurrency_disable_increment(): instance1 = AutoIncConcurrentModel(username=next(nextname)) assert instance1.version == 0 instance1.save() + instance1.refresh_from_db() assert instance1.version == 1 with concurrency_disable_increment(instance1): instance1.save() - instance1.save() + instance1.save(update_fields=("username",)) + instance1.refresh_from_db() assert instance1.version == 1 instance1.save() assert instance1.version == 2 diff --git a/tests/test_forms.py b/tests/test_forms.py index 76c10f5..d6af344 100644 --- a/tests/test_forms.py +++ b/tests/test_forms.py @@ -1,23 +1,25 @@ -from django.conf import settings +import pytest +from demo.models import Issue3TestModel, SimpleConcurrentModel from django.core.exceptions import ImproperlyConfigured, SuspiciousOperation from django.forms.models import modelform_factory from django.forms.widgets import HiddenInput, TextInput -from django.test import override_settings, TestCase +from django.test import TestCase, override_settings from django.test.testcases import SimpleTestCase from django.utils.encoding import smart_str from django.utils.translation import gettext as _ -import pytest - from concurrency.exceptions import VersionError -from concurrency.forms import ConcurrentForm, VersionField, VersionFieldSigner, VersionWidget - -from demo.models import Issue3TestModel, SimpleConcurrentModel +from concurrency.forms import ( + ConcurrentForm, + VersionField, + VersionFieldSigner, + VersionWidget, +) -__all__ = ['WidgetTest', 'FormFieldTest', 'ConcurrentFormTest'] +__all__ = ["WidgetTest", "FormFieldTest", "ConcurrentFormTest"] -class DummySigner(): +class DummySigner: def sign(self, value): return smart_str(value) @@ -28,142 +30,149 @@ def unsign(self, signed_value): class WidgetTest(TestCase): def test(self): w = VersionWidget() - self.assertHTMLEqual(w.render('ver', None), - '
') - self.assertHTMLEqual(w.render('ver', 100), - '
100
') + self.assertHTMLEqual(w.render("ver", None), '
') + self.assertHTMLEqual(w.render("ver", 100), '
100
') class FormFieldTest(SimpleTestCase): def test_with_wrong_signer(self): - with self.settings(CONCURRENCY_FIELD_SIGNER='invalid.Signer'): + with self.settings(CONCURRENCY_FIELD_SIGNER="invalid.Signer"): with pytest.raises(ImproperlyConfigured): VersionField() def test_with_dummy_signer(self): f = VersionField(signer=DummySigner()) self.assertEqual(1, f.clean(1)) - self.assertEqual(1, f.clean('1')) + self.assertEqual(1, f.clean("1")) self.assertEqual(0, f.clean(None)) - self.assertEqual(0, f.clean('')) - self.assertRaises(VersionError, f.clean, 'aa:bb') + self.assertEqual(0, f.clean("")) + self.assertRaises(VersionError, f.clean, "aa:bb") self.assertRaises(VersionError, f.clean, 1.5) def test(self): f = VersionField() self.assertEqual(1, f.clean(VersionFieldSigner().sign(1))) - self.assertEqual(1, f.clean(VersionFieldSigner().sign('1'))) + self.assertEqual(1, f.clean(VersionFieldSigner().sign("1"))) self.assertEqual(0, f.clean(None)) - self.assertEqual(0, f.clean('')) - self.assertRaises(VersionError, f.clean, '100') + self.assertEqual(0, f.clean("")) + self.assertRaises(VersionError, f.clean, "100") self.assertRaises(VersionError, f.clean, VersionFieldSigner().sign(1.5)) class ConcurrentFormTest(TestCase): def test_version(self): - Form = modelform_factory(SimpleConcurrentModel, ConcurrentForm, exclude=('char_field',)) + Form = modelform_factory(SimpleConcurrentModel, ConcurrentForm, exclude=("char_field",)) form = Form() - self.assertIsInstance(form.fields['version'].widget, HiddenInput) + self.assertIsInstance(form.fields["version"].widget, HiddenInput) def test_clean(self): pass def test_dummy_signer(self): - obj, __ = Issue3TestModel.objects.get_or_create(username='aaa') - Form = modelform_factory(Issue3TestModel, - fields=('id', 'revision'), - form=type('xxx', (ConcurrentForm,), {'revision': VersionField(signer=DummySigner())})) - data = {'id': 1, - 'revision': obj.revision} + obj, __ = Issue3TestModel.objects.get_or_create(username="aaa") + Form = modelform_factory( + Issue3TestModel, + fields=("id", "revision"), + form=type("xxx", (ConcurrentForm,), {"revision": VersionField(signer=DummySigner())}), + ) + data = {"id": 1, "revision": obj.revision} form = Form(data, instance=obj) self.assertTrue(form.is_valid(), form.non_field_errors()) def test_signer(self): - Form = modelform_factory(Issue3TestModel, form=ConcurrentForm, - exclude=('char_field',)) - form = Form({'username': 'aaa'}) + Form = modelform_factory(Issue3TestModel, form=ConcurrentForm, exclude=("char_field",)) + form = Form({"username": "aaa"}) self.assertTrue(form.is_valid(), form.non_field_errors()) def test_initial_value(self): - Form = modelform_factory(SimpleConcurrentModel, type('xxx', (ConcurrentForm,), {}), exclude=('char_field',)) - form = Form({'username': 'aaa'}) - if settings.IS_DJANGO_5: - self.assertHTMLEqual(str(form['version']), '') - else: - self.assertHTMLEqual(str(form['version']), '') + Form = modelform_factory(SimpleConcurrentModel, type("xxx", (ConcurrentForm,), {}), exclude=("char_field",)) + form = Form({"username": "aaa"}) + self.assertHTMLEqual( + str(form["version"]), + '', + ) self.assertTrue(form.is_valid(), form.non_field_errors()) def test_initial_value_with_custom_signer(self): - Form = modelform_factory(Issue3TestModel, exclude=('char_field',), - form=type('xxx', (ConcurrentForm,), - {'version': VersionField(signer=DummySigner())})) - form = Form({'username': 'aaa'}) - self.assertHTMLEqual(str(form['version']), '') + Form = modelform_factory( + Issue3TestModel, + exclude=("char_field",), + form=type("xxx", (ConcurrentForm,), {"version": VersionField(signer=DummySigner())}), + ) + form = Form({"username": "aaa"}) + self.assertHTMLEqual(str(form["version"]), '') self.assertTrue(form.is_valid(), form.non_field_errors()) def test_tamperig(self): - obj, __ = Issue3TestModel.objects.get_or_create(username='aaa') - Form = modelform_factory(Issue3TestModel, ConcurrentForm, exclude=('char_field',)) - data = {'username': 'aaa', - 'last_name': None, - 'date_field': None, - 'char_field': None, - 'version': 'abc', - 'id': 1, - 'revision': obj.revision} + obj, __ = Issue3TestModel.objects.get_or_create(username="aaa") + Form = modelform_factory(Issue3TestModel, ConcurrentForm, exclude=("char_field",)) + data = { + "username": "aaa", + "last_name": None, + "date_field": None, + "char_field": None, + "version": "abc", + "id": 1, + "revision": obj.revision, + } form = Form(data, instance=obj) self.assertRaises(SuspiciousOperation, form.is_valid) def test_custom_name(self): - Form = modelform_factory(Issue3TestModel, ConcurrentForm, exclude=('char_field',)) + Form = modelform_factory(Issue3TestModel, ConcurrentForm, exclude=("char_field",)) form = Form() - self.assertIsInstance(form.fields['version'].widget, TextInput) - self.assertIsInstance(form.fields['revision'].widget, HiddenInput) + self.assertIsInstance(form.fields["version"].widget, TextInput) + self.assertIsInstance(form.fields["revision"].widget, HiddenInput) def test_save(self): - obj, __ = Issue3TestModel.objects.get_or_create(username='aaa') + obj, __ = Issue3TestModel.objects.get_or_create(username="aaa") obj_copy = Issue3TestModel.objects.get(pk=obj.pk) - Form = modelform_factory(Issue3TestModel, ConcurrentForm, - fields=('username', 'last_name', 'date_field', - 'char_field', 'version', 'id', 'revision')) - data = {'username': 'aaa', - 'last_name': None, - 'date_field': None, - 'char_field': None, - 'version': 'abc', - 'id': 1, - 'revision': VersionFieldSigner().sign(obj.revision)} + Form = modelform_factory( + Issue3TestModel, + ConcurrentForm, + fields=("username", "last_name", "date_field", "char_field", "version", "id", "revision"), + ) + data = { + "username": "aaa", + "last_name": None, + "date_field": None, + "char_field": None, + "version": "abc", + "id": 1, + "revision": VersionFieldSigner().sign(obj.revision), + } form = Form(data, instance=obj) obj_copy.save() # save self.assertFalse(form.is_valid()) - self.assertIn(_('Record Modified'), form.non_field_errors()) + self.assertIn(_("Record Modified"), form.non_field_errors()) def test_is_valid(self): - obj, __ = Issue3TestModel.objects.get_or_create(username='aaa') - Form = modelform_factory(Issue3TestModel, ConcurrentForm, - fields=('username', 'last_name', 'date_field', - 'char_field', 'version', 'id', 'revision')) - data = {'username': 'aaa', - 'last_name': None, - 'date_field': None, - 'char_field': None, - 'version': 'abc', - 'id': 1, - 'revision': VersionFieldSigner().sign(obj.revision)} + obj, __ = Issue3TestModel.objects.get_or_create(username="aaa") + Form = modelform_factory( + Issue3TestModel, + ConcurrentForm, + fields=("username", "last_name", "date_field", "char_field", "version", "id", "revision"), + ) + data = { + "username": "aaa", + "last_name": None, + "date_field": None, + "char_field": None, + "version": "abc", + "id": 1, + "revision": VersionFieldSigner().sign(obj.revision), + } form = Form(data, instance=obj) obj.save() # save again simulate concurrent editing self.assertRaises(ValueError, form.save) def test_disabled(db, settings): - obj, __ = SimpleConcurrentModel.objects.get_or_create(username='aaa') - Form = modelform_factory(SimpleConcurrentModel, ConcurrentForm, - fields=('username', 'id', 'version')) - data = {'username': 'aaa', - 'id': 1, - 'version': VersionFieldSigner().sign(obj.version)} + obj, __ = SimpleConcurrentModel.objects.get_or_create(username="aaa") + Form = modelform_factory(SimpleConcurrentModel, ConcurrentForm, fields=("username", "id", "version")) + data = {"username": "aaa", "id": 1, "version": VersionFieldSigner().sign(obj.version)} form = Form(data, instance=obj) obj.save() # save again simulate concurrent editing with override_settings(CONCURRENCY_ENABLED=False): diff --git a/tests/test_issues.py b/tests/test_issues.py index 44d531a..f6b4626 100644 --- a/tests/test_issues.py +++ b/tests/test_issues.py @@ -1,5 +1,10 @@ import re +import pytest +from demo.admin import ActionsModelAdmin, admin_register +from demo.base import AdminTestCase +from demo.models import ListEditableConcurrentModel, SimpleConcurrentModel +from demo.util import attributes, unique_id from django.contrib.admin.sites import site from django.contrib.auth.models import User from django.core.management import call_command @@ -10,8 +15,6 @@ from django.test.testcases import SimpleTestCase from django.utils.encoding import force_str -import pytest - from concurrency.admin import ConcurrentModelAdmin from concurrency.compat import concurrency_param_name from concurrency.config import CONCURRENCY_LIST_EDITABLE_POLICY_SILENT @@ -20,16 +23,11 @@ from concurrency.templatetags.concurrency import identity from concurrency.utils import refetch -from demo.admin import ActionsModelAdmin, admin_register -from demo.base import AdminTestCase -from demo.models import ListEditableConcurrentModel, SimpleConcurrentModel -from demo.util import attributes, unique_id - def get_fake_request(params): - u, __ = User.objects.get_or_create(username='sax') - setattr(u, 'is_authenticated()', True) - setattr(u, 'selected_office', False) + u, __ = User.objects.get_or_create(username="sax") + setattr(u, "is_authenticated()", True) + setattr(u, "selected_office", False) request = RequestFactory().request() request.user = u @@ -45,21 +43,23 @@ def test_concurrency(self): id = 1 admin_register(ListEditableConcurrentModel, ActionsModelAdmin) model_admin = site._registry[ListEditableConcurrentModel] - with attributes((ConcurrentModelAdmin, 'list_editable_policy', CONCURRENCY_LIST_EDITABLE_POLICY_SILENT), - (ConcurrentModelAdmin, 'form', ConcurrentForm), ): + with attributes( + (ConcurrentModelAdmin, "list_editable_policy", CONCURRENCY_LIST_EDITABLE_POLICY_SILENT), + (ConcurrentModelAdmin, "form", ConcurrentForm), + ): obj, __ = ListEditableConcurrentModel.objects.get_or_create(pk=id) # post_param = 'form-_concurrency_version' if django.VERSION[:2] >= (4, 0) else '_concurrency_version' # request1 = get_fake_request('pk={}&{}_1=2'.format(id, post_param)) - request1 = get_fake_request(f'pk={id}&{concurrency_param_name}_1=2') + request1 = get_fake_request(f"pk={id}&{concurrency_param_name}_1=2") model_admin.save_model(request1, obj, None, True) self.assertIn(obj.pk, model_admin._get_conflicts(request1)) obj = refetch(obj) - request2 = get_fake_request(f'pk={id}&{concurrency_param_name}_1={obj.version}') + request2 = get_fake_request(f"pk={id}&{concurrency_param_name}_1={obj.version}") model_admin.save_model(request2, obj, None, True) self.assertNotIn(obj.pk, model_admin._get_conflicts(request2)) @@ -71,7 +71,7 @@ def test_identity_tag(self): obj = ListEditableConcurrentModel(pk=id) self.assertTrue(re.match(r"^%s,\d+$" % id, identity(obj))) - g = User(username='UserTest', pk=3) + g = User(username="UserTest", pk=3) self.assertEqual(identity(g), force_str(g.pk)) @@ -102,20 +102,16 @@ def test_issue_54(): @pytest.mark.django_db() def test_issue_81a(monkeypatch): - monkeypatch.setattr('demo.admin.ActionsModelAdmin.fields', ('id',)) + monkeypatch.setattr("demo.admin.ActionsModelAdmin.fields", ("id",)) with pytest.raises(SystemCheckError) as e: - call_command('check') - assert 'concurrency.A001' in str(e.value) + call_command("check") + assert "concurrency.A001" in str(e.value) @pytest.mark.django_db() def test_issue_81b(monkeypatch): - fieldsets = ( - ('Standard info', { - 'fields': ('id',) - }), - ) - monkeypatch.setattr('demo.admin.ActionsModelAdmin.fieldsets', fieldsets) + fieldsets = (("Standard info", {"fields": ("id",)}),) + monkeypatch.setattr("demo.admin.ActionsModelAdmin.fieldsets", fieldsets) with pytest.raises(SystemCheckError) as e: - call_command('check') - assert 'concurrency.A002' in str(e.value) + call_command("check") + assert "concurrency.A002" in str(e.value) diff --git a/tests/test_loaddata_dumpdata.py b/tests/test_loaddata_dumpdata.py index e2b93dc..fbfc0cf 100644 --- a/tests/test_loaddata_dumpdata.py +++ b/tests/test_loaddata_dumpdata.py @@ -3,11 +3,9 @@ import os from io import StringIO -from django.core.management import call_command - import pytest - from demo.models import SimpleConcurrentModel +from django.core.management import call_command logger = logging.getLogger(__name__) @@ -16,18 +14,18 @@ def test_dumpdata(): SimpleConcurrentModel.objects.create() out = StringIO() - call_command('dumpdata', 'demo', stdout=out) + call_command("dumpdata", "demo", stdout=out) data = json.loads(out.getvalue()) assert data @pytest.mark.django_db(transaction=True) def test_loaddata_fail(): - datafile = os.path.join(os.path.dirname(__file__), 'dumpdata.json') - with open(datafile, 'r') as f: + datafile = os.path.join(os.path.dirname(__file__), "dumpdata.json") + with open(datafile, "r") as f: data = json.load(f) - pk = data[0]['pk'] + pk = data[0]["pk"] - call_command('loaddata', datafile, stdout=StringIO()) + call_command("loaddata", datafile, stdout=StringIO()) - assert SimpleConcurrentModel.objects.get(id=pk).username == 'loaded' + assert SimpleConcurrentModel.objects.get(id=pk).username == "loaded" diff --git a/tests/test_manager.py b/tests/test_manager.py index 0c926b7..ffa8c2f 100644 --- a/tests/test_manager.py +++ b/tests/test_manager.py @@ -1,8 +1,4 @@ import pytest - -from concurrency.exceptions import RecordModifiedError -from concurrency.utils import refetch - from demo.models import ( AutoIncConcurrentModel, ConcreteModel, @@ -13,6 +9,9 @@ ) from demo.util import nextname, unique_id, with_models, with_std_models +from concurrency.exceptions import RecordModifiedError +from concurrency.utils import refetch + @pytest.mark.django_db @with_std_models @@ -42,9 +41,7 @@ def test_create(model_class=SimpleConcurrentModel): @pytest.mark.django_db -@with_models(SimpleConcurrentModel, AutoIncConcurrentModel, - InheritedModel, CustomSaveModel, - ConcreteModel, ProxyModel) +@with_models(SimpleConcurrentModel, AutoIncConcurrentModel, InheritedModel, CustomSaveModel, ConcreteModel, ProxyModel) def test_update(model_class): # Manager.update() does not change version number instance = model_class.objects.create(pk=next(unique_id), username=next(nextname).lower()) diff --git a/tests/test_middleware.py b/tests/test_middleware.py index 0ff3677..4ea4a5b 100644 --- a/tests/test_middleware.py +++ b/tests/test_middleware.py @@ -1,26 +1,24 @@ +import mock +from demo.base import AdminTestCase +from demo.models import SimpleConcurrentModel +from demo.util import DELETE_ATTRIBUTE, attributes, unique_id from django.conf import settings from django.contrib.admin.sites import site from django.http import HttpRequest from django.test.utils import override_settings from django.urls import reverse -import mock - from concurrency.admin import ConcurrentModelAdmin from concurrency.config import CONCURRENCY_LIST_EDITABLE_POLICY_ABORT_ALL from concurrency.exceptions import RecordModifiedError from concurrency.middleware import ConcurrencyMiddleware -from demo.base import AdminTestCase -from demo.models import SimpleConcurrentModel -from demo.util import attributes, DELETE_ATTRIBUTE, unique_id - def _get_request(path): request = HttpRequest() request.META = { - 'SERVER_NAME': 'testserver', - 'SERVER_PORT': 80, + "SERVER_NAME": "testserver", + "SERVER_PORT": 80, } request.path = request.path_info = "/middleware/%s" % path return request @@ -31,7 +29,7 @@ def test_middleware(): type(handler.return_value).status_code = mock.PropertyMock(return_value=409) with override_settings(CONCURRENCY_HANDLER409=handler): - request = _get_request('needsquoting#') + request = _get_request("needsquoting#") r = ConcurrencyMiddleware().process_exception(request, RecordModifiedError(target=SimpleConcurrentModel())) assert r.status_code == 409 @@ -40,13 +38,13 @@ class ConcurrencyMiddlewareTest1(AdminTestCase): def _get_request(self, path): request = HttpRequest() request.META = { - 'SERVER_NAME': 'testserver', - 'SERVER_PORT': 80, + "SERVER_NAME": "testserver", + "SERVER_PORT": 80, } request.path = request.path_info = "/middleware/%s" % path return request - @mock.patch('django.core.signals.got_request_exception.send', mock.Mock()) + @mock.patch("django.core.signals.got_request_exception.send", mock.Mock()) def test_process_exception(self): """ Tests that RecordModifiedError is handled correctly. @@ -55,7 +53,7 @@ def test_process_exception(self): m, __ = SimpleConcurrentModel.objects.get_or_create(pk=id) copy = SimpleConcurrentModel.objects.get(pk=m.pk) copy.save() - request = self._get_request('/') + request = self._get_request("/") r = ConcurrencyMiddleware().process_exception(request, RecordModifiedError(target=m)) self.assertEqual(r.status_code, 409) @@ -63,7 +61,7 @@ def test_process_exception(self): class ConcurrencyMiddlewareTest2(AdminTestCase): @property def settings_middleware(self): - return getattr(settings, self.middleware_setting_name) + ['concurrency.middleware.ConcurrencyMiddleware'] + return getattr(settings, self.middleware_setting_name) + ["concurrency.middleware.ConcurrencyMiddleware"] @settings_middleware.setter def settings_middleware(self, value): @@ -73,13 +71,15 @@ def test_in_admin(self): id = next(unique_id) model_admin = site._registry[SimpleConcurrentModel] - with attributes((model_admin.__class__, 'list_editable_policy', CONCURRENCY_LIST_EDITABLE_POLICY_ABORT_ALL), - (ConcurrentModelAdmin, 'form', DELETE_ATTRIBUTE)): + with attributes( + (model_admin.__class__, "list_editable_policy", CONCURRENCY_LIST_EDITABLE_POLICY_ABORT_ALL), + (ConcurrentModelAdmin, "form", DELETE_ATTRIBUTE), + ): saved, __ = SimpleConcurrentModel.objects.get_or_create(pk=id) - url = reverse('admin:demo_simpleconcurrentmodel_change', args=[saved.pk]) + url = reverse("admin:demo_simpleconcurrentmodel_change", args=[saved.pk]) res = self.app.get(url, user=self.user.username) - form = res.forms['simpleconcurrentmodel_form'] + form = res.forms["simpleconcurrentmodel_form"] saved.save() # create conflict here @@ -87,10 +87,10 @@ def test_in_admin(self): self.assertEqual(res.status_code, 409) - target = res.context['target'] - self.assertIn('target', res.context) - self.assertIn('saved', res.context) + target = res.context["target"] + self.assertIn("target", res.context) + self.assertIn("saved", res.context) - self.assertEqual(res.context['target'].version, target.version) - self.assertEqual(res.context['saved'].version, saved.version) - self.assertEqual(res.context['request_path'], url) + self.assertEqual(res.context["target"].version, target.version) + self.assertEqual(res.context["saved"].version, saved.version) + self.assertEqual(res.context["request_path"], url) diff --git a/tests/test_reversion.py b/tests/test_reversion.py index f88a01d..88ce9bf 100644 --- a/tests/test_reversion.py +++ b/tests/test_reversion.py @@ -1,27 +1,23 @@ +import pytest +from demo.models import ReversionConcurrentModel from django.urls import reverse - from reversion import add_to_revision, revisions, set_comment from reversion.models import Version -import pytest - -from demo.models import ReversionConcurrentModel - @pytest.mark.django_db @pytest.mark.functional def test_recover(admin_user, client): - concurrentmodel = ReversionConcurrentModel.objects.create(username='USERNAME-OLD') + concurrentmodel = ReversionConcurrentModel.objects.create(username="USERNAME-OLD") with revisions.create_revision(): set_comment("Initial revision") add_to_revision(concurrentmodel) ver = Version.objects.get_for_model(concurrentmodel).first() - url = reverse('admin:demo_reversionconcurrentmodel_recover', - args=[concurrentmodel.pk]) + url = reverse("admin:demo_reversionconcurrentmodel_recover", args=[concurrentmodel.pk]) res = client.get(url, user=admin_user.username) - res.forms['reversionconcurrentmodel_form'].submit().follow() + res.forms["reversionconcurrentmodel_form"].submit().follow() concurrentmodel2 = ReversionConcurrentModel.objects.get(pk=concurrentmodel.pk) - assert concurrentmodel2.username == ver.field_dict['username'] - assert concurrentmodel2.version > ver.field_dict['version'] + assert concurrentmodel2.username == ver.field_dict["username"] + assert concurrentmodel2.version > ver.field_dict["version"] diff --git a/tests/test_templatetags.py b/tests/test_templatetags.py index b7b1fbc..52134c7 100644 --- a/tests/test_templatetags.py +++ b/tests/test_templatetags.py @@ -1,11 +1,10 @@ import logging import pytest +from demo.models import SimpleConcurrentModel from concurrency.templatetags.concurrency import identity, is_version, version -from demo.models import SimpleConcurrentModel - logger = logging.getLogger(__name__) @@ -16,7 +15,7 @@ def obj(): @pytest.mark.django_db def test_identity(obj): - assert identity(obj).split(',') == [str(obj.pk), str(obj.version)] + assert identity(obj).split(",") == [str(obj.pk), str(obj.version)] @pytest.mark.django_db diff --git a/tests/test_threads.py b/tests/test_threads.py index bbffcff..fb794b8 100644 --- a/tests/test_threads.py +++ b/tests/test_threads.py @@ -1,21 +1,18 @@ +import pytest +from conftest import skippypy +from demo.models import TriggerConcurrentModel +from demo.util import concurrently from django import db from django.db import transaction -from conftest import skippypy - -import pytest - from concurrency.exceptions import RecordModifiedError from concurrency.utils import refetch -from demo.models import TriggerConcurrentModel -from demo.util import concurrently - @skippypy @pytest.mark.django_db(transaction=True) def test_threads(): - if db.connection.vendor == 'sqlite': + if db.connection.vendor == "sqlite": pytest.skip("in-memory sqlite db can't be used between threads") obj = TriggerConcurrentModel.objects.create() diff --git a/tests/test_triggerversionfield.py b/tests/test_triggerversionfield.py index 5747af4..cd4b319 100644 --- a/tests/test_triggerversionfield.py +++ b/tests/test_triggerversionfield.py @@ -1,16 +1,14 @@ -from django.core.signals import request_started -from django.db import connection, connections, IntegrityError - import mock - import pytest - -from concurrency.exceptions import RecordModifiedError -from concurrency.utils import refetch - from demo.models import TriggerConcurrentModel + # Register an event to reset saved queries when a Django request is started. from demo.util import nextname +from django.core.signals import request_started +from django.db import IntegrityError, connection, connections + +from concurrency.exceptions import RecordModifiedError +from concurrency.utils import refetch def reset_queries(**kwargs): @@ -72,9 +70,12 @@ def test_trigger_external_update(): @pytest.mark.django_db def test_trigger_external_create(): with connection.cursor() as c: - c.execute("INSERT INTO {} (username, count, cm_version_id) VALUES ('abc', 1, -1)".format( - TriggerConcurrentModel._meta.db_table)) - instance = TriggerConcurrentModel.objects.get(username='abc') + c.execute( + "INSERT INTO {} (username, count, cm_version_id) VALUES ('abc', 1, -1)".format( + TriggerConcurrentModel._meta.db_table + ) + ) + instance = TriggerConcurrentModel.objects.get(username="abc") obj = refetch(instance) assert obj.version == -1 @@ -114,7 +115,7 @@ def test_trigger_do_not_increase_version_if_error(): instance = TriggerConcurrentModel() assert instance.pk is None assert instance.version == 0 - with mock.patch('demo.models.TriggerConcurrentModel.save', side_effect=IntegrityError): + with mock.patch("demo.models.TriggerConcurrentModel.save", side_effect=IntegrityError): with pytest.raises(IntegrityError): instance.save() diff --git a/tests/test_utils.py b/tests/test_utils.py index 519770a..f1d6400 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,14 +1,12 @@ import logging -from django.test import TestCase - import pytest +from demo.models import SimpleConcurrentModel +from django.test import TestCase import concurrency.fields from concurrency.utils import ConcurrencyTestMixin, deprecated, fqn -from demo.models import SimpleConcurrentModel - logger = logging.getLogger(__name__) @@ -20,11 +18,11 @@ class TestConcurrencyTestMixin(ConcurrencyTestMixin, TestCase): def test_fqn(): with pytest.raises(ValueError): - fqn('str') + fqn("str") - assert fqn(SimpleConcurrentModel) == 'demo.models.SimpleConcurrentModel' - assert fqn(SimpleConcurrentModel()) == 'demo.models.SimpleConcurrentModel' - assert fqn(concurrency.fields) == 'concurrency.fields' + assert fqn(SimpleConcurrentModel) == "demo.models.SimpleConcurrentModel" + assert fqn(SimpleConcurrentModel()) == "demo.models.SimpleConcurrentModel" + assert fqn(concurrency.fields) == "concurrency.fields" def test_deprecated(): @@ -39,7 +37,7 @@ def foo1(x): def newfun(x): return 0 - @deprecated(newfun, '1.1') + @deprecated(newfun, "1.1") def foo2(x): return x diff --git a/tox.ini b/tox.ini index 0073eb8..a341252 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] -envlist = d{32,42,50}-py{310,311,312}-{pg,sqlite,mysql},docs,lint,package - +envlist = d{42,51}-py{311,312}-{pg,sqlite,mysql},docs,lint,package +skip_missing_interpreters = true [pytest] pythonpath=./tests/demoapp/ src @@ -8,6 +8,7 @@ django_find_project = false DJANGO_SETTINGS_MODULE=demo.settings norecursedirs = .tox docs ./demoapp/ python_files=tests/test_*.py +allowlist_externals=mkdir addopts = -q -p no:warnings @@ -45,21 +46,21 @@ setenv = pg: DBENGINE = pg mysql: DBENGINE = mysql sqlite: DBENGINE = sqlite - + d42: GROUP = dj4 + d51: GROUP = dj5 deps = - .[dev] pdm pypy-pg: psycopg2cffi mysql: mysqlclient - d32: django==3.2.* - d42: django==4.2.* - d50: django==5.0.* +; d42: django==4.2.* +; d51: django==5.1.* commands = - pdm install --dev - {posargs:pytest tests -x -rw --create-db} +; pdm sync -L {env:GROUP}.lock + pdm install -G {env:GROUP} -L {env:GROUP}-g.lock + {posargs:pdm run pytest tests -v -rw --create-db} [testenv:mysql] commands = @@ -81,7 +82,10 @@ commands = [testenv:docs] basepython = python312 +allowlist_externals= + mkdir deps = + pdm -rdocs/requirements.pip commands = mkdir -p ~build/docs