diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index fe6c95e4..5527c571 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -12,7 +12,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: 3.9 + python-version: '3.10' - name: Install flake8 run: pip install --upgrade flake8 - name: Run flake8 @@ -29,7 +29,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: 3.9 + python-version: '3.10' - run: python -m pip install isort - name: isort uses: liskin/gh-problem-matcher-wrap@v1 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fa839f07..37217349 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,14 +10,21 @@ jobs: strategy: fail-fast: false matrix: - python-version: [ 3.7, 3.8, 3.9 ] # latest release minus two + python-version: [ "3.10", "3.11", "3.12" ] # latest release minus two requirements-file: [ - dj22_cms40.txt, + dj50_cms41.txt, + dj42_cms41.txt, + dj42_cms40.txt, dj32_cms40.txt, ] os: [ ubuntu-20.04, ] + exclude: + - requirements-file: "dj42_cms40.txt" + python-version: "3.12" #cms40 not support py3.12 yet + - requirements-file: "dj32_cms40.txt" + python-version: "3.12" steps: - uses: actions/checkout@v4 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index bcd3de1a..c0963489 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -12,34 +12,34 @@ repos: rev: v2.31.0 hooks: - id: pyupgrade - args: ["--py37-plus"] + args: ["--py310-plus"] - repo: https://github.com/adamchainz/django-upgrade rev: '1.4.0' hooks: - id: django-upgrade - args: [--target-version, "2.2"] + args: [--target-version, "4.0"] - repo: https://github.com/PyCQA/flake8 - rev: 4.0.1 + rev: 7.0.0 hooks: - id: flake8 - repo: https://github.com/asottile/yesqa - rev: v1.3.0 + rev: v1.5.0 hooks: - id: yesqa - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.1.0 + rev: v4.5.0 hooks: - id: check-merge-conflict - id: debug-statements - id: mixed-line-ending - id: trailing-whitespace - +# upgrade the isort version to fix compatiable issue withe peotry: https://stackoverflow.com/questions/75269700/pre-commit-fails-to-install-isort-5-11-4-with-error-runtimeerror-the-poetry-co - repo: https://github.com/pycqa/isort - rev: 5.10.1 + rev: 5.13.2 hooks: - id: isort @@ -47,3 +47,7 @@ repos: rev: v2.1.0 hooks: - id: codespell + exclude: > + (?x)^( + .*\.(js|po) + )$ diff --git a/djangocms_moderation/__init__.py b/djangocms_moderation/__init__.py index 1e525f4c..edc60b35 100644 --- a/djangocms_moderation/__init__.py +++ b/djangocms_moderation/__init__.py @@ -1,3 +1 @@ __version__ = "2.1.6" - -default_app_config = "djangocms_moderation.apps.ModerationConfig" diff --git a/djangocms_moderation/admin.py b/djangocms_moderation/admin.py index 460bb8ee..f9320fa2 100644 --- a/djangocms_moderation/admin.py +++ b/djangocms_moderation/admin.py @@ -16,7 +16,7 @@ from cms.toolbar.utils import get_object_preview_url from cms.utils.helpers import is_editable_model -from adminsortable2.admin import SortableInlineAdminMixin +from adminsortable2.admin import SortableAdminMixin, SortableInlineAdminMixin from treebeard.admin import TreeAdmin from . import constants, signals @@ -73,12 +73,16 @@ def has_add_permission(self, request): def has_delete_permission(self, request, obj=None): return False + @admin.display( + description=_("Status") + ) def show_user(self, obj): _name = obj.get_by_user_name() return gettext("By {user}").format(user=_name) - show_user.short_description = _("Status") - + @admin.display( + description=_("Form Submission") + ) def form_submission(self, obj): instance = get_form_submission_for_step( obj.moderation_request, obj.step_approved @@ -89,15 +93,13 @@ def form_submission(self, obj): opts = ConfirmationFormSubmission._meta url = reverse( - "admin:{}_{}_change".format(opts.app_label, opts.model_name), + f"admin:{opts.app_label}_{opts.model_name}_change", args=[instance.pk], ) return format_html( '{}', url, obj.step_approved.role.name ) - form_submission.short_description = _("Form Submission") - def get_readonly_fields(self, request, obj=None): if obj.user_can_moderate(request.user) or obj.user_is_author(request.user): # Omit 'message' from readonly_fields when current user is a reviewer @@ -107,6 +109,7 @@ def get_readonly_fields(self, request, obj=None): return self.fields +@admin.register(ModerationRequestTreeNode) class ModerationRequestTreeAdmin(TreeAdmin): """ This admin is purely for the change list of Moderation Requests using the treebeard nodes to @@ -177,6 +180,9 @@ def get_list_display(self, request): ] return list_display + @admin.display( + description=_("actions") + ) def list_display_actions(self, obj): """Display links to state change endpoints """ @@ -184,8 +190,6 @@ def list_display_actions(self, obj): "", "{}", ((action(obj),) for action in self.get_list_display_actions()) ) - list_display_actions.short_description = _("actions") - def get_list_display_actions(self): actions = [] if conf.REQUEST_COMMENTS_ENABLED: @@ -210,6 +214,9 @@ def _get_configured_fields(self, request): return fields + @admin.display( + description=_('ID') + ) def get_id(self, obj): return format_html( '{id}', @@ -219,22 +226,30 @@ def get_id(self, obj): ), id=obj.moderation_request_id, ) - get_id.short_description = _('ID') + @admin.display( + description=_('Content type') + ) def get_content_type(self, obj): return ContentType.objects.get_for_model( obj.moderation_request.version.versionable.grouper_model ) - get_content_type.short_description = _('Content type') + @admin.display( + description=_('Title') + ) def get_title(self, obj): return obj.moderation_request.version.content - get_title.short_description = _('Title') + @admin.display( + description=_('Author') + ) def get_version_author(self, obj): return obj.moderation_request.version.created_by - get_version_author.short_description = _('Author') + @admin.display( + description=_("Preview") + ) def get_preview_link(self, obj): content = obj.moderation_request.version.content if is_editable_model(content.__class__): @@ -254,8 +269,9 @@ def get_preview_link(self, obj): object_preview_url, ) - get_preview_link.short_description = _("Preview") - + @admin.display( + description=_('Reviewer') + ) def get_reviewer(self, obj): last_action = obj.moderation_request.get_last_action() if not last_action: @@ -264,7 +280,6 @@ def get_reviewer(self, obj): next_step = obj.moderation_request.get_next_required() return next_step.role.name return last_action._get_user_name(last_action.by_user) - get_reviewer.short_description = _('Reviewer') def get_status(self, obj): # We can have moderation requests without any action (e.g. the @@ -466,6 +481,7 @@ def _traverse_moderation_nodes(node_item): return HttpResponseRedirect(redirect_url) +@admin.register(ModerationRequest) class ModerationRequestAdmin(admin.ModelAdmin): class Media: js = ('admin/js/jquery.init.js', 'djangocms_moderation/js/actions.js',) @@ -813,11 +829,13 @@ def changelist_view(self, request, extra_context=None): return tree_node_admin.changelist_view(request, extra_context) +@admin.register(Role) class RoleAdmin(admin.ModelAdmin): list_display = ["name", "user", "group", "confirmation_page"] fields = ["name", "user", "group", "confirmation_page"] +@admin.register(CollectionComment) class CollectionCommentAdmin(admin.ModelAdmin): list_display = ["date_created", "message", "author"] fields = ["collection", "message", "author"] @@ -900,6 +918,7 @@ def get_readonly_fields(self, request, obj=None): return self.list_display +@admin.register(RequestComment) class RequestCommentAdmin(admin.ModelAdmin): list_display = ["date_created", "message", "get_author"] fields = ["moderation_request", "message", "author"] @@ -907,11 +926,12 @@ class RequestCommentAdmin(admin.ModelAdmin): class Media: css = {"all": ("djangocms_moderation/css/comments_changelist.css",)} + @admin.display( + description=_("User") + ) def get_author(self, obj): return obj.author_name - get_author.short_description = _("User") - def get_changeform_initial_data(self, request): data = {"author": request.user} moderation_request_id = utils.extract_filter_param_from_changelist_url( @@ -990,7 +1010,8 @@ def get_extra(self, request, obj=None, **kwargs): return 1 -class WorkflowAdmin(admin.ModelAdmin): +@admin.register(Workflow) +class WorkflowAdmin(SortableAdminMixin, admin.ModelAdmin): inlines = [WorkflowStepInline] list_display = ["name", "is_default"] fields = [ @@ -1002,6 +1023,7 @@ class WorkflowAdmin(admin.ModelAdmin): ] +@admin.register(ModerationCollection) class ModerationCollectionAdmin(admin.ModelAdmin): class Media: js = ("admin/js/jquery.init.js", "djangocms_moderation/js/actions.js",) @@ -1033,11 +1055,16 @@ def get_list_display(self, request): def job_id(self, obj): return obj.pk + @admin.display( + description=_('reviewers') + ) def commaseparated_reviewers(self, obj): reviewers = self.model.objects.reviewers(obj) return ", ".join(map(get_user_model().get_full_name, reviewers)) - commaseparated_reviewers.short_description = _('reviewers') + @admin.display( + description=_("actions") + ) def list_display_actions(self, obj): """Display links to state change endpoints """ @@ -1045,8 +1072,6 @@ def list_display_actions(self, obj): "", "{}", ((action(obj),) for action in self.get_list_display_actions()) ) - list_display_actions.short_description = _("actions") - def get_list_display_actions(self): actions = [self.get_edit_link, self.get_requests_link] if conf.COLLECTION_COMMENTS_ENABLED: @@ -1136,6 +1161,7 @@ def has_delete_permission(self, request, obj=None): return False +@admin.register(ConfirmationPage) class ConfirmationPageAdmin(PlaceholderAdminMixin, admin.ModelAdmin): view_on_site = True @@ -1153,6 +1179,7 @@ def _url(regex, fn, name, **kwargs): return url_patterns + super().get_urls() +@admin.register(ConfirmationFormSubmission) class ConfirmationFormSubmissionAdmin(admin.ModelAdmin): list_display = ["moderation_request", "for_step", "submitted_at"] fields = [ @@ -1178,16 +1205,21 @@ def change_view(self, request, object_id, form_url="", extra_context=None): request, object_id, form_url, extra_context=extra_context ) + @admin.display( + description=_("Request") + ) def moderation_request(self, obj): return obj.moderation_request_id - moderation_request.short_description = _("Request") - + @admin.display( + description=_("By User") + ) def show_user(self, obj): return obj.get_by_user_name() - show_user.short_description = _("By User") - + @admin.display( + description=_("Form Data") + ) def form_data(self, obj): data = obj.get_form_data() return format_html_join( @@ -1198,16 +1230,3 @@ def form_data(self, obj): for d in data ), ) - - form_data.short_description = _("Form Data") - - -admin.site.register(ModerationRequestTreeNode, ModerationRequestTreeAdmin) -admin.site.register(ModerationRequest, ModerationRequestAdmin) -admin.site.register(CollectionComment, CollectionCommentAdmin) -admin.site.register(RequestComment, RequestCommentAdmin) -admin.site.register(ModerationCollection, ModerationCollectionAdmin) -admin.site.register(Role, RoleAdmin) -admin.site.register(Workflow, WorkflowAdmin) -admin.site.register(ConfirmationPage, ConfirmationPageAdmin) -admin.site.register(ConfirmationFormSubmission, ConfirmationFormSubmissionAdmin) diff --git a/djangocms_moderation/admin_actions.py b/djangocms_moderation/admin_actions.py index 07fc3eea..926e1282 100644 --- a/djangocms_moderation/admin_actions.py +++ b/djangocms_moderation/admin_actions.py @@ -148,14 +148,14 @@ def add_items_to_collection(modeladmin, request, queryset): args=(), ), version_ids=",".join(version_ids), - return_to_url=request.META.get("HTTP_REFERER", ""), + return_to_url=request.headers.get("referer", ""), ) return HttpResponseRedirect(admin_url) else: modeladmin.message_user( request, _("No suitable items found to add to moderation collection") ) - return HttpResponseRedirect(request.META.get("HTTP_REFERER", "")) + return HttpResponseRedirect(request.headers.get("referer", "")) add_items_to_collection.short_description = _("Add to moderation collection") diff --git a/djangocms_moderation/apps.py b/djangocms_moderation/apps.py index cbeed482..b2b7dbf1 100644 --- a/djangocms_moderation/apps.py +++ b/djangocms_moderation/apps.py @@ -7,6 +7,6 @@ class ModerationConfig(AppConfig): verbose_name = _("django CMS Moderation") def ready(self): - import djangocms_moderation.handlers # noqa: F401 - import djangocms_moderation.monkeypatch # noqa: F401 + import djangocms_moderation.handlers + import djangocms_moderation.monkeypatch import djangocms_moderation.signals # noqa: F401 diff --git a/djangocms_moderation/backends.py b/djangocms_moderation/backends.py index 416606c0..565e9390 100644 --- a/djangocms_moderation/backends.py +++ b/djangocms_moderation/backends.py @@ -20,4 +20,4 @@ def sequential_number_with_identifier_prefix_backend(**kwargs): semi-sequential numbers, prefixed with `workflow.identifier` field, if set """ moderation_request = kwargs["moderation_request"] - return "{}{}".format(moderation_request.workflow.identifier, moderation_request.pk) + return f"{moderation_request.workflow.identifier}{moderation_request.pk}" diff --git a/djangocms_moderation/cms_toolbars.py b/djangocms_moderation/cms_toolbars.py index 6181cfb7..7679807a 100644 --- a/djangocms_moderation/cms_toolbars.py +++ b/djangocms_moderation/cms_toolbars.py @@ -68,7 +68,7 @@ def _add_moderation_buttons(self): opts = ModerationRequest._meta codename = get_permission_codename("add", opts) if not self.request.user.has_perm( - "{app_label}.{codename}".format(app_label=opts.app_label, codename=codename) + f"{opts.app_label}.{codename}" ): return version = Version.objects.get_for_content(self.toolbar.obj) @@ -91,7 +91,7 @@ def _add_moderation_menu(self): opts = ModerationCollection._meta codename = get_permission_codename("change", opts) if not self.request.user.has_perm( - "{app_label}.{codename}".format(app_label=opts.app_label, codename=codename) + f"{opts.app_label}.{codename}" ): return admin_menu = self.toolbar.get_or_create_menu(ADMIN_MENU_IDENTIFIER) diff --git a/djangocms_moderation/conf.py b/djangocms_moderation/conf.py index e7008be7..e051e5ef 100644 --- a/djangocms_moderation/conf.py +++ b/djangocms_moderation/conf.py @@ -9,7 +9,7 @@ ) CORE_COMPLIANCE_NUMBER_BACKENDS = ( - (UUID_BACKEND, _("Unique alpha-numeric string")), + (UUID_BACKEND, _("Unique alphanumeric string")), (SEQUENTIAL_NUMBER_BACKEND, _("Sequential number")), ( SEQUENTIAL_NUMBER_WITH_IDENTIFIER_PREFIX_BACKEND, diff --git a/djangocms_moderation/contrib/__init__.py b/djangocms_moderation/contrib/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/djangocms_moderation/contrib/moderation_forms/__init__.py b/djangocms_moderation/contrib/moderation_forms/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/djangocms_moderation/contrib/moderation_forms/cms_plugins.py b/djangocms_moderation/contrib/moderation_forms/cms_plugins.py deleted file mode 100644 index 61e23625..00000000 --- a/djangocms_moderation/contrib/moderation_forms/cms_plugins.py +++ /dev/null @@ -1,32 +0,0 @@ -from django.utils.translation import gettext_lazy as _ - -from cms.plugin_pool import plugin_pool - -from aldryn_forms.cms_plugins import FormPlugin - -from djangocms_moderation.helpers import get_page_or_404 -from djangocms_moderation.signals import confirmation_form_submission - -from .models import ModerationForm - - -class ModerationFormPlugin(FormPlugin): - name = _("Moderation Form") - model = ModerationForm - fieldsets = ((None, {"fields": ("name",)}),) - - def form_valid(self, instance, request, form): - fields = form.get_serialized_fields(is_confirmation=False) - fields_as_dicts = [field._asdict() for field in fields] - page = get_page_or_404(request.GET.get("page"), request.GET.get("language")) - - confirmation_form_submission.send( - sender=self.__class__, - page=page, - language=request.GET.get("language"), - user=request.user, - form_data=fields_as_dicts, - ) - - -plugin_pool.register_plugin(ModerationFormPlugin) diff --git a/djangocms_moderation/contrib/moderation_forms/migrations/0001_initial.py b/djangocms_moderation/contrib/moderation_forms/migrations/0001_initial.py deleted file mode 100644 index c2ab629c..00000000 --- a/djangocms_moderation/contrib/moderation_forms/migrations/0001_initial.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 1.11.12 on 2018-05-03 09:15 -from django.db import migrations - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [("aldryn_forms", "0011_auto_20180110_1300")] - - operations = [ - migrations.CreateModel( - name="ModerationForm", - fields=[], - options={ - "verbose_name": "Moderation Form", - "verbose_name_plural": "Moderation Forms", - "proxy": True, - "indexes": [], - }, - bases=("aldryn_forms.formplugin",), - ) - ] diff --git a/djangocms_moderation/contrib/moderation_forms/migrations/__init__.py b/djangocms_moderation/contrib/moderation_forms/migrations/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/djangocms_moderation/contrib/moderation_forms/models.py b/djangocms_moderation/contrib/moderation_forms/models.py deleted file mode 100644 index 72027ccd..00000000 --- a/djangocms_moderation/contrib/moderation_forms/models.py +++ /dev/null @@ -1,10 +0,0 @@ -from django.utils.translation import gettext_lazy as _ - -from aldryn_forms.models import FormPlugin - - -class ModerationForm(FormPlugin): - class Meta: - proxy = True - verbose_name = _("Moderation Form") - verbose_name_plural = _("Moderation Forms") diff --git a/djangocms_moderation/emails.py b/djangocms_moderation/emails.py index 319a53e6..761c3b5b 100644 --- a/djangocms_moderation/emails.py +++ b/djangocms_moderation/emails.py @@ -35,7 +35,7 @@ def _send_email( "job_id": collection.job_id, "by_user": by_user, } - template = "djangocms_moderation/emails/moderation-request/{}".format(template) + template = f"djangocms_moderation/emails/moderation-request/{template}" # TODO What language should the email be sent in? e.g. `with force_language(lang):` subject = force_str(subject) @@ -61,7 +61,7 @@ def notify_collection_author(collection, moderation_requests, action, by_user): moderation_requests=moderation_requests, recipients=[collection.author.email], subject=email_subjects[action], - template="{}.txt".format(action), + template=f"{action}.txt", by_user=by_user, ) return status diff --git a/djangocms_moderation/handlers.py b/djangocms_moderation/handlers.py index 768ba335..50a9f9e4 100644 --- a/djangocms_moderation/handlers.py +++ b/djangocms_moderation/handlers.py @@ -11,7 +11,7 @@ def moderation_confirmation_form_submission( sender, page, language, user, form_data, **kwargs ): for field_data in form_data: - if not set(("label", "value")).issubset(field_data): + if not {"label", "value"}.issubset(field_data): raise ValueError("Each field dict should contain label and value keys.") # TODO Confirmation pages are not used/working in 1.0.x yet diff --git a/djangocms_moderation/helpers.py b/djangocms_moderation/helpers.py index 25249302..d266f7cd 100644 --- a/djangocms_moderation/helpers.py +++ b/djangocms_moderation/helpers.py @@ -21,9 +21,13 @@ User = get_user_model() try: - from djangocms_version_locking.helpers import content_is_unlocked_for_user + from djangocms_versioning.helpers import content_is_unlocked_for_user except ImportError: - content_is_unlocked_for_user = None + try: + # Before djangocms-versioning 2.0.0, version locking was in a separate package + from djangocms_version_locking.helpers import content_is_unlocked_for_user + except ImportError: + content_is_unlocked_for_user = None def get_page_or_404(obj_id, language): @@ -75,7 +79,7 @@ def get_active_moderation_request(content_object): If this returns None, it means there is no active_moderation request for this object, and it means it can be submitted for new moderation """ - from djangocms_moderation.models import ModerationRequest # noqa + from djangocms_moderation.models import ModerationRequest version = Version.objects.get_for_content(content_object) diff --git a/djangocms_moderation/migrations/0001_initial.py b/djangocms_moderation/migrations/0001_initial.py index b1464ba4..c714385e 100644 --- a/djangocms_moderation/migrations/0001_initial.py +++ b/djangocms_moderation/migrations/0001_initial.py @@ -311,7 +311,7 @@ class Migration(migrations.Migration): models.CharField( blank=True, default="", - help_text="Identifier is a 'free' field you could use for internal purposes. For example, it could be used as a workflow specific prefix of a compliance number", + help_text="Identifier is a 'free' field you could use for internal purposes. For example, it could be used as a workflow specific prefix of a compliance number", # noqa: E501 max_length=128, verbose_name="identifier", ), @@ -320,7 +320,7 @@ class Migration(migrations.Migration): "requires_compliance_number", models.BooleanField( default=False, - help_text="Does the Compliance number need to be generated before the moderation request is approved? Please select the compliance number backend below", + help_text="Does the Compliance number need to be generated before the moderation request is approved? Please select the compliance number backend below", # noqa: E501 verbose_name="requires compliance number?", ), ), @@ -454,14 +454,14 @@ class Migration(migrations.Migration): ), ), migrations.AlterUniqueTogether( - name="workflowstep", unique_together=set([("role", "workflow")]) + name="workflowstep", unique_together={("role", "workflow")} ), migrations.AlterUniqueTogether( name="moderationrequest", - unique_together=set([("collection", "object_id", "content_type")]), + unique_together={("collection", "object_id", "content_type")}, ), migrations.AlterUniqueTogether( name="confirmationformsubmission", - unique_together=set([("request", "for_step")]), + unique_together={("request", "for_step")}, ), ] diff --git a/djangocms_moderation/migrations/0001_squashed_0017_auto_20220831_0727.py b/djangocms_moderation/migrations/0001_squashed_0017_auto_20220831_0727.py index 7ff51e57..df8856a5 100644 --- a/djangocms_moderation/migrations/0001_squashed_0017_auto_20220831_0727.py +++ b/djangocms_moderation/migrations/0001_squashed_0017_auto_20220831_0727.py @@ -7,207 +7,657 @@ class Migration(migrations.Migration): - - replaces = [('djangocms_moderation', '0001_initial'), ('djangocms_moderation', '0002_auto_20180905_1152'), ('djangocms_moderation', '0003_auto_20180903_1206'), ('djangocms_moderation', '0004_auto_20180907_1206'), ('djangocms_moderation', '0005_auto_20180919_1348'), ('djangocms_moderation', '0006_auto_20181001_1840'), ('djangocms_moderation', '0007_auto_20181002_1725'), ('djangocms_moderation', '0008_auto_20181002_1833'), ('djangocms_moderation', '0009_auto_20181005_1534'), ('djangocms_moderation', '0010_auto_20181008_1317'), ('djangocms_moderation', '0011_auto_20181008_1328'), ('djangocms_moderation', '0012_auto_20181016_1319'), ('djangocms_moderation', '0013_auto_20181122_1110'), ('djangocms_moderation', '0014_auto_20190313_1638'), ('djangocms_moderation', '0014_auto_20190315_1723'), ('djangocms_moderation', '0016_moderationrequesttreenode'), ('djangocms_moderation', '0017_auto_20220831_0727')] + replaces = [ + ("djangocms_moderation", "0001_initial"), + ("djangocms_moderation", "0002_auto_20180905_1152"), + ("djangocms_moderation", "0003_auto_20180903_1206"), + ("djangocms_moderation", "0004_auto_20180907_1206"), + ("djangocms_moderation", "0005_auto_20180919_1348"), + ("djangocms_moderation", "0006_auto_20181001_1840"), + ("djangocms_moderation", "0007_auto_20181002_1725"), + ("djangocms_moderation", "0008_auto_20181002_1833"), + ("djangocms_moderation", "0009_auto_20181005_1534"), + ("djangocms_moderation", "0010_auto_20181008_1317"), + ("djangocms_moderation", "0011_auto_20181008_1328"), + ("djangocms_moderation", "0012_auto_20181016_1319"), + ("djangocms_moderation", "0013_auto_20181122_1110"), + ("djangocms_moderation", "0014_auto_20190313_1638"), + ("djangocms_moderation", "0014_auto_20190315_1723"), + ("djangocms_moderation", "0016_moderationrequesttreenode"), + ("djangocms_moderation", "0017_auto_20220831_0727"), + ] initial = True dependencies = [ - ('cms', '0028_remove_page_placeholders'), - ('djangocms_versioning', '0010_version_proxies'), + ("cms", "0028_remove_page_placeholders"), + ("djangocms_versioning", "0010_version_proxies"), migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('auth', '0008_alter_user_username_max_length'), - ('contenttypes', '0002_remove_content_type_name'), - ('cms', '0020_old_tree_cleanup'), + ("auth", "0008_alter_user_username_max_length"), + ("contenttypes", "0002_remove_content_type_name"), + ("cms", "0020_old_tree_cleanup"), ] operations = [ migrations.CreateModel( - name='ConfirmationPage', + name="ConfirmationPage", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=50, verbose_name='name')), - ('content_type', models.CharField(choices=[('plain', 'Plain'), ('form', 'Form')], default='form', max_length=50, verbose_name='Content Type')), - ('template', models.CharField(choices=[('djangocms_moderation/moderation_confirmation.html', 'Default')], default='djangocms_moderation/moderation_confirmation.html', max_length=100, verbose_name='Template')), - ('content', cms.models.fields.PlaceholderField(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, slotname='confirmation_content', to='cms.placeholder')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=50, verbose_name="name")), + ( + "content_type", + models.CharField( + choices=[("plain", "Plain"), ("form", "Form")], + default="form", + max_length=50, + verbose_name="Content Type", + ), + ), + ( + "template", + models.CharField( + choices=[ + ( + "djangocms_moderation/moderation_confirmation.html", + "Default", + ) + ], + default="djangocms_moderation/moderation_confirmation.html", + max_length=100, + verbose_name="Template", + ), + ), + ( + "content", + cms.models.fields.PlaceholderField( + editable=False, + null=True, + on_delete=django.db.models.deletion.CASCADE, + slotname="confirmation_content", + to="cms.placeholder", + ), + ), ], options={ - 'verbose_name': 'Confirmation Page', - 'verbose_name_plural': 'Confirmation Pages', + "verbose_name": "Confirmation Page", + "verbose_name_plural": "Confirmation Pages", }, ), migrations.CreateModel( - name='ModerationCollection', + name="ModerationCollection", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=128, verbose_name='collection name')), - ('status', models.CharField(choices=[('COLLECTING', 'Collecting'), ('IN_REVIEW', 'In Review'), ('ARCHIVED', 'Archived'), ('CANCELLED', 'Cancelled')], db_index=True, default='COLLECTING', max_length=10)), - ('date_created', models.DateTimeField(auto_now_add=True)), - ('date_modified', models.DateTimeField(auto_now=True)), - ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='author')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "name", + models.CharField(max_length=128, verbose_name="collection name"), + ), + ( + "status", + models.CharField( + choices=[ + ("COLLECTING", "Collecting"), + ("IN_REVIEW", "In Review"), + ("ARCHIVED", "Archived"), + ("CANCELLED", "Cancelled"), + ], + db_index=True, + default="COLLECTING", + max_length=10, + ), + ), + ("date_created", models.DateTimeField(auto_now_add=True)), + ("date_modified", models.DateTimeField(auto_now=True)), + ( + "author", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="+", + to=settings.AUTH_USER_MODEL, + verbose_name="author", + ), + ), ], ), migrations.CreateModel( - name='ModerationRequest', + name="ModerationRequest", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('language', models.CharField(choices=[('en', 'English'), ('de', 'German')], max_length=20, verbose_name='language')), - ('is_active', models.BooleanField(db_index=True, default=True, verbose_name='is active')), - ('date_sent', models.DateTimeField(auto_now_add=True, verbose_name='date sent')), - ('compliance_number', models.CharField(blank=True, editable=False, max_length=32, null=True, unique=True, verbose_name='compliance number')), - ('collection', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='moderation_requests', to='djangocms_moderation.moderationcollection')), - ('version', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='djangocms_versioning.version', verbose_name='version')), - ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='author')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "language", + models.CharField( + choices=[("en", "English"), ("de", "German")], + max_length=20, + verbose_name="language", + ), + ), + ( + "is_active", + models.BooleanField( + db_index=True, default=True, verbose_name="is active" + ), + ), + ( + "date_sent", + models.DateTimeField(auto_now_add=True, verbose_name="date sent"), + ), + ( + "compliance_number", + models.CharField( + blank=True, + editable=False, + max_length=32, + null=True, + unique=True, + verbose_name="compliance number", + ), + ), + ( + "collection", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="moderation_requests", + to="djangocms_moderation.moderationcollection", + ), + ), + ( + "version", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="djangocms_versioning.version", + verbose_name="version", + ), + ), + ( + "author", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="+", + to=settings.AUTH_USER_MODEL, + verbose_name="author", + ), + ), ], options={ - 'verbose_name': 'Request', - 'verbose_name_plural': 'Requests', - 'ordering': ['id'], - 'unique_together': {('collection', 'version')}, + "verbose_name": "Request", + "verbose_name_plural": "Requests", + "ordering": ["id"], + "unique_together": {("collection", "version")}, }, ), migrations.CreateModel( - name='Role', + name="Role", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=120, unique=True, verbose_name='name')), - ('confirmation_page', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='djangocms_moderation.confirmationpage', verbose_name='confirmation page')), - ('group', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='auth.group', verbose_name='group')), - ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='user')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "name", + models.CharField(max_length=120, unique=True, verbose_name="name"), + ), + ( + "confirmation_page", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="+", + to="djangocms_moderation.confirmationpage", + verbose_name="confirmation page", + ), + ), + ( + "group", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="auth.group", + verbose_name="group", + ), + ), + ( + "user", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + verbose_name="user", + ), + ), ], options={ - 'verbose_name': 'Role', - 'verbose_name_plural': 'Roles', + "verbose_name": "Role", + "verbose_name_plural": "Roles", }, ), migrations.CreateModel( - name='Workflow', + name="Workflow", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=120, unique=True, verbose_name='name')), - ('is_default', models.BooleanField(default=False, verbose_name='is default')), - ('identifier', models.CharField(blank=True, default='', help_text="Identifier is a 'free' field you could use for internal purposes. For example, it could be used as a workflow specific prefix of a compliance number", max_length=128, verbose_name='identifier')), - ('requires_compliance_number', models.BooleanField(default=False, help_text='Does the Compliance number need to be generated before the moderation request is approved? Please select the compliance number backend below', verbose_name='requires compliance number?')), - ('compliance_number_backend', models.CharField(choices=[('djangocms_moderation.backends.uuid4_backend', 'Unique alpha-numeric string'), ('djangocms_moderation.backends.sequential_number_backend', 'Sequential number'), ('djangocms_moderation.backends.sequential_number_with_identifier_prefix_backend', 'Sequential number with identifier prefix')], default='djangocms_moderation.backends.uuid4_backend', max_length=255, verbose_name='compliance number backend')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "name", + models.CharField(max_length=120, unique=True, verbose_name="name"), + ), + ( + "is_default", + models.BooleanField(default=False, verbose_name="is default"), + ), + ( + "identifier", + models.CharField( + blank=True, + default="", + help_text="Identifier is a 'free' field you could use for internal purposes. For example, " + "it could be used as a workflow specific prefix of a compliance number", + max_length=128, + verbose_name="identifier", + ), + ), + ( + "requires_compliance_number", + models.BooleanField( + default=False, + help_text="Does the Compliance number need to be generated before the moderation request is " + "approved? Please select the compliance number backend below", + verbose_name="requires compliance number?", + ), + ), + ( + "compliance_number_backend", + models.CharField( + choices=[ + ( + "djangocms_moderation.backends.uuid4_backend", + "Unique alpha-numeric string", + + ), + ( + "djangocms_moderation.backends.sequential_number_backend", + "Sequential number", + ), + ( + "djangocms_moderation.backends.sequential_number_with_identifier_prefix_backend", + "Sequential number with identifier prefix", + ), + ], + default="djangocms_moderation.backends.uuid4_backend", + max_length=255, + verbose_name="compliance number backend", + ), + ), ], options={ - 'verbose_name': 'Workflow', - 'verbose_name_plural': 'Workflows', + "verbose_name": "Workflow", + "verbose_name_plural": "Workflows", }, ), migrations.CreateModel( - name='WorkflowStep', + name="WorkflowStep", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('is_required', models.BooleanField(default=True, verbose_name='is mandatory')), - ('order', models.PositiveIntegerField()), - ('role', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='djangocms_moderation.role', verbose_name='role')), - ('workflow', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='steps', to='djangocms_moderation.workflow', verbose_name='workflow')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "is_required", + models.BooleanField(default=True, verbose_name="is mandatory"), + ), + ("order", models.PositiveIntegerField()), + ( + "role", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="djangocms_moderation.role", + verbose_name="role", + ), + ), + ( + "workflow", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="steps", + to="djangocms_moderation.workflow", + verbose_name="workflow", + ), + ), ], options={ - 'verbose_name': 'Step', - 'verbose_name_plural': 'Steps', - 'ordering': ('order',), - 'unique_together': {('role', 'workflow')}, + "verbose_name": "Step", + "verbose_name_plural": "Steps", + "ordering": ("order",), + "unique_together": {("role", "workflow")}, }, ), migrations.AddField( - model_name='moderationcollection', - name='workflow', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='moderation_collections', to='djangocms_moderation.workflow', verbose_name='workflow'), + model_name="moderationcollection", + name="workflow", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="moderation_collections", + to="djangocms_moderation.workflow", + verbose_name="workflow", + ), ), migrations.CreateModel( - name='RequestComment', + name="RequestComment", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('message', models.TextField(blank=True, verbose_name='message')), - ('date_created', models.DateTimeField(auto_now_add=True)), - ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='author')), - ('moderation_request', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='djangocms_moderation.moderationrequest')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("message", models.TextField(blank=True, verbose_name="message")), + ("date_created", models.DateTimeField(auto_now_add=True)), + ( + "author", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + verbose_name="author", + ), + ), + ( + "moderation_request", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="djangocms_moderation.moderationrequest", + ), + ), ], options={ - 'abstract': False, + "abstract": False, }, ), migrations.AlterField( - model_name='moderationcollection', - name='author', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='owner'), + model_name="moderationcollection", + name="author", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="+", + to=settings.AUTH_USER_MODEL, + verbose_name="owner", + ), ), migrations.AlterModelOptions( - name='moderationcollection', - options={'permissions': (('can_change_author', 'Can change collection author'),), 'verbose_name': 'collection'}, + name="moderationcollection", + options={ + "permissions": (("can_change_author", "Can change collection author"),), + "verbose_name": "collection", + }, ), migrations.CreateModel( - name='CollectionComment', + name="CollectionComment", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('message', models.TextField(blank=True, verbose_name='message')), - ('date_created', models.DateTimeField(auto_now_add=True)), - ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='author')), - ('collection', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='djangocms_moderation.moderationcollection')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("message", models.TextField(blank=True, verbose_name="message")), + ("date_created", models.DateTimeField(auto_now_add=True)), + ( + "author", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + verbose_name="author", + ), + ), + ( + "collection", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="djangocms_moderation.moderationcollection", + ), + ), ], options={ - 'abstract': False, + "abstract": False, }, ), migrations.CreateModel( - name='ConfirmationFormSubmission', + name="ConfirmationFormSubmission", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('data', models.TextField(blank=True, editable=False)), - ('submitted_at', models.DateTimeField(auto_now_add=True)), - ('by_user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='by user')), - ('confirmation_page', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='djangocms_moderation.confirmationpage', verbose_name='confirmation page')), - ('for_step', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to='djangocms_moderation.workflowstep', verbose_name='for step')), - ('moderation_request', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='form_submissions', to='djangocms_moderation.moderationrequest', verbose_name='moderation request')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("data", models.TextField(blank=True, editable=False)), + ("submitted_at", models.DateTimeField(auto_now_add=True)), + ( + "by_user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="+", + to=settings.AUTH_USER_MODEL, + verbose_name="by user", + ), + ), + ( + "confirmation_page", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="+", + to="djangocms_moderation.confirmationpage", + verbose_name="confirmation page", + ), + ), + ( + "for_step", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="+", + to="djangocms_moderation.workflowstep", + verbose_name="for step", + ), + ), + ( + "moderation_request", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="form_submissions", + to="djangocms_moderation.moderationrequest", + verbose_name="moderation request", + ), + ), ], options={ - 'verbose_name': 'Confirmation Form Submission', - 'verbose_name_plural': 'Confirmation Form Submissions', - 'unique_together': {('moderation_request', 'for_step')}, + "verbose_name": "Confirmation Form Submission", + "verbose_name_plural": "Confirmation Form Submissions", + "unique_together": {("moderation_request", "for_step")}, }, ), migrations.CreateModel( - name='ModerationRequestAction', + name="ModerationRequestAction", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('action', models.CharField(choices=[('resubmitted', 'Resubmitted'), ('start', 'Started'), ('rejected', 'Rejected'), ('approved', 'Approved'), ('cancelled', 'Cancelled'), ('finished', 'Finished')], max_length=30, verbose_name='status')), - ('message', models.TextField(blank=True, verbose_name='message')), - ('date_taken', models.DateTimeField(auto_now_add=True, verbose_name='date taken')), - ('is_archived', models.BooleanField(default=False)), - ('by_user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='by user')), - ('step_approved', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='djangocms_moderation.workflowstep', verbose_name='step approved')), - ('to_role', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='+', to='djangocms_moderation.role', verbose_name='to role')), - ('to_user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='to user')), - ('moderation_request', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='actions', to='djangocms_moderation.moderationrequest', verbose_name='moderation_request')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "action", + models.CharField( + choices=[ + ("resubmitted", "Resubmitted"), + ("start", "Started"), + ("rejected", "Rejected"), + ("approved", "Approved"), + ("cancelled", "Cancelled"), + ("finished", "Finished"), + ], + max_length=30, + verbose_name="status", + ), + ), + ("message", models.TextField(blank=True, verbose_name="message")), + ( + "date_taken", + models.DateTimeField(auto_now_add=True, verbose_name="date taken"), + ), + ("is_archived", models.BooleanField(default=False)), + ( + "by_user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="+", + to=settings.AUTH_USER_MODEL, + verbose_name="by user", + ), + ), + ( + "step_approved", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="djangocms_moderation.workflowstep", + verbose_name="step approved", + ), + ), + ( + "to_role", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="+", + to="djangocms_moderation.role", + verbose_name="to role", + ), + ), + ( + "to_user", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + verbose_name="to user", + ), + ), + ( + "moderation_request", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="actions", + to="djangocms_moderation.moderationrequest", + verbose_name="moderation_request", + ), + ), ], options={ - 'verbose_name': 'Action', - 'verbose_name_plural': 'Actions', - 'ordering': ('date_taken',), + "verbose_name": "Action", + "verbose_name_plural": "Actions", + "ordering": ("date_taken",), }, ), migrations.AlterField( - model_name='moderationcollection', - name='author', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='moderator'), + model_name="moderationcollection", + name="author", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + verbose_name="moderator", + ), ), migrations.AlterModelOptions( - name='moderationcollection', - options={'permissions': (('can_change_author', 'Can change collection author'), ('cancel_moderationcollection', 'Can cancel collection')), 'verbose_name': 'collection'}, + name="moderationcollection", + options={ + "permissions": ( + ("can_change_author", "Can change collection author"), + ("cancel_moderationcollection", "Can cancel collection"), + ), + "verbose_name": "collection", + }, ), migrations.CreateModel( - name='ModerationRequestTreeNode', + name="ModerationRequestTreeNode", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('path', models.CharField(max_length=255, unique=True)), - ('depth', models.PositiveIntegerField()), - ('numchild', models.PositiveIntegerField(default=0)), - ('moderation_request', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='djangocms_moderation.moderationrequest', verbose_name='moderation_request')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("path", models.CharField(max_length=255, unique=True)), + ("depth", models.PositiveIntegerField()), + ("numchild", models.PositiveIntegerField(default=0)), + ( + "moderation_request", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="djangocms_moderation.moderationrequest", + verbose_name="moderation_request", + ), + ), ], options={ - 'ordering': ('id',), + "ordering": ("id",), }, ), ] diff --git a/djangocms_moderation/migrations/0003_auto_20180903_1206.py b/djangocms_moderation/migrations/0003_auto_20180903_1206.py index 8bbc691c..46e11fa2 100644 --- a/djangocms_moderation/migrations/0003_auto_20180903_1206.py +++ b/djangocms_moderation/migrations/0003_auto_20180903_1206.py @@ -22,7 +22,7 @@ class Migration(migrations.Migration): preserve_default=False, ), migrations.AlterUniqueTogether( - name="moderationrequest", unique_together=set([("collection", "version")]) + name="moderationrequest", unique_together={("collection", "version")} ), migrations.RemoveField(model_name="moderationrequest", name="content_type"), migrations.RemoveField(model_name="moderationrequest", name="object_id"), diff --git a/djangocms_moderation/migrations/0011_auto_20181008_1328.py b/djangocms_moderation/migrations/0011_auto_20181008_1328.py index 23829d74..cca7ac2b 100644 --- a/djangocms_moderation/migrations/0011_auto_20181008_1328.py +++ b/djangocms_moderation/migrations/0011_auto_20181008_1328.py @@ -1,6 +1,4 @@ # Generated by Django 1.11.13 on 2018-10-08 12:28 -from django.conf import settings -from django.db import migrations from django.db import migrations, models import django.db.models.deletion @@ -13,7 +11,7 @@ class Migration(migrations.Migration): # AlterUniqueTogether needs to happen before removing field migrations.AlterUniqueTogether( name="confirmationformsubmission", - unique_together=set([("moderation_request", "for_step")]), + unique_together={("moderation_request", "for_step")}, ), migrations.RemoveField(model_name="confirmationformsubmission", name="request"), migrations.RemoveField(model_name="moderationrequestaction", name="request"), diff --git a/djangocms_moderation/migrations/0014_auto_20190315_1723.py b/djangocms_moderation/migrations/0014_auto_20190315_1723.py index d420dc9c..6cc014da 100644 --- a/djangocms_moderation/migrations/0014_auto_20190315_1723.py +++ b/djangocms_moderation/migrations/0014_auto_20190315_1723.py @@ -11,6 +11,11 @@ class Migration(migrations.Migration): operations = [ migrations.AlterModelOptions( name='moderationcollection', - options={'permissions': (('can_change_author', 'Can change collection author'), ('cancel_moderationcollection', 'Can cancel collection')), 'verbose_name': 'collection'}, + options={'permissions': ( + ('can_change_author', 'Can change collection author'), + ('cancel_moderationcollection', 'Can cancel collection') + ), + 'verbose_name': 'collection' + }, ), ] diff --git a/djangocms_moderation/migrations/0016_moderationrequesttreenode.py b/djangocms_moderation/migrations/0016_moderationrequesttreenode.py index 592c7f89..6f37e42c 100644 --- a/djangocms_moderation/migrations/0016_moderationrequesttreenode.py +++ b/djangocms_moderation/migrations/0016_moderationrequesttreenode.py @@ -17,7 +17,11 @@ class Migration(migrations.Migration): ('path', models.CharField(max_length=255, unique=True)), ('depth', models.PositiveIntegerField()), ('numchild', models.PositiveIntegerField(default=0)), - ('moderation_request', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='djangocms_moderation.ModerationRequest', verbose_name='moderation_request')), + ('moderation_request', models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to='djangocms_moderation.ModerationRequest', + verbose_name='moderation_request') + ), # noqa: E124 ], options={ 'ordering': ('id',), diff --git a/djangocms_moderation/models.py b/djangocms_moderation/models.py index 1e92dccc..c143770f 100644 --- a/djangocms_moderation/models.py +++ b/djangocms_moderation/models.py @@ -22,6 +22,16 @@ from . import conf, constants, signals # isort:skip +try: + from djangocms_versioning.helpers import version_is_locked + + def version_is_unlocked_for_moderation(version, user): + return version_is_locked(version) is None +except ImportError: + def version_is_unlocked_for_moderation(version, user): + return version.created_by == user + + class ConfirmationPage(models.Model): CONTENT_TYPES = ( (constants.CONTENT_TYPE_PLAIN, _("Plain")), @@ -144,6 +154,7 @@ class Workflow(models.Model): class Meta: verbose_name = _("Workflow") verbose_name_plural = _("Workflows") + ordering = ("name",) def __str__(self): return self.name @@ -240,7 +251,7 @@ def __str__(self): @property def job_id(self): - return "{}".format(self.pk) + return f"{self.pk}" @property def author_name(self): @@ -381,8 +392,8 @@ def _add_nested_children(self, version, parent_node): for child_version in get_moderated_children_from_placeholder( placeholder, version.versionable.grouping_values(parent) ): - # Don't add the version if it's already part of the collection or another users item - if version.created_by == child_version.created_by: + # Don't add the version if it's already part of the collection or locked by another user + if version_is_unlocked_for_moderation(child_version, version.created_by): moderation_request, _added_items = self.add_version( child_version, parent=parent_node, include_children=True ) @@ -444,7 +455,7 @@ class Meta: ordering = ["id"] def __str__(self): - return "{} {}".format(self.pk, self.version_id) + return f"{self.pk} {self.version_id}" @cached_property def workflow(self): @@ -651,7 +662,7 @@ class Meta: verbose_name_plural = _("Actions") def __str__(self): - return "{} - {}".format(self.moderation_request_id, self.get_action_display()) + return f"{self.moderation_request_id} - {self.get_action_display()}" def get_by_user_name(self): if not self.to_user: @@ -686,7 +697,7 @@ def save(self, **kwargs): if next_step: self.to_role_id = next_step.role_id - super(ModerationRequestAction, self).save(**kwargs) + super().save(**kwargs) class AbstractComment(models.Model): @@ -743,7 +754,7 @@ class ConfirmationFormSubmission(models.Model): ) def __str__(self): - return "{} - {}".format(self.request_id, self.for_step) + return f"{self.request_id} - {self.for_step}" class Meta: verbose_name = _("Confirmation Form Submission") diff --git a/djangocms_moderation/static/djangocms_moderation/css/actions.css b/djangocms_moderation/static/djangocms_moderation/css/actions.css index e86c02d9..d1c2c989 100644 --- a/djangocms_moderation/static/djangocms_moderation/css/actions.css +++ b/djangocms_moderation/static/djangocms_moderation/css/actions.css @@ -33,9 +33,9 @@ a.btn.cms-moderation-action-btn span { display: inline-block; } span.svg-juxtaposed-font { - text-rendering: auto; - display: inline-block; - line-height: 1rem; - vertical-align: 20%; - margin-top: -2px !important; + text-rendering: auto; + display: inline-block; + line-height: 1rem; + vertical-align: 20%; + margin-top: -2px !important; } \ No newline at end of file diff --git a/djangocms_moderation/static/djangocms_moderation/js/burger.js b/djangocms_moderation/static/djangocms_moderation/js/burger.js index 85f6fa8d..3fbcdf9e 100644 --- a/djangocms_moderation/static/djangocms_moderation/js/burger.js +++ b/djangocms_moderation/static/djangocms_moderation/js/burger.js @@ -12,7 +12,7 @@ .on('click', function (e) { e.preventDefault(); - // action currently being targetted + // action currently being targeted let action = $(e.currentTarget); // get the form method being used? let formMethod = action.attr('class').indexOf('cms-form-get-method') === 1 ? 'POST' : 'GET'; diff --git a/djangocms_moderation/static/djangocms_moderation/js/libs/diffview.js b/djangocms_moderation/static/djangocms_moderation/js/libs/diffview.js index 65e2e411..999f8f6f 100644 --- a/djangocms_moderation/static/djangocms_moderation/js/libs/diffview.js +++ b/djangocms_moderation/static/djangocms_moderation/js/libs/diffview.js @@ -177,7 +177,7 @@ var diffview = { var botrows = []; for (var i = 0; i < rowcnt; i++) { - // jump ahead if we've alredy provided leading context or if this is the first range + // jump ahead if we've already provided leading context or if this is the first range if ( contextSize && opcodes.length > 1 && diff --git a/djangocms_moderation/templates/djangocms_moderation/base_confirmation_build.html b/djangocms_moderation/templates/djangocms_moderation/base_confirmation_build.html index 89b3eab2..cc55a8dd 100644 --- a/djangocms_moderation/templates/djangocms_moderation/base_confirmation_build.html +++ b/djangocms_moderation/templates/djangocms_moderation/base_confirmation_build.html @@ -16,7 +16,7 @@ {% block post_content %}{% endblock %} - + {% render_block "js" %} {% block extrajs %}{% endblock %} diff --git a/djangocms_moderation/utils.py b/djangocms_moderation/utils.py index 1f29c242..cc19722b 100644 --- a/djangocms_moderation/utils.py +++ b/djangocms_moderation/utils.py @@ -17,7 +17,7 @@ def get_absolute_url(location, site=None): scheme = "https" else: scheme = "http" - domain = "{}://{}".format(scheme, site.domain) + domain = f"{scheme}://{site.domain}" return urljoin(domain, location) diff --git a/djangocms_moderation/views.py b/djangocms_moderation/views.py index 03f899e3..bab4d071 100644 --- a/djangocms_moderation/views.py +++ b/djangocms_moderation/views.py @@ -6,7 +6,7 @@ from django.shortcuts import get_object_or_404, render from django.urls import reverse from django.utils.decorators import method_decorator -from django.utils.http import is_safe_url +from django.utils.http import url_has_allowed_host_and_scheme from django.utils.translation import gettext_lazy as _, ngettext from django.views.generic import FormView @@ -83,7 +83,7 @@ def _get_success_redirect(self): """ return_to_url = self.request.GET.get("return_to_url") if return_to_url: - url_is_safe = is_safe_url( + url_is_safe = url_has_allowed_host_and_scheme( url=return_to_url, allowed_hosts=self.request.get_host(), require_https=self.request.is_secure(), diff --git a/docs/comment.rst b/docs/comment.rst index 8bdc8c25..d9959034 100644 --- a/docs/comment.rst +++ b/docs/comment.rst @@ -2,7 +2,7 @@ Comment ================================================ -Comments may be added to various moderation entities: +Comments may be added to various moderation entities: * :ref:`moderation_collection` * :ref:`moderation_request` * :ref:`moderation_request_action` \ No newline at end of file diff --git a/docs/conf.py b/docs/conf.py index 1273cc40..6e6aa86e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Configuration file for the Sphinx documentation builder. # diff --git a/docs/lock.rst b/docs/lock.rst index 06e7a0b9..05553d0f 100644 --- a/docs/lock.rst +++ b/docs/lock.rst @@ -2,8 +2,8 @@ Moderation Review Lock ================================================ -As soon as a :ref:`moderation_collection` status becomes in review then its drafts are automatically locked, in the sense that their content can no longer be edited (not at all, not by anyone, not even the collection author). Also once a collection is in Review then content versions cannot be added to the collection. This means that once you’ve clicked “Submit for review”: +As soon as a :ref:`moderation_collection` status becomes in review then its drafts are automatically locked, in the sense that their content can no longer be edited (not at all, not by anyone, not even the collection author). Also once a collection is in Review then content versions cannot be added to the collection. This means that once you’ve clicked “Submit for review”: * Collection Lock: New drafts cannot be added to the :ref:`moderation_collection` * Version Lock: Drafts in the :ref:`moderation_collection` cannot be edited unless rejected - -Once a version is published the Moderation Version Lock is removed automatically. \ No newline at end of file + +Once a version is published the Moderation Version Lock is removed automatically. \ No newline at end of file diff --git a/docs/moderation_collection.rst b/docs/moderation_collection.rst index cdddf4fc..4a3cc12a 100644 --- a/docs/moderation_collection.rst +++ b/docs/moderation_collection.rst @@ -3,10 +3,10 @@ Moderation Collection ================================================ -A Moderation Collection is primarily intended as a way of being able to group draft content versions together for: -a) review and +A Moderation Collection is primarily intended as a way of being able to group draft content versions together for: +a) review and b) publishing - + The rules for adding items to a Collection, removing items from a Collection and the actions that can be taken on items the Collection may vary by :ref:`workflow`. Publishing is a `djangocms-versioning` feature, thus `djangocms-moderation` depends on and extends the functionality made available by the Versioning addon. @@ -16,7 +16,7 @@ Collections are stateful. The available states are: * In review * Archived * Cancelled - + Drafts can only be added to a Collection during the `Collecting` phase (see :ref:`lock`) Buttons @@ -76,7 +76,7 @@ A collection can also be flagged as cancelled. This is similar to Archived excep Bulk Actions ------------------------------------------------- -These will appear in the Collection’s action drop-down for each content-type registered with Moderation. +These will appear in the Collection’s action drop-down for each content-type registered with Moderation. Remove from collection ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -92,4 +92,4 @@ Flags a draft as being in need of further editing Submit for review ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Useful for items that have been flagged for rework - resubmits them for review, sending out notifications again. +Useful for items that have been flagged for rework - resubmits them for review, sending out notifications again. diff --git a/docs/overview.rst b/docs/overview.rst index 57207d65..806bfec9 100644 --- a/docs/overview.rst +++ b/docs/overview.rst @@ -5,7 +5,7 @@ Overview Moderation provides an approval workflow mechanism for organisations who need to ensure that content is approved before it is published. It is designed to extend and compliment the Versioning addon and has that as a dependency. -The general idea is that a draft version can be submitted for moderation. This involves adding that draft to a :ref:`moderation_collection`, which can be thought of as a chapter, edition or batch of content that aims to all be published simultaneously. Various drafts can be added to the same :ref:`moderation_collection`. +The general idea is that a draft version can be submitted for moderation. This involves adding that draft to a :ref:`moderation_collection`, which can be thought of as a chapter, edition or batch of content that aims to all be published simultaneously. Various drafts can be added to the same :ref:`moderation_collection`. Drafts within the :ref:`moderation_collection` can then be approved rejected by various parties according to :ref:`role`s defined within the :ref:`workflow` assigned to the :ref:`moderation_collection`. diff --git a/setup.cfg b/setup.cfg index 56767f48..56cdaaf2 100644 --- a/setup.cfg +++ b/setup.cfg @@ -46,3 +46,6 @@ exclude_lines = raise NotImplementedError if 0: if __name__ == .__main__.: + +[codespell] +ignore-words-list = alpha-numeric diff --git a/setup.py b/setup.py index 86e47922..8259e8e9 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ INSTALL_REQUIREMENTS = [ - "Django>=2.2", + "Django>=3.2", "django-cms", "django-sekizai>=0.7", "django-admin-sortable2>=0.6.4", @@ -22,7 +22,18 @@ description=djangocms_moderation.__doc__, long_description=open("README.rst").read(), classifiers=[ + 'Development Status :: 5 - Production/Stable', + "Programming Language :: Python", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", "Framework :: Django", + "Framework :: Django :: 3.2", + "Framework :: Django :: 4.2", + "Framework :: Django :: 5.0", + "Framework :: Django CMS", + "Framework :: Django CMS :: 4.0", + "Framework :: Django CMS :: 4.1", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", diff --git a/tests/requirements/dj22_cms40.txt b/tests/requirements/dj22_cms40.txt deleted file mode 100644 index 4e51203c..00000000 --- a/tests/requirements/dj22_cms40.txt +++ /dev/null @@ -1,6 +0,0 @@ --r ./requirements_base.txt - -Django>=2.2,<3.0 - -django-admin-sortable2<1.0 -django_polymorphic==2.0.3 diff --git a/tests/requirements/dj32_cms40.txt b/tests/requirements/dj32_cms40.txt index 6dd211b1..55f6c506 100644 --- a/tests/requirements/dj32_cms40.txt +++ b/tests/requirements/dj32_cms40.txt @@ -4,3 +4,9 @@ Django>=3.2,<4.0 django-admin-sortable2<2 django_polymorphic + +https://github.com/django-cms/django-cms/tarball/release/4.0.1.x#egg=django-cms +https://github.com/django-cms/djangocms-text-ckeditor/tarball/support/4.0.x#egg=djangocms-text-ckeditor +https://github.com/django-cms/djangocms-versioning/tarball/1.2.2#egg=djangocms-versioning +https://github.com/FidelityInternational/djangocms-version-locking/tarball/1.2.0#egg=djangocms-version-locking +https://github.com/django-cms/djangocms-alias/tarball/support/django-cms-4.0.x#egg=djangocms-alias diff --git a/tests/requirements/dj42_cms40.txt b/tests/requirements/dj42_cms40.txt new file mode 100644 index 00000000..deb73e0f --- /dev/null +++ b/tests/requirements/dj42_cms40.txt @@ -0,0 +1,11 @@ +-r ./requirements_base.txt + +Django>=4.2,<5.0 + +django-admin-sortable2 +django_polymorphic + +https://github.com/django-cms/django-cms/tarball/release/4.0.1.x#egg=django-cms +https://github.com/django-cms/djangocms-versioning/tarball/support/django-cms-4.0.x#egg=djangocms-versioning +https://github.com/joshyu/djangocms-version-locking/tarball/feat/django-42-compatible#egg=djangocms-version-locking +https://github.com/django-cms/djangocms-alias/tarball/support/django-cms-4.0.x#egg=djangocms-alias diff --git a/tests/requirements/dj42_cms41.txt b/tests/requirements/dj42_cms41.txt new file mode 100644 index 00000000..d2ef85cc --- /dev/null +++ b/tests/requirements/dj42_cms41.txt @@ -0,0 +1,7 @@ +-r ./requirements_base.txt + +Django>=4.2,<5.0 +django-cms>=4.1,<4.2 + +djangocms-versioning>=2.0.0 +djangocms-alias>=2.0.0 diff --git a/tests/requirements/dj50_cms41.txt b/tests/requirements/dj50_cms41.txt new file mode 100644 index 00000000..53ea1d28 --- /dev/null +++ b/tests/requirements/dj50_cms41.txt @@ -0,0 +1,7 @@ +-r ./requirements_base.txt + +Django>=5.0,<5.1 +django-cms>=4.1,<4.2 + +djangocms-versioning>=2.0.0 +djangocms-alias>=2.0.0 diff --git a/tests/requirements/requirements_base.txt b/tests/requirements/requirements_base.txt index 0c3d55fa..3d7e24f8 100644 --- a/tests/requirements/requirements_base.txt +++ b/tests/requirements/requirements_base.txt @@ -1,7 +1,6 @@ -aldryn-forms cachetools coverage -django-classy-tags +django-classy-tags>=0.7.2 django-filer django-sekizai django-simple-captcha @@ -12,10 +11,4 @@ isort mock pyflakes>=2.1.1 python-dateutil>=2.4 - -# Unreleased django-cms 4.0 compatible packages -https://github.com/django-cms/django-cms/tarball/release/4.0.1.x#egg=django-cms -https://github.com/django-cms/djangocms-text-ckeditor/tarball/support/4.0.x#egg=djangocms-text-ckeditor -https://github.com/django-cms/djangocms-versioning/tarball/1.2.2#egg=djangocms-versioning -https://github.com/FidelityInternational/djangocms-version-locking/tarball/1.2.0#egg=djangocms-version-locking -https://github.com/django-cms/djangocms-alias/tarball/support/django-cms-4.0.x#egg=djangocms-alias +djangocms-text-ckeditor diff --git a/tests/settings.py b/tests/settings.py index c016c9f2..067e6670 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -1,12 +1,13 @@ +from cms import __version__ as cms_version + + HELPER_SETTINGS = { "SECRET_KEY": "moderationtestsuitekey", "INSTALLED_APPS": [ "tests.utils.app_1", "tests.utils.app_2", "djangocms_versioning", - "djangocms_version_locking", # the following 4 apps are related - "aldryn_forms", "filer", "easy_thumbnails", "captcha", @@ -26,11 +27,16 @@ "djangocms_version_locking": None, "filer": None, "djangocms_moderation": None, - "aldryn_forms": None, + "djangocms_text_ckeditor": None, }, "DEFAULT_AUTO_FIELD": "django.db.models.AutoField", + "DJANGOCMS_VERSIONING_LOCK_VERSIONS": True, + "CMS_CONFIRM_VERSION4": True, } +if cms_version < "4.1.0": + HELPER_SETTINGS["INSTALLED_APPS"].append("djangocms_version_locking") + def run(): from djangocms_helper import runner diff --git a/tests/test_admin_actions.py b/tests/test_admin_actions.py index b0c3fbd8..c026b84d 100644 --- a/tests/test_admin_actions.py +++ b/tests/test_admin_actions.py @@ -1,5 +1,5 @@ -import mock import unittest +from unittest import mock from django.contrib.admin.helpers import ACTION_CHECKBOX_NAME from django.contrib.auth.models import Group @@ -66,7 +66,7 @@ def setUp(self): # Set up the url data self.url = reverse("admin:djangocms_moderation_moderationrequesttreenode_changelist") - self.url += "?moderation_request__collection__id={}".format(self.collection.pk) + self.url += f"?moderation_request__collection__id={self.collection.pk}" # Asserts to check data set up is ok. Ideally wouldn't need them, but # the set up is so complex that it's safer to have them. @@ -409,7 +409,7 @@ def setUp(self): # Set up the url data self.url = reverse("admin:djangocms_moderation_moderationrequesttreenode_changelist") - self.url += "?moderation_request__collection__id={}".format(self.collection.pk) + self.url += f"?moderation_request__collection__id={self.collection.pk}" # Asserts to check data set up is ok. Ideally wouldn't need them, but # the set up is so complex that it's safer to have them. @@ -586,7 +586,7 @@ def setUp(self): # Set up the url data self.url = reverse("admin:djangocms_moderation_moderationrequesttreenode_changelist") - self.url += "?moderation_request__collection__id={}".format(self.collection.pk) + self.url += f"?moderation_request__collection__id={self.collection.pk}" # Asserts to check data set up is ok. Ideally wouldn't need them, but # the set up is so complex that it's safer to have them. @@ -813,7 +813,7 @@ def setUp(self): self.client.force_login(self.user) self.url = reverse("admin:djangocms_moderation_moderationrequesttreenode_changelist") - self.url += "?moderation_request__collection__id={}".format(self.collection.pk) + self.url += f"?moderation_request__collection__id={self.collection.pk}" # Asserts to check data set up is ok. Ideally wouldn't need them, but # the set up is so complex that it's safer to have them. @@ -963,7 +963,7 @@ def setUp(self): id=6, moderation_request=self.moderation_request2) self.url = reverse("admin:djangocms_moderation_moderationrequesttreenode_changelist") - self.url += "?moderation_request__collection__id={}".format(self.collection.pk) + self.url += f"?moderation_request__collection__id={self.collection.pk}" @mock.patch.object(ModerationRequestTreeAdmin, "has_delete_permission", mock.Mock(return_value=True)) def test_delete_selected_action_cannot_be_accessed_if_not_collection_author(self): @@ -1112,7 +1112,7 @@ def setUp(self): # Generate url and POST data self.url = reverse("admin:djangocms_moderation_moderationrequesttreenode_changelist") - self.url += "?moderation_request__collection__id={}".format(self.collection.pk) + self.url += f"?moderation_request__collection__id={self.collection.pk}" self.data = get_url_data(self, "delete_selected") def tearDown(self): diff --git a/tests/test_app_registration.py b/tests/test_app_registration.py index 1b2423dc..eb28f94a 100644 --- a/tests/test_app_registration.py +++ b/tests/test_app_registration.py @@ -1,7 +1,7 @@ try: from unittest.mock import Mock except ImportError: - from mock import Mock + from unittest.mock import Mock from unittest import TestCase, skip from unittest.mock import patch diff --git a/tests/test_cms_toolbars.py b/tests/test_cms_toolbars.py index 62727ebe..7b539ae8 100644 --- a/tests/test_cms_toolbars.py +++ b/tests/test_cms_toolbars.py @@ -1,4 +1,4 @@ -import mock +from unittest import mock from django.contrib.auth.models import Permission, User from django.test.client import RequestFactory @@ -8,6 +8,7 @@ from cms.test_utils.testcases import CMSTestCase from cms.toolbar.toolbar import CMSToolbar +from djangocms_versioning import __version__ as versioning_version from djangocms_versioning.test_utils.factories import PageVersionFactory from djangocms_moderation import constants @@ -72,7 +73,8 @@ def func(button): return button.name == callable_or_name for button_list in toolbar.get_right_items(): - found = found + [button for button in button_list.buttons if func(button)] + if hasattr(button_list, "buttons"): + found = found + [button for button in button_list.buttons if func(button)] return found def _button_exists(self, callable_or_name, toolbar): @@ -139,7 +141,7 @@ def test_page_in_collection_collection(self): self.assertTrue( self._button_exists( - 'In collection "{} ({})"'.format(collection.name, collection.id), + f'In collection "{collection.name} ({collection.id})"', toolbar.toolbar, ) ) @@ -157,7 +159,7 @@ def test_page_in_collection_moderating(self): self.assertTrue( self._button_exists( - 'In moderation "{} ({})"'.format(collection.name, collection.id), + f'In moderation "{collection.name} ({collection.id})"', toolbar.toolbar, ) ) @@ -187,16 +189,19 @@ def test_add_edit_button_with_version_lock(self): toolbar.populate() toolbar.post_template_populate() - self.assertTrue( - self._button_exists( - lambda button: button.name.endswith("Edit"), toolbar.toolbar + if versioning_version < "2": + self.assertTrue( + self._button_exists(lambda button: button.name.endswith("Edit"), toolbar.toolbar) + ) + # Edit button should not be clickable + button = self._find_buttons(lambda button: button.name.endswith("Edit"), toolbar.toolbar) + self.assertTrue(button[0].disabled) + else: + self.assertFalse( + self._button_exists( + lambda button: button.name.endswith("Edit"), toolbar.toolbar + ) ) - ) - # Edit button should not be clickable - button = self._find_buttons( - lambda button: button.name.endswith("Edit"), toolbar.toolbar - ) - self.assertTrue(button[0].disabled) def test_add_edit_button(self): user = self.get_superuser() @@ -222,9 +227,12 @@ def test_add_edit_button(self): toolbar.populate() toolbar.post_template_populate() - self.assertTrue(self._button_exists("Edit", toolbar.toolbar)) - button = self._find_buttons("Edit", toolbar.toolbar) - self.assertTrue(button[0].disabled) + if versioning_version < "2": + self.assertTrue(self._button_exists("Edit", toolbar.toolbar)) + button = self._find_buttons("Edit", toolbar.toolbar) + self.assertTrue(button[0].disabled) + else: + self.assertFalse(self._button_exists("Edit", toolbar.toolbar)) def test_add_edit_button_without_toolbar_object(self): toolbar = self._get_toolbar(None) diff --git a/tests/test_forms.py b/tests/test_forms.py index 571e591f..4439508a 100644 --- a/tests/test_forms.py +++ b/tests/test_forms.py @@ -1,4 +1,4 @@ -import mock +from unittest import mock from django.contrib.auth.models import User from django.forms import HiddenInput @@ -248,7 +248,7 @@ def test_collection_choice_should_be_limited_to_current_user_and_collecting_stat form = CollectionItemsForm(data=data, user=user) self.assertEqual( - form.is_valid(), fixture[1], "{} failed".format(fixture[0]) + form.is_valid(), fixture[1], f"{fixture[0]} failed" ) if not form.is_valid(): self.assertIn("collection", form.errors) diff --git a/tests/test_handlers.py b/tests/test_handlers.py deleted file mode 100644 index de31e72a..00000000 --- a/tests/test_handlers.py +++ /dev/null @@ -1,44 +0,0 @@ -from unittest import skip - -from djangocms_moderation.contrib.moderation_forms.cms_plugins import ( - ModerationFormPlugin, -) -from djangocms_moderation.handlers import moderation_confirmation_form_submission -from djangocms_moderation.models import ConfirmationFormSubmission, ConfirmationPage - -from .utils.base import BaseTestCase - - -@skip("Confirmation page feature doesn't support 1.0.x yet") -class ModerationConfirmationFormSubmissionTest(BaseTestCase): - def setUp(self): - self.cp = ConfirmationPage.objects.create(name="Checklist Form") - self.role1.confirmation_page = self.cp - self.role1.save() - - def test_throws_exception_when_form_data_is_invalid(self): - with self.assertRaises(ValueError) as context: - moderation_confirmation_form_submission( - sender=ModerationFormPlugin, - page=self.pg1_version, - language="en", - user=self.user, - form_data=[{"label": "Question 1", "answer": "Yes"}], - ) - self.assertTrue( - "Each field dict should contain label and value keys." - in str(context.exception) - ) - - def test_creates_new_form_submission_when_form_data_is_valid(self): - moderation_confirmation_form_submission( - sender=ModerationFormPlugin, - page=self.pg1_version, - language="en", - user=self.user, - form_data=[{"label": "Question 1", "value": "Yes"}], - ) - result = ConfirmationFormSubmission.objects.filter( - request=self.moderation_request1 - ) - self.assertEqual(result.count(), 1) diff --git a/tests/test_helpers.py b/tests/test_helpers.py index ebdacf10..40cc4777 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -1,6 +1,5 @@ import json -import mock -from unittest import skip +from unittest import mock, skip from django.template.defaultfilters import truncatechars from django.urls import reverse @@ -115,7 +114,7 @@ def setUp(self): def test_get_moderation_button_title_and_url_when_collection(self): title, url = get_moderation_button_title_and_url(self.mr) - self.assertEqual(title, 'In collection "C1 ({})"'.format(self.collection.id)) + self.assertEqual(title, f'In collection "C1 ({self.collection.id})"') self.assertEqual(url, self.expected_url) def test_get_moderation_button_title_and_url_when_in_review(self): @@ -123,7 +122,7 @@ def test_get_moderation_button_title_and_url_when_in_review(self): self.collection.save() title, url = get_moderation_button_title_and_url(self.mr) - self.assertEqual(title, 'In moderation "C1 ({})"'.format(self.collection.id)) + self.assertEqual(title, f'In moderation "C1 ({self.collection.id})"') self.assertEqual(url, self.expected_url) def test_get_moderation_button_truncated_title_and_url(self): @@ -135,7 +134,7 @@ def test_get_moderation_button_truncated_title_and_url(self): self.assertEqual( title, # By default, truncate will shorten the name - 'In collection "{} ({})"'.format(expected_title, self.collection.id), + f'In collection "{expected_title} ({self.collection.id})"', ) with mock.patch("djangocms_moderation.helpers.COLLECTION_NAME_LENGTH_LIMIT", 3): title, url = get_moderation_button_title_and_url(self.mr) @@ -143,7 +142,7 @@ def test_get_moderation_button_truncated_title_and_url(self): self.assertEqual( title, # As the limit is only 3, the truncate will produce `...` - 'In collection "{} ({})"'.format(expected_title, self.collection.id), + f'In collection "{expected_title} ({self.collection.id})"', ) with mock.patch( diff --git a/tests/test_integration.py b/tests/test_integration.py index c0d1b01f..9110714e 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -1,4 +1,4 @@ -import mock +from unittest import mock from djangocms_versioning.test_utils.factories import PageVersionFactory diff --git a/tests/test_models.py b/tests/test_models.py index b00b6bfa..53d48d5d 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -1,5 +1,5 @@ import json -from mock import patch +from unittest.mock import patch from django.contrib.auth.models import Permission, User from django.core.exceptions import ValidationError @@ -390,7 +390,7 @@ def test_compliance_number_sequential_number_with_identifier_prefix_backend(self request.refresh_from_db() self.assertIsNone(request.compliance_number) - expected = "SSO{}".format(request.pk) + expected = f"SSO{request.pk}" request.set_compliance_number() request.refresh_from_db() self.assertEqual(request.compliance_number, expected) diff --git a/tests/test_moderation_flows.py b/tests/test_moderation_flows.py index 0531e02e..41fe54d1 100644 --- a/tests/test_moderation_flows.py +++ b/tests/test_moderation_flows.py @@ -53,7 +53,7 @@ def _process_moderation_request(self, user, action, message="Test message"): self.client.force_login(user) response = self.client.post( get_admin_url( - name="cms_moderation_{}_request".format(action), + name=f"cms_moderation_{action}_request", language="en", args=(self.page.pk, "en"), ), diff --git a/tests/test_monkeypatch.py b/tests/test_monkeypatch.py index 6f5dad2f..3817b0b6 100644 --- a/tests/test_monkeypatch.py +++ b/tests/test_monkeypatch.py @@ -1,4 +1,4 @@ -import mock +from unittest import mock from django.contrib import admin from django.urls import reverse @@ -6,7 +6,7 @@ from cms.models import PageContent from cms.models.fields import PlaceholderRelationField -from djangocms_versioning import versionables +from djangocms_versioning import __version__ as versioning_version, versionables from djangocms_versioning.admin import VersionAdmin from djangocms_versioning.constants import DRAFT, PUBLISHED from djangocms_versioning.test_utils.factories import ( @@ -14,6 +14,7 @@ PlaceholderFactory, ) +from djangocms_moderation.helpers import is_obj_version_unlocked from djangocms_moderation.monkeypatch import _is_placeholder_review_unlocked from .utils.base import BaseTestCase, MockRequest @@ -41,8 +42,13 @@ def test_get_edit_link(self, mock_is_obj_review_locked): ) # We test that moderation check is called when getting an edit link self.assertTrue(mock_is_obj_review_locked.called) - # Edit link is inactive as `mock_is_obj_review_locked` is True - self.assertIn("inactive", edit_link) + if versioning_version < "2": + # Edit link is inactive as `mock_is_obj_review_locked` is True + self.assertIn("inactive", edit_link) + else: + # Edit link is removed as `mock_is_obj_review_locked` is True + self.assertEqual("", edit_link) + # self.assertIn("inactive", edit_link) @mock.patch("djangocms_moderation.monkeypatch.is_registered_for_moderation") @mock.patch("djangocms_moderation.monkeypatch.is_obj_review_locked") @@ -76,19 +82,28 @@ def test_get_archive_link(self, _mock): ), args=(version.pk,), ) - _mock.return_value = True - archive_link = self.version_admin._get_archive_link(version, self.mock_request) + if versioning_version != "2.0.0": + archive_link = self.version_admin._get_archive_link(version, self.mock_request) + else: + # Bug in djangocms-verisoning 2.0.0: _get_archive_link does not call check_archive + # So we do it by hand + version.check_archive.as_bool(self.mock_request.user) + archive_link = "" # We test that moderation check is called when getting an edit link self.assertEqual(1, _mock.call_count) - # Edit link is inactive as `is_obj_review_locked` is True - self.assertIn("inactive", archive_link) - self.assertNotIn(archive_url, archive_link) + if versioning_version < "2": + # Edit link is inactive as `mock_is_obj_review_locked` is True + self.assertIn("inactive", archive_link) + else: + # Edit link is unavailable + self.assertEqual("", archive_link) _mock.return_value = None archive_link = self.version_admin._get_archive_link(version, self.mock_request) # We test that moderation check is called when getting the link - self.assertEqual(2, _mock.call_count) + if versioning_version != "2.0.0": + self.assertEqual(2, _mock.call_count) # Archive link is active there as `get_active_moderation_request` is None self.assertNotIn("inactive", archive_link) self.assertIn(archive_url, archive_link) @@ -120,6 +135,7 @@ def test_get_moderation_link(self): draft_version = PageVersionFactory(created_by=self.user3) # Request has self.user, so the moderation link won't be displayed. # This is version lock in place + self.assertFalse(is_obj_version_unlocked(draft_version.content, self.user)) link = self.version_admin._get_moderation_link(draft_version, self.mock_request) self.assertEqual("", link) diff --git a/tests/test_views.py b/tests/test_views.py index b7ecee19..b6e86c6c 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -1,4 +1,4 @@ -import mock +from unittest import mock from django.contrib.admin.widgets import RelatedFieldWidgetWrapper from django.contrib.messages import get_messages @@ -30,6 +30,12 @@ ) +try: + from djangocms_versioning.helpers import remove_version_lock, version_is_locked +except ImportError: + from djangocms_version_locking.helpers import remove_version_lock, version_is_locked + + class CollectionItemsViewAddingRequestsTestCase(CMSTestCase): def test_no_eligible_items_to_add_to_collection(self): """ @@ -215,6 +221,9 @@ def test_add_pages_moderated_children_to_collection(self): poll2_version = PollVersionFactory(created_by=user, content__language=language) PollPluginFactory(placeholder=placeholder, poll=poll1_version.content.poll) PollPluginFactory(placeholder=placeholder, poll=poll2_version.content.poll) + remove_version_lock(page_version) + remove_version_lock(poll1_version) + remove_version_lock(poll2_version) admin_endpoint = get_admin_url( name="cms_moderation_items_to_collection", language="en", args=() @@ -248,13 +257,14 @@ def test_add_pages_moderated_children_to_collection(self): mr1 = ModerationRequest.objects.filter( collection=collection, version=poll1_version ) + mr2 = ModerationRequest.objects.filter( + collection=collection, version=poll2_version + ) + self.assertEqual(mr1.count(), 1) self.assertEqual( ModerationRequestTreeNode.objects.filter(moderation_request=mr1.first()).count(), 1 ) - mr2 = ModerationRequest.objects.filter( - collection=collection, version=poll2_version - ) self.assertEqual(mr2.count(), 1) self.assertEqual( ModerationRequestTreeNode.objects.filter(moderation_request=mr2.first()).count(), 1 @@ -276,6 +286,7 @@ def test_add_pages_moderated_duplicated_children_to_collection(self): poll_version = PollVersionFactory(created_by=user, content__language=language) PollPluginFactory(placeholder=placeholder, poll=poll_version.content.poll) PollPluginFactory(placeholder=placeholder, poll=poll_version.content.poll) + remove_version_lock(poll_version) admin_endpoint = get_admin_url( name="cms_moderation_items_to_collection", language="en", args=() @@ -303,7 +314,8 @@ def test_add_pages_moderated_duplicated_children_to_collection(self): ).count(), 1, ) - self.assertEqual(stored_collection.filter(version=poll_version).count(), 1) + mr = stored_collection.filter(version=poll_version) + self.assertEqual(mr.count(), 1) self.assertEqual( ModerationRequestTreeNode.objects.filter( moderation_request=stored_collection.get(version=poll_version) @@ -329,7 +341,9 @@ def test_add_pages_moderated_duplicated_children_to_collection_for_author_only( poll2_version = PollVersionFactory(created_by=user2, content__language=language) PollPluginFactory(placeholder=placeholder, poll=poll1_version.content.poll) PollPluginFactory(placeholder=placeholder, poll=poll2_version.content.poll) - + remove_version_lock(page_version) + remove_version_lock(poll1_version) + # poll2_version remains is locked, so will not be added to collection admin_endpoint = get_admin_url( name="cms_moderation_items_to_collection", language="en", args=() ) @@ -350,6 +364,7 @@ def test_add_pages_moderated_duplicated_children_to_collection_for_author_only( self.assertEqual(302, response.status_code) self.assertEqual(admin_endpoint, response.url) self.assertEqual(stored_collection.count(), 2) + self.assertEqual(stored_collection.filter(version=page_version).count(), 1) self.assertEqual( ModerationRequestTreeNode.objects.filter( @@ -357,13 +372,15 @@ def test_add_pages_moderated_duplicated_children_to_collection_for_author_only( ).count(), 1, ) - self.assertEqual(stored_collection.filter(version=poll1_version).count(), 1) + mr1 = stored_collection.filter(version=poll1_version) + self.assertEqual(mr1.count(), 1) self.assertEqual( ModerationRequestTreeNode.objects.filter( moderation_request=stored_collection.get(version=poll1_version) ).count(), 1, ) + self.assertEqual(stored_collection.filter(version=poll2_version).count(), 0) self.assertEqual( ModerationRequestTreeNode.objects.filter( @@ -384,10 +401,12 @@ def test_add_pages_moderated_traversed_children_to_collection(self): language = page_version.content.language # Populate page placeholder = PlaceholderFactory(source=page_version.content) + remove_version_lock(page_version) poll_version = PollVersionFactory(created_by=user, content__language=language) poll_plugin = PollPluginFactory( placeholder=placeholder, poll=poll_version.content.poll ) + remove_version_lock(poll_version) # Populate page poll child layer 1 poll_child_1_version = PollVersionFactory( created_by=user, content__language=language @@ -395,6 +414,7 @@ def test_add_pages_moderated_traversed_children_to_collection(self): poll_child_1_plugin = PollPluginFactory( placeholder=poll_plugin.placeholder, poll=poll_child_1_version.content.poll ) + remove_version_lock(poll_child_1_version) # Populate page poll child layer 2 poll_child_2_version = PollVersionFactory( created_by=user, content__language=language @@ -403,6 +423,7 @@ def test_add_pages_moderated_traversed_children_to_collection(self): placeholder=poll_child_1_plugin.placeholder, poll=poll_child_2_version.content.poll, ) + remove_version_lock(poll_child_2_version) admin_endpoint = get_admin_url( name="cms_moderation_items_to_collection", language="en", args=() @@ -424,6 +445,7 @@ def test_add_pages_moderated_traversed_children_to_collection(self): self.assertEqual(302, response.status_code) self.assertEqual(admin_endpoint, response.url) self.assertEqual(stored_collection.count(), 4) + self.assertEqual(stored_collection.filter(version=page_version).count(), 1) self.assertEqual( ModerationRequestTreeNode.objects.filter( @@ -431,7 +453,9 @@ def test_add_pages_moderated_traversed_children_to_collection(self): ).count(), 1, ) - self.assertEqual(stored_collection.filter(version=poll_version).count(), 1) + + mr = stored_collection.filter(version=poll_version) + self.assertEqual(mr.count(), 1) self.assertEqual( ModerationRequestTreeNode.objects.filter( moderation_request=stored_collection.get(version=poll_version) @@ -554,7 +578,7 @@ def test_collection_with_redirect_url_query_redirect_sanitisation(self): """ Reflected XSS Protection by ensuring that harmful characters are encoded - When a collection is succesful a redirect occurs back to the grouper in versioning, + When a collection is successful a redirect occurs back to the grouper in versioning, this functionality should continue to function even when sanitised! """ user = self.get_superuser() @@ -598,6 +622,90 @@ def test_collection_with_redirect_url_query_redirect_sanitisation(self): ) +class ModerationCollectionTestCase(CMSTestCase): + def setUp(self): + self.language = "en" + self.user_1 = self.get_superuser() + self.user_2 = UserFactory() + self.collection = ModerationCollectionFactory(author=self.user_1) + self.page_version = PageVersionFactory(created_by=self.user_1) + self.placeholder = PlaceholderFactory(source=self.page_version.content) + self.poll_version = PollVersionFactory(created_by=self.user_2, content__language=self.language) + + def test_add_version_with_locked_plugins(self): + """ + Locked plugins should not be allowed to be added to a collection + """ + PollPluginFactory(placeholder=self.placeholder, poll=self.poll_version.content.poll) + + admin_endpoint = get_admin_url( + name="cms_moderation_items_to_collection", language="en", args=() + ) + + url = add_url_parameters( + admin_endpoint, + return_to_url="http://example.com", + version_ids=self.page_version.pk, + collection_id=self.collection.pk, + ) + + # Poll should be locked by default + poll_is_locked = version_is_locked(self.poll_version) + self.assertTrue(poll_is_locked) + + with self.login_user_context(self.user_1): + self.client.post( + path=url, + data={"collection": self.collection.pk, "versions": [self.page_version.pk, self.poll_version.pk]}, + follow=False, + ) + + # Get all moderation request objects for the collection + moderation_requests = ModerationRequest.objects.filter(collection=self.collection) + + self.assertEqual(moderation_requests.count(), 1) + self.assertTrue(moderation_requests.filter(version=self.page_version).exists()) + self.assertFalse(moderation_requests.filter(version=self.poll_version).exists()) + + def test_add_version_with_unlocked_child(self): + """ + Only plugins that are unlocked should be added to collection + """ + + PollPluginFactory(placeholder=self.placeholder, poll=self.poll_version.content.poll) + + admin_endpoint = get_admin_url( + name="cms_moderation_items_to_collection", language="en", args=() + ) + + url = add_url_parameters( + admin_endpoint, + return_to_url="http://example.com", + version_ids=self.page_version.pk, + collection_id=self.collection.pk, + ) + + # Poll should be locked by default + poll_is_locked = version_is_locked(self.poll_version) + self.assertTrue(poll_is_locked) + + # Unlock the poll version + remove_version_lock(self.poll_version) + + with self.login_user_context(self.user_1): + self.client.post( + path=url, + data={"collection": self.collection.pk, "versions": [self.page_version.pk, self.poll_version.pk]}, + follow=False, + ) + + # Get all moderation request objects for the collection + moderation_requests = ModerationRequest.objects.filter(collection=self.collection) + self.assertEqual(moderation_requests.count(), 2) + self.assertTrue(moderation_requests.filter(version=self.page_version).exists()) + self.assertTrue(moderation_requests.filter(version=self.poll_version).exists()) + + class CollectionItemsViewTest(CMSTestCase): def setUp(self): self.client.force_login(self.get_superuser()) @@ -629,7 +737,7 @@ def test_initial_form_values_when_collection_id_passed(self): pg_version = PageVersionFactory() poll_version = PollVersionFactory() self.url += "?collection_id=" + str(collection.pk) - self.url += "&version_ids={},{}".format(pg_version.pk, poll_version.pk) + self.url += f"&version_ids={pg_version.pk},{poll_version.pk}" response = self.client.get(self.url) @@ -648,7 +756,7 @@ def test_initial_form_values_when_collection_id_passed(self): def test_initial_form_values_when_collection_id_not_passed(self): pg_version = PageVersionFactory() poll_version = PollVersionFactory() - self.url += "?version_ids={},{}".format(pg_version.pk, poll_version.pk) + self.url += f"?version_ids={pg_version.pk},{poll_version.pk}" response = self.client.get(self.url) @@ -704,11 +812,13 @@ def test_tree_nodes_are_created(self): PollPluginFactory( placeholder=placeholder, poll=poll_version.content.poll ) + remove_version_lock(poll_version) # Populate poll child poll_child_version = PollVersionFactory( created_by=user, content__language=language ) + remove_version_lock(poll_child_version) PollPluginFactory( placeholder=poll_version.content.placeholder, poll=poll_child_version.content.poll, @@ -718,6 +828,7 @@ def test_tree_nodes_are_created(self): poll_grandchild_version = PollVersionFactory( created_by=user, content__language=language ) + remove_version_lock(poll_grandchild_version) PollPluginFactory( placeholder=poll_child_version.content.placeholder, poll=poll_grandchild_version.content.poll, @@ -747,7 +858,7 @@ def test_tree_nodes_are_created(self): moderation_request__collection_id=collection.pk ) - # The correct amount of nodes exist + # The correct number of nodes exists self.assertEqual(nodes.count(), 6) # Now assert the tree structure... # Check root refers to correct version & has correct number of children @@ -774,7 +885,7 @@ def test_tree_nodes_are_created(self): class SubmitCollectionForModerationViewTest(BaseViewTestCase): def setUp(self): - super(SubmitCollectionForModerationViewTest, self).setUp() + super().setUp() self.url = reverse( "admin:cms_moderation_submit_collection_for_moderation", args=(self.collection2.pk,), @@ -820,7 +931,7 @@ def test_submit_collection_for_moderation(self, cancel_mock): class ModerationRequestChangeListView(BaseViewTestCase): def setUp(self): - super(ModerationRequestChangeListView, self).setUp() + super().setUp() self.collection_submit_url = reverse( "admin:cms_moderation_submit_collection_for_moderation", args=(self.collection2.pk,), @@ -942,11 +1053,15 @@ def _set_up_initial_page_data(self): self.poll_child_version = PollVersionFactory(created_by=self.user, content__language=language) PollPluginFactory( placeholder=self.poll_version.content.placeholder, poll=self.poll_child_version.content.poll) + remove_version_lock(self.page_1_version) + remove_version_lock(self.poll_version) + remove_version_lock(self.poll_child_version) # Page 2 self.page_2_version = PageVersionFactory(created_by=self.user, content__language=language) page_2_placeholder = PlaceholderFactory(source=self.page_2_version.content) PollPluginFactory(placeholder=page_2_placeholder, poll=self.poll_child_version.content.poll) + remove_version_lock(self.page_2_version) def _add_pages_to_collection(self): """ @@ -976,26 +1091,25 @@ def _add_pages_to_collection(self): self.assertEqual(302, response.status_code) self.assertEqual(admin_endpoint, response.url) # The correct amount of moderation requests has been created - self.assertEqual( - ModerationRequest.objects.filter(collection=self.collection).count(), - 4 - ) + mr = ModerationRequest.objects.filter(collection=self.collection) + # The tree structure for page_1_version is correct + root_1 = ModerationRequestTreeNode.get_root_nodes().get(moderation_request__version=self.page_1_version) + self.assertEqual(mr.count(), 4) # The correct amount of tree nodes has been created # Poll is repeated twice and will therefore have an additional node self.assertEqual( ModerationRequestTreeNode.objects.filter(moderation_request__collection=self.collection).count(), 5 ) - # The tree structure for page_1_version is correct - root_1 = ModerationRequestTreeNode.get_root_nodes().get( - moderation_request__version=self.page_1_version) self.assertEqual(root_1.get_children().count(), 1) + child_1 = root_1.get_children().get() self.assertEqual(child_1.moderation_request.version, self.poll_version) self.assertEqual(child_1.get_children().count(), 1) grandchild = child_1.get_children().get() self.assertEqual( grandchild.moderation_request.version, self.poll_child_version) + # The tree structure for page_2_version is correct root_2 = ModerationRequestTreeNode.get_root_nodes().get( moderation_request__version=self.page_2_version) @@ -1038,7 +1152,7 @@ def test_moderation_workflow_node_deletion_1(self): # Load the changelist and check that the page loads without an error changelist_url = reverse('admin:djangocms_moderation_moderationrequesttreenode_changelist') - changelist_url += "?moderation_request__collection__id={}".format(self.collection.pk) + changelist_url += f"?moderation_request__collection__id={self.collection.pk}" response = self.client.get(changelist_url) self.assertEqual(response.status_code, 200) @@ -1093,7 +1207,7 @@ def test_moderation_workflow_node_deletion_2(self): # Load the changelist and check that the page loads without an error changelist_url = reverse('admin:djangocms_moderation_moderationrequesttreenode_changelist') - changelist_url += "?moderation_request__collection__id={}".format(self.collection.pk) + changelist_url += f"?moderation_request__collection__id={self.collection.pk}" response = self.client.get(changelist_url) self.assertEqual(response.status_code, 200) @@ -1138,18 +1252,20 @@ def test_moderation_workflow_node_deletion_3(self): # Now remove poll_version from the collection page_1_root = ModerationRequestTreeNode.get_root_nodes().get( moderation_request__version=self.page_1_version) - poll_1_node = page_1_root.get_children().get() - delete_url = "{}?ids={}&collection_id={}".format( - reverse('admin:djangocms_moderation_moderationrequesttreenode_delete'), - ",".join([str(poll_1_node.pk)]), - self.collection.pk, - ) - response = self.client.post(delete_url, follow=True) - self.assertEqual(response.status_code, 200) + page_1_root_children = page_1_root.get_children() + if page_1_root_children.count() > 0: + poll_1_node = page_1_root_children.get() + delete_url = "{}?ids={}&collection_id={}".format( + reverse('admin:djangocms_moderation_moderationrequesttreenode_delete'), + ",".join([str(poll_1_node.pk)]), + self.collection.pk, + ) + response = self.client.post(delete_url, follow=True) + self.assertEqual(response.status_code, 200) # Load the changelist and check that the page loads without an error changelist_url = reverse('admin:djangocms_moderation_moderationrequesttreenode_changelist') - changelist_url += "?moderation_request__collection__id={}".format(self.collection.pk) + changelist_url += f"?moderation_request__collection__id={self.collection.pk}" response = self.client.get(changelist_url) self.assertEqual(response.status_code, 200) @@ -1198,18 +1314,20 @@ def test_moderation_workflow_node_deletion_4(self): # Now remove poll_version from the collection page_1_root = ModerationRequestTreeNode.get_root_nodes().get( moderation_request__version=self.page_1_version) - poll_grandchild_node = page_1_root.get_children().get().get_children().get() - delete_url = "{}?ids={}&collection_id={}".format( - reverse('admin:djangocms_moderation_moderationrequesttreenode_delete'), - ",".join([str(poll_grandchild_node.pk)]), - self.collection.pk, - ) - response = self.client.post(delete_url, follow=True) - self.assertEqual(response.status_code, 200) + page_1_root_children = page_1_root.get_children() + if page_1_root_children.count() > 0: + poll_grandchild_node = page_1_root_children.get().get_children().get() + delete_url = "{}?ids={}&collection_id={}".format( + reverse('admin:djangocms_moderation_moderationrequesttreenode_delete'), + ",".join([str(poll_grandchild_node.pk)]), + self.collection.pk, + ) + response = self.client.post(delete_url, follow=True) + self.assertEqual(response.status_code, 200) # Load the changelist and check that the page loads without an error changelist_url = reverse('admin:djangocms_moderation_moderationrequesttreenode_changelist') - changelist_url += "?moderation_request__collection__id={}".format(self.collection.pk) + changelist_url += f"?moderation_request__collection__id={self.collection.pk}" response = self.client.get(changelist_url) self.assertEqual(response.status_code, 200) @@ -1230,7 +1348,6 @@ def test_moderation_workflow_node_deletion_4(self): moderation_request__version=self.page_2_version).get() self.assertEqual(root_1.get_children().count(), 1) self.assertEqual(root_2.get_children().count(), 0) - self.assertEqual(root_1.get_children().get().moderation_request.version, self.poll_version) def test_moderation_workflow_node_deletion_5(self): """ @@ -1257,18 +1374,20 @@ def test_moderation_workflow_node_deletion_5(self): # Now remove poll_version from the collection page_2_root = ModerationRequestTreeNode.get_root_nodes().get( moderation_request__version=self.page_2_version) - poll_child_node = page_2_root.get_children().get() - delete_url = "{}?ids={}&collection_id={}".format( - reverse('admin:djangocms_moderation_moderationrequesttreenode_delete'), - ",".join([str(poll_child_node.pk)]), - self.collection.pk, - ) - response = self.client.post(delete_url, follow=True) - self.assertEqual(response.status_code, 200) + page_2_root_children = page_2_root.get_children() + if page_2_root_children.count() > 0: + poll_child_node = page_2_root_children.get() + delete_url = "{}?ids={}&collection_id={}".format( + reverse('admin:djangocms_moderation_moderationrequesttreenode_delete'), + ",".join([str(poll_child_node.pk)]), + self.collection.pk, + ) + response = self.client.post(delete_url, follow=True) + self.assertEqual(response.status_code, 200) # Load the changelist and check that the page loads without an error changelist_url = reverse('admin:djangocms_moderation_moderationrequesttreenode_changelist') - changelist_url += "?moderation_request__collection__id={}".format(self.collection.pk) + changelist_url += f"?moderation_request__collection__id={self.collection.pk}" response = self.client.get(changelist_url) self.assertEqual(response.status_code, 200) @@ -1287,6 +1406,7 @@ def test_moderation_workflow_node_deletion_5(self): moderation_request__version=self.page_1_version).get() root_2 = ModerationRequestTreeNode.get_root_nodes().filter( moderation_request__version=self.page_2_version).get() + self.assertEqual(root_1.get_children().count(), 1) - self.assertEqual(root_2.get_children().count(), 0) self.assertEqual(root_1.get_children().get().moderation_request.version, self.poll_version) + self.assertEqual(root_2.get_children().count(), 0) diff --git a/tests/utils/factories.py b/tests/utils/factories.py index 811f0955..2c9c442b 100644 --- a/tests/utils/factories.py +++ b/tests/utils/factories.py @@ -134,7 +134,7 @@ class UserFactory(DjangoModelFactory): first_name = factory.Faker("first_name") last_name = factory.Faker("last_name") email = factory.LazyAttribute( - lambda u: "%s.%s@example.com" % (u.first_name.lower(), u.last_name.lower()) + lambda u: f"{u.first_name.lower()}.{u.last_name.lower()}@example.com" ) class Meta: diff --git a/tests/utils/moderated_polls/models.py b/tests/utils/moderated_polls/models.py index 81147b54..eb0785ba 100644 --- a/tests/utils/moderated_polls/models.py +++ b/tests/utils/moderated_polls/models.py @@ -10,7 +10,7 @@ class Poll(models.Model): name = models.TextField() def __str__(self): - return "{} ({})".format(self.name, self.pk) + return f"{self.name} ({self.pk})" class PollContent(models.Model): diff --git a/tests/utils/versioned_none_moderated_app/models.py b/tests/utils/versioned_none_moderated_app/models.py index 674ff192..dafffb62 100644 --- a/tests/utils/versioned_none_moderated_app/models.py +++ b/tests/utils/versioned_none_moderated_app/models.py @@ -9,7 +9,7 @@ class NoneModeratedPoll(models.Model): name = models.TextField() def __str__(self): - return "{} ({})".format(self.name, self.pk) + return f"{self.name} ({self.pk})" class NoneModeratedPollContent(models.Model):