diff --git a/.github/ISSUE_TEMPLATE/---bug-report.md b/.github/ISSUE_TEMPLATE/---bug-report.md new file mode 100644 index 00000000..1d5d21b9 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/---bug-report.md @@ -0,0 +1,65 @@ +--- +name: "\U0001F41E Bug report" +about: Something isn't working as expected? Here is the right place to report. +title: "[BUG]" +labels: '' +assignees: '' + +--- + + + +## Description + + + +## Steps to reproduce + + + +## Expected behaviour + + + +## Actual behaviour + + + +## Screenshots + + + +## Additional information (CMS/Python/Django versions) + + + +## Do you want to help fix this issue? + + + +* [ ] Yes, I want to help fix this issue and I will join #workgroup-pr-review on [Slack](https://www.django-cms.org/slack) to confirm with the community that a PR is welcome. +* [ ] No, I only want to report the issue. diff --git a/.github/ISSUE_TEMPLATE/---documentation-report.md b/.github/ISSUE_TEMPLATE/---documentation-report.md new file mode 100644 index 00000000..2b599f7a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/---documentation-report.md @@ -0,0 +1,43 @@ +--- +name: "\U0001F4D8 Documentation report" +about: "Something isn't described correctly in the documentation or needs to be updated? + Here is the right place to report." +title: "[DOC]" +labels: 'component: documentation' +assignees: '' + +--- + + + +## Description + + + +## Screenshots + + + +## Additional information (CMS/Python/Django versions) + + + +## Do you want to help fix this documentation issue? + + + +* [ ] Yes, I want to help fix this issue and I will join #workgroup-documentation on [Slack](https://www.django-cms.org/slack) to confirm with the team that a PR is welcome. +* [ ] No, I only want to report the issue. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..4f76fcfa --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,28 @@ +## Description + + + +## Related resources + + + +* #... +* #... + +## Checklist + + + +* [ ] I have added or modified the tests when changing logic +* [ ] I have followed [the conventional commits guidelines](https://www.conventionalcommits.org/) to add meaningful information into the changelog +* [ ] I have read the [contribution guidelines ](https://github.com/django-cms/django-cms/blob/develop/CONTRIBUTING.rst) diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..14f01789 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" # Location of package manifests + schedule: + interval: "weekly" diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 00000000..b005cb83 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,35 @@ +name: Docs + +on: [push, pull_request] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + docs: + runs-on: ubuntu-latest + name: docs + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + cache: 'pip' + - name: Cache dependencies + uses: actions/cache@v4 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('docs/requirements.txt') }} + restore-keys: | + ${{ runner.os }}-pip- + - run: python -m pip install -r docs/requirements.txt + - run: python setup.py install + - run: codespell -w *.rst + - run: codespell -w --skip docs/spelling_wordlist docs + - name: Build docs + run: | + cd docs + sphinx-build -b dirhtml -n -d build/doctrees . build/dirhtml diff --git a/.github/workflows/frontend.yml b/.github/workflows/frontend.yml index 4d88962e..cfdcaed4 100644 --- a/.github/workflows/frontend.yml +++ b/.github/workflows/frontend.yml @@ -10,9 +10,9 @@ jobs: node-version: [6.x] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v1 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - run: npm install diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 6760a90a..b6da3f9d 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -8,15 +8,15 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v2 + 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 - uses: liskin/gh-problem-matcher-wrap@v1 + uses: liskin/gh-problem-matcher-wrap@v3 with: linters: flake8 run: flake8 @@ -25,14 +25,14 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v2 + 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 + uses: liskin/gh-problem-matcher-wrap@v3 with: linters: isort run: isort -c -rc -df ./ diff --git a/.github/workflows/publish-to-live-pypi.yml b/.github/workflows/publish-to-live-pypi.yml new file mode 100644 index 00000000..0776261d --- /dev/null +++ b/.github/workflows/publish-to-live-pypi.yml @@ -0,0 +1,41 @@ +name: Publish Python 🐍 distributions 📦 to pypi + +on: + release: + types: + - published + +jobs: + build-n-publish: + name: Build and publish Python 🐍 distributions 📦 to pypi + runs-on: ubuntu-latest + environment: + name: pypi + url: https://pypi.org/p/djangocms-moderation + permissions: + id-token: write + steps: + - uses: actions/checkout@v4 + - name: Set up Python 3.10 + uses: actions/setup-python@v5 + with: + python-version: '3.10' + + - name: Install pypa/build + run: >- + python -m + pip install + build + --user + - name: Build a binary wheel and a source tarball + run: >- + python -m + build + --sdist + --wheel + --outdir dist/ + . + + - name: Publish distribution 📦 to PyPI + if: startsWith(github.ref, 'refs/tags') + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.github/workflows/publish-to-test-pypi.yml b/.github/workflows/publish-to-test-pypi.yml new file mode 100644 index 00000000..ae0fd790 --- /dev/null +++ b/.github/workflows/publish-to-test-pypi.yml @@ -0,0 +1,43 @@ +name: Publish Python 🐍 distributions 📦 to TestPyPI + +on: + push: + branches: + - master + +jobs: + build-n-publish: + name: Build and publish Python 🐍 distributions 📦 to TestPyPI + runs-on: ubuntu-latest + environment: + name: test + url: https://test.pypi.org/p/djangocms-moderation + permissions: + id-token: write + steps: + - uses: actions/checkout@v4 + - name: Set up Python 3.10 + uses: actions/setup-python@v5 + with: + python-version: '3.10' + + - name: Install pypa/build + run: >- + python -m + pip install + build + --user + - name: Build a binary wheel and a source tarball + run: >- + python -m + build + --sdist + --wheel + --outdir dist/ + . + + - name: Publish distribution 📦 to Test PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + repository-url: https://test.pypi.org/legacy/ + skip_existing: true diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 704336cc..c489abb2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,20 +10,26 @@ 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, - dj32_cms40.txt, + dj50_cms41.txt, + dj42_cms41.txt, + dj42_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@v1 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies @@ -33,7 +39,7 @@ jobs: python setup.py install - name: Run coverage - run: coverage run setup.py test + run: coverage run ./tests/settings.py - name: Upload Coverage to Codecov - uses: codecov/codecov-action@v1 + uses: codecov/codecov-action@v5 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..e99f5e66 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,53 @@ +ci: + autofix_commit_msg: | + ci: auto fixes from pre-commit hooks + + for more information, see https://pre-commit.ci + autofix_prs: false + autoupdate_commit_msg: 'ci: pre-commit autoupdate' + autoupdate_schedule: monthly + +repos: + - repo: https://github.com/asottile/pyupgrade + rev: v3.19.0 + hooks: + - id: pyupgrade + args: ["--py310-plus"] + + - repo: https://github.com/adamchainz/django-upgrade + rev: '1.22.1' + hooks: + - id: django-upgrade + args: [--target-version, "4.0"] + + - repo: https://github.com/PyCQA/flake8 + rev: 7.1.1 + hooks: + - id: flake8 + + - repo: https://github.com/asottile/yesqa + rev: v1.5.0 + hooks: + - id: yesqa + + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.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.13.2 + hooks: + - id: isort + + - repo: https://github.com/codespell-project/codespell + rev: v2.3.0 + hooks: + - id: codespell + exclude: > + (?x)^( + .*\.(js|po|json) + )$ diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 00000000..e1bd672f --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,20 @@ +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details +version: 2 + +build: + os: ubuntu-22.04 + tools: + python: "3.11" + +sphinx: + configuration: docs/conf.py + fail_on_warning: false + +formats: + - epub + - pdf + +python: + install: + - requirements: docs/requirements.txt diff --git a/.tx/transifex.yaml b/.tx/transifex.yaml new file mode 100644 index 00000000..2370ac4c --- /dev/null +++ b/.tx/transifex.yaml @@ -0,0 +1,7 @@ +git: + filters: + - filter_type: file + file_format: PO + source_file: djangocms_moderation/locale/en/LC_MESSAGES/django.po + source_language: en + translation_files_expression: 'djangocms_moderation/locale//LC_MESSAGES/django.po' diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 8f224355..1d7eabb0 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,6 +4,56 @@ Changelog Unreleased ========== +fix: Add data-popup attr to a tag in burger menu item +fix: Replace SortableAdminMixin by SortableAdminBase for WorkflowAdmin +fix: Restore "In Collection" button in the toolbar +fix: Update README.rst and add overview of settings + +2.2.1 (2024-07-02) +================== +* feat: Add multi select and add to moderation in admin for moderated_models + +2.2.0 (2024-05-16) +================== +* Python 3.8, 3.9 support removed +* Python 3.10, 3.11 and 3.12 support added +* Django 2.2 support removed +* Django 4.2 support added +* fix: Treebeard support improved by inheriting a treebeard template + +2.1.6 (2022-09-07) +================== +* fix: Language max_length too short for certain language codes + +2.1.5 (2022-07-11) +================== +* fix: Broken markup in the ModerationCollection changelist view, and missing attributes in the burger menu + +2.1.4 (2022-07-08) +================== +* fix: Broken markup and js scripts in the ModerationRequest changelist views + +2.1.3 (2022-06-24) +================== +* fix: Retain classes which define whether a link should open in a modal or not + +2.1.2 (2022-06-24) +================== +* fix: Alter burger menu js to prioritise injected static url + +2.1.1 (2022-06-24) +================== +* fix: Updated moderation_request_change_list to pass static url for svg asset in burger menu + +2.1.0 (2022-06-23) +================== +* feat: Added burgermenu for ModerationRequestTreeAdmin icons +* fix: Avoid errors thrown when nested plugins are M2M fields +* fix: Add to collection should automatically add deeply nested draft versioned objects #205 +* fix: Refactor flawed add to collection XSS redirect sanitisation added in 1.0.26 + +2.0.0 (2022-01-18) +=================== * Python 3.8, 3.9 support added * Django 3.0, 3.1 and 3.2 support added * Python 3.5 and 3.6 support removed diff --git a/README.rst b/README.rst index 9e13b946..93572458 100644 --- a/README.rst +++ b/README.rst @@ -11,18 +11,20 @@ Requirements django CMS Moderation requires that you have a django CMS 4.0 (or higher) project already running and set up. +djangocms-versioning is also required along with django-fsm which should be installed with versioning. + To install ========== Run:: - pip install git+git://github.com/divio/djangocms-moderation@develop#egg=djangocms-moderation + pip install git+git://github.com/django-cms/djangocms-moderation@master#egg=djangocms-moderation Add the following to your project's ``INSTALLED_APPS``: - - ``'djangocms_moderation'`` - - ``'adminsortable2'`` +- ``'djangocms_moderation'`` +- ``'adminsortable2'`` Run:: @@ -30,6 +32,69 @@ Run:: to perform the application's database migrations. +Configuration +============= + +The following settings can be added to your project's settings file to configure django CMS Moderation's behavior: + +.. list-table:: + :header-rows: 1 + :widths: 50 50 + + * - Setting + - Description + * - ``CMS_MODERATION_DEFAULT_COMPLIANCE_NUMBER_BACKEND`` + - Default backend class for generating compliance numbers. + Default is ``djangocms_moderation.backends.uuid4_backend``. + * - ``CMS_MODERATION_COMPLIANCE_NUMBER_BACKENDS`` + - List of available compliance number backend classes. + By default, three backends are configured: ``uuid4_backend``, + ``sequential_number_backend``, and + ``sequential_number_with_identifier_prefix_backend``. + * - ``CMS_MODERATION_ENABLE_WORKFLOW_OVERRIDE`` + - Enable/disable workflow override functionality. Defaults to ``False``. + * - ``CMS_MODERATION_DEFAULT_CONFIRMATION_PAGE_TEMPLATE`` + - Default template for confirmation pages. Defaults to + ``djangocms_moderation/moderation_confirmation.html`` + * - ``CMS_MODERATION_CONFIRMATION_PAGE_TEMPLATES`` + - List of available confirmation page templates. Only includes the + default template by default. + * - ``CMS_MODERATION_COLLECTION_COMMENTS_ENABLED`` + - Enable/disable comments on collections. Defaults to ``True``. + * - ``CMS_MODERATION_REQUEST_COMMENTS_ENABLED`` + - Enable/disable comments on requests. Defaults to ``True``. + * - ``CMS_MODERATION_COLLECTION_NAME_LENGTH_LIMIT`` + - Maximum length for collection names. Defaults to ``24``. + * - ``EMAIL_NOTIFICATIONS_FAIL_SILENTLY`` + - Control email notification error handling. Defaults to ``False``. + +Example Configuration +--------------------- + +Add these settings to your project's settings file: + +.. code-block:: python + + # Custom compliance number backend + CMS_MODERATION_DEFAULT_COMPLIANCE_NUMBER_BACKEND = 'myapp.backends.CustomComplianceNumberBackend' + + # Enable workflow override + CMS_MODERATION_ENABLE_WORKFLOW_OVERRIDE = True + + # Custom confirmation template + CMS_MODERATION_DEFAULT_CONFIRMATION_PAGE_TEMPLATE = 'custom_confirmation.html' + + # Enable comments + CMS_MODERATION_COLLECTION_COMMENTS_ENABLED = True + CMS_MODERATION_REQUEST_COMMENTS_ENABLED = True + + # Set collection name length limit + CMS_MODERATION_COLLECTION_NAME_LENGTH_LIMIT = 100 + + # Control email notification errors + EMAIL_NOTIFICATIONS_FAIL_SILENTLY = False + +============= Documentation ============= diff --git a/djangocms_moderation/__init__.py b/djangocms_moderation/__init__.py index 51479893..b19ee4b7 100644 --- a/djangocms_moderation/__init__.py +++ b/djangocms_moderation/__init__.py @@ -1,3 +1 @@ -__version__ = "1.0.28" - -default_app_config = "djangocms_moderation.apps.ModerationConfig" +__version__ = "2.2.1" diff --git a/djangocms_moderation/admin.py b/djangocms_moderation/admin.py index 6e66ca7e..f5c2b031 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 SortableAdminBase, 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 @@ -114,8 +117,14 @@ class ModerationRequestTreeAdmin(TreeAdmin): more than once, i.e. they are present in more than one parent. """ class Media: - js = ("djangocms_moderation/js/actions.js",) - css = {"all": ("djangocms_moderation/css/actions.css",)} + js = ( + "admin/js/jquery.init.js", + "djangocms_moderation/js/actions.js", + "djangocms_moderation/js/burger.js", + ) + css = { + "all": ("djangocms_moderation/css/actions.css", "djangocms_moderation/css/burger.css") + } actions = [ # filtered out in `self.get_actions` delete_selected, @@ -171,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 """ @@ -178,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: @@ -204,6 +214,9 @@ def _get_configured_fields(self, request): return fields + @admin.display( + description=_('ID') + ) def get_id(self, obj): return format_html( '{id}', @@ -213,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__): @@ -248,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: @@ -258,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 @@ -460,9 +481,10 @@ def _traverse_moderation_nodes(node_item): return HttpResponseRedirect(redirect_url) +@admin.register(ModerationRequest) class ModerationRequestAdmin(admin.ModelAdmin): class Media: - js = ('djangocms_moderation/js/actions.js',) + js = ('admin/js/jquery.init.js', 'djangocms_moderation/js/actions.js',) inlines = [ModerationRequestActionInline] @@ -807,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"] @@ -894,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"] @@ -901,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( @@ -984,7 +1010,8 @@ def get_extra(self, request, obj=None, **kwargs): return 1 -class WorkflowAdmin(admin.ModelAdmin): +@admin.register(Workflow) +class WorkflowAdmin(SortableAdminBase, admin.ModelAdmin): inlines = [WorkflowStepInline] list_display = ["name", "is_default"] fields = [ @@ -996,9 +1023,10 @@ class WorkflowAdmin(admin.ModelAdmin): ] +@admin.register(ModerationCollection) class ModerationCollectionAdmin(admin.ModelAdmin): class Media: - js = ("djangocms_moderation/js/actions.js",) + js = ("admin/js/jquery.init.js", "djangocms_moderation/js/actions.js",) css = {"all": ("djangocms_moderation/css/actions.css",)} actions = None # remove `delete_selected` for now, it will be handled later @@ -1027,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 """ @@ -1039,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: @@ -1130,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 @@ -1147,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 = [ @@ -1172,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( @@ -1192,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_config.py b/djangocms_moderation/cms_config.py index 6dfba873..4c71d13e 100644 --- a/djangocms_moderation/cms_config.py +++ b/djangocms_moderation/cms_config.py @@ -1,8 +1,11 @@ +from django.contrib import admin from django.core.exceptions import ImproperlyConfigured from cms.app_base import CMSAppConfig, CMSAppExtension from cms.models import PageContent +from .admin_actions import add_items_to_collection + class ModerationExtension(CMSAppExtension): def __init__(self): @@ -16,6 +19,16 @@ def handle_moderation_request_changelist_actions(self, moderation_request_change def handle_moderation_request_changelist_fields(self, moderation_request_changelist_fields): self.moderation_request_changelist_fields.extend(moderation_request_changelist_fields) + def handle_admin_actions(self, moderated_models): + """ + Add items to collection to admin actions in model admin + """ + for model in moderated_models: + if admin.site.is_registered(model): + admin_instance = admin.site._registry[model] + admin_instance.actions = admin_instance.actions or [] + admin_instance.actions.append(add_items_to_collection) + def configure_app(self, cms_config): versioning_enabled = getattr(cms_config, "djangocms_versioning_enabled", False) moderated_models = getattr(cms_config, "moderated_models", []) @@ -24,6 +37,8 @@ def configure_app(self, cms_config): raise ImproperlyConfigured("Versioning needs to be enabled for Moderation") self.moderated_models.extend(moderated_models) + if moderated_models: + self.handle_admin_actions(moderated_models) if hasattr(cms_config, "moderation_request_changelist_actions"): self.handle_moderation_request_changelist_actions(cms_config.moderation_request_changelist_actions) diff --git a/djangocms_moderation/cms_toolbars.py b/djangocms_moderation/cms_toolbars.py index 6181cfb7..a99a7b66 100644 --- a/djangocms_moderation/cms_toolbars.py +++ b/djangocms_moderation/cms_toolbars.py @@ -54,7 +54,7 @@ def _add_moderation_buttons(self): if not helpers.is_registered_for_moderation(self.toolbar.obj): return - if self._is_versioned() and self.toolbar.edit_mode_active: + if self._is_versioned() and (self.toolbar.edit_mode_active or self.toolbar.preview_mode_active): moderation_request = helpers.get_active_moderation_request(self.toolbar.obj) if moderation_request: title, url = helpers.get_moderation_button_title_and_url( @@ -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/compact.py b/djangocms_moderation/compact.py new file mode 100644 index 00000000..a010273a --- /dev/null +++ b/djangocms_moderation/compact.py @@ -0,0 +1,9 @@ +from django import get_version + +from packaging.version import Version + + +DJANGO_VERSION = get_version() + + +DJANGO_4_1 = Version(DJANGO_VERSION) < Version('4.2') 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 c3f38e62..d266f7cd 100644 --- a/djangocms_moderation/helpers.py +++ b/djangocms_moderation/helpers.py @@ -6,6 +6,7 @@ from django.urls import reverse from django.utils.translation import gettext_lazy as _ +from cms.models import CMSPlugin from cms.utils.plugins import downcast_plugins from djangocms_versioning import versionables @@ -20,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): @@ -74,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) @@ -157,21 +162,45 @@ def _get_moderatable_version(versionable, grouper, parent_version_filters): return +def _get_nested_moderated_children_from_placeholder_plugin(instance, placeholder, parent_version_filters): + """ + Find all nested versionable objects, traverses through all attached models until it finds + any models that are versioned. + """ + # Catch Many to many fields that don't have _meta + # FIXME: Handle nested M2M instances + if not hasattr(instance, "_meta"): + return + + for field in instance._meta.get_fields(): + if not field.is_relation or field.auto_created: + continue + + candidate = getattr(instance, field.name) + + # Break early if the field is None, a placeholder, or is a CMSPlugin instance + # We do this to save unnecessary processing + if not candidate or candidate == placeholder or isinstance(candidate, CMSPlugin): + continue + + try: + versionable = versionables.for_grouper(candidate) + except KeyError: + yield from _get_nested_moderated_children_from_placeholder_plugin( + candidate, placeholder, parent_version_filters + ) + continue + + version = _get_moderatable_version( + versionable, candidate, parent_version_filters + ) + if version: + yield version + + def get_moderated_children_from_placeholder(placeholder, parent_version_filters): """ Get all moderated children version objects from a placeholder """ for plugin in downcast_plugins(placeholder.get_plugins()): - for field in plugin._meta.get_fields(): - if not field.is_relation or field.auto_created: - continue - candidate = getattr(plugin, field.name) - try: - versionable = versionables.for_grouper(candidate) - except KeyError: - continue - version = _get_moderatable_version( - versionable, candidate, parent_version_filters - ) - if version: - yield version + yield from _get_nested_moderated_children_from_placeholder_plugin(plugin, placeholder, parent_version_filters) diff --git a/djangocms_moderation/locale/en/LC_MESSAGES/django.po b/djangocms_moderation/locale/en/LC_MESSAGES/django.po new file mode 100644 index 00000000..26ebb0bf --- /dev/null +++ b/djangocms_moderation/locale/en/LC_MESSAGES/django.po @@ -0,0 +1,914 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-01-23 23:31+0100\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: admin.py:67 models.py:650 +#: templates/djangocms_moderation/comment_list.html:10 +msgid "Action" +msgstr "" + +#: admin.py:68 models.py:651 +msgid "Actions" +msgstr "" + +#: admin.py:78 +#, python-brace-format +msgid "By {user}" +msgstr "" + +#: admin.py:80 +#: templates/admin/djangocms_moderation/moderationrequest/approve_confirmation.html:25 +#: templates/admin/djangocms_moderation/moderationrequest/delete_confirmation.html:26 +#: templates/admin/djangocms_moderation/moderationrequest/publish_confirmation.html:25 +#: templates/admin/djangocms_moderation/moderationrequest/resubmit_confirmation.html:25 +#: templates/admin/djangocms_moderation/moderationrequest/rework_confirmation.html:25 +#: templates/djangocms_moderation/items_to_collection.html:47 +#: templates/djangocms_moderation/moderation_request_change_list.html:23 +msgid "Status" +msgstr "" + +#: admin.py:99 +msgid "Form Submission" +msgstr "" + +#: admin.py:187 admin.py:1048 +msgid "actions" +msgstr "" + +#: admin.py:222 +msgid "ID" +msgstr "" + +#: admin.py:228 +msgid "Content type" +msgstr "" + +#: admin.py:232 +msgid "Title" +msgstr "" + +#: admin.py:236 +#: templates/admin/djangocms_moderation/moderationrequest/approve_confirmation.html:22 +#: templates/admin/djangocms_moderation/moderationrequest/delete_confirmation.html:23 +#: templates/admin/djangocms_moderation/moderationrequest/publish_confirmation.html:22 +#: templates/admin/djangocms_moderation/moderationrequest/resubmit_confirmation.html:22 +#: templates/admin/djangocms_moderation/moderationrequest/rework_confirmation.html:22 +#: templates/djangocms_moderation/comment_list.html:9 +#: templates/djangocms_moderation/items_to_collection.html:44 +msgid "Author" +msgstr "" + +#: admin.py:257 +msgid "Preview" +msgstr "" + +#: admin.py:267 +msgid "Reviewer" +msgstr "" + +#: admin.py:276 +msgid "Ready for publishing" +msgstr "" + +#: admin.py:278 +msgid "Pending author rework" +msgstr "" + +#: admin.py:282 +#, python-format +msgid "Pending %(role)s approval" +msgstr "" + +#: admin.py:291 +#, python-format +msgid "%(action)s by %(name)s" +msgstr "" + +#: admin.py:293 +msgid "Ready for submission" +msgstr "" + +#: admin.py:457 +#, python-format +msgid "%(count)d request successfully deleted" +msgid_plural "%(count)d requests successfully deleted" +msgstr[0] "" +msgstr[1] "" + +#: admin.py:599 +#, python-format +msgid "%(count)d request successfully resubmitted for review" +msgid_plural "%(count)d requests successfully resubmitted for review" +msgstr[0] "" +msgstr[1] "" + +#: admin.py:646 +#, python-format +msgid "%(count)d request successfully published" +msgid_plural "%(count)d requests successfully published" +msgstr[0] "" +msgstr[1] "" + +#: admin.py:705 +#, python-format +msgid "%(count)d request successfully submitted for rework" +msgid_plural "%(count)d requests successfully submitted for rework" +msgstr[0] "" +msgstr[1] "" + +#: admin.py:797 +#, python-format +msgid "%(count)d request successfully approved" +msgid_plural "%(count)d requests successfully approved" +msgstr[0] "" +msgstr[1] "" + +#: admin.py:863 +#: templates/admin/djangocms_moderation/collectioncomment/change_form.html:10 +msgid "Collection comments" +msgstr "" + +#: admin.py:913 +msgid "User" +msgstr "" + +#: admin.py:947 +msgid "Request comments" +msgstr "" + +#: admin.py:1039 +msgid "reviewers" +msgstr "" + +#: admin.py:1057 +msgid "Modify collection" +msgstr "" + +#: admin.py:1184 models.py:441 +msgid "Request" +msgstr "" + +#: admin.py:1189 +msgid "By User" +msgstr "" + +#: admin.py:1197 +msgid "Question" +msgstr "" + +#: admin.py:1197 +msgid "Answer" +msgstr "" + +#: admin.py:1202 +msgid "Form Data" +msgstr "" + +#: admin_actions.py:36 +msgid "Resubmit changes for review" +msgstr "" + +#: admin_actions.py:53 +msgid "Submit for rework" +msgstr "" + +#: admin_actions.py:66 +msgid "Approve" +msgstr "" + +#: admin_actions.py:82 +msgid "Remove selected" +msgstr "" + +#: admin_actions.py:99 +msgid "Publish selected requests" +msgstr "" + +#: admin_actions.py:156 +msgid "No suitable items found to add to moderation collection" +msgstr "" + +#: admin_actions.py:161 +msgid "Add to moderation collection" +msgstr "" + +#: admin_actions.py:165 +msgid "Add items to a collection to unpublish" +msgstr "" + +#: apps.py:7 +msgid "django CMS Moderation" +msgstr "" + +#: cms_toolbars.py:84 monkeypatch.py:56 +msgid "Submit for moderation" +msgstr "" + +#: cms_toolbars.py:109 +#: templates/djangocms_moderation/moderation_request_change_list.html:48 +msgid "Moderation collections" +msgstr "" + +#: conf.py:12 +msgid "Unique alpha-numeric string" +msgstr "" + +#: conf.py:13 +msgid "Sequential number" +msgstr "" + +#: conf.py:16 +msgid "Sequential number with identifier prefix" +msgstr "" + +#: conf.py:40 +msgid "Default" +msgstr "" + +#: constants.py:18 +msgid "Current page" +msgstr "" + +#: constants.py:19 +msgid "Page children (immediate)" +msgstr "" + +#: constants.py:20 +msgid "Page and children (immediate)" +msgstr "" + +#: constants.py:21 +msgid "Page descendants" +msgstr "" + +#: constants.py:22 +msgid "Page and descendants" +msgstr "" + +#: constants.py:33 +msgid "Resubmitted" +msgstr "" + +#: constants.py:34 +msgid "Started" +msgstr "" + +#: constants.py:35 +msgid "Rejected" +msgstr "" + +#: constants.py:36 +msgid "Approved" +msgstr "" + +#: constants.py:37 constants.py:54 +msgid "Cancelled" +msgstr "" + +#: constants.py:38 +msgid "Finished" +msgstr "" + +#: constants.py:51 +msgid "Collecting" +msgstr "" + +#: constants.py:52 +msgid "In Review" +msgstr "" + +#: constants.py:53 +msgid "Archived" +msgstr "" + +#: contrib/moderation_forms/cms_plugins.py:14 +#: contrib/moderation_forms/models.py:9 +msgid "Moderation Form" +msgstr "" + +#: contrib/moderation_forms/models.py:10 +msgid "Moderation Forms" +msgstr "" + +#: emails.py:16 +#: templates/djangocms_moderation/emails/moderation-request/approved.txt:8 +msgid "Approved moderation requests" +msgstr "" + +#: emails.py:17 +#: templates/djangocms_moderation/emails/moderation-request/rejected.txt:11 +msgid "Rejected moderation requests" +msgstr "" + +#: emails.py:18 +msgid "Request for moderation deleted" +msgstr "" + +#: emails.py:86 +msgid "Review requested" +msgstr "" + +#: filters.py:18 forms.py:64 models.py:213 +msgid "moderator" +msgstr "" + +#: filters.py:36 +msgid "reviewer" +msgstr "" + +#: filters.py:77 +msgid "All" +msgstr "" + +#: forms.py:67 +msgid "comment" +msgstr "" + +#: forms.py:100 forms.py:198 +#, python-brace-format +msgid "Any {role}" +msgstr "" + +#: forms.py:172 +msgid "" +"Your item is either locked, not enabled for moderation,or is part of another " +"active moderation request" +msgid_plural "" +"Your items are either locked, not enabled for moderation,or are part of " +"another active moderation request" +msgstr[0] "" +msgstr[1] "" + +#: forms.py:184 +msgid "Select review group" +msgstr "" + +#: forms.py:205 +msgid "This collection can't be submitted for a review" +msgstr "" + +#: forms.py:222 +msgid "This collection can't be cancelled" +msgstr "" + +#: forms.py:266 +msgid "You can only change your own comments" +msgstr "" + +#: helpers.py:112 +#, python-format +msgid "In collection \"%(collection_name)s (%(collection_id)s)\"" +msgstr "" + +#: helpers.py:117 +#, python-format +msgid "In moderation \"%(collection_name)s (%(collection_id)s)\"" +msgstr "" + +#: models.py:27 +msgid "Plain" +msgstr "" + +#: models.py:28 +msgid "Form" +msgstr "" + +#: models.py:31 models.py:72 models.py:115 +msgid "name" +msgstr "" + +#: models.py:34 +#: templates/admin/djangocms_moderation/moderationrequest/approve_confirmation.html:20 +#: templates/admin/djangocms_moderation/moderationrequest/delete_confirmation.html:21 +#: templates/admin/djangocms_moderation/moderationrequest/publish_confirmation.html:20 +#: templates/admin/djangocms_moderation/moderationrequest/resubmit_confirmation.html:20 +#: templates/admin/djangocms_moderation/moderationrequest/rework_confirmation.html:20 +#: templates/djangocms_moderation/items_to_collection.html:42 +msgid "Content Type" +msgstr "" + +#: models.py:40 +msgid "Template" +msgstr "" + +#: models.py:47 +msgid "Confirmation Page" +msgstr "" + +#: models.py:48 +msgid "Confirmation Pages" +msgstr "" + +#: models.py:77 +msgid "user" +msgstr "" + +#: models.py:80 +msgid "group" +msgstr "" + +#: models.py:84 models.py:740 +msgid "confirmation page" +msgstr "" + +#: models.py:92 +msgid "Role" +msgstr "" + +#: models.py:93 +msgid "Roles" +msgstr "" + +#: models.py:100 +msgid "Can't pick both user and group. Only one." +msgstr "" + +#: models.py:116 +msgid "is default" +msgstr "" + +#: models.py:118 +msgid "identifier" +msgstr "" + +#: models.py:123 +msgid "" +"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" +msgstr "" + +#: models.py:129 +msgid "requires compliance number?" +msgstr "" + +#: models.py:132 +msgid "" +"Does the Compliance number need to be generated before the moderation " +"request is approved? Please select the compliance number backend below" +msgstr "" + +#: models.py:138 +msgid "compliance number backend" +msgstr "" + +#: models.py:145 +#: templates/djangocms_moderation/moderation_request_change_list.html:24 +msgid "Workflow" +msgstr "" + +#: models.py:146 +msgid "Workflows" +msgstr "" + +#: models.py:161 +msgid "Can't have two default workflows, only one is allowed." +msgstr "" + +#: models.py:171 +msgid "role" +msgstr "" + +#: models.py:173 +msgid "is mandatory" +msgstr "" + +#: models.py:176 models.py:217 +msgid "workflow" +msgstr "" + +#: models.py:185 +msgid "Step" +msgstr "" + +#: models.py:186 +msgid "Steps" +msgstr "" + +#: models.py:210 +msgid "collection name" +msgstr "" + +#: models.py:232 +msgid "collection" +msgstr "" + +#: models.py:234 +msgid "Can change collection author" +msgstr "" + +#: models.py:235 +msgid "Can cancel collection" +msgstr "" + +#: models.py:318 +msgid "Cancelled collection" +msgstr "" + +#: models.py:398 models.py:635 +msgid "moderation_request" +msgstr "" + +#: models.py:416 +msgid "version" +msgstr "" + +#: models.py:419 +msgid "language" +msgstr "" + +#: models.py:422 +msgid "is active" +msgstr "" + +#: models.py:424 +msgid "date sent" +msgstr "" + +#: models.py:426 +msgid "compliance number" +msgstr "" + +#: models.py:435 models.py:695 +msgid "author" +msgstr "" + +#: models.py:442 +#: templates/admin/djangocms_moderation/moderationrequest/change_form.html:10 +#: templates/admin/djangocms_moderation/requestcomment/change_form.html:10 +#: templates/admin/djangocms_moderation/requestcomment/change_list.html:10 +#: templates/djangocms_moderation/moderation_request_change_list.html:49 +msgid "Requests" +msgstr "" + +#: models.py:598 +msgid "status" +msgstr "" + +#: models.py:603 models.py:732 +msgid "by user" +msgstr "" + +#: models.py:609 +msgid "to user" +msgstr "" + +#: models.py:617 +msgid "to role" +msgstr "" + +#: models.py:627 +msgid "step approved" +msgstr "" + +#: models.py:632 models.py:693 +msgid "message" +msgstr "" + +#: models.py:640 +msgid "date taken" +msgstr "" + +#: models.py:720 +msgid "moderation request" +msgstr "" + +#: models.py:726 +msgid "for step" +msgstr "" + +#: models.py:749 +msgid "Confirmation Form Submission" +msgstr "" + +#: models.py:750 +msgid "Confirmation Form Submissions" +msgstr "" + +#: monkeypatch.py:134 monkeypatch.py:144 +msgid "Cannot archive a version in an active moderation collection" +msgstr "" + +#: monkeypatch.py:139 +msgid "Cannot revert when draft version is in an active moderation collection" +msgstr "" + +#: monkeypatch.py:148 +msgid "Version is in an active moderation collection" +msgstr "" + +#: monkeypatch.py:152 +msgid "Cannot edit a version in an active moderation collection" +msgstr "" + +#: templates/admin/djangocms_moderation/collectioncomment/change_form.html:7 +#: templates/admin/djangocms_moderation/collectioncomment/change_list.html:16 +#: templates/admin/djangocms_moderation/moderationrequest/change_form.html:7 +#: templates/admin/djangocms_moderation/requestcomment/change_form.html:7 +#: templates/admin/djangocms_moderation/requestcomment/change_list.html:7 +#: templates/djangocms_moderation/moderation_request_change_list.html:46 +msgid "Home" +msgstr "" + +#: templates/admin/djangocms_moderation/collectioncomment/change_form.html:9 +#: templates/admin/djangocms_moderation/collectioncomment/change_list.html:18 +#: templates/admin/djangocms_moderation/moderationrequest/change_form.html:9 +#: templates/admin/djangocms_moderation/requestcomment/change_form.html:9 +#: templates/admin/djangocms_moderation/requestcomment/change_list.html:9 +msgid "Collections" +msgstr "" + +#: templates/admin/djangocms_moderation/collectioncomment/change_form.html:11 +#: templates/admin/djangocms_moderation/moderationrequest/change_form.html:11 +#: templates/admin/djangocms_moderation/requestcomment/change_form.html:12 +#, python-format +msgid "Add %(name)s" +msgstr "" + +#: templates/admin/djangocms_moderation/collectioncomment/change_list.html:7 +#, python-format +msgid "" +"\n" +" %(collection_name)s comments\n" +" " +msgstr "" + +#: templates/admin/djangocms_moderation/collectioncomment/change_list.html:19 +#: templates/admin/djangocms_moderation/requestcomment/change_form.html:11 +#: templates/admin/djangocms_moderation/requestcomment/change_list.html:11 +msgid "Comments" +msgstr "" + +#: templates/admin/djangocms_moderation/moderationrequest/approve_confirmation.html:3 +#: templates/admin/djangocms_moderation/moderationrequest/delete_confirmation.html:3 +#: templates/admin/djangocms_moderation/moderationrequest/publish_confirmation.html:3 +#: templates/admin/djangocms_moderation/moderationrequest/resubmit_confirmation.html:3 +#: templates/admin/djangocms_moderation/moderationrequest/rework_confirmation.html:3 +msgid "Delete Confirmation" +msgstr "" + +#: templates/admin/djangocms_moderation/moderationrequest/approve_confirmation.html:14 +msgid "Are you sure you want to approve these items for publishing?" +msgstr "" + +#: templates/admin/djangocms_moderation/moderationrequest/approve_confirmation.html:19 +#: templates/admin/djangocms_moderation/moderationrequest/delete_confirmation.html:20 +#: templates/admin/djangocms_moderation/moderationrequest/publish_confirmation.html:19 +#: templates/admin/djangocms_moderation/moderationrequest/resubmit_confirmation.html:19 +#: templates/admin/djangocms_moderation/moderationrequest/rework_confirmation.html:19 +msgid "Id" +msgstr "" + +#: templates/admin/djangocms_moderation/moderationrequest/approve_confirmation.html:21 +#: templates/admin/djangocms_moderation/moderationrequest/delete_confirmation.html:22 +#: templates/admin/djangocms_moderation/moderationrequest/publish_confirmation.html:21 +#: templates/admin/djangocms_moderation/moderationrequest/resubmit_confirmation.html:21 +#: templates/admin/djangocms_moderation/moderationrequest/rework_confirmation.html:21 +#: templates/djangocms_moderation/items_to_collection.html:43 +msgid "Identifier" +msgstr "" + +#: templates/admin/djangocms_moderation/moderationrequest/approve_confirmation.html:23 +#: templates/admin/djangocms_moderation/moderationrequest/delete_confirmation.html:24 +#: templates/admin/djangocms_moderation/moderationrequest/publish_confirmation.html:23 +#: templates/admin/djangocms_moderation/moderationrequest/resubmit_confirmation.html:23 +#: templates/admin/djangocms_moderation/moderationrequest/rework_confirmation.html:23 +#: templates/djangocms_moderation/items_to_collection.html:45 +msgid "Edit Date" +msgstr "" + +#: templates/admin/djangocms_moderation/moderationrequest/approve_confirmation.html:24 +#: templates/admin/djangocms_moderation/moderationrequest/delete_confirmation.html:25 +#: templates/admin/djangocms_moderation/moderationrequest/publish_confirmation.html:24 +#: templates/admin/djangocms_moderation/moderationrequest/resubmit_confirmation.html:24 +#: templates/admin/djangocms_moderation/moderationrequest/rework_confirmation.html:24 +#: templates/djangocms_moderation/items_to_collection.html:46 +msgid "Version #" +msgstr "" + +#: templates/admin/djangocms_moderation/moderationrequest/approve_confirmation.html:47 +#: templates/admin/djangocms_moderation/moderationrequest/delete_confirmation.html:48 +#: templates/admin/djangocms_moderation/moderationrequest/publish_confirmation.html:47 +#: templates/admin/djangocms_moderation/moderationrequest/resubmit_confirmation.html:47 +#: templates/admin/djangocms_moderation/moderationrequest/rework_confirmation.html:47 +msgid "Yes, I\\" +msgstr "" + +#: templates/admin/djangocms_moderation/moderationrequest/approve_confirmation.html:51 +#: templates/admin/djangocms_moderation/moderationrequest/delete_confirmation.html:52 +#: templates/admin/djangocms_moderation/moderationrequest/publish_confirmation.html:51 +#: templates/admin/djangocms_moderation/moderationrequest/resubmit_confirmation.html:51 +#: templates/admin/djangocms_moderation/moderationrequest/rework_confirmation.html:51 +msgid "No, take me back" +msgstr "" + +#: templates/admin/djangocms_moderation/moderationrequest/delete_confirmation.html:15 +msgid "Are you sure you want to remove these items from this collection?" +msgstr "" + +#: templates/admin/djangocms_moderation/moderationrequest/publish_confirmation.html:14 +msgid "Are you sure you want to publish these items?" +msgstr "" + +#: templates/admin/djangocms_moderation/moderationrequest/resubmit_confirmation.html:14 +msgid " Are you sure you want to re-submit these items for review?" +msgstr "" + +#: templates/admin/djangocms_moderation/moderationrequest/rework_confirmation.html:14 +msgid "Are you sure you want to submit these items back for rework?" +msgstr "" + +#: templates/djangocms_moderation/cancel_collection.html:12 +msgid "Are you sure you'd like to cancel this collection?" +msgstr "" + +#: templates/djangocms_moderation/cancel_collection.html:16 +msgid "Cancel" +msgstr "" + +#: templates/djangocms_moderation/cancel_collection.html:18 +msgid "Confirm" +msgstr "" + +#: templates/djangocms_moderation/comment_icon.html:2 +msgid "View Comments" +msgstr "" + +#: templates/djangocms_moderation/comment_list.html:8 +#: templates/djangocms_moderation/emails/moderation-request/approved.txt:20 +#: templates/djangocms_moderation/emails/moderation-request/cancelled.txt:20 +#: templates/djangocms_moderation/emails/moderation-request/rejected.txt:23 +#: templates/djangocms_moderation/emails/moderation-request/request.txt:20 +msgid "Comment" +msgstr "" + +#: templates/djangocms_moderation/comment_list.html:11 +msgid "Date" +msgstr "" + +#: templates/djangocms_moderation/comment_list.html:21 +msgid "no comment provided" +msgstr "" + +#: templates/djangocms_moderation/confirmation_page.html:9 +#: templates/djangocms_moderation/select_workflow_form.html:27 +msgid "Next" +msgstr "" + +#: templates/djangocms_moderation/edit_icon.html:2 +msgid "Edit Collection Settings" +msgstr "" + +#: templates/djangocms_moderation/emails/moderation-request/approved.txt:2 +#, python-format +msgid "" +"\n" +"Hello %(collection_author)s, %(by_user)s has approved some moderation\n" +"requests in the collection %(collection_name)s.\n" +msgstr "" + +#: templates/djangocms_moderation/emails/moderation-request/approved.txt:25 +#: templates/djangocms_moderation/emails/moderation-request/cancelled.txt:25 +#: templates/djangocms_moderation/emails/moderation-request/rejected.txt:28 +#: templates/djangocms_moderation/emails/moderation-request/request.txt:25 +#: templates/djangocms_moderation/moderation_request_change_list.html:22 +msgid "Job ID" +msgstr "" + +#: templates/djangocms_moderation/emails/moderation-request/approved.txt:29 +#: templates/djangocms_moderation/emails/moderation-request/cancelled.txt:29 +#: templates/djangocms_moderation/emails/moderation-request/rejected.txt:32 +#: templates/djangocms_moderation/emails/moderation-request/request.txt:29 +msgid "Admin Url" +msgstr "" + +#: templates/djangocms_moderation/emails/moderation-request/cancelled.txt:2 +#, python-format +msgid "" +"\n" +"Hello %(collection_author)s, the following moderation requests in the " +"collection\n" +"%(collection_name)s have been cancelled\n" +msgstr "" + +#: templates/djangocms_moderation/emails/moderation-request/cancelled.txt:8 +msgid "Cancelled moderation requests" +msgstr "" + +#: templates/djangocms_moderation/emails/moderation-request/rejected.txt:2 +#, python-format +msgid "" +"\n" +"Hello %(collection_author)s, %(by_user)s has rejected some moderation\n" +"requests in the collection %(collection_name)s.\n" +msgstr "" + +#: templates/djangocms_moderation/emails/moderation-request/rejected.txt:7 +msgid "" +"You can resubmit the necessary changes for another review, or remove the " +"moderation requests from the collection." +msgstr "" + +#: templates/djangocms_moderation/emails/moderation-request/request.txt:2 +#, python-format +msgid "" +"\n" +"Hello, %(by_user)s has requested your approval for\n" +"changes in the collection %(collection_name)s.\n" +msgstr "" + +#: templates/djangocms_moderation/emails/moderation-request/request.txt:8 +msgid "Items included in this request" +msgstr "" + +#: templates/djangocms_moderation/items_to_collection.html:11 +msgid "Add items to collection" +msgstr "" + +#: templates/djangocms_moderation/items_to_collection.html:13 +#, python-format +msgid "" +"\n" +" Add '%(content_name)s' to collection\n" +" " +msgstr "" + +#: templates/djangocms_moderation/items_to_collection.html:18 +msgid "No items to add" +msgstr "" + +#: templates/djangocms_moderation/items_to_collection.html:36 +msgid "The selected collection currently contains the following items:" +msgstr "" + +#: templates/djangocms_moderation/items_to_collection.html:66 +msgid "This collection contains no items yet" +msgstr "" + +#: templates/djangocms_moderation/items_to_collection.html:70 +#: templates/djangocms_moderation/request_form.html:37 +msgid "Submit" +msgstr "" + +#: templates/djangocms_moderation/moderation_request_change_list.html:25 +msgid "Owner" +msgstr "" + +#: templates/djangocms_moderation/moderation_request_change_list.html:34 +msgid "Cancel this collection" +msgstr "" + +#: templates/djangocms_moderation/moderation_request_change_list.html:39 +#: views.py:221 +msgid "Submit collection for review" +msgstr "" + +#: templates/djangocms_moderation/request_form.html:7 +#: templates/djangocms_moderation/select_workflow_form.html:7 +msgid "Please correct the error below." +msgid_plural "Please correct the errors below." +msgstr[0] "" +msgstr[1] "" + +#: templates/djangocms_moderation/request_form.html:26 +msgid "Completed Forms" +msgstr "" + +#: templates/djangocms_moderation/request_icon.html:2 +msgid "View Requests" +msgstr "" + +#: views.py:74 +#, python-format +msgid "%(count)d item successfully added to moderation collection" +msgid_plural "%(count)d items successfully added to moderation collection" +msgstr[0] "" +msgstr[1] "" + +#: views.py:206 +msgid "Your collection has been submitted for review" +msgstr "" + +#: views.py:250 +msgid "Your collection has been cancelled" +msgstr "" + +#: views.py:259 +msgid "Cancel collection" +msgstr "" 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 new file mode 100644 index 00000000..df8856a5 --- /dev/null +++ b/djangocms_moderation/migrations/0001_squashed_0017_auto_20220831_0727.py @@ -0,0 +1,663 @@ +# Generated by Django 3.2.25 on 2024-03-11 10:45 + +import cms.models.fields +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +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"), + ] + + initial = True + + dependencies = [ + ("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"), + ] + + operations = [ + migrations.CreateModel( + 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", + ), + ), + ], + options={ + "verbose_name": "Confirmation Page", + "verbose_name_plural": "Confirmation Pages", + }, + ), + migrations.CreateModel( + 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", + ), + ), + ], + ), + migrations.CreateModel( + 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", + ), + ), + ], + options={ + "verbose_name": "Request", + "verbose_name_plural": "Requests", + "ordering": ["id"], + "unique_together": {("collection", "version")}, + }, + ), + migrations.CreateModel( + 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", + ), + ), + ], + options={ + "verbose_name": "Role", + "verbose_name_plural": "Roles", + }, + ), + migrations.CreateModel( + 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", + ), + ), + ], + options={ + "verbose_name": "Workflow", + "verbose_name_plural": "Workflows", + }, + ), + migrations.CreateModel( + 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", + ), + ), + ], + options={ + "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", + ), + ), + migrations.CreateModel( + 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", + ), + ), + ], + options={ + "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", + ), + ), + migrations.AlterModelOptions( + name="moderationcollection", + options={ + "permissions": (("can_change_author", "Can change collection author"),), + "verbose_name": "collection", + }, + ), + migrations.CreateModel( + 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", + ), + ), + ], + options={ + "abstract": False, + }, + ), + migrations.CreateModel( + 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", + ), + ), + ], + options={ + "verbose_name": "Confirmation Form Submission", + "verbose_name_plural": "Confirmation Form Submissions", + "unique_together": {("moderation_request", "for_step")}, + }, + ), + migrations.CreateModel( + 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", + ), + ), + ], + options={ + "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", + ), + ), + migrations.AlterModelOptions( + name="moderationcollection", + options={ + "permissions": ( + ("can_change_author", "Can change collection author"), + ("cancel_moderationcollection", "Can cancel collection"), + ), + "verbose_name": "collection", + }, + ), + migrations.CreateModel( + 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", + ), + ), + ], + options={ + "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/0007_auto_20181002_1725.py b/djangocms_moderation/migrations/0007_auto_20181002_1725.py index 651be569..ba92c49c 100644 --- a/djangocms_moderation/migrations/0007_auto_20181002_1725.py +++ b/djangocms_moderation/migrations/0007_auto_20181002_1725.py @@ -13,4 +13,4 @@ class Migration(migrations.Migration): dependencies = [("djangocms_moderation", "0006_auto_20181001_1840")] - operations = [migrations.RunPython(moderationrequest_author)] + operations = [migrations.RunPython(moderationrequest_author, elidable=True)] diff --git a/djangocms_moderation/migrations/0010_auto_20181008_1317.py b/djangocms_moderation/migrations/0010_auto_20181008_1317.py index a6f69b54..87168449 100644 --- a/djangocms_moderation/migrations/0010_auto_20181008_1317.py +++ b/djangocms_moderation/migrations/0010_auto_20181008_1317.py @@ -33,6 +33,6 @@ class Migration(migrations.Migration): operations = [ migrations.RunPython( - forward_copy_moderation_requests, reverse_copy_moderation_requests + forward_copy_moderation_requests, reverse_copy_moderation_requests, elidable=True ) ] 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/migrations/0017_auto_20220831_0727.py b/djangocms_moderation/migrations/0017_auto_20220831_0727.py new file mode 100644 index 00000000..df1d65d6 --- /dev/null +++ b/djangocms_moderation/migrations/0017_auto_20220831_0727.py @@ -0,0 +1,19 @@ +# Generated by Django 2.2.24 on 2022-08-31 07:27 + +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('djangocms_moderation', '0016_moderationrequesttreenode'), + ] + + operations = [ + migrations.AlterField( + model_name='moderationrequest', + name='language', + field=models.CharField(choices=settings.LANGUAGES, max_length=20, verbose_name='language'), + ) + ] diff --git a/djangocms_moderation/models.py b/djangocms_moderation/models.py index 25c2afd6..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 ) @@ -416,7 +427,7 @@ class ModerationRequest(models.Model): to=Version, verbose_name=_("version"), on_delete=models.CASCADE ) language = models.CharField( - verbose_name=_("language"), max_length=5, choices=settings.LANGUAGES + verbose_name=_("language"), max_length=20, choices=settings.LANGUAGES ) is_active = models.BooleanField( verbose_name=_("is active"), default=True, db_index=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/monkeypatch.py b/djangocms_moderation/monkeypatch.py index a439850e..a1ad0f06 100644 --- a/djangocms_moderation/monkeypatch.py +++ b/djangocms_moderation/monkeypatch.py @@ -120,6 +120,17 @@ def inner(self, obj, request): return inner +def _check_registered_for_moderation(message): + """ + Fail check if object is registered for moderation + """ + def inner(version, user): + if is_registered_for_moderation(version.content): + raise ConditionFailed(message) + + return inner + + admin.VersionAdmin._get_publish_link = _get_publish_link( admin.VersionAdmin._get_publish_link ) @@ -152,5 +163,8 @@ def inner(self, obj, request): _("Cannot edit a version in an active moderation collection") ) ] +models.Version.check_publish += [ + _check_registered_for_moderation(_("Content cannot be published directly. Use the moderation process.")) +] fields.PlaceholderRelationField.default_checks += [_is_placeholder_review_unlocked] 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/css/burger.css b/djangocms_moderation/static/djangocms_moderation/css/burger.css new file mode 100644 index 00000000..61750a05 --- /dev/null +++ b/djangocms_moderation/static/djangocms_moderation/css/burger.css @@ -0,0 +1,177 @@ +/*------------------------------------- +Classes for Action btn & Burger menu +---------------------------------------*/ + +a.btn.cms-moderation-action-btn { + position: relative; + display: -webkit-inline-box; + display: -ms-inline-flexbox; + display: inline-flex; + padding: 0 !important; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: center; + -ms-flex-pack: center; + justify-content: center; + width: 34px; + height: 34px; + margin-top: -12px !important; + position: relative; + bottom: -6px; + -webkit-box-sizing: border-box; + box-sizing: border-box; + cursor: pointer; +} +a.btn.cms-moderation-action-btn img { + width: 20px; + height: 20px; +} + +a.btn.cms-moderation-action-btn.inactive { + opacity: 0.3; + filter: alpha(opacity=30); +} + + +/* disable clicking for inactive buttons */ +.btn.cms-moderation-action-btn.inactive { + pointer-events: none; + background-color: #e1e1e1 !important; +} + +.btn.cms-moderation-action-btn.inactive img { + opacity: 0.5; +} + +/* set size and spacing between for the action icons */ +a.btn.cms-moderation-action-btn img { + width: 20px; + height: 20px; + margin-right: 4px; +} + + +/*------------------------------------- +This governs the drop-down behaviour +extending the pagetree classes provided by CMS +---------------------------------------*/ + +.cms-actions-dropdown-menu { + display: none; + position: absolute; + top: 30px; + right: -1px; + z-index: 1000; + min-width: 180px; + margin: 0; + padding: 0 !important; + border-radius: 5px; + background: #fff; + box-shadow: 0 0 10px rgba(0,0,0,.25); + -webkit-transform: translateZ(0); + transform: translateZ(0); +} + +/* Dropdown menu shadow */ +.cms-actions-dropdown-menu::before { + content: ""; + position: absolute; + left: 100%; + z-index: -1; + width: 10px; + height: 10px; + margin-left: -5px; + background-color: #fff; + box-shadow: 0 0 10px rgba(0,0,0,.25); + -webkit-transform: rotate(45deg) translateZ(0); + transform: rotate(45deg) translateZ(0); + } + +.cms-actions-dropdown-menu.open { + display: block; + width: 200px; +} + +.cms-actions-dropdown-menu.closed { + display: none; +} + +.cms-actions-dropdown-menu-arrow-right-top::before { + top: 16px; +} + +/* add shadow on burger menu trigger */ +a.btn.cms-moderation-action-btn:hover, a.btn.cms-moderation-action-btn.open { + box-shadow: inset 0 3px 5px rgba(0,0,0,.125); +} + +/* style for each option row */ +ul.cms-actions-dropdown-menu-inner { + margin: 0; + padding: 0 !important; + border-radius: 5px; + background-color: #fff; +} + +ul.cms-actions-dropdown-menu-inner li { + border: 1px solid transparent; + border-radius: 5px; + padding: 2px 6px; + list-style-type: none; +} +ul.cms-actions-dropdown-menu-inner li:hover { + border: 1px solid #ccc; + background-color: #0bf; +} + +a.cms-actions-dropdown-menu-item-anchor { + display: block; + line-height: 1.5; + text-align: left; + text-decoration: none; + padding: 10px 15px; + border-top-left-radius: 5px; + border-top-right-radius: 5px; +} + +/* Explicitly defining anchor states to overwrite djangocms-admin styles! */ +a.cms-actions-dropdown-menu-item-anchor, +a.cms-actions-dropdown-menu-item-anchor:visited, +a.cms-actions-dropdown-menu-item-anchor:link, +a.cms-actions-dropdown-menu-item-anchor:link:visited +{ + color: #666 !important; +} +a.cms-actions-dropdown-menu-item-anchor:hover, +a.cms-actions-dropdown-menu-item-anchor:active, +a.cms-actions-dropdown-menu-item-anchor:link:hover, +a.cms-actions-dropdown-menu-item-anchor:link:active +{ + color: #fff !important; + background: #0bf; +} + +/* set the size of the option icon */ +a.cms-actions-dropdown-menu-item-anchor img { + width: 20px; + height: 20px; +} +/* align the option text with it's icon */ +a.cms-actions-dropdown-menu-item-anchor span { + line-height: 1rem; + vertical-align: 20%; + margin-left: 10px; +} +/* disable any inactive option */ +a.cms-actions-dropdown-menu-item-anchor.inactive { + cursor: not-allowed; + pointer-events: none; + opacity: 0.3; + filter: alpha(opacity=30); +} +/* Override the related-widget-wrapper-link css from the cms, only if it's within the ModerationRequestAdmin */ +a.cms-actions-dropdown-menu-item-anchor.related-widget-wrapper-link { + width: auto !important; + height: auto !important; +} diff --git a/djangocms_moderation/static/djangocms_moderation/js/burger.js b/djangocms_moderation/static/djangocms_moderation/js/burger.js new file mode 100644 index 00000000..93efd262 --- /dev/null +++ b/djangocms_moderation/static/djangocms_moderation/js/burger.js @@ -0,0 +1,217 @@ +(function ($) { + if (!$) { + return; + } + + $(function () { + // INFO: it is not possible to put a form inside a form, so the moderation actions have to create their own form + // on click. + $(` .cms-moderation-action-btn, + .js-moderation-action, + .cms-actions-dropdown-menu-item-anchor`) + .on('click', function (e) { + e.preventDefault(); + + // 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'; + let csrfToken = formMethod === 'GET' ? '' : ''; + let fakeForm = $( + '
' + csrfToken + + '
' + ); + let body = window.top.document.body; + let keepSideFrame = action.attr('class').indexOf('js-versioning-keep-sideframe') !== -1; + + // always break out of the sideframe, cause it was never meant to open cms views inside it + try { + if (!keepSideFrame) { + window.top.CMS.API.Sideframe.close(); + } + } catch (err) { } + if (keepSideFrame) { + body = window.document.body; + } + + fakeForm.appendTo(body).submit(); + }); + + $('.js-versioning-close-sideframe').on('click', function () { + try { + window.top.CMS.API.Sideframe.close(); + } catch (e) { } + }); + }); + + // Hide django messages after timeout occurs to prevent content overlap + $('document').ready(function () { + // Targeting first item returned (there's only ever one messagelist per template): + let messageList = document.getElementsByClassName('messagelist')[0]; + let interval = 20; + let timeout = 500; + + if (messageList !== undefined) { + for (let item of messageList.children) { + item.style.opacity = 1; + setTimeout(() => { + let fader = setInterval(() => { + item.style.opacity -= 0.05; + if (item.style.opacity < 0) { + item.style.display = 'none'; + clearInterval(fader); + } + + }, interval); + }, timeout); + } + } + }); + + let closeBurgerMenu = function closeBurgerMenu() { + $('.cms-actions-dropdown-menu').removeClass('open'); + $('.cms-actions-dropdown-menu').addClass('closed'); + $('.cms-moderation-action-btn').removeClass('open'); + $('.cms-moderation-action-btn').addClass('closed'); + }; + + let toggleBurgerMenu = function toggleBurgerMenu(burgerMenuAnchor, optionsContainer) { + let bm = $(burgerMenuAnchor); + let op = $(optionsContainer); + let closed = bm.hasClass('closed'); + + closeBurgerMenu(); + + if (closed) { + bm.removeClass('closed').addClass('open'); + op.removeClass('closed').addClass('open'); + } else { + bm.addClass('closed').removeClass('open'); + op.addClass('closed').removeClass('open'); + } + + let pos = bm.offset(); + let leftOffset = 200; + + op.css('left', pos.left - leftOffset); + op.css('top', pos.top); + }; + + // Create burger menu: + $(function () { + let burger_menu_icon; + + if (typeof moderation_static_url_prefix === 'undefined') { + burger_menu_icon = '/static/djangocms_moderation/svg/menu.svg'; + } else { + // eslint-disable-next-line no-undef + burger_menu_icon = `${moderation_static_url_prefix}svg/menu.svg`; + } + + let createBurgerMenu = function createBurgerMenu(row) { + + let actions = $(row).children('.field-list_display_actions'); + + if (!actions.length) { + /* skip any rows without actions to avoid errors */ + return; + } + + /* create burger menu anchor icon */ + let anchor = document.createElement('a'); + let icon = document.createElement('img'); + + icon.setAttribute('src', burger_menu_icon); + anchor.setAttribute('class', 'btn cms-moderation-action-btn closed'); + anchor.setAttribute('title', 'Actions'); + anchor.appendChild(icon); + + /* create options container */ + let optionsContainer = document.createElement('div'); + let ul = document.createElement('ul'); + + /* 'cms-actions-dropdown-menu' class is the main selector for the menu, + 'cms-actions-dropdown-menu-arrow-right-top' keeps the menu arrow in position. */ + optionsContainer.setAttribute( + 'class', + 'cms-actions-dropdown-menu cms-actions-dropdown-menu-arrow-right-top'); + ul.setAttribute('class', 'cms-actions-dropdown-menu-inner'); + + /* get the existing actions and move them into the options container */ + $(actions[0]).children('.cms-moderation-action-btn').each(function (index, item) { + + let li = document.createElement('li'); + /* create an anchor from the item */ + let li_anchor = document.createElement('a'); + const itemId = $(item).attr('id'); + const itemTarget = $(item).attr('target'); + const itemDataPopup = $(item).attr('data-popup'); + + li_anchor.setAttribute('class', 'cms-actions-dropdown-menu-item-anchor'); + li_anchor.setAttribute('href', $(item).attr('href')); + // Copy the id attribute if it is set + if (itemId !== undefined) { + li_anchor.setAttribute('id', itemId); + } + // Copy the target attribute if it is set + if (itemTarget !== undefined) { + li_anchor.setAttribute('target', itemTarget); + } + // Copy the data-popup attribute if it is set + if (itemDataPopup !== undefined) { + li_anchor.setAttribute('data-popup', itemDataPopup); + } + + if ($(item).hasClass('cms-form-get-method')) { + // Ensure the fake-form selector is propagated to the new anchor + li_anchor.classList.add('cms-form-get-method'); + } + if ($(item).hasClass('related-widget-wrapper-link')) { + // Ensure we retain the class which defines whether an item opens in a modal + li_anchor.classList.add('related-widget-wrapper-link'); + } + /* get the span which contains the img */ + let value = $(item).children('span')[0]; + + /* move the icon image */ + li_anchor.appendChild($(value).children('img')[0]); + + /* create the button text and construct the button */ + let span = document.createElement('span'); + + span.appendChild( + document.createTextNode(item.title) + ); + + li_anchor.appendChild(span); + li.appendChild(li_anchor); + ul.appendChild(li); + + /* destroy original replaced buttons */ + actions[0].removeChild(item); + }); + + /* add the options to the drop-down */ + optionsContainer.appendChild(ul); + actions[0].appendChild(anchor); + document.body.appendChild(optionsContainer); + + /* listen for burger menu clicks */ + anchor.addEventListener('click', function (ev) { + ev.stopPropagation(); + toggleBurgerMenu(anchor, optionsContainer); + }); + + /* close burger menu if clicking outside */ + $(window).click(function () { + closeBurgerMenu(); + }); + }; + + $('#result_list').find('tr').each(function (index, item) { + createBurgerMenu(item); + }); + }); +})((typeof django !== 'undefined' && django.jQuery) || (typeof CMS !== 'undefined' && CMS.$) || false); 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/static/djangocms_moderation/svg/menu.svg b/djangocms_moderation/static/djangocms_moderation/svg/menu.svg new file mode 100644 index 00000000..30e00516 --- /dev/null +++ b/djangocms_moderation/static/djangocms_moderation/svg/menu.svg @@ -0,0 +1,2 @@ + + diff --git a/djangocms_moderation/templates/admin/djangocms_moderation/moderationcollection/change_list.html b/djangocms_moderation/templates/admin/djangocms_moderation/moderationcollection/change_list.html index f934f9f8..5c2b68f0 100644 --- a/djangocms_moderation/templates/admin/djangocms_moderation/moderationcollection/change_list.html +++ b/djangocms_moderation/templates/admin/djangocms_moderation/moderationcollection/change_list.html @@ -2,10 +2,10 @@ {% load i18n cms_static %} {% block extrahead %} {{ block.super }} - {# - INFO: we need to add styles here instead of "extrastyle" to avoid + {% comment "INFO" %} + We need to add styles here instead of "extrastyle" to avoid conflicts with adminstyle. We are adding cms.base.css to gain the icon support, e.g. `` - #} + {% endcomment %} -{% endblock extrahead %} \ No newline at end of file +{% endblock extrahead %} diff --git a/djangocms_moderation/templates/admin/djangocms_moderation/treebeard/tree_change_list.html b/djangocms_moderation/templates/admin/djangocms_moderation/treebeard/tree_change_list.html index 401ee10b..d78b39bf 100644 --- a/djangocms_moderation/templates/admin/djangocms_moderation/treebeard/tree_change_list.html +++ b/djangocms_moderation/templates/admin/djangocms_moderation/treebeard/tree_change_list.html @@ -1,15 +1,6 @@ {# Used for MP and NS trees #} -{% extends "admin/change_list.html" %} -{% load admin_list admin_tree i18n %} - -{% block extrastyle %} - {{ block.super }} - {% treebeard_css %} -{% endblock %} - -{% block extrahead %} - {{ block.super }} -{% endblock %} +{% extends "admin/tree_change_list.html" %} +{% load admin_list admin_tree %} {% block result_list %} {% if action_form and actions_on_top and cl.full_result_count %} 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/templates/djangocms_moderation/moderation_request_change_list.html b/djangocms_moderation/templates/djangocms_moderation/moderation_request_change_list.html index 5cc7f11c..7681dcab 100644 --- a/djangocms_moderation/templates/djangocms_moderation/moderation_request_change_list.html +++ b/djangocms_moderation/templates/djangocms_moderation/moderation_request_change_list.html @@ -1,13 +1,17 @@ {% extends "admin/djangocms_moderation/treebeard/tree_change_list.html" %} -{% load i18n cms_static %} +{% load i18n cms_static static %} {% block extrahead %} + {# INFO: moderation_static_url_prefix variable is used to inject static_url into actions.js #} + {{ block.super }} - {# - INFO: we need to add styles here instead of "extrastyle" to avoid + {% comment "INFO" %} + We need to add styles here instead of "extrastyle" to avoid conflicts with adminstyle. We are adding cms.base.css to gain the icon support, e.g. `` - #} + {% endcomment %} {% endblock extrahead %} 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 95d4e839..bab4d071 100644 --- a/djangocms_moderation/views.py +++ b/djangocms_moderation/views.py @@ -1,10 +1,12 @@ +from urllib.parse import quote + from django.contrib import admin, messages from django.db import transaction from django.http import Http404, HttpResponseRedirect 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, urlquote +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 @@ -81,12 +83,14 @@ 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(), ) - return_to_url = urlquote(return_to_url) + # Protect against refracted XSS attacks + # Allow : in http://, ?=& for GET parameters + return_to_url = quote(return_to_url, safe='/:?=&') if not url_is_safe: return_to_url = self.request.path return HttpResponseRedirect(return_to_url) diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 00000000..85c55ebc --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,2 @@ +.env +.venv 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 834c2434..6e6aa86e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Configuration file for the Sphinx documentation builder. # @@ -62,7 +61,7 @@ # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. -language = None +language = "en" # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. @@ -78,7 +77,16 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # -html_theme = "alabaster" +try: + import furo + + html_theme = 'furo' + html_theme_options = { + "navigation_with_keys": True, + } +except ImportError: + html_theme = 'default' + # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the 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/docs/requirements.in b/docs/requirements.in new file mode 100644 index 00000000..c9231608 --- /dev/null +++ b/docs/requirements.in @@ -0,0 +1,9 @@ +furo +Sphinx>=4.2.0 +sphinx-copybutton +sphinxext-opengraph +sphinxcontrib-spelling +sphinx-autobuild +codespell +pip-tools +docstrfmt diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 00000000..f8a8a421 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,136 @@ +# +# This file is autogenerated by pip-compile with Python 3.11 +# by the following command: +# +# pip-compile +# +alabaster==0.7.16 + # via sphinx +babel==2.14.0 + # via sphinx +beautifulsoup4==4.12.3 + # via furo +black==23.12.1 + # via docstrfmt +build==1.0.3 + # via pip-tools +certifi==2024.7.4 + # via requests +charset-normalizer==3.3.2 + # via requests +click==8.1.7 + # via + # black + # docstrfmt + # pip-tools +codespell==2.2.6 + # via -r requirements.in +colorama==0.4.6 + # via sphinx-autobuild +docstrfmt==1.6.1 + # via -r requirements.in +docutils==0.20.1 + # via + # docstrfmt + # sphinx +furo==2023.9.10 + # via -r requirements.in +idna==3.7 + # via requests +imagesize==1.4.1 + # via sphinx +jinja2==3.1.4 + # via sphinx +libcst==1.1.0 + # via docstrfmt +livereload==2.6.3 + # via sphinx-autobuild +markupsafe==2.1.4 + # via jinja2 +mypy-extensions==1.0.0 + # via + # black + # typing-inspect +packaging==23.2 + # via + # black + # build + # sphinx +pathspec==0.12.1 + # via black +pip-tools==7.3.0 + # via -r requirements.in +platformdirs==4.1.0 + # via + # black + # docstrfmt +pyenchant==3.2.2 + # via sphinxcontrib-spelling +pygments==2.17.2 + # via + # furo + # sphinx +pyproject-hooks==1.0.0 + # via build +pyyaml==6.0.1 + # via libcst +requests==2.32.0 + # via sphinx +six==1.16.0 + # via livereload +snowballstemmer==2.2.0 + # via sphinx +soupsieve==2.5 + # via beautifulsoup4 +sphinx==7.2.6 + # via + # -r requirements.in + # docstrfmt + # furo + # sphinx-autobuild + # sphinx-basic-ng + # sphinx-copybutton + # sphinxcontrib-spelling + # sphinxext-opengraph +sphinx-autobuild==2021.3.14 + # via -r requirements.in +sphinx-basic-ng==1.0.0b2 + # via furo +sphinx-copybutton==0.5.2 + # via -r requirements.in +sphinxcontrib-applehelp==1.0.8 + # via sphinx +sphinxcontrib-devhelp==1.0.6 + # via sphinx +sphinxcontrib-htmlhelp==2.0.5 + # via sphinx +sphinxcontrib-jsmath==1.0.1 + # via sphinx +sphinxcontrib-qthelp==1.0.7 + # via sphinx +sphinxcontrib-serializinghtml==1.1.10 + # via sphinx +sphinxcontrib-spelling==8.0.0 + # via -r requirements.in +sphinxext-opengraph==0.9.1 + # via -r requirements.in +tabulate==0.9.0 + # via docstrfmt +toml==0.10.2 + # via docstrfmt +tornado==6.4.2 + # via livereload +typing-extensions==4.9.0 + # via + # libcst + # typing-inspect +typing-inspect==0.9.0 + # via libcst +urllib3==2.2.2 + # via requests +wheel==0.42.0 + # via pip-tools + +# The following packages are considered to be unsafe in a requirements file: +# pip +# setuptools diff --git a/package-lock.json b/package-lock.json index c4fa32ac..22703980 100644 --- a/package-lock.json +++ b/package-lock.json @@ -385,17 +385,43 @@ "dev": true }, "autoprefixer": { - "version": "6.7.7", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-6.7.7.tgz", - "integrity": "sha1-Hb0cg1ZY41zj+ZhAmdsAWFx4IBQ=", + "version": "10.4.17", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.17.tgz", + "integrity": "sha512-/cpVNRLSfhOtcGflT13P2794gVSgmPgTR+erw5ifnMLZb0UnSlkK4tquLmkd3BhA+nLo5tX8Cu0upUsGKvKbmg==", "dev": true, "requires": { - "browserslist": "^1.7.6", - "caniuse-db": "^1.0.30000634", + "browserslist": "^4.22.2", + "caniuse-lite": "^1.0.30001578", + "fraction.js": "^4.3.7", "normalize-range": "^0.1.2", - "num2fraction": "^1.2.2", - "postcss": "^5.2.16", - "postcss-value-parser": "^3.2.3" + "picocolors": "^1.0.0", + "postcss-value-parser": "^4.2.0" + }, + "dependencies": { + "browserslist": { + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.2.tgz", + "integrity": "sha512-0UgcrvQmBDvZHFGdYUehrCNIazki7/lUP3kkoi/r3YB2amZbFM9J43ZRkJTXBUZK4gmx56+Sqk9+Vs9mwZx9+A==", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30001565", + "electron-to-chromium": "^1.4.601", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.0.13" + } + }, + "caniuse-lite": { + "version": "1.0.30001579", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001579.tgz", + "integrity": "sha512-u5AUVkixruKHJjw/pj9wISlcMpgFWzSrczLZbrqBSxukQixmg0SJ5sZTpvaFvxU0HoQKd4yoyAogyrAz9pzJnA==", + "dev": true + }, + "electron-to-chromium": { + "version": "1.4.644", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.644.tgz", + "integrity": "sha512-zOnPndwz3u1sVFSyBcRWcn0529Kz+jr+tDxN9iP69I3CpC5wlvYmjLrK2O7TEsg2oDDoUqooeXqbiHLvXvl6Lg==", + "dev": true + } } }, "aws-sign2": { @@ -1185,6 +1211,15 @@ "integrity": "sha512-DYWGk01lDcxeS/K9IHPGWfT8PsJmbXRtRd2Sx72Tnb8pcYZQFF1oSDb8hJtS1vhp212q1Rzi5dUf9+nq0o9UIg==", "dev": true }, + "bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dev": true, + "requires": { + "file-uri-to-path": "1.0.0" + } + }, "block-stream": { "version": "0.0.9", "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", @@ -1293,18 +1328,117 @@ } }, "browserify-sign": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.0.4.tgz", - "integrity": "sha1-qk62jl17ZYuqa/alfmMMvXqT0pg=", - "dev": true, - "requires": { - "bn.js": "^4.1.1", - "browserify-rsa": "^4.0.0", - "create-hash": "^1.1.0", - "create-hmac": "^1.1.2", - "elliptic": "^6.0.0", - "inherits": "^2.0.1", - "parse-asn1": "^5.0.0" + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.2.tgz", + "integrity": "sha512-1rudGyeYY42Dk6texmv7c4VcQ0EsvVbLwZkA+AQB7SxvXxmcD93jcHie8bzecJ+ChDlmAm2Qyu0+Ccg5uhZXCg==", + "dev": true, + "requires": { + "bn.js": "^5.2.1", + "browserify-rsa": "^4.1.0", + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "elliptic": "^6.5.4", + "inherits": "^2.0.4", + "parse-asn1": "^5.1.6", + "readable-stream": "^3.6.2", + "safe-buffer": "^5.2.1" + }, + "dependencies": { + "asn1.js": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", + "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", + "dev": true, + "requires": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "safer-buffer": "^2.1.0" + }, + "dependencies": { + "bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", + "dev": true + } + } + }, + "bn.js": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", + "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==", + "dev": true + }, + "browserify-rsa": { + "version": "4.1.0", + "resolved": "http://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.1.0.tgz", + "integrity": "sha512-AdEER0Hkspgno2aR97SAf6vi0y0k8NuOpGnVH3O99rcA5Q6sh8QxcngtHuJ6uXwnfAXNM4Gn1Gb7/MV1+Ymbog==", + "dev": true, + "requires": { + "bn.js": "^5.0.0", + "randombytes": "^2.0.1" + } + }, + "elliptic": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", + "integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==", + "dev": true, + "requires": { + "bn.js": "^4.11.9", + "brorand": "^1.1.0", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.1", + "inherits": "^2.0.4", + "minimalistic-assert": "^1.0.1", + "minimalistic-crypto-utils": "^1.0.1" + }, + "dependencies": { + "bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", + "dev": true + } + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "parse-asn1": { + "version": "5.1.6", + "resolved": "http://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.6.tgz", + "integrity": "sha512-RnZRo1EPU6JBnra2vGHj0yhp6ebyjBZpmUCLHWiFhxlzvBCCpAuZ7elsBp1PVAbQN0/04VD/19rfzlBSwLstMw==", + "dev": true, + "requires": { + "asn1.js": "^5.2.0", + "browserify-aes": "^1.0.0", + "evp_bytestokey": "^1.0.0", + "pbkdf2": "^3.0.3", + "safe-buffer": "^5.1.1" + } + }, + "readable-stream": { + "version": "3.6.2", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true + } } }, "browserify-zlib": { @@ -1768,21 +1902,6 @@ "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", "dev": true }, - "cosmiconfig": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-2.2.2.tgz", - "integrity": "sha512-GiNXLwAFPYHy25XmTPpafYvn3CLAkJ8FLsscq78MQd1Kh0OU6Yzhn4eV2MVF4G9WEQZoWEGltatdR+ntGPMl5A==", - "dev": true, - "requires": { - "is-directory": "^0.3.1", - "js-yaml": "^3.4.3", - "minimist": "^1.2.0", - "object-assign": "^4.1.0", - "os-homedir": "^1.0.1", - "parse-json": "^2.2.0", - "require-from-string": "^1.1.0" - } - }, "create-ecdh": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.3.tgz", @@ -1948,9 +2067,9 @@ "dev": true }, "decode-uri-component": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", - "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", + "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", "dev": true }, "deep-is": { @@ -2168,18 +2287,32 @@ "dev": true }, "elliptic": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.3.tgz", - "integrity": "sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==", + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.0.tgz", + "integrity": "sha512-dpwoQcLc/2WLQvJvLRHKZ+f9FgOdjnq11rurqwekGQygGPsYSK29OMMD2WalatiqQ+XGFDglTNixpPfI+lpaAA==", "dev": true, "requires": { - "bn.js": "^4.4.0", - "brorand": "^1.0.1", + "bn.js": "^4.11.9", + "brorand": "^1.1.0", "hash.js": "^1.0.0", - "hmac-drbg": "^1.0.0", - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0", - "minimalistic-crypto-utils": "^1.0.0" + "hmac-drbg": "^1.0.1", + "inherits": "^2.0.4", + "minimalistic-assert": "^1.0.1", + "minimalistic-crypto-utils": "^1.0.1" + }, + "dependencies": { + "bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", + "dev": true + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + } } }, "emoji-regex": { @@ -2315,6 +2448,12 @@ "es6-symbol": "^3.1.1" } }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true + }, "escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -2807,6 +2946,12 @@ "object-assign": "^4.0.1" } }, + "file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "dev": true + }, "fill-range": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", @@ -2954,6 +3099,12 @@ } } }, + "fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "dev": true + }, "fragment-cache": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", @@ -2970,588 +3121,20 @@ "dev": true }, "fsevents": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.4.tgz", - "integrity": "sha512-z8H8/diyk76B7q5wg+Ud0+CqzcAF3mBBI/bA5ne5zrRUUIvNkJY//D3BqyH571KuAC4Nr7Rw7CjWX4r0y9DvNg==", + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", + "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", "dev": true, - "optional": true, "requires": { - "nan": "^2.9.2", - "node-pre-gyp": "^0.10.0" + "bindings": "^1.5.0", + "nan": "^2.12.1" }, "dependencies": { - "abbrev": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "ansi-regex": { - "version": "2.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "aproba": { - "version": "1.2.0", - "bundled": true, - "dev": true, - "optional": true - }, - "are-we-there-yet": { - "version": "1.1.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" - } - }, - "balanced-match": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "brace-expansion": { - "version": "1.1.11", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "chownr": { - "version": "1.0.1", - "bundled": true - }, - "code-point-at": { - "version": "1.1.0", - "bundled": true, - "dev": true, - "optional": true - }, - "concat-map": { - "version": "0.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "console-control-strings": { - "version": "1.1.0", - "bundled": true, - "dev": true, - "optional": true - }, - "core-util-is": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "debug": { - "version": "2.6.9", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ms": "2.0.0" - } - }, - "deep-extend": { - "version": "0.5.1", - "bundled": true, - "dev": true, - "optional": true - }, - "delegates": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "detect-libc": { - "version": "1.0.3", - "bundled": true, - "dev": true, - "optional": true - }, - "fs-minipass": { - "version": "1.2.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minipass": "^2.2.1" - } - }, - "fs.realpath": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "gauge": { - "version": "2.7.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "aproba": "^1.0.3", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", - "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" - } - }, - "glob": { - "version": "7.1.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "has-unicode": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "iconv-lite": { - "version": "0.4.21", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safer-buffer": "^2.1.0" - } - }, - "ignore-walk": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minimatch": "^3.0.4" - } - }, - "inflight": { - "version": "1.0.6", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.3", - "bundled": true, - "dev": true, - "optional": true - }, - "ini": { - "version": "1.3.5", - "bundled": true, - "dev": true, - "optional": true - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "isarray": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "minimatch": { - "version": "3.0.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "0.0.8", - "bundled": true, - "dev": true, - "optional": true - }, - "minipass": { - "version": "2.2.4", - "bundled": true, - "optional": true, - "requires": { - "safe-buffer": "^5.1.1", - "yallist": "^3.0.0" - } - }, - "minizlib": { - "version": "1.1.0", - "bundled": true, - "requires": { - "minipass": "^2.2.1" - } - }, - "mkdirp": { - "version": "0.5.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minimist": "0.0.8" - } - }, - "ms": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "needle": { - "version": "2.2.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "debug": "^2.1.2", - "iconv-lite": "^0.4.4", - "sax": "^1.2.4" - } - }, - "node-pre-gyp": { - "version": "0.10.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "detect-libc": "^1.0.2", - "mkdirp": "^0.5.1", - "needle": "^2.2.0", - "nopt": "^4.0.1", - "npm-packlist": "^1.1.6", - "npmlog": "^4.0.2", - "rc": "^1.1.7", - "rimraf": "^2.6.1", - "semver": "^5.3.0", - "tar": "^4" - } - }, - "nopt": { - "version": "4.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "abbrev": "1", - "osenv": "^0.1.4" - } - }, - "npm-bundled": { - "version": "1.0.3", - "bundled": true, - "dev": true, - "optional": true - }, - "npm-packlist": { - "version": "1.1.10", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ignore-walk": "^3.0.1", - "npm-bundled": "^1.0.1" - } - }, - "npmlog": { - "version": "4.1.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "object-assign": { - "version": "4.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "once": { - "version": "1.4.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "wrappy": "1" - } - }, - "os-homedir": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "os-tmpdir": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "osenv": { - "version": "0.1.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "process-nextick-args": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "rc": { - "version": "1.2.7", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "deep-extend": "^0.5.1", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "bundled": true, - "dev": true, - "optional": true - } - } - }, - "readable-stream": { - "version": "2.3.6", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "rimraf": { - "version": "2.6.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "glob": "^7.0.5" - } - }, - "safe-buffer": { - "version": "5.1.1", - "bundled": true, - "optional": true - }, - "safer-buffer": { - "version": "2.1.2", - "bundled": true, - "dev": true, - "optional": true - }, - "sax": { - "version": "1.2.4", - "bundled": true, - "dev": true, - "optional": true - }, - "semver": { - "version": "5.5.0", - "bundled": true, - "dev": true, - "optional": true - }, - "set-blocking": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "signal-exit": { - "version": "3.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "string-width": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - }, - "string_decoder": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "strip-json-comments": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "tar": { - "version": "4.4.13", - "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.13.tgz", - "integrity": "sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==", - "dev": true, - "optional": true, - "requires": { - "chownr": "^1.1.1", - "fs-minipass": "^1.2.5", - "minipass": "^2.8.6", - "minizlib": "^1.2.1", - "mkdirp": "^0.5.0", - "safe-buffer": "^5.1.2", - "yallist": "^3.0.3" - }, - "dependencies": { - "chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "dev": true, - "optional": true - }, - "minipass": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz", - "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", - "dev": true, - "optional": true, - "requires": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" - } - }, - "minizlib": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz", - "integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==", - "dev": true, - "optional": true, - "requires": { - "minipass": "^2.9.0" - } - }, - "safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, - "optional": true - }, - "yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true, - "optional": true - } - } - }, - "util-deprecate": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "wide-align": { - "version": "1.1.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "string-width": "^1.0.2" - } - }, - "wrappy": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "yallist": { - "version": "3.0.2", - "bundled": true, - "optional": true + "nan": { + "version": "2.18.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.18.0.tgz", + "integrity": "sha512-W7tfG7vMOGtD30sHoZSSc/JVYiyDPEyQVso/Zz+/uQd0B0L46gtC+pHha5FFMRpil6fm/AoEcRWyOVi4+E/f8w==", + "dev": true } } }, @@ -4357,80 +3940,33 @@ } }, "gulp-postcss": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/gulp-postcss/-/gulp-postcss-6.4.0.tgz", - "integrity": "sha1-eKMuPIeqbNzsWuHJBeGW1HjoxdU=", + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/gulp-postcss/-/gulp-postcss-9.1.0.tgz", + "integrity": "sha512-a843mcKPApfeI987uqQbc8l50xXeWIXBsiVvYxtCI5XtVAMzTi/HnU2qzQpGwkB/PAOfsLV8OsqDv2iJZ9qvdw==", "dev": true, "requires": { - "gulp-util": "^3.0.8", - "postcss": "^5.2.12", - "postcss-load-config": "^1.2.0", + "fancy-log": "^2.0.0", + "plugin-error": "^2.0.1", + "postcss-load-config": "^5.0.0", "vinyl-sourcemaps-apply": "^0.2.1" }, "dependencies": { - "clone": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", - "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=", - "dev": true - }, - "dateformat": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-2.2.0.tgz", - "integrity": "sha1-QGXiATz5+5Ft39gu+1Bq1MZ2kGI=", - "dev": true - }, - "gulp-util": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/gulp-util/-/gulp-util-3.0.8.tgz", - "integrity": "sha1-AFTh50RQLifATBh8PsxQXdVLu08=", - "dev": true, - "requires": { - "array-differ": "^1.0.0", - "array-uniq": "^1.0.2", - "beeper": "^1.0.0", - "chalk": "^1.0.0", - "dateformat": "^2.0.0", - "fancy-log": "^1.1.0", - "gulplog": "^1.0.0", - "has-gulplog": "^0.1.0", - "lodash._reescape": "^3.0.0", - "lodash._reevaluate": "^3.0.0", - "lodash._reinterpolate": "^3.0.0", - "lodash.template": "^3.0.0", - "minimist": "^1.1.0", - "multipipe": "^0.1.2", - "object-assign": "^3.0.0", - "replace-ext": "0.0.1", - "through2": "^2.0.0", - "vinyl": "^0.5.0" - } - }, - "object-assign": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-3.0.0.tgz", - "integrity": "sha1-m+3VygiXlJvKR+f/QIBi1Un1h/I=", - "dev": true - }, - "through2": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.3.tgz", - "integrity": "sha1-AARWmzfHx0ujnEPzzteNGtlBQL4=", + "fancy-log": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fancy-log/-/fancy-log-2.0.0.tgz", + "integrity": "sha512-9CzxZbACXMUXW13tS0tI8XsGGmxWzO2DmYrGuBJOJ8k8q2K7hwfJA5qHjuPPe8wtsco33YR9wc+Rlr5wYFvhSA==", "dev": true, "requires": { - "readable-stream": "^2.1.5", - "xtend": "~4.0.1" + "color-support": "^1.1.3" } }, - "vinyl": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-0.5.3.tgz", - "integrity": "sha1-sEVbOPxeDPMNQyUTLkYZcMIJHN4=", + "plugin-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-2.0.1.tgz", + "integrity": "sha512-zMakqvIDyY40xHOvzXka0kUvf40nYIuwRE8dWhti2WtjQZ31xAgBZBhxsK7vK3QbRXS1Xms/LO7B5cuAsfB2Gg==", "dev": true, "requires": { - "clone": "^1.0.0", - "clone-stats": "^0.0.1", - "replace-ext": "0.0.1" + "ansi-colors": "^1.0.1" } } } @@ -4584,12 +4120,6 @@ "ansi-regex": "^2.0.0" } }, - "has-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", - "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=", - "dev": true - }, "has-gulplog": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/has-gulplog/-/has-gulplog-0.1.0.tgz", @@ -4688,9 +4218,9 @@ } }, "hosted-git-info": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.7.1.tgz", - "integrity": "sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w==", + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", "dev": true }, "http-signature": { @@ -4984,12 +4514,6 @@ } } }, - "is-directory": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz", - "integrity": "sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=", - "dev": true - }, "is-extendable": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", @@ -5348,6 +4872,12 @@ "resolve": "^1.1.7" } }, + "lilconfig": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.0.0.tgz", + "integrity": "sha512-K2U4W2Ff5ibV7j7ydLr+zLAkIg5JJ4lPn1Ltsdt+Tz/IjQ8buJ55pZAxoP34lqIiwtF9iAvtLv3JGv7CAyAg+g==", + "dev": true + }, "load-json-file": { "version": "1.1.0", "resolved": "http://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", @@ -5742,9 +5272,9 @@ } }, "minimist": { - "version": "1.2.3", - "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.3.tgz", - "integrity": "sha512-+bMdgqjMN/Z77a6NlY/I3U5LlRDbnmaAk6lDveAPKwSpcPM4tKAuYsvYF8xjhOPXhOYGe/73vVLVez5PW+jqhw==", + "version": "1.2.6", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", "dev": true }, "mixin-deep": { @@ -5806,13 +5336,6 @@ "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=", "dev": true }, - "nan": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.11.0.tgz", - "integrity": "sha512-F4miItu2rGnV2ySkXOQoA8FKz/SR2Q2sWP0sbTxNxz/tuokeC8WxOhPMcwi0qIyGtVn/rrSeLbvVkznqCdwYnw==", - "dev": true, - "optional": true - }, "nanomatch": { "version": "1.2.13", "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", @@ -5915,6 +5438,12 @@ "vm-browserify": "0.0.4" } }, + "node-releases": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", + "dev": true + }, "node-sass": { "version": "4.14.1", "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.14.1.tgz", @@ -6202,12 +5731,6 @@ "strip-ansi": "^5.0.0" } }, - "y18n": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", - "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", - "dev": true - }, "yargs": { "version": "13.3.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", @@ -6271,7 +5794,7 @@ "normalize-range": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", - "integrity": "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", "dev": true }, "npm-run-path": { @@ -6295,12 +5818,6 @@ "set-blocking": "~2.0.0" } }, - "num2fraction": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz", - "integrity": "sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4=", - "dev": true - }, "number-is-nan": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", @@ -6643,6 +6160,12 @@ "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", "dev": true }, + "picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, "pify": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", @@ -6697,54 +6220,20 @@ "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", "dev": true }, - "postcss": { - "version": "5.2.18", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", - "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", - "dev": true, - "requires": { - "chalk": "^1.1.3", - "js-base64": "^2.1.9", - "source-map": "^0.5.6", - "supports-color": "^3.2.3" - } - }, "postcss-load-config": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-1.2.0.tgz", - "integrity": "sha1-U56a/J3chiASHr+djDZz4M5Q0oo=", - "dev": true, - "requires": { - "cosmiconfig": "^2.1.0", - "object-assign": "^4.1.0", - "postcss-load-options": "^1.2.0", - "postcss-load-plugins": "^2.3.0" - } - }, - "postcss-load-options": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/postcss-load-options/-/postcss-load-options-1.2.0.tgz", - "integrity": "sha1-sJixVZ3awt8EvAuzdfmaXP4rbYw=", - "dev": true, - "requires": { - "cosmiconfig": "^2.1.0", - "object-assign": "^4.1.0" - } - }, - "postcss-load-plugins": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/postcss-load-plugins/-/postcss-load-plugins-2.3.0.tgz", - "integrity": "sha1-dFdoEWWZrKLwCfrUJrABdQSdjZI=", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-5.0.2.tgz", + "integrity": "sha512-Q8QR3FYbqOKa0bnC1UQ2bFq9/ulHX5Bi34muzitMr8aDtUelO5xKeJEYC/5smE0jNE9zdB/NBnOwXKexELbRlw==", "dev": true, "requires": { - "cosmiconfig": "^2.1.1", - "object-assign": "^4.1.0" + "lilconfig": "^3.0.0", + "yaml": "^2.3.4" } }, "postcss-value-parser": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.0.tgz", - "integrity": "sha1-h/OPnxj3dKSrTIojL1xc6IcqnRU=", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "dev": true }, "prelude-ls": { @@ -6821,9 +6310,9 @@ "dev": true }, "qs": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", + "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", "dev": true }, "querystring": { @@ -7116,12 +6605,6 @@ "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", "dev": true }, - "require-from-string": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-1.2.1.tgz", - "integrity": "sha1-UpyczvJzgK3+yaL5ZbZJu+5jZBg=", - "dev": true - }, "require-main-filename": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", @@ -7757,15 +7240,6 @@ "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", "dev": true }, - "supports-color": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", - "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", - "dev": true, - "requires": { - "has-flag": "^1.0.0" - } - }, "table": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/table/-/table-4.0.2.tgz", @@ -8207,6 +7681,16 @@ "integrity": "sha512-bzpH/oBhoS/QI/YtbkqCg6VEiPYjSZtrHQM6/QnJS6OL9pKUFLqb3aFh4Scvwm45+7iAgiMkLhSbaZxUqmrprw==", "dev": true }, + "update-browserslist-db": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", + "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "dev": true, + "requires": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + } + }, "uri-js": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", @@ -8539,6 +8023,12 @@ "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", "dev": true }, + "y18n": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.2.tgz", + "integrity": "sha512-uGZHXkHnhF0XeeAPgnKfPv1bgKAYyVvmNL1xlKsPYZPaIHxGti2hHqvOCQv71XMsLxu1QjergkqogUnms5D3YQ==", + "dev": true + }, "yargs": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-8.0.2.tgz", @@ -8673,9 +8163,9 @@ "dev": true }, "y18n": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", - "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", "dev": true }, "yallist": { @@ -8683,6 +8173,12 @@ "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", "dev": true + }, + "yaml": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz", + "integrity": "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==", + "dev": true } } } diff --git a/package.json b/package.json index af5a7748..9f70ede9 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "djangoCMS", "private": true, "devDependencies": { - "autoprefixer": "^6.3.6", + "autoprefixer": "^10.4.17", "babel-core": "^6.24.1", "babel-eslint": "^7.2.3", "babel-loader": "^7.1.0", @@ -19,13 +19,13 @@ "gulp-eslint": "^3.0.1", "gulp-if": "1.2.5", "gulp-plumber": "^1.1.0", - "gulp-postcss": "^6.4.0", + "gulp-postcss": "^9.1.0", "gulp-sass": "3.1.0", "gulp-sourcemaps": "2.4.1", "gulp-util": "3.0.5", "imports-loader": "^0.7.1", "karma-sourcemap-loader": "^0.3.7", - "minimist": "1.2.3", + "minimist": "1.2.6", "webpack": "^3.0.0" }, "dependencies": { diff --git a/setup.cfg b/setup.cfg index 5a79ae38..f5268d24 100644 --- a/setup.cfg +++ b/setup.cfg @@ -7,6 +7,7 @@ exclude = **/migrations/, build/, .tox/, + docs/conf.py, node_modules/, [isort] @@ -45,3 +46,6 @@ exclude_lines = raise NotImplementedError if 0: if __name__ == .__main__.: + +[codespell] +ignore-words-list = alpha-numeric,assertIn,THIRDPARTY diff --git a/setup.py b/setup.py index 0835848d..8259e8e9 100644 --- a/setup.py +++ b/setup.py @@ -4,11 +4,15 @@ INSTALL_REQUIREMENTS = [ - "Django>=2.2,<4.0", + "Django>=3.2", "django-cms", "django-sekizai>=0.7", "django-admin-sortable2>=0.6.4", ] +DEPENDENCY_LINKS = [ + "https://github.com/django-cms/django-cms/tarball/release/4.0.1.x#egg=django-cms", + "https://github.com/django-cms/djangocms-versioning/tarball/1.2.2#egg=djangocms-versioning", +] setup( name="djangocms-moderation", @@ -18,16 +22,30 @@ 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", "Topic :: Software Development", ], install_requires=INSTALL_REQUIREMENTS, + dependency_links=DEPENDENCY_LINKS, author="Divio AG", author_email="info@divio.ch", - url="http://github.com/divio/djangocms-moderation", + maintainer='Django CMS Association and contributors', + maintainer_email='info@django-cms.org', + url="https://github.com/django-cms/djangocms-moderation", license="BSD", test_suite="tests.settings.run", ) 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 d19b191e..55f6c506 100644 --- a/tests/requirements/dj32_cms40.txt +++ b/tests/requirements/dj32_cms40.txt @@ -2,5 +2,11 @@ Django>=3.2,<4.0 -django-admin-sortable2 +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..1f5e7b9c --- /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/FidelityInternational/djangocms-version-locking/tarball/master#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..824d76f5 --- /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.1 +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..8461ac03 --- /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.1 +djangocms-alias>=2.0.0 diff --git a/tests/requirements/requirements_base.txt b/tests/requirements/requirements_base.txt index b7cd9390..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,9 +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/develop-4#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/master#egg=djangocms-versioning -https://github.com/FidelityInternational/djangocms-version-locking/tarball/master#egg=djangocms-version-locking +djangocms-text-ckeditor diff --git a/tests/settings.py b/tests/settings.py index 67561ba3..067e6670 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -1,17 +1,17 @@ +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", - + "djangocms_alias", "djangocms_text_ckeditor", "tests.utils.moderated_polls", "tests.utils.versioned_none_moderated_app", @@ -22,15 +22,21 @@ "auth": None, "cms": None, "menus": None, + "djangocms_alias": None, "djangocms_versioning": None, "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.py b/tests/test_admin.py index b72e1eae..2c44b403 100644 --- a/tests/test_admin.py +++ b/tests/test_admin.py @@ -4,6 +4,8 @@ from django.test.client import RequestFactory from django.urls import reverse +from cms.utils.urlutils import admin_reverse + from djangocms_versioning.test_utils import factories from djangocms_moderation import conf, constants @@ -352,6 +354,7 @@ def test_tree_admin_list_links_to_moderation_request_change_view(self): ), self.mr1.pk, ) + self.assertHTMLEqual(result, expected) @@ -394,6 +397,7 @@ def setUp(self): self.mr_tree_admin = ModerationRequestTreeAdmin( ModerationRequest, admin.AdminSite() ) + self.mra = ModerationRequestAdmin(ModerationRequest, admin.AdminSite()) self.mca = ModerationCollectionAdmin(ModerationCollection, admin.AdminSite()) @@ -412,3 +416,35 @@ def test_tree_admin_change_list_shows_additional_configured_fields(self): self.assertIn("get_poll_additional_changelist_field", self.mr_tree_admin.get_list_display(mock_request)) + + def test_tree_admin_burger_menu_present(self): + redirect_url = reverse('admin:djangocms_moderation_moderationrequest_changelist') + url = "{}?moderation_request__collection__id={}".format( + redirect_url, + self.collection.id + ) + with self.login_user_context(self.user): + response = self.client.get(url) + + self.assertContains(response, '/static/djangocms_moderation/js/burger.js') + + def test_workflow_admin_renders_correctly(self): + url = admin_reverse("djangocms_moderation_workflow_change", args=(self.wf.pk,)) + + with self.login_user_context(self.get_superuser()): + result = self.client.get(url, follow=True) + + self.assertEqual(result.status_code, 200) + self.assertContains(result, self.wf.name) + + # django-admin-sortable2 injected its inputs + self.assertContains(result, '') + self.assertContains(result, '= "2.0.2": + self.assertEqual("", edit_link) + else: + 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 +79,23 @@ 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) _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 +127,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 d496d48d..3c4ed742 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 @@ -6,7 +6,7 @@ from django.urls import reverse from cms.test_utils.testcases import CMSTestCase -from cms.utils.urlutils import add_url_parameters +from cms.utils.urlutils import add_url_parameters, admin_reverse from djangocms_versioning.test_utils.factories import PageVersionFactory @@ -17,7 +17,7 @@ ) from djangocms_moderation.utils import get_admin_url -from .utils.base import BaseViewTestCase +from .utils.base import AssertQueryMixin, BaseViewTestCase from .utils.factories import ( ChildModerationRequestTreeNodeFactory, ModerationCollectionFactory, @@ -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) @@ -550,15 +574,20 @@ def test_adding_page_not_by_the_author_doesnt_trigger_nested_collection_mechanis ModerationRequestTreeNode.objects.filter(moderation_request=mr1.first()).count(), 0 ) - def test_collection_with_redirect_url_query_string_parameter_sanitisation(self): + def test_collection_with_redirect_url_query_redirect_sanitisation(self): + """ + Reflected XSS Protection by ensuring that harmful characters are encoded + + 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() collection = ModerationCollectionFactory(author=user) page1_version = PageVersionFactory(created_by=user) page2_version = PageVersionFactory(created_by=user) - - redirect_to_url = f"""{reverse( - "admin:djangocms_moderation_moderationcollection_changelist" - )}?=""" + opts = page1_version.versionable.version_model_proxy._meta + redirect_to_url = admin_reverse(f"{opts.app_label}_{opts.model_name}_changelist") + redirect_to_url += f"?page={page1_version.content.page.id}&" url = add_url_parameters( get_admin_url( @@ -578,11 +607,106 @@ def test_collection_with_redirect_url_query_string_parameter_sanitisation(self): follow=False, ) + messages = [message.message for message in list(get_messages(response.wsgi_request))] + self.assertEqual(response.status_code, 302) - self.assertIn("%3F%3D%3Cscript%3Ealert%28%27attack%21%27%29%3C/script%3E", response.url) + self.assertIn("%3Cscript%3Ealert%28%27attack%21%27%29%3C/script%3E", response.url) + self.assertIn(f"?page={page1_version.content.page.id}", response.url) + self.assertIn( + "2 items successfully added to moderation collection", + messages, + ) + self.assertNotIn( + "Perhaps it was deleted", + messages, + ) + + +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) -class CollectionItemsViewTest(CMSTestCase): + 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(AssertQueryMixin, CMSTestCase): def setUp(self): self.client.force_login(self.get_superuser()) self.url = get_admin_url( @@ -613,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) @@ -622,7 +746,7 @@ def test_initial_form_values_when_collection_id_passed(self): self.assertEqual( response.context["form"].initial["collection"], str(collection.pk) ) - self.assertQuerysetEqual( + self.assertQuerySetEqual( response.context["form"].initial["versions"], [pg_version.pk, poll_version.pk], transform=lambda o: o.pk, @@ -632,13 +756,13 @@ 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) self.assertEqual(response.status_code, 200) self.assertEqual(len(response.context["form"].initial.keys()), 1) - self.assertQuerysetEqual( + self.assertQuerySetEqual( response.context["form"].initial["versions"], [pg_version.pk, poll_version.pk], transform=lambda o: o.pk, @@ -688,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, @@ -702,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, @@ -731,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 @@ -758,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,), @@ -804,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,), @@ -926,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): """ @@ -960,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) @@ -1022,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) @@ -1077,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) @@ -1122,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) @@ -1182,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) @@ -1214,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): """ @@ -1241,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) @@ -1271,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/app_1/__init__.py b/tests/utils/app_1/__init__.py index 03f81427..e69de29b 100644 --- a/tests/utils/app_1/__init__.py +++ b/tests/utils/app_1/__init__.py @@ -1 +0,0 @@ -default_app_config = "tests.utils.app_1.apps.App1Config" diff --git a/tests/utils/app_2/__init__.py b/tests/utils/app_2/__init__.py index ff4e78c0..e69de29b 100644 --- a/tests/utils/app_2/__init__.py +++ b/tests/utils/app_2/__init__.py @@ -1 +0,0 @@ -default_app_config = "tests.utils.app_2.apps.App2Config" diff --git a/tests/utils/base.py b/tests/utils/base.py index 073308fb..ee9997a0 100644 --- a/tests/utils/base.py +++ b/tests/utils/base.py @@ -6,6 +6,7 @@ from djangocms_versioning.test_utils.factories import PageVersionFactory from djangocms_moderation import constants +from djangocms_moderation.compact import DJANGO_4_1 from djangocms_moderation.models import ( ModerationCollection, ModerationRequest, @@ -18,6 +19,16 @@ class MockRequest: GET = {} +class AssertQueryMixin: + """Mixin to append uppercase `assertQuerySetEqual` for TestCase class + if django version below 4.2 + """ + + if DJANGO_4_1: + def assertQuerySetEqual(self, *args, **kwargs): + return self.assertQuerysetEqual(*args, **kwargs) + + class BaseTestCase(CMSTestCase): @classmethod def setUpTestData(cls): diff --git a/tests/utils/factories.py b/tests/utils/factories.py index 74a44fe4..2c9c442b 100644 --- a/tests/utils/factories.py +++ b/tests/utils/factories.py @@ -10,6 +10,7 @@ AbstractVersionFactory, PageVersionFactory, ) +from factory.django import DjangoModelFactory from factory.fuzzy import FuzzyChoice, FuzzyInteger, FuzzyText from djangocms_moderation.models import ( @@ -45,7 +46,7 @@ def get_plugin_language(plugin): # also be None unless set manually -class PlaceholderFactory(factory.django.DjangoModelFactory): +class PlaceholderFactory(DjangoModelFactory): default_width = FuzzyInteger(0, 25) slot = FuzzyText(length=2, chars=string.digits) # NOTE: When using this factory you will probably want to set @@ -55,14 +56,14 @@ class Meta: model = Placeholder -class PollFactory(factory.django.DjangoModelFactory): +class PollFactory(DjangoModelFactory): name = FuzzyText(length=6) class Meta: model = Poll -class PollContentFactory(factory.django.DjangoModelFactory): +class PollContentFactory(DjangoModelFactory): poll = factory.SubFactory(PollFactory) language = FuzzyChoice(["en", "fr", "it"]) text = FuzzyText(length=24) @@ -71,7 +72,7 @@ class Meta: model = PollContent -class PollPluginFactory(factory.django.DjangoModelFactory): +class PollPluginFactory(DjangoModelFactory): language = factory.LazyAttribute(get_plugin_language) placeholder = factory.SubFactory(PlaceholderFactory) parent = None @@ -93,14 +94,14 @@ class Meta: # None Moderated Poll App factories -class NoneModeratedPollFactory(factory.django.DjangoModelFactory): +class NoneModeratedPollFactory(DjangoModelFactory): name = FuzzyText(length=6) class Meta: model = NoneModeratedPoll -class NoneModeratedPollContentFactory(factory.django.DjangoModelFactory): +class NoneModeratedPollContentFactory(DjangoModelFactory): poll = factory.SubFactory(NoneModeratedPollFactory) language = FuzzyChoice(["en", "fr", "it"]) text = FuzzyText(length=24) @@ -116,7 +117,7 @@ class Meta: model = Version -class NoneModeratedPollPluginFactory(factory.django.DjangoModelFactory): +class NoneModeratedPollPluginFactory(DjangoModelFactory): language = factory.LazyAttribute(get_plugin_language) placeholder = factory.SubFactory(PlaceholderFactory) parent = None @@ -128,12 +129,12 @@ class Meta: model = NoneModeratedPollPlugin -class UserFactory(factory.django.DjangoModelFactory): +class UserFactory(DjangoModelFactory): username = FuzzyText(length=12) 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: @@ -145,14 +146,14 @@ def _create(cls, model_class, *args, **kwargs): return manager.create_user(*args, **kwargs) -class WorkflowFactory(factory.django.DjangoModelFactory): +class WorkflowFactory(DjangoModelFactory): name = FuzzyText(length=12) class Meta: model = Workflow -class ModerationCollectionFactory(factory.django.DjangoModelFactory): +class ModerationCollectionFactory(DjangoModelFactory): name = FuzzyText(length=12) author = factory.SubFactory(UserFactory) workflow = factory.SubFactory(WorkflowFactory) @@ -161,7 +162,7 @@ class Meta: model = ModerationCollection -class ModerationRequestFactory(factory.django.DjangoModelFactory): +class ModerationRequestFactory(DjangoModelFactory): collection = factory.SubFactory(ModerationCollectionFactory) version = factory.SubFactory(PageVersionFactory) language = 'en' @@ -171,7 +172,7 @@ class Meta: model = ModerationRequest -class RootModerationRequestTreeNodeFactory(factory.django.DjangoModelFactory): +class RootModerationRequestTreeNodeFactory(DjangoModelFactory): moderation_request = factory.SubFactory(ModerationRequestFactory) class Meta: @@ -183,7 +184,7 @@ def _create(cls, model_class, *args, **kwargs): return model_class.add_root(*args, **kwargs) -class ChildModerationRequestTreeNodeFactory(factory.django.DjangoModelFactory): +class ChildModerationRequestTreeNodeFactory(DjangoModelFactory): moderation_request = factory.SubFactory(ModerationRequestFactory) parent = factory.SubFactory(RootModerationRequestTreeNodeFactory) diff --git a/tests/utils/moderated_polls/__init__.py b/tests/utils/moderated_polls/__init__.py index d70b54ee..e69de29b 100644 --- a/tests/utils/moderated_polls/__init__.py +++ b/tests/utils/moderated_polls/__init__.py @@ -1 +0,0 @@ -default_app_config = "tests.utils.moderated_polls.apps.PollsConfig" diff --git a/tests/utils/moderated_polls/cms_plugins.py b/tests/utils/moderated_polls/cms_plugins.py index 4603205e..0e5cd10b 100644 --- a/tests/utils/moderated_polls/cms_plugins.py +++ b/tests/utils/moderated_polls/cms_plugins.py @@ -1,7 +1,12 @@ from cms.plugin_base import CMSPluginBase from cms.plugin_pool import plugin_pool -from .models import PollPlugin as Poll +from .models import ( + DeeplyNestedPollPlugin as DeeplyNestedPoll, + ManytoManyPollPlugin as ManytoManyPoll, + NestedPollPlugin as NestedPoll, + PollPlugin as Poll, +) @plugin_pool.register_plugin @@ -10,3 +15,27 @@ class PollPlugin(CMSPluginBase): name = "Poll" allow_children = True render_template = "polls/poll.html" + + +@plugin_pool.register_plugin +class NestedPollPlugin(CMSPluginBase): + model = NestedPoll + name = "NestedPoll" + allow_children = True + render_template = "polls/nested_poll.html" + + +@plugin_pool.register_plugin +class DeeplyNestedPollPlugin(CMSPluginBase): + model = DeeplyNestedPoll + name = "DeeplyNestedPoll" + allow_children = True + render_template = "polls/deeply_nested_poll.html" + + +@plugin_pool.register_plugin +class ManytoManyPollPlugin(CMSPluginBase): + model = ManytoManyPoll + name = "ManytoManyPoll" + allow_children = True + render_template = "polls/many_polls.html" diff --git a/tests/utils/moderated_polls/factories.py b/tests/utils/moderated_polls/factories.py new file mode 100644 index 00000000..4819f262 --- /dev/null +++ b/tests/utils/moderated_polls/factories.py @@ -0,0 +1,76 @@ +import factory +from factory.django import DjangoModelFactory + +from ..factories import ( + PlaceholderFactory, + PollFactory, + get_plugin_language, + get_plugin_position, +) +from .models import ( + DeeplyNestedPoll, + DeeplyNestedPollPlugin, + ManytoManyPollPlugin, + NestedPoll, + NestedPollPlugin, +) + + +class NestedPollFactory(DjangoModelFactory): + poll = factory.SubFactory(PollFactory) + + class Meta: + model = NestedPoll + + +class ManytoManyPollPluginFactory(DjangoModelFactory): + language = factory.LazyAttribute(get_plugin_language) + placeholder = factory.SubFactory(PlaceholderFactory) + parent = None + position = factory.LazyAttribute(get_plugin_position) + plugin_type = "ManytoManyPollPlugin" + + class Meta: + model = ManytoManyPollPlugin + + @factory.post_generation + def polls(self, create, extracted, **kwargs): + if not create: + # Simple build, do nothing. + return + + if extracted: + # A list of groups were passed in, use them + for poll in extracted: + self.polls.add(poll) + + +class NestedPollPluginFactory(DjangoModelFactory): + language = factory.LazyAttribute(get_plugin_language) + placeholder = factory.SubFactory(PlaceholderFactory) + parent = None + position = factory.LazyAttribute(get_plugin_position) + plugin_type = "NestedPollPlugin" + nested_poll = factory.SubFactory(NestedPollFactory) + + class Meta: + model = NestedPollPlugin + + +class DeeplyNestedPollFactory(DjangoModelFactory): + nested_poll = factory.SubFactory(NestedPollFactory) + + class Meta: + model = DeeplyNestedPoll + + +class DeeplyNestedPollPluginFactory(DjangoModelFactory): + language = factory.LazyAttribute(get_plugin_language) + placeholder = factory.SubFactory(PlaceholderFactory) + parent = None + position = factory.LazyAttribute(get_plugin_position) + plugin_type = "DeeplyNestedPollPlugin" + deeply_nested_poll = factory.SubFactory(DeeplyNestedPollFactory) + + class Meta: + model = DeeplyNestedPollPlugin diff --git a/tests/utils/moderated_polls/models.py b/tests/utils/moderated_polls/models.py index 32437bb6..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): @@ -61,3 +61,59 @@ class PollPlugin(CMSPlugin): def __str__(self): return str(self.poll) + + +class NestedPoll(models.Model): + poll = models.ForeignKey(Poll, on_delete=models.CASCADE) + + def __str__(self): + return self.poll + + +class NestedPollPlugin(CMSPlugin): + cmsplugin_ptr = models.OneToOneField( + CMSPlugin, + on_delete=models.CASCADE, + related_name="%(app_label)s_%(class)s", + parent_link=True, + ) + + nested_poll = models.ForeignKey(NestedPoll, on_delete=models.CASCADE) + + def __str__(self): + return str(self.nested_poll) + + +class DeeplyNestedPoll(models.Model): + nested_poll = models.ForeignKey(NestedPoll, on_delete=models.CASCADE) + + def __str__(self): + return self.nested_poll + + +class DeeplyNestedPollPlugin(CMSPlugin): + cmsplugin_ptr = models.OneToOneField( + CMSPlugin, + on_delete=models.CASCADE, + related_name="%(app_label)s_%(class)s", + parent_link=True, + ) + + deeply_nested_poll = models.ForeignKey(DeeplyNestedPoll, on_delete=models.CASCADE) + + def __str__(self): + return str(self.deeply_nested_poll) + + +class ManytoManyPollPlugin(CMSPlugin): + cmsplugin_ptr = models.OneToOneField( + CMSPlugin, + on_delete=models.CASCADE, + related_name="%(app_label)s_%(class)s", + parent_link=True, + ) + + polls = models.ManyToManyField(Poll) + + def __str__(self): + return str(self.polls.first()) diff --git a/tests/utils/moderated_polls/templates/polls/deeply_nested_poll.html b/tests/utils/moderated_polls/templates/polls/deeply_nested_poll.html new file mode 100644 index 00000000..ce2d8874 --- /dev/null +++ b/tests/utils/moderated_polls/templates/polls/deeply_nested_poll.html @@ -0,0 +1 @@ +{% load polls_tags %}{% render_poll instance.deeply_nested_poll.nested_poll.poll %} diff --git a/tests/utils/moderated_polls/templates/polls/many_polls.html b/tests/utils/moderated_polls/templates/polls/many_polls.html new file mode 100644 index 00000000..d419d98b --- /dev/null +++ b/tests/utils/moderated_polls/templates/polls/many_polls.html @@ -0,0 +1 @@ +{% load polls_tags %}{% render_poll instance.polls.first %} diff --git a/tests/utils/moderated_polls/templates/polls/nested_poll.html b/tests/utils/moderated_polls/templates/polls/nested_poll.html new file mode 100644 index 00000000..7d62f3ef --- /dev/null +++ b/tests/utils/moderated_polls/templates/polls/nested_poll.html @@ -0,0 +1 @@ +{% load polls_tags %}{% render_poll instance.nested_poll.poll %} diff --git a/tests/utils/versioned_none_moderated_app/__init__.py b/tests/utils/versioned_none_moderated_app/__init__.py index dc0b47c0..e69de29b 100644 --- a/tests/utils/versioned_none_moderated_app/__init__.py +++ b/tests/utils/versioned_none_moderated_app/__init__.py @@ -1,3 +0,0 @@ -default_app_config = ( - "tests.utils.versioned_none_moderated_app.apps.VersionedNoneModeratedAppConfig" -) 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): diff --git a/tox.ini b/tox.ini index 24c8a857..3012f5a8 100644 --- a/tox.ini +++ b/tox.ini @@ -1,21 +1,20 @@ [tox] envlist = - dj32-flake8 - dj32-isort - py{37,38,39}-dj{22,32}-sqlite-cms4 + flake8 + isort + py{38,39}-dj{22,32}-sqlite-cms4 skip_missing_interpreters=True [testenv] deps = - dj22: -r{toxinidir}/tests/requirements/django_22.txt + dj22: -r{toxinidir}/tests/requirements/dj22_cms40.txt dj22: Django>=2.2,<2.3 - dj32: -r{toxinidir}/tests/requirements/django_32.txt + dj32: -r{toxinidir}/tests/requirements/dj32_cms40.txt dj32: Django>=3.2,<3.3 basepython = - py37: python3.7 py38: python3.8 py39: python3.9 @@ -26,9 +25,11 @@ commands = {env:COMMAND:coverage} report [testenv:flake8] -commands = flake8 basepython = python3.9 +commands = flake8 +deps = flake8 [testenv:isort] -commands = isort --recursive --check-only --diff {toxinidir} basepython = python3.9 +commands = isort --check-only --diff {toxinidir} +deps = isort