├── .coveragerc ├── .editorconfig ├── .github ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── codeql.yml │ ├── lint.yml │ ├── publish-to-live-pypi.yml │ ├── publish-to-test-pypi.yml │ └── test.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .tx └── config ├── CHANGELOG.rst ├── LICENSE ├── MANIFEST.in ├── README.rst ├── addon.json ├── aldryn_config.py ├── djangocms_transfer ├── __init__.py ├── apps.py ├── cms_plugins.py ├── cms_toolbars.py ├── compat.py ├── datastructures.py ├── exporter.py ├── forms.py ├── importer.py ├── locale │ ├── de │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── en │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── es │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ └── fr │ │ └── LC_MESSAGES │ │ ├── django.mo │ │ └── django.po ├── static │ └── djangocms_transfer │ │ └── css │ │ └── transfer.css ├── templates │ └── djangocms_transfer │ │ ├── import_plugins.html │ │ └── placeholder_close_frame.html └── utils.py ├── preview.gif ├── pyproject.toml ├── setup.py ├── tests ├── __init__.py ├── conftest.py ├── requirements │ ├── base.txt │ ├── dj42_cms41.txt │ ├── dj42_cms50.txt │ ├── dj50_cms41.txt │ ├── dj50_cms50.txt │ ├── dj51_cms41.txt │ ├── dj51_cms50.txt │ ├── dj52_cms41.txt │ └── dj52_cms50.txt ├── settings.py ├── templates │ ├── base.html │ └── page.html ├── test_migrations.py └── transfer │ ├── __init__.py │ ├── abstract.py │ ├── test_export.py │ ├── test_forms.py │ ├── test_helper.py │ └── test_import.py ├── tools └── black └── tox.ini /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | source = djangocms_transfer 4 | omit = 5 | migrations/* 6 | tests/* 7 | 8 | [report] 9 | exclude_lines = 10 | pragma: no cover 11 | def __repr__ 12 | if self.debug: 13 | if settings.DEBUG 14 | raise AssertionError 15 | raise NotImplementedError 16 | if 0: 17 | if __name__ == .__main__.: 18 | ignore_errors = True 19 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 4 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | max_line_length = 80 13 | 14 | [*.py] 15 | max_line_length = 120 16 | quote_type = single 17 | 18 | [*.{scss,js,html}] 19 | max_line_length = 120 20 | indent_style = space 21 | quote_type = double 22 | 23 | [*.js] 24 | max_line_length = 120 25 | quote_type = single 26 | 27 | [*.rst] 28 | max_line_length = 80 29 | 30 | [*.yml] 31 | indent_size = 2 32 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | 7 | 8 | ## Related resources 9 | 10 | 14 | 15 | * #... 16 | * #... 17 | 18 | ## Checklist 19 | 20 | 25 | 26 | * [ ] I have opened this pull request against ``master`` 27 | * [ ] I have added or modified the tests when changing logic 28 | * [ ] I have followed [the conventional commits guidelines](https://www.conventionalcommits.org/) to add meaningful information into the changelog 29 | * [ ] I have read the [contribution guidelines ](https://github.com/django-cms/django-cms/blob/develop/CONTRIBUTING.rst) and I have joined #workgroup-pr-review on 30 | [Discord](https://www.django-cms.org/discord) to find a “pr review buddy” who is going to review my pull request. 31 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | schedule: 9 | - cron: "36 2 * * 2" 10 | 11 | jobs: 12 | analyze: 13 | name: Analyze 14 | runs-on: ubuntu-latest 15 | permissions: 16 | actions: read 17 | contents: read 18 | security-events: write 19 | 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | language: [ javascript, python ] 24 | 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v4 28 | 29 | - name: Initialize CodeQL 30 | uses: github/codeql-action/init@v2 31 | with: 32 | languages: ${{ matrix.language }} 33 | queries: +security-and-quality 34 | 35 | - name: Autobuild 36 | uses: github/codeql-action/autobuild@v2 37 | if: ${{ matrix.language == 'javascript' || matrix.language == 'python' }} 38 | 39 | - name: Perform CodeQL Analysis 40 | uses: github/codeql-action/analyze@v2 41 | with: 42 | category: "/language:${{ matrix.language }}" 43 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: [push, pull_request] 4 | 5 | concurrency: 6 | group: ${{ github.workflow }}-${{ github.ref }} 7 | cancel-in-progress: true 8 | 9 | jobs: 10 | ruff: 11 | name: ruff 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | - name: Set up Python 17 | uses: actions/setup-python@v5 18 | with: 19 | python-version: "3.12" 20 | cache: "pip" 21 | - run: | 22 | python -m pip install --upgrade pip 23 | pip install ruff 24 | - name: Run Ruff 25 | run: ruff check djangocms_transfer tests 26 | -------------------------------------------------------------------------------- /.github/workflows/publish-to-live-pypi.yml: -------------------------------------------------------------------------------- 1 | name: Publish Python 🐍 distributions 📦 to pypi 2 | 3 | on: 4 | release: 5 | types: 6 | - published 7 | 8 | jobs: 9 | build-n-publish: 10 | name: Build and publish Python 🐍 distributions 📦 to pypi 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - name: Set up Python 3.12 15 | uses: actions/setup-python@v5 16 | with: 17 | python-version: '3.12' 18 | 19 | - name: Install pypa/build 20 | run: >- 21 | python -m 22 | pip install 23 | build 24 | --user 25 | - name: Build a binary wheel and a source tarball 26 | run: >- 27 | python -m 28 | build 29 | --sdist 30 | --wheel 31 | --outdir dist/ 32 | . 33 | 34 | - name: Publish distribution 📦 to PyPI 35 | if: startsWith(github.ref, 'refs/tags') 36 | uses: pypa/gh-action-pypi-publish@release/v1 37 | with: 38 | user: __token__ 39 | password: ${{ secrets.PYPI_API_TOKEN }} 40 | -------------------------------------------------------------------------------- /.github/workflows/publish-to-test-pypi.yml: -------------------------------------------------------------------------------- 1 | name: Publish Python 🐍 distributions 📦 to TestPyPI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | build-n-publish: 10 | name: Build and publish Python 🐍 distributions 📦 to TestPyPI 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - name: Set up Python 3.12 15 | uses: actions/setup-python@v5 16 | with: 17 | python-version: '3.12' 18 | 19 | - name: Install pypa/build 20 | run: >- 21 | python -m 22 | pip install 23 | build 24 | --user 25 | - name: Build a binary wheel and a source tarball 26 | run: >- 27 | python -m 28 | build 29 | --sdist 30 | --wheel 31 | --outdir dist/ 32 | . 33 | 34 | - name: Publish distribution 📦 to Test PyPI 35 | uses: pypa/gh-action-pypi-publish@release/v1 36 | with: 37 | user: __token__ 38 | password: ${{ secrets.TEST_PYPI_API_TOKEN }} 39 | repository_url: https://test.pypi.org/legacy/ 40 | skip_existing: true 41 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: CodeCov 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | unit-tests: 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | fail-fast: false 10 | matrix: 11 | python-version: [ "3.9", "3.10", "3.11", "3.12", "3.13"] 12 | requirements-file: [ 13 | dj42_cms41.txt, 14 | dj50_cms41.txt, 15 | dj51_cms41.txt, 16 | dj52_cms41.txt, 17 | dj42_cms50.txt, 18 | dj50_cms50.txt, 19 | dj51_cms50.txt, 20 | dj52_cms50.txt, 21 | ] 22 | os: [ 23 | ubuntu-latest, 24 | ] 25 | exclude: 26 | - requirements-file: dj50_cms41.txt 27 | python-version: 3.9 28 | - requirements-file: dj51_cms41.txt 29 | python-version: 3.9 30 | - requirements-file: dj52_cms41.txt 31 | python-version: 3.9 32 | - requirements-file: dj50_cms50.txt 33 | python-version: 3.9 34 | - requirements-file: dj51_cms50.txt 35 | python-version: 3.9 36 | - requirements-file: dj52_cms50.txt 37 | python-version: 3.9 38 | steps: 39 | - uses: actions/checkout@v4 40 | - name: Set up Python ${{ matrix.python-version }} 41 | uses: actions/setup-python@v5 42 | with: 43 | python-version: ${{ matrix.python-version }} 44 | - name: Install dependencies 45 | run: | 46 | python -m pip install --upgrade pip 47 | pip install -r tests/requirements/${{ matrix.requirements-file }} 48 | python setup.py install 49 | 50 | - name: Run coverage 51 | run: coverage run -m pytest 52 | 53 | - name: Upload Coverage to Codecov 54 | uses: codecov/codecov-action@v5 55 | with: 56 | token: ${{ secrets.CODECOV_TOKEN }} 57 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | *$py.class 3 | *.egg-info 4 | *.log 5 | *.pot 6 | .DS_Store 7 | .coverage 8 | .coverage/ 9 | .eggs/ 10 | .idea/ 11 | .project/ 12 | .pydevproject/ 13 | .vscode/ 14 | .settings/ 15 | .tox/ 16 | __pycache__/ 17 | build/ 18 | dist/ 19 | env/ 20 | .venv/ 21 | 22 | /~ 23 | /node_modules 24 | .sass-cache 25 | *.css.map 26 | npm-debug.log 27 | package-lock.json 28 | 29 | local.sqlite 30 | 31 | # vi 32 | *.swp 33 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | ci: 2 | autofix_commit_msg: | 3 | ci: auto fixes from pre-commit hooks 4 | for more information, see https://pre-commit.ci 5 | autofix_prs: false 6 | autoupdate_commit_msg: 'ci: pre-commit autoupdate' 7 | autoupdate_schedule: monthly 8 | 9 | repos: 10 | - repo: https://github.com/pre-commit/pre-commit-hooks 11 | rev: v4.1.0 12 | hooks: 13 | - id: trailing-whitespace 14 | - id: end-of-file-fixer 15 | - id: check-yaml 16 | - id: check-added-large-files 17 | - id: check-merge-conflict 18 | - id: debug-statements 19 | - id: mixed-line-ending 20 | - id: trailing-whitespace 21 | - repo: https://github.com/asottile/pyupgrade 22 | rev: v3.2.0 23 | hooks: 24 | - id: pyupgrade 25 | args: ["--py39-plus"] 26 | - repo: https://github.com/adamchainz/django-upgrade 27 | rev: '1.4.0' 28 | hooks: 29 | - id: django-upgrade 30 | args: [--target-version, "4.2"] 31 | - repo: https://github.com/PyCQA/flake8 32 | rev: 4.0.1 33 | hooks: 34 | - id: flake8 35 | - repo: https://github.com/asottile/yesqa 36 | rev: v1.3.0 37 | hooks: 38 | - id: yesqa 39 | - repo: https://github.com/pycqa/isort 40 | rev: 5.10.1 41 | hooks: 42 | - id: isort 43 | - repo: https://github.com/codespell-project/codespell 44 | rev: v2.1.0 45 | hooks: 46 | - id: codespell 47 | args: ["--ignore-words-list", "ist, oder, alle"] 48 | -------------------------------------------------------------------------------- /.tx/config: -------------------------------------------------------------------------------- 1 | [main] 2 | host = https://www.transifex.com 3 | 4 | [djangocms-transfer.djangocms_transfer] 5 | file_filter = djangocms_transfer/locale//LC_MESSAGES/django.po 6 | source_file = djangocms_transfer/locale/en/LC_MESSAGES/django.po 7 | source_lang = en 8 | type = PO 9 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | ========= 2 | Changelog 3 | ========= 4 | 5 | 2.0.0 (2025-08-09) 6 | ================== 7 | 8 | * Add support for django CMS 4 9 | * Drop support for django CMS 3.7 - 3.11 10 | * Add support for Django 4.2, 5.0 and 5.1 11 | 12 | 13 | 1.0.2 (2024-02-29) 14 | ================== 15 | 16 | Changes 17 | ------- 18 | 19 | * make export filenames a bit more unique by @arneb in https://github.com/django-cms/djangocms-transfer/pull/15 20 | * added customization of export/import data via user defined functions by @wfehr in https://github.com/django-cms/djangocms-transfer/pull/20 21 | * Fix error on export when getting filename by @wfehr in https://github.com/django-cms/djangocms-transfer/pull/33 22 | * Improved testing environment and fixed existing errors by @wfehr in https://github.com/django-cms/djangocms-transfer/pull/35 23 | 24 | New Contributors 25 | ---------------- 26 | 27 | * @wfehr made their first contribution in https://github.com/django-cms/djangocms-transfer/pull/20 28 | 29 | 30 | 1.0.1 (2023-03-13) 31 | ================== 32 | 33 | * Added support for Django 3.2 and 4.1 34 | * Added support for django up to CMS 3.11 35 | * Serializer made configurable through Django setting DJANGO_CMS_TRANSFER_SERIALIZER 36 | 37 | 1.0.0 (2020-09-02) 38 | ================== 39 | 40 | * Added support for Django 3.1 41 | * Dropped support for Python 2.7 and Python 3.4 42 | * Dropped support for Django < 2.2 43 | 44 | 45 | 0.3.0 (2020-04-23) 46 | ================== 47 | 48 | * Added support for Django 3.0 49 | * Added support for Python 3.8 50 | 51 | 52 | 0.2.0 (2019-05-23) 53 | ================== 54 | 55 | * Added support for Django 2.2 and django CMS 3.7 56 | * Removed support for Django 2.0 57 | * Extended test matrix 58 | * Added isort and adapted imports 59 | * Adapted code base to align with other supported addons 60 | 61 | 62 | 0.1.0 (2018-12-18) 63 | ================== 64 | 65 | * Public release 66 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017, Divio AG 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of Divio AG nor the 12 | names of its contributors may be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL DIVIO AG BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.rst 3 | recursive-include djangocms_transfer/static * 4 | recursive-include djangocms_transfer/templates * 5 | recursive-exclude * *.py[co] 6 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | =================== 2 | django CMS Transfer 3 | =================== 4 | 5 | |pypi| |coverage| |python| |django| |djangocms| 6 | 7 | 8 | **django CMS Transfer** is an **experimental** package that allows you to export 9 | and import plugin data from a page or a placeholder. It does not support foreign 10 | key relations and won't import/export related data, such as `media `_. 11 | 12 | .. note:: 13 | 14 | This project is endorsed by the `django CMS Association `_. 15 | That means that it is officially accepted by the dCA as being in line with our roadmap vision and development/plugin policy. 16 | Join us on `Slack `_. 17 | 18 | .. image:: preview.gif 19 | 20 | 21 | Documentation 22 | ============= 23 | 24 | The setting ``DJANGO_CMS_TRANSFER_SERIALIZER`` allows registration of a custom JSON serializer. An example use case would be subclassing Django's built-in Python serializer to base64-encode inline image data. 25 | 26 | See ``REQUIREMENTS`` in the `setup.py `_ 27 | file for additional dependencies: 28 | 29 | 30 | 31 | Installation 32 | ------------ 33 | 34 | For a manual install: 35 | 36 | * run ``pip install djangocms-transfer`` 37 | * add ``djangocms_transfer`` to your ``INSTALLED_APPS`` 38 | 39 | 40 | Version Compatibility 41 | --------------------- 42 | 43 | For django CMS 4.0 or later, you must use djangocms-transfer 2.0 or later. 44 | 45 | For django CMS 3.7 through 3.11 use versions 1.x of djangocms-transfer. 46 | 47 | 48 | How to Use 49 | ---------- 50 | 51 | To export/import a page, click on the "*Page*" menu on the toolbar 52 | and select your desired choice. 53 | 54 | To export/import a plugin, Open the "*Structure board*", click on the 55 | dropdown menu for the specific plugin and select your choice. 56 | 57 | 58 | Customization 59 | ------------- 60 | 61 | Following settings are available: 62 | 63 | * **DJANGOCMS_TRANSFER_PROCESS_EXPORT_PLUGIN_DATA**: 64 | 65 | Enables processing of plugin instances prior to serialization, e.g. 66 | ``myapp.module.function``. 67 | 68 | * **DJANGOCMS_TRANSFER_PROCESS_IMPORT_PLUGIN_DATA**: 69 | 70 | Enables processing of plugin instances prior to saving, e.g. 71 | ``myapp.module.function``. 72 | For example: set default-values for ForeignKeys (images for django_filer, ..) 73 | 74 | As an example the combination of ``_PROCESS_EXPORT_PLUGIN_DATA`` and 75 | ``_PROCESS_IMPORT_PLUGIN_DATA`` lets you export and import the data between 76 | different systems while setting the contents as you need it:: 77 | 78 | # settings.py 79 | .._PROCESS_EXPORT_PLUGIN_DATA = "myapp.some.module.export_function" 80 | .._PROCESS_IMPORT_PLUGIN_DATA = "myapp.some.module.import_function" 81 | 82 | # custom functions 83 | def export_function(plugin, plugin_data): 84 | # remove child-plugins which can't be handled 85 | if plugin.parent_id and plugin.parent.plugin_type == "SomeParentPlugin": 86 | return None 87 | # change data 88 | if plugin.plugin_type == "SomePlugin": 89 | plugin_data["data"].update({ 90 | "some_field": "TODO: change me", 91 | }) 92 | return plugin_data 93 | 94 | def import_function(plugin, plugin_data): 95 | some_related_fallback_object = MyModel.objects.first() 96 | for field in plugin._meta.fields: 97 | # example of setting a default value for a related field 98 | if isinstance(field, ForeignKey): 99 | plugin_value = getattr(plugin, field.attname) 100 | raw_value = plugin_data[field.name] 101 | if ( 102 | field.related_model == MyModel 103 | # related object is referenced, but does not exist 104 | and plugin_value is None and raw_value is not None 105 | ): 106 | setattr(plugin, field.name, some_related_fallback_object) 107 | # There is no guarantee on whether djangocms-transfer saves the 108 | # plugin. It is recommended to do this yourself in case you changed 109 | # the plugin. 110 | plugin.save() 111 | 112 | 113 | Running Tests 114 | ------------- 115 | 116 | You can run tests by executing:: 117 | 118 | python -m venv env 119 | source env/bin/activate 120 | pip install -r tests/requirements/dj51_cms41.txt 121 | coverage run -m pytest 122 | 123 | 124 | ******************************************* 125 | Contribute to this project and win rewards 126 | ******************************************* 127 | 128 | Because this is an open-source project, we welcome everyone to 129 | `get involved in the project `_ and 130 | `receive a reward `_ for their contribution. 131 | Become part of a fantastic community and help us make django CMS the best CMS in the world. 132 | 133 | We'll be delighted to receive your 134 | feedback in the form of issues and pull requests. Before submitting your 135 | pull request, please review our `contribution guidelines 136 | `_. 137 | 138 | We're grateful to all contributors who have helped create and maintain this package. 139 | Contributors are listed at the `contributors `_ 140 | section. 141 | 142 | 143 | .. |pypi| image:: https://badge.fury.io/py/djangocms-transfer.svg 144 | :target: http://badge.fury.io/py/djangocms-transfer 145 | .. |coverage| image:: https://codecov.io/gh/django-cms/djangocms-transfer/branch/master/graph/badge.svg 146 | :target: https://codecov.io/gh/django-cms/djangocms-transfer 147 | 148 | .. |python| image:: https://img.shields.io/badge/python-3.9+-blue.svg 149 | :target: https://pypi.org/project/djangocms-transfer/ 150 | .. |django| image:: https://img.shields.io/badge/django-4.2,%205.0,%205.1-blue.svg 151 | :target: https://www.djangoproject.com/ 152 | .. |djangocms| image:: https://img.shields.io/badge/django%20CMS-4-blue.svg 153 | :target: https://www.django-cms.org/ 154 | -------------------------------------------------------------------------------- /addon.json: -------------------------------------------------------------------------------- 1 | { 2 | "package-name": "djangocms-transfer", 3 | "installed-apps": [ 4 | "djangocms_transfer" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /aldryn_config.py: -------------------------------------------------------------------------------- 1 | from aldryn_client import forms 2 | 3 | 4 | class Form(forms.BaseForm): 5 | def to_settings(self, data, settings): 6 | return settings 7 | -------------------------------------------------------------------------------- /djangocms_transfer/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "2.0.0" 2 | 3 | default_app_config = "djangocms_transfer.apps.TranferConfig" 4 | 5 | 6 | def get_serializer_name(default="python"): 7 | from django.conf import settings 8 | 9 | return getattr(settings, "DJANGO_CMS_TRANSFER_SERIALIZER", default) 10 | 11 | 12 | def custom_process_hook(transfer_hook, plugin, plugin_data=None): 13 | from importlib import import_module 14 | 15 | from django.conf import settings 16 | 17 | hook = getattr(settings, transfer_hook, None) 18 | if not hook: 19 | return plugin_data 20 | 21 | module, function = hook.rsplit(".", 1) 22 | try: 23 | func = getattr(import_module(module), function) 24 | except (ImportError, AttributeError) as e: 25 | raise ImportError(f"Could not import '{hook}': {e}") 26 | 27 | if plugin_data: 28 | return func(plugin, plugin_data) 29 | else: 30 | return func(plugin) 31 | -------------------------------------------------------------------------------- /djangocms_transfer/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | from django.utils.translation import gettext_lazy as _ 3 | 4 | 5 | class TranferConfig(AppConfig): 6 | name = "djangocms_transfer" 7 | verbose_name = _("django CMS Transfer") 8 | -------------------------------------------------------------------------------- /djangocms_transfer/cms_plugins.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from cms.plugin_base import CMSPluginBase, PluginMenuItem 4 | from cms.plugin_pool import plugin_pool 5 | from cms.utils import get_language_from_request 6 | from cms.utils.urlutils import admin_reverse 7 | from django.core.exceptions import PermissionDenied 8 | from django.http import HttpResponse, HttpResponseBadRequest 9 | from django.shortcuts import render 10 | from django.urls import re_path, reverse 11 | from django.utils.http import urlencode 12 | from django.utils.translation import gettext_lazy as _ 13 | 14 | from .forms import ExportImportForm, PluginExportForm, PluginImportForm 15 | 16 | 17 | class PluginImporter(CMSPluginBase): 18 | system = True 19 | render_plugin = False 20 | 21 | def get_plugin_urls(self): 22 | urlpatterns = [ 23 | re_path( 24 | r"^export-plugins/$", 25 | self.export_plugins_view, 26 | name="cms_export_plugins", 27 | ), 28 | re_path( 29 | r"^import-plugins/$", 30 | self.import_plugins_view, 31 | name="cms_import_plugins", 32 | ), 33 | ] 34 | return urlpatterns 35 | 36 | def get_extra_global_plugin_menu_items(self, request, plugin): 37 | # django-cms 3.4 compatibility 38 | return self.get_extra_plugin_menu_items(request, plugin) 39 | 40 | @classmethod 41 | def get_extra_plugin_menu_items(cls, request, plugin): 42 | if plugin.plugin_type == cls.__name__: 43 | return 44 | 45 | data = urlencode( 46 | { 47 | "language": get_language_from_request(request), 48 | "plugin": plugin.pk, 49 | } 50 | ) 51 | return [ 52 | PluginMenuItem( 53 | _("Export plugins"), 54 | admin_reverse("cms_export_plugins") + "?" + data, 55 | data={}, 56 | action="none", 57 | attributes={ 58 | "icon": "export", 59 | }, 60 | ), 61 | PluginMenuItem( 62 | _("Import plugins"), 63 | admin_reverse("cms_import_plugins") + "?" + data, 64 | data={}, 65 | action="modal", 66 | attributes={ 67 | "icon": "import", 68 | }, 69 | ), 70 | ] 71 | 72 | @classmethod 73 | def get_extra_placeholder_menu_items(cls, request, placeholder): # noqa 74 | data = urlencode( 75 | { 76 | "language": get_language_from_request(request), 77 | "placeholder": placeholder.pk, 78 | } 79 | ) 80 | return [ 81 | PluginMenuItem( 82 | _("Export plugins"), 83 | admin_reverse("cms_export_plugins") + "?" + data, 84 | data={}, 85 | action="none", 86 | attributes={ 87 | "icon": "export", 88 | }, 89 | ), 90 | PluginMenuItem( 91 | _("Import plugins"), 92 | admin_reverse("cms_import_plugins") + "?" + data, 93 | data={}, 94 | action="modal", 95 | attributes={ 96 | "icon": "import", 97 | }, 98 | ), 99 | ] 100 | 101 | @classmethod 102 | def import_plugins_view(cls, request): 103 | if not request.user.is_staff: 104 | raise PermissionDenied 105 | 106 | new_form = ExportImportForm(request.GET or None) 107 | 108 | if new_form.is_valid(): 109 | initial_data = new_form.cleaned_data 110 | else: 111 | initial_data = None 112 | 113 | if request.method == "GET" and not new_form.is_valid(): 114 | return HttpResponseBadRequest(_("Form received unexpected values.")) 115 | 116 | import_form = PluginImportForm( 117 | data=request.POST or None, 118 | files=request.FILES or None, 119 | initial=initial_data, 120 | ) 121 | 122 | if not import_form.is_valid(): 123 | opts = cls.model._meta 124 | context = { 125 | "form": import_form, 126 | "has_change_permission": True, 127 | "opts": opts, 128 | "root_path": reverse("admin:index"), 129 | "is_popup": True, 130 | "app_label": opts.app_label, 131 | "media": (cls().media + import_form.media), 132 | } 133 | return render(request, "djangocms_transfer/import_plugins.html", context) 134 | 135 | plugin = import_form.cleaned_data.get("plugin") 136 | language = import_form.cleaned_data["language"] 137 | 138 | if plugin: 139 | root_id = plugin.pk 140 | placeholder = plugin.placeholder 141 | else: 142 | root_id = None 143 | placeholder = import_form.cleaned_data.get("placeholder") 144 | 145 | if not placeholder: 146 | # Page placeholders/plugins import 147 | # TODO: Check permissions 148 | import_form.run_import() 149 | return HttpResponse( 150 | '
' 151 | ) 152 | 153 | tree_order = placeholder.get_plugin_tree_order(language, parent_id=root_id) 154 | # TODO: Check permissions 155 | import_form.run_import() 156 | 157 | if plugin: 158 | new_plugins = plugin.reload().get_descendants().exclude(pk__in=tree_order) 159 | return plugin.get_plugin_class_instance().render_close_frame( 160 | request, obj=new_plugins[0] 161 | ) 162 | 163 | from cms.toolbar.utils import get_plugin_tree 164 | 165 | # Placeholder plugins import 166 | new_plugins = placeholder.get_plugins(language).exclude(pk__in=tree_order) 167 | data = get_plugin_tree(request, list(new_plugins)) 168 | data["plugin_order"] = tree_order + ["__COPY__"] 169 | data["target_placeholder_id"] = placeholder.pk 170 | context = {"structure_data": json.dumps(data)} 171 | return render( 172 | request, "djangocms_transfer/placeholder_close_frame.html", context 173 | ) 174 | 175 | @classmethod 176 | def export_plugins_view(cls, request): 177 | if not request.user.is_staff: 178 | raise PermissionDenied 179 | 180 | form = PluginExportForm(request.GET or None) 181 | 182 | if not form.is_valid(): 183 | return HttpResponseBadRequest(_("Form received unexpected values.")) 184 | 185 | # TODO: Check permissions 186 | filename = form.get_filename() 187 | response = HttpResponse(form.run_export(), content_type='application/json') 188 | response['Content-Disposition'] = f'attachment; filename={filename}' 189 | return response 190 | 191 | 192 | plugin_pool.register_plugin(PluginImporter) 193 | -------------------------------------------------------------------------------- /djangocms_transfer/cms_toolbars.py: -------------------------------------------------------------------------------- 1 | from cms.models import PageContent 2 | from cms.toolbar_base import CMSToolbar 3 | from cms.toolbar_pool import toolbar_pool 4 | from cms.utils.page_permissions import user_can_change_page 5 | from cms.utils.urlutils import admin_reverse 6 | from django.utils.http import urlencode 7 | from django.utils.translation import gettext 8 | 9 | 10 | @toolbar_pool.register 11 | class PluginImporter(CMSToolbar): 12 | class Media: 13 | css = {"all": ("djangocms_transfer/css/transfer.css",)} 14 | 15 | def populate(self): 16 | # always use draft if we have a page 17 | page = self.request.current_page 18 | 19 | if not page: 20 | return 21 | 22 | if not user_can_change_page(self.request.user, page): 23 | return 24 | 25 | page_menu = self.toolbar.get_menu("page") 26 | 27 | if not page_menu or page_menu.disabled: 28 | return 29 | 30 | obj = self.toolbar.get_object() 31 | if not obj: 32 | return 33 | if not isinstance(obj, PageContent): 34 | return 35 | 36 | data = urlencode( 37 | { 38 | "language": self.current_lang, 39 | "cms_pagecontent": obj.pk, 40 | } 41 | ) 42 | 43 | not_edit_mode = not self.toolbar.edit_mode_active 44 | 45 | page_menu.add_break("Page menu importer break") 46 | page_menu.add_link_item( 47 | gettext("Export"), 48 | url=admin_reverse("cms_export_plugins") + "?" + data, 49 | disabled=not_edit_mode, 50 | ) 51 | page_menu.add_modal_item( 52 | gettext("Import"), 53 | url=admin_reverse("cms_import_plugins") + "?" + data, 54 | disabled=not_edit_mode, 55 | on_close=getattr(self.toolbar, "request_path", self.request.path), 56 | ) 57 | -------------------------------------------------------------------------------- /djangocms_transfer/compat.py: -------------------------------------------------------------------------------- 1 | import cms 2 | from packaging.version import Version 3 | 4 | cms_version = Version(cms.__version__) 5 | -------------------------------------------------------------------------------- /djangocms_transfer/datastructures.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | from collections import namedtuple 3 | from typing import Callable 4 | 5 | from cms.api import add_plugin 6 | from cms.models import CMSPlugin 7 | from django.conf import settings 8 | from django.core.serializers import deserialize 9 | from django.db import transaction 10 | from django.utils.encoding import force_str 11 | from django.utils.functional import cached_property 12 | 13 | from . import custom_process_hook, get_serializer_name 14 | from .utils import get_plugin_model 15 | 16 | BaseArchivedPlugin = namedtuple( 17 | "ArchivedPlugin", 18 | ["pk", "creation_date", "position", "plugin_type", "parent_id", "data"], 19 | ) 20 | 21 | ArchivedPlaceholder = namedtuple("ArchivedPlaceholder", ["slot", "plugins"]) 22 | 23 | 24 | class ArchivedPlugin(BaseArchivedPlugin): 25 | @cached_property 26 | def model(self): 27 | return get_plugin_model(self.plugin_type) 28 | 29 | @cached_property 30 | def deserialized_instance(self): 31 | data = { 32 | "model": force_str(self.model._meta), 33 | "fields": self.data, 34 | } 35 | 36 | # TODO: Handle deserialization error 37 | return list(deserialize(get_serializer_name(), [data]))[0] 38 | 39 | @transaction.atomic 40 | def restore(self, placeholder, language, parent=None): 41 | m2m_data = {} 42 | data = self.data.copy() 43 | 44 | if self.model is not CMSPlugin: 45 | fields = self.model._meta.get_fields() 46 | for field in fields: 47 | if field.related_model is not None: 48 | if field.many_to_many: 49 | if data.get(field.name): 50 | m2m_data[field.name] = data[field.name] 51 | data.pop(field.name, None) 52 | elif data.get(field.name): 53 | try: 54 | obj = field.related_model.objects.get(pk=data[field.name]) 55 | except field.related_model.DoesNotExist: 56 | obj = None 57 | data[field.name] = obj 58 | 59 | # The "data" field for LinkPlugin have key, "target" 60 | # which clashes with :func:add_plugin when unpacked. 61 | plugin_target = data.pop("target", "") 62 | 63 | plugin = add_plugin( 64 | placeholder, 65 | self.plugin_type, 66 | language, 67 | position="last-child", 68 | target=parent, 69 | **data, 70 | ) 71 | self._call_user_site_import_processor_if_necessary(plugin, self.data) 72 | 73 | field = ("target",) if plugin_target else () 74 | # An empty *update_fields* iterable will skip the save 75 | plugin.target = plugin_target 76 | plugin.save(update_fields=field) 77 | 78 | if self.model is not CMSPlugin: 79 | fields = self.model._meta.get_fields() 80 | for field in fields: 81 | if field.related_model is not None and m2m_data.get(field.name): 82 | if field.many_to_many: 83 | objs = field.related_model.objects.filter( 84 | pk__in=m2m_data[field.name] 85 | ) 86 | attr = getattr(plugin, field.name) 87 | attr.set(objs) 88 | 89 | # customize plugin-data on import with configured function 90 | custom_process_hook( 91 | "DJANGOCMS_TRANSFER_PROCESS_IMPORT_PLUGIN_DATA", 92 | plugin 93 | ) 94 | 95 | plugin.save() 96 | return plugin 97 | 98 | def _call_user_site_import_processor_if_necessary( 99 | self, plugin: CMSPlugin, plugin_data: dict 100 | ): 101 | # customize plugin-data on import with configured function 102 | if processor_symbol := getattr( 103 | settings, "DJANGOCMS_TRANSFER_PROCESS_IMPORT_PLUGIN_DATA", None 104 | ): 105 | function = self._resolve_function_from_full_path(processor_symbol) 106 | function(plugin, plugin_data) 107 | 108 | def _resolve_function_from_full_path( 109 | self, fully_qualified_path: str 110 | ) -> Callable: 111 | try: 112 | module_name, function_name = fully_qualified_path.rsplit(".", 1) 113 | module = importlib.import_module(module_name) 114 | return getattr(module, function_name) 115 | except (AttributeError, ImportError): 116 | raise ImportError(f"could not resolve {fully_qualified_path}") 117 | -------------------------------------------------------------------------------- /djangocms_transfer/exporter.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import json 3 | 4 | from cms.utils.plugins import get_bound_plugins 5 | from django.core import serializers 6 | from django.core.serializers.json import DjangoJSONEncoder 7 | 8 | from . import custom_process_hook, get_serializer_name 9 | from .utils import get_plugin_fields 10 | 11 | dump_json = functools.partial(json.dumps, cls=DjangoJSONEncoder) 12 | 13 | 14 | def get_plugin_data(plugin, only_meta=False): 15 | if only_meta: 16 | custom_data = None 17 | else: 18 | plugin_fields = get_plugin_fields(plugin.plugin_type) 19 | _plugin_data = serializers.serialize( 20 | get_serializer_name(), (plugin,), fields=plugin_fields 21 | )[0] 22 | custom_data = _plugin_data["fields"] 23 | 24 | plugin_data = { 25 | "pk": plugin.pk, 26 | "creation_date": plugin.creation_date, 27 | "position": plugin.position, 28 | "plugin_type": plugin.plugin_type, 29 | "parent_id": plugin.parent_id, 30 | "data": custom_data, 31 | } 32 | 33 | # customize plugin-data on export 34 | return custom_process_hook( 35 | "DJANGOCMS_TRANSFER_PROCESS_EXPORT_PLUGIN_DATA", 36 | plugin, 37 | plugin_data 38 | ) 39 | 40 | 41 | def export_plugin(plugin): 42 | data = get_plugin_export_data(plugin) 43 | return dump_json(data) 44 | 45 | 46 | def export_placeholder(placeholder, language): 47 | data = get_placeholder_export_data(placeholder, language) 48 | return dump_json(data) 49 | 50 | 51 | def export_page(cms_pagecontent, language): 52 | data = get_page_export_data(cms_pagecontent, language) 53 | return dump_json(data) 54 | 55 | 56 | def get_plugin_export_data(plugin): 57 | descendants = plugin.get_descendants() 58 | plugin_data = [get_plugin_data(plugin=plugin)] 59 | plugin_data[0]["parent_id"] = None 60 | plugin_data.extend( 61 | get_plugin_data(plugin) for plugin in get_bound_plugins(descendants) 62 | ) 63 | return plugin_data 64 | 65 | 66 | def get_placeholder_export_data(placeholder, language): 67 | plugins = placeholder.get_plugins(language) 68 | # The following results in two queries; 69 | # First all the root plugins are fetched, then all child plugins. 70 | # This is needed to account for plugin path corruptions. 71 | 72 | return [get_plugin_data(plugin) for plugin in get_bound_plugins(list(plugins))] 73 | 74 | 75 | def get_page_export_data(cms_pagecontent, language): 76 | data = [] 77 | placeholders = cms_pagecontent.rescan_placeholders().values() 78 | 79 | for placeholder in list(placeholders): 80 | plugins = get_placeholder_export_data(placeholder, language) 81 | data.append({"placeholder": placeholder.slot, "plugins": plugins}) 82 | return data 83 | -------------------------------------------------------------------------------- /djangocms_transfer/forms.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from cms.models import CMSPlugin, PageContent, Placeholder 4 | from django import forms 5 | from django.conf import settings 6 | from django.core.exceptions import ValidationError 7 | from django.utils.text import slugify 8 | from django.utils.translation import gettext_lazy as _ 9 | 10 | from .datastructures import ArchivedPlaceholder, ArchivedPlugin 11 | from .exporter import export_page, export_placeholder, export_plugin 12 | from .importer import import_plugins, import_plugins_to_page 13 | 14 | 15 | def _object_version_data_hook(data, for_page=False): 16 | if not data: 17 | return data 18 | 19 | if "plugins" in data: 20 | return ArchivedPlaceholder( 21 | slot=data["placeholder"], 22 | plugins=data["plugins"], 23 | ) 24 | 25 | if "plugin_type" in data: 26 | return ArchivedPlugin(**data) 27 | return data 28 | 29 | 30 | def _get_parsed_data(file_obj, for_page=False): 31 | raw = file_obj.read().decode("utf-8") 32 | return json.loads(raw, object_hook=_object_version_data_hook) 33 | 34 | 35 | class ExportImportForm(forms.Form): 36 | plugin = forms.ModelChoiceField( 37 | CMSPlugin.objects.all(), 38 | required=False, 39 | widget=forms.HiddenInput(), 40 | ) 41 | placeholder = forms.ModelChoiceField( 42 | queryset=Placeholder.objects.all(), 43 | required=False, 44 | widget=forms.HiddenInput(), 45 | ) 46 | cms_pagecontent = forms.ModelChoiceField( 47 | queryset=PageContent.admin_manager.latest_content(), 48 | required=False, 49 | widget=forms.HiddenInput(), 50 | ) 51 | language = forms.ChoiceField( 52 | choices=settings.LANGUAGES, 53 | required=True, 54 | widget=forms.HiddenInput(), 55 | ) 56 | 57 | def clean(self): 58 | if self.errors: 59 | return self.cleaned_data 60 | 61 | plugin = self.cleaned_data.get("plugin") 62 | placeholder = self.cleaned_data.get("placeholder") 63 | cms_pagecontent = self.cleaned_data.get("cms_pagecontent") 64 | 65 | if not any([plugin, placeholder, cms_pagecontent]): 66 | message = _("A plugin, placeholder or page is required.") 67 | raise forms.ValidationError(message) 68 | 69 | if cms_pagecontent and (plugin or placeholder): 70 | message = _( 71 | "Plugins can be imported to pages, plugins or placeholders. Not all three." 72 | ) 73 | raise forms.ValidationError(message) 74 | 75 | if placeholder and (cms_pagecontent or plugin): 76 | message = _( 77 | "Plugins can be imported to pages, plugins or placeholders. Not all three." 78 | ) 79 | raise forms.ValidationError(message) 80 | 81 | if plugin and (cms_pagecontent or placeholder): 82 | message = _( 83 | "Plugins can be imported to pages, plugins or placeholders. Not all three." 84 | ) 85 | raise forms.ValidationError(message) 86 | 87 | if plugin: 88 | plugin_model = plugin.get_plugin_class().model 89 | plugin_is_bound = plugin_model.objects.filter(cmsplugin_ptr=plugin).exists() 90 | else: 91 | plugin_is_bound = False 92 | 93 | if plugin and not plugin_is_bound: 94 | raise ValidationError("Plugin is unbound.") 95 | return self.cleaned_data 96 | 97 | 98 | class PluginExportForm(ExportImportForm): 99 | def get_filename(self): 100 | data = self.cleaned_data 101 | language = data["language"] 102 | cms_pagecontent = data["cms_pagecontent"] 103 | plugin = data["plugin"] 104 | placeholder = data["placeholder"] 105 | 106 | if cms_pagecontent: 107 | return "{}.json".format(cms_pagecontent.page.get_slug(language=language)) 108 | elif placeholder and placeholder.page is not None: 109 | return "{}_{}.json".format( 110 | placeholder.page.get_slug(language=language), 111 | slugify(placeholder.slot), 112 | ) 113 | elif plugin is not None and plugin.page is not None: 114 | return "{}_{}.json".format( 115 | plugin.page.get_slug(language=language), 116 | slugify(plugin.get_short_description()), 117 | ) 118 | else: 119 | return "plugins.json" 120 | 121 | def run_export(self): 122 | data = self.cleaned_data 123 | language = data["language"] 124 | plugin = data["plugin"] 125 | placeholder = data["placeholder"] 126 | 127 | if plugin: 128 | return export_plugin(plugin.get_bound_plugin()) 129 | 130 | if placeholder: 131 | return export_placeholder(placeholder, language) 132 | return export_page(data["cms_pagecontent"], language) 133 | 134 | 135 | class PluginImportForm(ExportImportForm): 136 | import_file = forms.FileField( 137 | required=True, 138 | widget=forms.FileInput(attrs={"accept": "application/json"}), 139 | ) 140 | 141 | def clean(self): 142 | if self.errors: 143 | return self.cleaned_data 144 | 145 | import_file = self.cleaned_data["import_file"] 146 | 147 | try: 148 | data = _get_parsed_data(import_file) 149 | except (ValueError, TypeError): 150 | raise ValidationError("File is not valid") 151 | 152 | first_item = data[0] 153 | is_placeholder = isinstance(first_item, ArchivedPlaceholder) 154 | page_import = bool(self.cleaned_data["cms_pagecontent"]) 155 | plugins_import = not page_import 156 | 157 | if (is_placeholder and plugins_import) or (page_import and not is_placeholder): 158 | raise ValidationError("Incorrect json format used.") 159 | 160 | self.cleaned_data["import_data"] = data 161 | return self.cleaned_data 162 | 163 | def run_import(self): 164 | data = self.cleaned_data 165 | language = data["language"] 166 | target_page = data["cms_pagecontent"] 167 | target_plugin = data["plugin"] 168 | target_placeholder = data["placeholder"] 169 | 170 | if target_plugin: 171 | target_plugin_id = target_plugin.pk 172 | target_placeholder = target_plugin.placeholder 173 | else: 174 | target_plugin_id = None 175 | 176 | if target_page: 177 | import_plugins_to_page( 178 | placeholders=data["import_data"], 179 | pagecontent=target_page, 180 | language=language, 181 | ) 182 | else: 183 | import_plugins( 184 | plugins=data["import_data"], 185 | placeholder=target_placeholder, 186 | language=language, 187 | root_plugin_id=target_plugin_id, 188 | ) 189 | -------------------------------------------------------------------------------- /djangocms_transfer/importer.py: -------------------------------------------------------------------------------- 1 | from cms.models import CMSPlugin 2 | from django.db import transaction 3 | 4 | 5 | @transaction.atomic 6 | def import_plugins(plugins, placeholder, language, root_plugin_id=None): 7 | source_map = {} 8 | new_plugins = [] 9 | 10 | if root_plugin_id: 11 | root_plugin = CMSPlugin.objects.get(pk=root_plugin_id) 12 | source_map[root_plugin_id] = root_plugin 13 | else: 14 | root_plugin = None 15 | 16 | for archived_plugin in plugins: 17 | # custom handling via "get_plugin_data" can lead to "null"-values 18 | # instead of plugin-dictionaries. We skip those here. 19 | if archived_plugin is None: 20 | continue 21 | 22 | if archived_plugin.parent_id: 23 | parent = source_map[archived_plugin.parent_id] 24 | else: 25 | parent = root_plugin 26 | 27 | if parent and parent.__class__ != CMSPlugin: 28 | parent = parent.cmsplugin_ptr 29 | 30 | plugin = archived_plugin.restore( 31 | placeholder=placeholder, 32 | language=language, 33 | parent=parent, 34 | ) 35 | source_map[archived_plugin.pk] = plugin 36 | 37 | new_plugins.append((plugin, archived_plugin)) 38 | 39 | for new_plugin, _ in new_plugins: 40 | # Replace all internal child plugins with their new ids 41 | new_plugin.post_copy(new_plugin, new_plugins) 42 | 43 | 44 | @transaction.atomic 45 | def import_plugins_to_page(placeholders, pagecontent, language): 46 | page_placeholders = pagecontent.rescan_placeholders() 47 | 48 | for archived_placeholder in placeholders: 49 | plugins = archived_placeholder.plugins 50 | placeholder = page_placeholders.get(archived_placeholder.slot) 51 | 52 | if placeholder and plugins: 53 | import_plugins(plugins, placeholder, language) 54 | -------------------------------------------------------------------------------- /djangocms_transfer/locale/de/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-cms/djangocms-transfer/ca9c318736bfa5345a3e7bfac04828c7c377faf7/djangocms_transfer/locale/de/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /djangocms_transfer/locale/de/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | # Translators: 7 | # Angelo Dini , 2019 8 | # 9 | #, fuzzy 10 | msgid "" 11 | msgstr "" 12 | "Project-Id-Version: PACKAGE VERSION\n" 13 | "Report-Msgid-Bugs-To: \n" 14 | "POT-Creation-Date: 2019-01-22 11:52+0100\n" 15 | "PO-Revision-Date: 2019-01-22 10:49+0000\n" 16 | "Last-Translator: Angelo Dini , 2019\n" 17 | "Language-Team: German (https://www.transifex.com/divio/teams/58664/de/)\n" 18 | "MIME-Version: 1.0\n" 19 | "Content-Type: text/plain; charset=UTF-8\n" 20 | "Content-Transfer-Encoding: 8bit\n" 21 | "Language: de\n" 22 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 23 | 24 | #: djangocms_transfer/apps.py:10 25 | msgid "django CMS Transfer" 26 | msgstr "django CMS Transfer" 27 | 28 | #: djangocms_transfer/cms_plugins.py:53 djangocms_transfer/cms_plugins.py:80 29 | msgid "Export plugins" 30 | msgstr "Plugins exportieren" 31 | 32 | #: djangocms_transfer/cms_plugins.py:62 djangocms_transfer/cms_plugins.py:89 33 | #: djangocms_transfer/templates/djangocms_transfer/import_plugins.html:5 34 | msgid "Import plugins" 35 | msgstr "Plugins importieren" 36 | 37 | #: djangocms_transfer/cms_plugins.py:112 djangocms_transfer/cms_plugins.py:178 38 | msgid "Form received unexpected values." 39 | msgstr "Das Formular hat unerwartete Inhalte erhalten." 40 | 41 | #: djangocms_transfer/cms_toolbars.py:50 42 | msgid "Export" 43 | msgstr "Exportieren" 44 | 45 | #: djangocms_transfer/cms_toolbars.py:55 46 | #: djangocms_transfer/templates/djangocms_transfer/import_plugins.html:31 47 | msgid "Import" 48 | msgstr "Importieren" 49 | 50 | #: djangocms_transfer/forms.py:69 51 | msgid "A plugin, placeholder or page is required." 52 | msgstr "Ein Plugin, Platzhalter oder Seite ist erforderlich." 53 | 54 | #: djangocms_transfer/forms.py:73 djangocms_transfer/forms.py:77 55 | #: djangocms_transfer/forms.py:81 56 | msgid "" 57 | "Plugins can be imported to pages, plugins or placeholders. Not all three." 58 | msgstr "" 59 | "Plugins können in Seiten, Plugins oder Platzhalter importiert werden. " 60 | "Allerdings nicht in alle drei gleichzeitig." 61 | 62 | #: djangocms_transfer/templates/djangocms_transfer/import_plugins.html:8 63 | msgid "Please correct the error below." 64 | msgid_plural "Please correct the errors below." 65 | msgstr[0] "Bitte korrigiere den unten aufgeführten Fehler." 66 | msgstr[1] "Bitte korrigiere die unten aufgeführten Fehler." 67 | -------------------------------------------------------------------------------- /djangocms_transfer/locale/en/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-cms/djangocms-transfer/ca9c318736bfa5345a3e7bfac04828c7c377faf7/djangocms_transfer/locale/en/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /djangocms_transfer/locale/en/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2019-01-22 11:52+0100\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 20 | 21 | #: djangocms_transfer/apps.py:10 22 | msgid "django CMS Transfer" 23 | msgstr "" 24 | 25 | #: djangocms_transfer/cms_plugins.py:53 djangocms_transfer/cms_plugins.py:80 26 | msgid "Export plugins" 27 | msgstr "" 28 | 29 | #: djangocms_transfer/cms_plugins.py:62 djangocms_transfer/cms_plugins.py:89 30 | #: djangocms_transfer/templates/djangocms_transfer/import_plugins.html:5 31 | msgid "Import plugins" 32 | msgstr "" 33 | 34 | #: djangocms_transfer/cms_plugins.py:112 djangocms_transfer/cms_plugins.py:178 35 | msgid "Form received unexpected values." 36 | msgstr "" 37 | 38 | #: djangocms_transfer/cms_toolbars.py:50 39 | msgid "Export" 40 | msgstr "" 41 | 42 | #: djangocms_transfer/cms_toolbars.py:55 43 | #: djangocms_transfer/templates/djangocms_transfer/import_plugins.html:31 44 | msgid "Import" 45 | msgstr "" 46 | 47 | #: djangocms_transfer/forms.py:69 48 | msgid "A plugin, placeholder or page is required." 49 | msgstr "" 50 | 51 | #: djangocms_transfer/forms.py:73 djangocms_transfer/forms.py:77 52 | #: djangocms_transfer/forms.py:81 53 | msgid "" 54 | "Plugins can be imported to pages, plugins or placeholders. Not all three." 55 | msgstr "" 56 | 57 | #: djangocms_transfer/templates/djangocms_transfer/import_plugins.html:8 58 | msgid "Please correct the error below." 59 | msgid_plural "Please correct the errors below." 60 | msgstr[0] "" 61 | msgstr[1] "" 62 | -------------------------------------------------------------------------------- /djangocms_transfer/locale/es/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-cms/djangocms-transfer/ca9c318736bfa5345a3e7bfac04828c7c377faf7/djangocms_transfer/locale/es/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /djangocms_transfer/locale/es/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2019-01-22 11:52+0100\n" 12 | "PO-Revision-Date: 2019-01-22 10:49+0000\n" 13 | "Language-Team: Spanish (https://www.transifex.com/divio/teams/58664/es/)\n" 14 | "MIME-Version: 1.0\n" 15 | "Content-Type: text/plain; charset=UTF-8\n" 16 | "Content-Transfer-Encoding: 8bit\n" 17 | "Language: es\n" 18 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 19 | 20 | #: djangocms_transfer/apps.py:10 21 | msgid "django CMS Transfer" 22 | msgstr "" 23 | 24 | #: djangocms_transfer/cms_plugins.py:53 djangocms_transfer/cms_plugins.py:80 25 | msgid "Export plugins" 26 | msgstr "" 27 | 28 | #: djangocms_transfer/cms_plugins.py:62 djangocms_transfer/cms_plugins.py:89 29 | #: djangocms_transfer/templates/djangocms_transfer/import_plugins.html:5 30 | msgid "Import plugins" 31 | msgstr "" 32 | 33 | #: djangocms_transfer/cms_plugins.py:112 djangocms_transfer/cms_plugins.py:178 34 | msgid "Form received unexpected values." 35 | msgstr "" 36 | 37 | #: djangocms_transfer/cms_toolbars.py:50 38 | msgid "Export" 39 | msgstr "" 40 | 41 | #: djangocms_transfer/cms_toolbars.py:55 42 | #: djangocms_transfer/templates/djangocms_transfer/import_plugins.html:31 43 | msgid "Import" 44 | msgstr "" 45 | 46 | #: djangocms_transfer/forms.py:69 47 | msgid "A plugin, placeholder or page is required." 48 | msgstr "" 49 | 50 | #: djangocms_transfer/forms.py:73 djangocms_transfer/forms.py:77 51 | #: djangocms_transfer/forms.py:81 52 | msgid "" 53 | "Plugins can be imported to pages, plugins or placeholders. Not all three." 54 | msgstr "" 55 | 56 | #: djangocms_transfer/templates/djangocms_transfer/import_plugins.html:8 57 | msgid "Please correct the error below." 58 | msgid_plural "Please correct the errors below." 59 | msgstr[0] "" 60 | msgstr[1] "" 61 | -------------------------------------------------------------------------------- /djangocms_transfer/locale/fr/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-cms/djangocms-transfer/ca9c318736bfa5345a3e7bfac04828c7c377faf7/djangocms_transfer/locale/fr/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /djangocms_transfer/locale/fr/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2019-01-22 11:52+0100\n" 12 | "PO-Revision-Date: 2019-01-22 10:49+0000\n" 13 | "Language-Team: French (https://www.transifex.com/divio/teams/58664/fr/)\n" 14 | "MIME-Version: 1.0\n" 15 | "Content-Type: text/plain; charset=UTF-8\n" 16 | "Content-Transfer-Encoding: 8bit\n" 17 | "Language: fr\n" 18 | "Plural-Forms: nplurals=2; plural=(n > 1);\n" 19 | 20 | #: djangocms_transfer/apps.py:10 21 | msgid "django CMS Transfer" 22 | msgstr "" 23 | 24 | #: djangocms_transfer/cms_plugins.py:53 djangocms_transfer/cms_plugins.py:80 25 | msgid "Export plugins" 26 | msgstr "" 27 | 28 | #: djangocms_transfer/cms_plugins.py:62 djangocms_transfer/cms_plugins.py:89 29 | #: djangocms_transfer/templates/djangocms_transfer/import_plugins.html:5 30 | msgid "Import plugins" 31 | msgstr "" 32 | 33 | #: djangocms_transfer/cms_plugins.py:112 djangocms_transfer/cms_plugins.py:178 34 | msgid "Form received unexpected values." 35 | msgstr "" 36 | 37 | #: djangocms_transfer/cms_toolbars.py:50 38 | msgid "Export" 39 | msgstr "" 40 | 41 | #: djangocms_transfer/cms_toolbars.py:55 42 | #: djangocms_transfer/templates/djangocms_transfer/import_plugins.html:31 43 | msgid "Import" 44 | msgstr "" 45 | 46 | #: djangocms_transfer/forms.py:69 47 | msgid "A plugin, placeholder or page is required." 48 | msgstr "" 49 | 50 | #: djangocms_transfer/forms.py:73 djangocms_transfer/forms.py:77 51 | #: djangocms_transfer/forms.py:81 52 | msgid "" 53 | "Plugins can be imported to pages, plugins or placeholders. Not all three." 54 | msgstr "" 55 | 56 | #: djangocms_transfer/templates/djangocms_transfer/import_plugins.html:8 57 | msgid "Please correct the error below." 58 | msgid_plural "Please correct the errors below." 59 | msgstr[0] "" 60 | msgstr[1] "" 61 | -------------------------------------------------------------------------------- /djangocms_transfer/static/djangocms_transfer/css/transfer.css: -------------------------------------------------------------------------------- 1 | div.cms .cms-structure .cms-submenu-item a[data-icon=import]:before, 2 | div.cms .cms-structure .cms-submenu-item a[data-icon=export]:before { 3 | position: absolute; 4 | content: ''; 5 | display: block !important; 6 | width: 16px !important; 7 | height: 16px !important; 8 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='currentColor' class='bi bi-arrow-bar-down' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M1 3.5a.5.5 0 0 1 .5-.5h13a.5.5 0 0 1 0 1h-13a.5.5 0 0 1-.5-.5M8 6a.5.5 0 0 1 .5.5v5.793l2.146-2.147a.5.5 0 0 1 .708.708l-3 3a.5.5 0 0 1-.708 0l-3-3a.5.5 0 0 1 .708-.708L7.5 12.293V6.5A.5.5 0 0 1 8 6'/%3E%3C/svg%3E"); 9 | top: 15px !important; 10 | background-size: 16px 16px; 11 | overflow: hidden !important; 12 | line-height: 16px !important; 13 | min-height: 16px !important; 14 | } 15 | div.cms .cms-structure .cms-submenu-item a[data-icon=import]:before { 16 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='currentColor' class='bi bi-arrow-bar-up' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M8 10a.5.5 0 0 0 .5-.5V3.707l2.146 2.147a.5.5 0 0 0 .708-.708l-3-3a.5.5 0 0 0-.708 0l-3 3a.5.5 0 1 0 .708.708L7.5 3.707V9.5a.5.5 0 0 0 .5.5m-7 2.5a.5.5 0 0 1 .5-.5h13a.5.5 0 0 1 0 1h-13a.5.5 0 0 1-.5-.5'/%3E%3C/svg%3E"); 17 | } 18 | div.cms .cms-structure .cms-submenu-item a[data-icon=export]:hover:before, 19 | div.cms .cms-structure .cms-submenu-item a[data-icon=export]:focus:before { 20 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='currentColor' class='bi bi-arrow-bar-down' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M1 3.5a.5.5 0 0 1 .5-.5h13a.5.5 0 0 1 0 1h-13a.5.5 0 0 1-.5-.5M8 6a.5.5 0 0 1 .5.5v5.793l2.146-2.147a.5.5 0 0 1 .708.708l-3 3a.5.5 0 0 1-.708 0l-3-3a.5.5 0 0 1 .708-.708L7.5 12.293V6.5A.5.5 0 0 1 8 6'/%3E%3C/svg%3E"); 21 | } 22 | div.cms .cms-structure .cms-submenu-item a[data-icon=import]:hover:before, 23 | div.cms .cms-structure .cms-submenu-item a[data-icon=import]:focus:before { 24 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='currentColor' class='bi bi-arrow-bar-up' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M8 10a.5.5 0 0 0 .5-.5V3.707l2.146 2.147a.5.5 0 0 0 .708-.708l-3-3a.5.5 0 0 0-.708 0l-3 3a.5.5 0 1 0 .708.708L7.5 3.707V9.5a.5.5 0 0 0 .5.5m-7 2.5a.5.5 0 0 1 .5-.5h13a.5.5 0 0 1 0 1h-13a.5.5 0 0 1-.5-.5'/%3E%3C/svg%3E"); 25 | } 26 | div.cms .cms-structure.cms-structure-condensed .cms-submenu-item a[data-icon=export]:before, 27 | div.cms .cms-structure.cms-structure-condensed .cms-submenu-item a[data-icon=import]:before { 28 | top: 12px !important; 29 | } 30 | -------------------------------------------------------------------------------- /djangocms_transfer/templates/djangocms_transfer/import_plugins.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/change_form.html" %} 2 | {% load cms_admin cms_static i18n %} 3 | 4 | {% block content %} 5 |

{% trans "Import plugins" %}

6 | {% if form.errors %} 7 |

8 | {% blocktrans count form.errors|length as counter %}Please correct the error below.{% plural %}Please correct the errors below.{% endblocktrans %} 9 |

10 | {{ form.non_field_errors }} 11 | {% endif %} 12 |
13 | {% csrf_token %} 14 | {% for hidden in form.hidden_fields %} 15 | {{ hidden }} 16 | {% endfor %} 17 |
18 |
19 | {% for field in form.visible_fields %} 20 |
21 | 22 | {% if field.errors %}{{ field.errors }}{% endif %} 23 | {{ field.label_tag }} 24 | {{ field }} 25 |
26 |
27 | {% endfor %} 28 | 29 | 30 |
31 | 32 |
33 |
34 | {% endblock %} 35 | -------------------------------------------------------------------------------- /djangocms_transfer/templates/djangocms_transfer/placeholder_close_frame.html: -------------------------------------------------------------------------------- 1 | 4 |
5 | -------------------------------------------------------------------------------- /djangocms_transfer/utils.py: -------------------------------------------------------------------------------- 1 | from functools import lru_cache 2 | 3 | from cms.models import CMSPlugin 4 | from cms.plugin_pool import plugin_pool 5 | 6 | 7 | @lru_cache() 8 | def get_local_fields(model): 9 | opts = model._meta.concrete_model._meta 10 | fields = opts.local_fields 11 | return [ 12 | field.name 13 | for field in fields 14 | if not field.is_relation and not field.primary_key 15 | ] 16 | 17 | 18 | @lru_cache() 19 | def get_related_fields(model): 20 | opts = model._meta.concrete_model._meta 21 | fields = opts.local_fields + list(opts.many_to_many) 22 | return [field.name for field in fields if field.is_relation] 23 | 24 | 25 | @lru_cache() 26 | def get_plugin_class(plugin_type): 27 | return plugin_pool.get_plugin(plugin_type) 28 | 29 | 30 | @lru_cache() 31 | def get_plugin_fields(plugin_type): 32 | klass = get_plugin_class(plugin_type) 33 | if klass.model is CMSPlugin: 34 | return [] 35 | opts = klass.model._meta.concrete_model._meta 36 | fields = opts.local_fields + opts.local_many_to_many 37 | return [field.name for field in fields] 38 | 39 | 40 | @lru_cache() 41 | def get_plugin_model(plugin_type): 42 | return get_plugin_class(plugin_type).model 43 | -------------------------------------------------------------------------------- /preview.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-cms/djangocms-transfer/ca9c318736bfa5345a3e7bfac04828c7c377faf7/preview.gif -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=42", "wheel"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "djangocms-transfer" 7 | description = "Adds import and export of plugin data." 8 | dependencies = [ 9 | "django-cms>=4.1", 10 | ] 11 | dynamic = [ "version" ] 12 | readme = "README.rst" 13 | requires-python = ">=3.9" 14 | license = {text = "BSD-3-Clause"} 15 | authors = [ 16 | {name = "Divio AG", email = "info@divio.ch"}, 17 | ] 18 | maintainers = [ 19 | {name = "Django CMS Association and contributors", email = "info@django-cms.org"}, 20 | ] 21 | classifiers = [ 22 | "Development Status :: 5 - Production/Stable", 23 | "Environment :: Web Environment", 24 | "Intended Audience :: Developers", 25 | "License :: OSI Approved :: BSD License", 26 | "Operating System :: OS Independent", 27 | "Programming Language :: Python", 28 | "Programming Language :: Python :: 3.9", 29 | "Programming Language :: Python :: 3.10", 30 | "Programming Language :: Python :: 3.11", 31 | "Programming Language :: Python :: 3.12", 32 | "Programming Language :: Python :: 3.13", 33 | "Framework :: Django", 34 | "Framework :: Django :: 4.2", 35 | "Framework :: Django :: 5.0", 36 | "Framework :: Django :: 5.1", 37 | "Framework :: Django CMS", 38 | "Framework :: Django CMS :: 4.1", 39 | "Framework :: Django CMS :: 5.0", 40 | "Topic :: Internet :: WWW/HTTP", 41 | "Topic :: Internet :: WWW/HTTP :: Dynamic Content", 42 | "Topic :: Software Development", 43 | "Topic :: Software Development :: Libraries", 44 | ] 45 | 46 | [project.urls] 47 | Homepage = "https://github.com/django-cms/djangocms-transfer" 48 | 49 | [tool.setuptools] 50 | packages = [ "djangocms_transfer" ] 51 | 52 | [tool.setuptools.dynamic] 53 | version = { attr = "djangocms_transfer.__version__" } 54 | 55 | [tool.ruff] 56 | lint.exclude = [ 57 | ".env", 58 | ".venv", 59 | "**/migrations/**", 60 | ] 61 | lint.ignore = [ 62 | "E501", # line too long 63 | "F403", # 'from module import *' used; unable to detect undefined names 64 | "E701", # multiple statements on one line (colon) 65 | "F401", # module imported but unused 66 | ] 67 | line-length = 119 68 | lint.select = [ 69 | "I", 70 | "E", 71 | "F", 72 | "W", 73 | ] 74 | 75 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from setuptools import setup 3 | 4 | 5 | setup() 6 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-cms/djangocms-transfer/ca9c318736bfa5345a3e7bfac04828c7c377faf7/tests/__init__.py -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | import django 5 | import pytest 6 | from django.conf import settings 7 | from django.test.utils import get_runner 8 | 9 | 10 | def transfer(first, second=None): 11 | if second: 12 | return first, second 13 | return first 14 | 15 | 16 | @pytest.fixture 17 | def use_nonexistent_transfer_hook(settings): 18 | settings.DJANGOCMS_TRANSFER_PROCESS_EXPORT_PLUGIN_DATA = "a.b.c" 19 | settings.DJANGOCMS_TRANSFER_PROCESS_IMPORT_PLUGIN_DATA = "a.b.c" 20 | 21 | 22 | @pytest.fixture 23 | def use_existent_transfer_hook(settings): 24 | settings.DJANGOCMS_TRANSFER_PROCESS_EXPORT_PLUGIN_DATA = "tests.conftest.transfer" 25 | settings.DJANGOCMS_TRANSFER_PROCESS_IMPORT_PLUGIN_DATA = "tests.conftest.transfer" 26 | 27 | 28 | def pytest_configure(): 29 | os.environ["DJANGO_SETTINGS_MODULE"] = "tests.settings" 30 | django.setup() 31 | 32 | 33 | def run(path): 34 | TestRunner = get_runner(settings) 35 | test_runner = TestRunner() 36 | failures = test_runner.run_tests(path) 37 | sys.exit(bool(failures)) 38 | 39 | 40 | if __name__ == "__main__": 41 | run(sys.argv[1:]) 42 | -------------------------------------------------------------------------------- /tests/requirements/base.txt: -------------------------------------------------------------------------------- 1 | # other requirements 2 | tox 3 | coverage 4 | black 5 | freezegun 6 | djangocms-text 7 | pytest 8 | pytest-django 9 | 10 | # for testing purposes 11 | djangocms-link 12 | -------------------------------------------------------------------------------- /tests/requirements/dj42_cms41.txt: -------------------------------------------------------------------------------- 1 | -r base.txt 2 | 3 | Django>=4.2,<5.0 4 | django-cms>=4.1,<4.2 5 | -------------------------------------------------------------------------------- /tests/requirements/dj42_cms50.txt: -------------------------------------------------------------------------------- 1 | -r base.txt 2 | 3 | Django>=4.2,<5.0 4 | django-cms>=5.0,<5.1 5 | -------------------------------------------------------------------------------- /tests/requirements/dj50_cms41.txt: -------------------------------------------------------------------------------- 1 | -r base.txt 2 | 3 | Django>=5.0,<5.1 4 | django-cms>=4.1,<4.2 5 | -------------------------------------------------------------------------------- /tests/requirements/dj50_cms50.txt: -------------------------------------------------------------------------------- 1 | -r base.txt 2 | 3 | Django>=5.0,<5.1 4 | django-cms>=5.0,<5.1 5 | -------------------------------------------------------------------------------- /tests/requirements/dj51_cms41.txt: -------------------------------------------------------------------------------- 1 | -r base.txt 2 | 3 | Django>=5.1,<5.2 4 | django-cms>=4.1,<4.2 5 | -------------------------------------------------------------------------------- /tests/requirements/dj51_cms50.txt: -------------------------------------------------------------------------------- 1 | -r base.txt 2 | 3 | Django>=5.1,<5.2 4 | django-cms>=5.0,<5.1 5 | -------------------------------------------------------------------------------- /tests/requirements/dj52_cms41.txt: -------------------------------------------------------------------------------- 1 | -r base.txt 2 | 3 | Django>=5.2,<5.3 4 | django-cms>=4.1,<4.2 5 | -------------------------------------------------------------------------------- /tests/requirements/dj52_cms50.txt: -------------------------------------------------------------------------------- 1 | -r base.txt 2 | 3 | Django>=5.2,<5.3 4 | django-cms>=5.0,<5.1 5 | -------------------------------------------------------------------------------- /tests/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | SECRET_KEY = "djangocms-transfer-test" 4 | 5 | ALLOWED_HOSTS = ["localhost"] 6 | 7 | INSTALLED_APPS = [ 8 | "django.contrib.contenttypes", 9 | "django.contrib.auth", 10 | "django.contrib.sites", 11 | "django.contrib.sessions", 12 | "django.contrib.admin", 13 | "django.contrib.messages", 14 | 15 | "cms", 16 | "menus", 17 | "treebeard", 18 | "djangocms_text", 19 | "djangocms_link", 20 | "djangocms_transfer", 21 | ] 22 | 23 | MIDDLEWARE = [ 24 | "django.contrib.sessions.middleware.SessionMiddleware", 25 | "django.contrib.auth.middleware.AuthenticationMiddleware", 26 | "django.contrib.messages.middleware.MessageMiddleware", 27 | ] 28 | 29 | TEMPLATES = [ 30 | { 31 | "BACKEND": "django.template.backends.django.DjangoTemplates", 32 | "DIRS": [ 33 | os.path.join((os.path.dirname(__file__)), "templates"), 34 | ], 35 | "APP_DIRS": True, 36 | "OPTIONS": { 37 | "context_processors": [ 38 | "django.template.context_processors.debug", 39 | "django.template.context_processors.request", 40 | "django.contrib.auth.context_processors.auth", 41 | "django.contrib.messages.context_processors.messages", 42 | ], 43 | }, 44 | }, 45 | ] 46 | 47 | SITE_ID = 1 48 | 49 | CMS_TEMPLATES = ( 50 | ("page.html", "Normal page"), 51 | ) 52 | 53 | CMS_LANGUAGES = { 54 | 1: [{ 55 | "code": "en", 56 | "name": "English", 57 | }] 58 | } 59 | 60 | DATABASES = { 61 | "default": { 62 | "ENGINE": "django.db.backends.sqlite3", 63 | "NAME": ":memory:", 64 | "TEST": { 65 | # disable migrations when creating test database 66 | "MIGRATE": False, 67 | }, 68 | } 69 | } 70 | 71 | CMS_CONFIRM_VERSION4 = True 72 | 73 | USE_TZ = True 74 | 75 | LANGUAGE_CODE = "en" 76 | 77 | DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" 78 | -------------------------------------------------------------------------------- /tests/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {% block content %} 7 | {% endblock %} 8 | 9 | 10 | -------------------------------------------------------------------------------- /tests/templates/page.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load cms_tags %} 3 | 4 | {% block content %} 5 | {% placeholder "content" %} 6 | {% endblock content %} 7 | -------------------------------------------------------------------------------- /tests/test_migrations.py: -------------------------------------------------------------------------------- 1 | # original from 2 | # http://tech.octopus.energy/news/2016/01/21/testing-for-missing-migrations-in-django.html 3 | from io import StringIO 4 | 5 | from django.core.management import call_command 6 | from django.test import TestCase, override_settings 7 | 8 | 9 | class MigrationTestCase(TestCase): 10 | @override_settings(MIGRATION_MODULES={"djangocms_link": None}, DEFAULT_AUTO_FIELD="django.db.models.AutoField") 11 | def test_for_missing_migrations(self): 12 | output = StringIO() 13 | options = { 14 | "interactive": False, 15 | "dry_run": True, 16 | "stdout": output, 17 | "check_changes": True, 18 | } 19 | 20 | try: 21 | call_command("makemigrations", "djangocms_transfer", **options) 22 | except SystemExit as e: 23 | status_code = str(e) 24 | else: 25 | # the "no changes" exit code is 0 26 | status_code = "0" 27 | 28 | if status_code == '1': 29 | self.fail(f'There are missing migrations:\n {output.getvalue()}') 30 | -------------------------------------------------------------------------------- /tests/transfer/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-cms/djangocms-transfer/ca9c318736bfa5345a3e7bfac04828c7c377faf7/tests/transfer/__init__.py -------------------------------------------------------------------------------- /tests/transfer/abstract.py: -------------------------------------------------------------------------------- 1 | from cms.api import add_plugin, create_page 2 | from cms.test_utils.testcases import CMSTestCase 3 | from freezegun import freeze_time 4 | 5 | 6 | @freeze_time("2024-02-28 00:00:00") 7 | class FunctionalityBaseTestCase(CMSTestCase): 8 | 9 | TEXT_BODY = "Hello World!" 10 | 11 | def setUp(self): 12 | self.page = self._create_page() 13 | self.page_content = self.page.pagecontent_set(manager="admin_manager").first() 14 | self.page.set_as_homepage() 15 | 16 | def _create_plugin(self, plugin_type="TextPlugin", parent=None, **kwargs): 17 | if plugin_type == "TextPlugin": 18 | kwargs["body"] = self.TEXT_BODY 19 | elif plugin_type == "LinkPlugin": 20 | kwargs["name"] = plugin_type.lower() 21 | kwargs["link"] = {"external_link": "https://www.django-cms.org"} 22 | 23 | return self._add_plugin_to_page( 24 | plugin_type, 25 | "last-child", 26 | parent, 27 | **kwargs 28 | ) 29 | 30 | def _create_page(self, **kwargs): 31 | if "template" not in kwargs: 32 | kwargs["template"] = "page.html" 33 | if "title" not in kwargs: 34 | kwargs["title"] = "Home" 35 | if "language" not in kwargs: 36 | kwargs["language"] = "en" 37 | return create_page(**kwargs) 38 | 39 | def _add_plugin_to_page(self, plugin_publisher, *args, page=None, **kwargs): 40 | if page is None: 41 | page = self.page 42 | return add_plugin( 43 | page.pagecontent_set(manager="admin_manager") 44 | .filter(language="en") 45 | .first() 46 | .get_placeholders() 47 | .get(slot="content"), 48 | plugin_publisher, 49 | "en", 50 | *args, 51 | **kwargs, 52 | ) 53 | 54 | def _render_page(self, page=None): 55 | if page is None: 56 | page = self.page 57 | page.publish("en") 58 | response = self.client.get(page.get_absolute_url()) 59 | return response.content.decode("utf-8") 60 | 61 | def _get_expected_plugin_export_data(self): 62 | return [ 63 | { 64 | "pk": 1, 65 | "creation_date": "2024-02-28T00:00:00Z", 66 | "position": 1, 67 | "plugin_type": "TextPlugin", 68 | "parent_id": None, 69 | "data": {'body': 'Hello World!', 'json': None, 'rte': ''}, 70 | }, 71 | ] 72 | 73 | def _get_expected_placeholder_export_data(self): 74 | return self._get_expected_plugin_export_data() 75 | 76 | def _get_expected_page_export_data(self): 77 | return [ 78 | { 79 | "placeholder": "content", 80 | "plugins": self._get_expected_plugin_export_data(), 81 | }, 82 | ] 83 | -------------------------------------------------------------------------------- /tests/transfer/test_export.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from djangocms_transfer.exporter import ( 4 | export_page, 5 | export_placeholder, 6 | export_plugin, 7 | ) 8 | 9 | from .abstract import FunctionalityBaseTestCase 10 | 11 | 12 | class ExportTest(FunctionalityBaseTestCase): 13 | def test_export_plugin(self): 14 | plugin = self._create_plugin() 15 | 16 | actual = json.loads(export_plugin(plugin)) 17 | 18 | self.assertEqual(self._get_expected_plugin_export_data(), actual) 19 | 20 | def test_export_placeholder(self): 21 | placeholder = self.page_content.get_placeholders().get(slot="content") 22 | 23 | with self.subTest("empty placeholder"): 24 | actual = json.loads(export_placeholder(placeholder, "en")) 25 | self.assertEqual([], actual) 26 | 27 | with self.subTest("placeholder with plugin"): 28 | self._create_plugin() 29 | actual = json.loads(export_placeholder(placeholder, "en")) 30 | self.assertEqual(self._get_expected_placeholder_export_data(), actual) 31 | 32 | def test_export_page(self): 33 | with self.subTest("empty page"): 34 | actual = json.loads(export_page(self.page_content, "en")) 35 | self.assertEqual([{"placeholder": "content", "plugins": []}], actual) 36 | 37 | with self.subTest("page with plugin"): 38 | self._create_plugin() 39 | actual = json.loads(export_page(self.page_content, "en")) 40 | self.assertEqual(self._get_expected_page_export_data(), actual) 41 | -------------------------------------------------------------------------------- /tests/transfer/test_forms.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import unittest 4 | from tempfile import mkdtemp 5 | 6 | from django.core.files import File 7 | 8 | from djangocms_transfer.forms import PluginExportForm, PluginImportForm 9 | 10 | from .abstract import FunctionalityBaseTestCase 11 | 12 | 13 | class PluginExportFormTest(FunctionalityBaseTestCase): 14 | def test_get_filename(self): 15 | placeholder = self.page_content.get_placeholders().get(slot="content") 16 | plugin = self._create_plugin() 17 | 18 | data = { 19 | "plugin": plugin, 20 | "placeholder": placeholder, 21 | "cms_pagecontent": self.page_content, 22 | "language": "en", 23 | } 24 | 25 | with self.subTest("filename from page"): 26 | form = PluginExportForm(data=data) 27 | form.clean() 28 | self.assertEqual("home.json", form.get_filename()) 29 | 30 | with self.subTest("filename from placeholder"): 31 | data["cms_pagecontent"] = None 32 | form = PluginExportForm(data=data) 33 | form.clean() 34 | self.assertEqual("home_content.json", form.get_filename()) 35 | 36 | with self.subTest("filename from plugin"): 37 | data["placeholder"] = None 38 | form = PluginExportForm(data=data) 39 | form.clean() 40 | self.assertEqual("home_hello-world.json", form.get_filename()) 41 | 42 | with self.subTest("filename from fallback"): 43 | data["plugin"] = None 44 | form = PluginExportForm(data=data) 45 | form.clean() 46 | self.assertEqual("plugins.json", form.get_filename()) 47 | 48 | def test_validation(self): 49 | placeholder = self.page_content.get_placeholders().get(slot="content") 50 | plugin = self._create_plugin() 51 | 52 | with self.subTest("language missing"): 53 | form = PluginExportForm(data={}) 54 | self.assertEqual(["This field is required."], form.errors["language"]) 55 | 56 | with self.subTest("one of plugin/placeholder/page required"): 57 | form = PluginExportForm(data={"language": "en"}) 58 | self.assertEqual(["A plugin, placeholder or page is required."], form.errors["__all__"]) 59 | 60 | with self.subTest("cms_pagecontent + plugin given"): 61 | form = PluginExportForm(data={"language": "en", "cms_pagecontent": self.page_content, "plugin": plugin}) 62 | self.assertEqual( 63 | ["Plugins can be imported to pages, plugins or placeholders. Not all three."], 64 | form.errors["__all__"], 65 | ) 66 | 67 | with self.subTest("cms_pagecontent + placeholder given"): 68 | form = PluginExportForm( 69 | data={"language": "en", "cms_pagecontent": self.page_content, "placeholder": placeholder} 70 | ) 71 | self.assertEqual( 72 | ["Plugins can be imported to pages, plugins or placeholders. Not all three."], 73 | form.errors["__all__"], 74 | ) 75 | 76 | with self.subTest("plugin + placeholder given"): 77 | form = PluginExportForm(data={"language": "en", "plugin": plugin, "placeholder": placeholder}) 78 | self.assertEqual( 79 | ["Plugins can be imported to pages, plugins or placeholders. Not all three."], 80 | form.errors["__all__"], 81 | ) 82 | 83 | def test_run_export(self): 84 | placeholder = self.page_content.get_placeholders().get(slot="content") 85 | plugin = self._create_plugin() 86 | 87 | data = { 88 | "plugin": plugin, 89 | "placeholder": None, 90 | "cms_page": None, 91 | "language": "en", 92 | } 93 | 94 | with self.subTest("export plugin"): 95 | form = PluginExportForm(data=data) 96 | form.clean() 97 | actual = json.loads(form.run_export()) 98 | self.assertEqual(self._get_expected_plugin_export_data(), actual) 99 | 100 | with self.subTest("export placeholder"): 101 | data["placeholder"] = placeholder 102 | data["plugin"] = None 103 | form = PluginExportForm(data=data) 104 | form.clean() 105 | actual = json.loads(form.run_export()) 106 | self.assertEqual(self._get_expected_placeholder_export_data(), actual) 107 | 108 | with self.subTest("export page"): 109 | data["cms_pagecontent"] = self.page_content 110 | data["placeholder"] = None 111 | form = PluginExportForm(data=data) 112 | form.clean() 113 | actual = json.loads(form.run_export()) 114 | self.assertEqual(self._get_expected_page_export_data(), actual) 115 | 116 | 117 | class PluginImportFormTest(FunctionalityBaseTestCase): 118 | def test_validation(self): 119 | page = self.page 120 | placeholder = self.page_content.get_placeholders().get(slot="content") 121 | plugin = self._create_plugin() 122 | file_ = self._get_file() 123 | 124 | with self.subTest("file missing"): 125 | form = PluginImportForm(data={}) 126 | self.assertEqual(["This field is required."], form.errors["import_file"]) 127 | 128 | with self.subTest("language missing"): 129 | form = PluginImportForm(data={"import_file": file_}) 130 | self.assertEqual(["This field is required."], form.errors["language"]) 131 | 132 | # TODO: when setting the form, the `form.errors` is filled for "missing 133 | # import_file" although it is given/set in `data` 134 | self.skipTest("TODO: fix validation with 'import_file'") 135 | 136 | with self.subTest("one of plugin/placeholder/page required"): 137 | form = PluginImportForm(data={"language": "en", "import_file": file_}) 138 | self.assertEqual(["A plugin, placeholder or page is required."], form.errors["__all__"]) 139 | 140 | with self.subTest("cms_page + plugin given"): 141 | form = PluginImportForm(data={"import_file": file_, "language": "en", "cms_page": page, "plugin": plugin}) 142 | self.assertEqual( 143 | ["Plugins can be imported to pages, plugins or placeholders. Not all three."], 144 | form.errors["__all__"], 145 | ) 146 | 147 | with self.subTest("cms_page + placeholder given"): 148 | form = PluginImportForm( 149 | data={"import_file": file_, "language": "en", "cms_page": page, "placeholder": placeholder}, 150 | ) 151 | self.assertEqual( 152 | ["Plugins can be imported to pages, plugins or placeholders. Not all three."], 153 | form.errors["__all__"], 154 | ) 155 | 156 | with self.subTest("plugin + placeholder given"): 157 | form = PluginImportForm( 158 | data={"import_file": file_, "language": "en", "plugin": plugin, "placeholder": placeholder}, 159 | ) 160 | self.assertEqual( 161 | ["Plugins can be imported to pages, plugins or placeholders. Not all three."], 162 | form.errors["__all__"], 163 | ) 164 | 165 | @unittest.skip("TODO: fix validation with 'import_file'") 166 | def test_run_import(self): 167 | # TODO: when setting the form, the `form.errors` is filled for "missing 168 | # import_file" although it is given/set in `data` 169 | page = self.page 170 | placeholder = self.page_content.get_placeholders().get(slot="content") 171 | plugin = self._create_plugin() 172 | file_ = self._get_file() 173 | 174 | data = { 175 | "plugin": plugin, 176 | "placeholder": None, 177 | "cms_page": None, 178 | "language": "en", 179 | "import_file": file_, 180 | } 181 | 182 | with self.subTest("import plugin"): 183 | form = PluginImportForm(data=data) 184 | form.clean() 185 | form.run_import() 186 | 187 | with self.subTest("import placeholder"): 188 | data["placeholder"] = placeholder 189 | data["plugin"] = None 190 | form = PluginImportForm(data=data) 191 | form.clean() 192 | form.run_import() 193 | 194 | with self.subTest("import page"): 195 | data["cms_page"] = page 196 | data["placeholder"] = None 197 | form = PluginImportForm(data=data) 198 | form.clean() 199 | form.run_import() 200 | 201 | def _get_file(self): 202 | content = json.dumps(self._get_expected_plugin_export_data()) 203 | 204 | tmp_dir = mkdtemp() 205 | filename = os.path.join(tmp_dir, "dummy-test.json") 206 | with open(filename, "w") as f: 207 | f.write(content) 208 | 209 | return File(open(filename, "rb"), name=filename) 210 | -------------------------------------------------------------------------------- /tests/transfer/test_helper.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from djangocms_transfer import custom_process_hook 4 | 5 | from .abstract import FunctionalityBaseTestCase 6 | 7 | 8 | class CustomProcessHookTest(FunctionalityBaseTestCase): 9 | def setUp(self): 10 | super().setUp() 11 | self.plugin = self._create_plugin() 12 | self.plugin_data = self._get_expected_page_export_data 13 | 14 | def test_empty_nonexistent_custom_process_hook(self): 15 | self.assertEqual( 16 | custom_process_hook("", self.plugin, self.plugin_data), 17 | self.plugin_data 18 | ) 19 | self.assertEqual( 20 | custom_process_hook("UNKNOWN_TRANSFER", self.plugin, self.plugin_data), 21 | self.plugin_data 22 | ) 23 | 24 | @pytest.mark.usefixtures("use_nonexistent_transfer_hook") 25 | def test_nonexistent_module(self): 26 | self.assertRaises( 27 | ImportError, 28 | custom_process_hook, 29 | "DJANGOCMS_TRANSFER_PROCESS_IMPORT_PLUGIN_DATA", 30 | self.plugin 31 | ) 32 | self.assertRaises( 33 | ImportError, 34 | custom_process_hook, 35 | "DJANGOCMS_TRANSFER_PROCESS_EXPORT_PLUGIN_DATA", 36 | self.plugin, self.plugin_data 37 | ) 38 | 39 | @pytest.mark.usefixtures("use_existent_transfer_hook") 40 | def test_existent_module(self): 41 | self.assertTrue( 42 | custom_process_hook( 43 | "DJANGOCMS_TRANSFER_PROCESS_IMPORT_PLUGIN_DATA", 44 | self.plugin 45 | ), 46 | self.plugin 47 | ) 48 | self.assertTrue( 49 | custom_process_hook( 50 | "DJANGOCMS_TRANSFER_PROCESS_EXPORT_PLUGIN_DATA", 51 | self.plugin, self.plugin_data 52 | ), 53 | (self.plugin, self.plugin_data) 54 | ) 55 | -------------------------------------------------------------------------------- /tests/transfer/test_import.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from cms.models import CMSPlugin 4 | from djangocms_text.utils import plugin_to_tag 5 | 6 | from djangocms_transfer.datastructures import ( 7 | ArchivedPlaceholder, 8 | ArchivedPlugin, 9 | ) 10 | from djangocms_transfer.exporter import export_placeholder, export_plugin 11 | from djangocms_transfer.importer import import_plugins, import_plugins_to_page 12 | 13 | from .abstract import FunctionalityBaseTestCase 14 | 15 | 16 | class ImportTest(FunctionalityBaseTestCase): 17 | def test_import(self): 18 | pagecontent = self.page_content 19 | placeholder = pagecontent.get_placeholders().get(slot="content") 20 | plugin = self._create_plugin() 21 | 22 | # create link plugin 23 | link_plugin = self._create_plugin(plugin_type="LinkPlugin", parent=plugin) 24 | 25 | # Add plugin to text body 26 | plugin.body = f"{plugin.body} {plugin_to_tag(link_plugin)}" 27 | plugin.save() 28 | 29 | link_plugin_data = ArchivedPlugin( 30 | **json.loads(export_plugin(link_plugin))[0] 31 | ) 32 | plugin_data = ArchivedPlugin(**json.loads(export_plugin(plugin))[0]) 33 | placeholder_data = ArchivedPlaceholder( 34 | "content", 35 | [ArchivedPlugin(**data) for data in json.loads(export_placeholder(placeholder, "en"))], 36 | ) 37 | 38 | with self.subTest("import plugins"): 39 | import_plugins([plugin_data, link_plugin_data], placeholder, "en") 40 | 41 | # test import updates child plugin 42 | new_plugin, new_link_plugin = map( 43 | lambda plugin: plugin.get_bound_plugin(), CMSPlugin.objects.filter(pk__in=[3,4]) 44 | ) 45 | self.assertEqual( 46 | new_plugin.body, 47 | f"{self.TEXT_BODY} {plugin_to_tag(new_link_plugin)}" 48 | ) 49 | 50 | with self.subTest("import placeholder"): 51 | import_plugins(placeholder_data.plugins, placeholder, "en") 52 | 53 | with self.subTest("import page"): 54 | import_plugins_to_page([placeholder_data], pagecontent, "en") 55 | -------------------------------------------------------------------------------- /tools/black: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | root_dir=$(readlink -f "$(dirname $0)/..") 4 | 5 | black --target-version py310 \ 6 | --line-length 119 \ 7 | $@ \ 8 | $root_dir/djangocms_transfer \ 9 | $root_dir/tests \ 10 | $root_dir/tools 11 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | py{39}-dj{42}-cms{41}, 4 | py{310,311,312,313}-dj{42,50,51,52}-cms{41,50} 5 | 6 | skip_missing_interpreters=True 7 | 8 | [flake8] 9 | max-line-length = 119 10 | exclude = 11 | env, 12 | *.egg-info, 13 | .eggs, 14 | .git, 15 | .settings, 16 | .tox, 17 | build, 18 | data, 19 | dist, 20 | docs, 21 | *migrations*, 22 | requirements, 23 | tmp 24 | 25 | [isort] 26 | line_length = 79 27 | skip = manage.py, *migrations*, .tox, .eggs, data, env 28 | include_trailing_comma = true 29 | multi_line_output = 5 30 | lines_after_imports = 2 31 | default_section = THIRDPARTY 32 | sections = FUTURE, STDLIB, DJANGO, CMS, THIRDPARTY, FIRSTPARTY, LOCALFOLDER 33 | known_first_party = djangocms_transfer 34 | known_cms = cms, menus 35 | known_django = django 36 | 37 | [testenv] 38 | deps = 39 | -r{toxinidir}/tests/requirements/base.txt 40 | dj42: Django>=4.2,<5.0 41 | dj50: Django>=5.0,<5.1 42 | dj51: Django>=5.1,<5.2 43 | dj52: Django>=5.2,<5.3 44 | cms41: django-cms>=4.1,<4.2 45 | cms50: django-cms>=5.0,<5.1 46 | 47 | commands = 48 | {envpython} --version 49 | {env:COMMAND:coverage} erase 50 | {env:COMMAND:coverage} run tests/settings.py 51 | {env:COMMAND:coverage} report 52 | 53 | --------------------------------------------------------------------------------