diff --git a/.circleci/report_nightly_build_failure.py b/.circleci/report_nightly_build_failure.py index 7834801..59c42df 100644 --- a/.circleci/report_nightly_build_failure.py +++ b/.circleci/report_nightly_build_failure.py @@ -7,13 +7,18 @@ import requests -if 'SLACK_WEBHOOK_URL' in os.environ: +if "SLACK_WEBHOOK_URL" in os.environ: print("Reporting to #nightly-build-failures slack channel") - response = requests.post(os.environ['SLACK_WEBHOOK_URL'], json={ - "text": "A Nightly build failed. See " + os.environ['CIRCLE_BUILD_URL'], - }) + response = requests.post( + os.environ["SLACK_WEBHOOK_URL"], + json={ + "text": "A Nightly build failed. See " + os.environ["CIRCLE_BUILD_URL"], + }, + ) print("Slack responded with:", response) else: - print("Unable to report to #nightly-build-failures slack channel because SLACK_WEBHOOK_URL is not set") + print( + "Unable to report to #nightly-build-failures slack channel because SLACK_WEBHOOK_URL is not set" + ) diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 0000000..50e5759 --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,6 @@ +# Set up ruff and pre-commit, apply fixes +84f991d844dcbfc29e4ce9a0b0afb3846c7dcd06 +c6cf87660d0e685121d8c9e62729c30c4641df07 +b83aab8c4442b3e1cf95950a5b1fcbae0d7ad5b1 +e166089173b2337803ba17cdff051d24b4b0515e +ca3b4000068d19e736149a038b0d479e5e78b8e9 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..396ffc6 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,40 @@ +name: wagtail-transfer CI + +on: + push: + branches: + - main + - master + - 'stable/**' + pull_request: + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: actions/setup-python@v4 + with: + python-version: ${{env.PYTHON_LATEST}} + - uses: pre-commit/action@v3.0.0 + + test: + runs-on: ubuntu-latest + strategy: + matrix: + python: ["3.8", "3.9", "3.10", "3.11"] + + steps: + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install tox tox-gh-actions + - name: Test with tox + run: tox diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..d059c78 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,32 @@ +exclude: | + (?x)( + wagtail_transfer.js + |document.txt + |.babelrc + ) +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.0 + hooks: + - id: check-added-large-files + - id: check-case-conflict + - id: check-json + - id: check-merge-conflict + - id: check-symlinks + - id: check-toml + - id: check-yaml + args: ['--unsafe'] + - id: end-of-file-fixer + - id: trailing-whitespace + - repo: https://github.com/adamchainz/blacken-docs + rev: 1.16.0 + hooks: + - id: blacken-docs + additional_dependencies: [black==23.10.0] + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: 'v0.1.2' + hooks: + - id: ruff + args: [--fix, --exit-non-zero-on-fix] + - id: ruff-format + args: [--check] diff --git a/docs/img/wagtail_transfer_logo.svg b/docs/img/wagtail_transfer_logo.svg index f041aeb..311fffd 100644 --- a/docs/img/wagtail_transfer_logo.svg +++ b/docs/img/wagtail_transfer_logo.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/docs/management_commands.md b/docs/management_commands.md index 0f2060d..2a89aea 100644 --- a/docs/management_commands.md +++ b/docs/management_commands.md @@ -30,4 +30,3 @@ Suppose a site has been developed and populated with content on a staging enviro * On both instances, run: `./manage.py preseed_transfer_table wagtailcore.page --range=1-199` The `preseed_transfer_table` command generates consistent UUIDs between the two site instances, so any transfers involving this ID range will recognise the pages as matching, and handle them as updates rather than creations. - \ No newline at end of file diff --git a/docs/settings.md b/docs/settings.md index d922fca..4a0561a 100644 --- a/docs/settings.md +++ b/docs/settings.md @@ -5,24 +5,24 @@ ### `WAGTAILTRANSFER_SECRET_KEY` ```python -WAGTAILTRANSFER_SECRET_KEY = '7cd5de8229be75e1e0c2af8abc2ada7e' +WAGTAILTRANSFER_SECRET_KEY = "7cd5de8229be75e1e0c2af8abc2ada7e" ``` -The secret key used to authenticate requests to import content from this site to another. The secret key in the -matching part of the importing site's `WAGTAILTRANSFER_SOURCES` must be identical, or the transfer will be rejected - -this prevents unauthorised import of sensitive data. +The secret key used to authenticate requests to import content from this site to another. The secret key in the +matching part of the importing site's `WAGTAILTRANSFER_SOURCES` must be identical, or the transfer will be rejected - +this prevents unauthorised import of sensitive data. ### `WAGTAILTRANSFER_SOURCES` ```python WAGTAILTRANSFER_SOURCES = { - 'staging': { - 'BASE_URL': 'https://staging.example.com/wagtail-transfer/', - 'SECRET_KEY': '4ac4822149691395773b2a8942e1a472', + "staging": { + "BASE_URL": "https://staging.example.com/wagtail-transfer/", + "SECRET_KEY": "4ac4822149691395773b2a8942e1a472", }, - 'production': { - 'BASE_URL': 'https://www.example.com/wagtail-transfer/', - 'SECRET_KEY': 'a36476ffc6af34dc935570d97369eca0', + "production": { + "BASE_URL": "https://www.example.com/wagtail-transfer/", + "SECRET_KEY": "a36476ffc6af34dc935570d97369eca0", }, } ``` @@ -32,44 +32,44 @@ A dictionary defining the sites available to import from, and their secret keys. ### `WAGTAILTRANSFER_UPDATE_RELATED_MODELS` ```python -WAGTAILTRANSFER_UPDATE_RELATED_MODELS = ['wagtailimages.image', 'adverts.advert'] +WAGTAILTRANSFER_UPDATE_RELATED_MODELS = ["wagtailimages.image", "adverts.advert"] ``` -Specifies a list of models that, whenever we encounter references to them in imported content, should be updated to the +Specifies a list of models that, whenever we encounter references to them in imported content, should be updated to the latest version from the source site as part of the import. -Whenever an object being imported contains a reference to a related object (through a ForeignKey, RichTextField or -StreamField), the 'importance' of that related object will tend to vary according to its type. For example, a reference -to an Image object within a page usually means that the image will be shown on that page; in this case, the Image model -is sufficiently important to the imported page that we want the importer to not only ensure that image exists at the -destination, but is updated to its newest version as well. Contrast this with the example of an 'author' snippet -attached to blog posts, containing various fields of data about that person (e.g. bio, social media links); in this -case, the author information is not really part of the blog post, and it's not expected that we would update it when +Whenever an object being imported contains a reference to a related object (through a ForeignKey, RichTextField or +StreamField), the 'importance' of that related object will tend to vary according to its type. For example, a reference +to an Image object within a page usually means that the image will be shown on that page; in this case, the Image model +is sufficiently important to the imported page that we want the importer to not only ensure that image exists at the +destination, but is updated to its newest version as well. Contrast this with the example of an 'author' snippet +attached to blog posts, containing various fields of data about that person (e.g. bio, social media links); in this +case, the author information is not really part of the blog post, and it's not expected that we would update it when running an import of blog posts. ### `WAGTAILTRANSFER_LOOKUP_FIELDS` ```python -WAGTAILTRANSFER_LOOKUP_FIELDS = {'blog.author': ['first_name', 'surname']} +WAGTAILTRANSFER_LOOKUP_FIELDS = {"blog.author": ["first_name", "surname"]} ``` Specifies a list of fields to use for object lookups on the given models. -Normally, imported objects will be assigned a random UUID known across all sites, so that those objects can be -recognised on subsequent imports and be updated rather than creating a duplicate. This behaviour is less useful for -models that already have a uniquely identifying field, or set of fields, such as an author identified by first name -and surname - if the same author exists on the source and destination site, but this was not the result of a previous -import, then the UUID-based matching will consider them distinct, and attempt to create a duplicate author record at the -destination. Adding an entry in WAGTAILTRANSFER_LOOKUP_FIELDS will mean that any imported instances of the given model +Normally, imported objects will be assigned a random UUID known across all sites, so that those objects can be +recognised on subsequent imports and be updated rather than creating a duplicate. This behaviour is less useful for +models that already have a uniquely identifying field, or set of fields, such as an author identified by first name +and surname - if the same author exists on the source and destination site, but this was not the result of a previous +import, then the UUID-based matching will consider them distinct, and attempt to create a duplicate author record at the +destination. Adding an entry in WAGTAILTRANSFER_LOOKUP_FIELDS will mean that any imported instances of the given model will be looked up based on the specified fields, rather than by UUID. The default value for `WAGTAILTRANSFER_LOOKUP_FIELDS` is: ```python { - 'taggit.tag': ['slug'], - 'wagtailcore.locale': ["language_code"], - 'contenttypes.contenttype': ['app_label', 'model'], + "taggit.tag": ["slug"], + "wagtailcore.locale": ["language_code"], + "contenttypes.contenttype": ["app_label", "model"], } ``` @@ -78,31 +78,33 @@ Overriding these values may result in issues as described above, particularly in ### `WAGTAILTRANSFER_NO_FOLLOW_MODELS` ```python -WAGTAILTRANSFER_NO_FOLLOW_MODELS = ['wagtailcore.page', 'organisations.Company'] +WAGTAILTRANSFER_NO_FOLLOW_MODELS = ["wagtailcore.page", "organisations.Company"] ``` -Specifies a list of models that should not be imported by association when they are referenced from imported content. +Specifies a list of models that should not be imported by association when they are referenced from imported content. Defaults to `['wagtailcore.page', 'contenttypes.contenttype']`. -By default, objects referenced within imported content will be recursively imported to ensure that those references are -still valid on the destination site. However, this is not always desirable - for example, if this happened for the Page -model, this would imply that any pages linked from an imported page would get imported as well, along with any pages -linked from those pages, and so on, leading to an unpredictable number of extra pages being added anywhere in the page -tree as a side-effect of the import. Models listed in WAGTAILTRANSFER_NO_FOLLOW_MODELS will thus be skipped in this -process, leaving the reference unresolved. The effect this has on the referencing page will vary according to the kind -of relation: nullable foreign keys, one-to-many and many-to-many relations will simply omit the missing object; -references in rich text and StreamField will become broken links (just as linking a page and then deleting it would); -while non-nullable foreign keys will prevent the object from being created at all (meaning that any objects referencing +By default, objects referenced within imported content will be recursively imported to ensure that those references are +still valid on the destination site. However, this is not always desirable - for example, if this happened for the Page +model, this would imply that any pages linked from an imported page would get imported as well, along with any pages +linked from those pages, and so on, leading to an unpredictable number of extra pages being added anywhere in the page +tree as a side-effect of the import. Models listed in WAGTAILTRANSFER_NO_FOLLOW_MODELS will thus be skipped in this +process, leaving the reference unresolved. The effect this has on the referencing page will vary according to the kind +of relation: nullable foreign keys, one-to-many and many-to-many relations will simply omit the missing object; +references in rich text and StreamField will become broken links (just as linking a page and then deleting it would); +while non-nullable foreign keys will prevent the object from being created at all (meaning that any objects referencing that object will end up with unresolved references, to be handled by the same set of rules). -Note that these settings do not accept models that are defined as subclasses through multi-table inheritance - in +Note that these settings do not accept models that are defined as subclasses through multi-table inheritance - in particular, they cannot be used to define behaviour that only applies to specific subclasses of Page. ### `WAGTAILTRANSFER_FOLLOWED_REVERSE_RELATIONS` ```python -WAGTAILTRANSFER_FOLLOWED_REVERSE_RELATIONS = [('wagtailimages.image', 'tagged_items', True)] +WAGTAILTRANSFER_FOLLOWED_REVERSE_RELATIONS = [ + ("wagtailimages.image", "tagged_items", True) +] ``` Specifies a list of models, their reverse relations to follow, and whether deletions should be synced, when identifying object references that should be imported to the destination site. Defaults to `[('wagtailimages.image', 'tagged_items', True)]`. @@ -145,10 +147,9 @@ class MyCustomAdapter(FieldAdapter): pass -@hooks.register('register_field_adapters') +@hooks.register("register_field_adapters") def register_my_custom_adapter(): return {models.Field: MyCustomAdapter} - ``` @@ -168,18 +169,17 @@ from wagtail_transfer.serializers import PageSerializer from myapp.models import MyModel -class MyModelCustomSerializer(PageSerializer): +class MyModelCustomSerializer(PageSerializer): ignored_fields = PageSerializer.ignored_fields + [ - 'secret_field_1', - 'environment_specific_data_field_123', - ... + "secret_field_1", + "environment_specific_data_field_123", + ..., ] pass -@hooks.register('register_custom_serializers') +@hooks.register("register_custom_serializers") def register_my_custom_serializer(): return {MyModel: MyModelCustomSerializer} - ``` diff --git a/docs/setup.md b/docs/setup.md index 1e4bb01..eb4c783 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -7,13 +7,13 @@ 3. In your project's top-level urls.py, add: from wagtail_transfer import urls as wagtailtransfer_urls - + and add: url(r'^wagtail-transfer/', include(wagtailtransfer_urls)), - + to the `urlpatterns` list above `include(wagtail_urls)`. - + 4. Add the settings `WAGTAILTRANSFER_SOURCES` and `WAGTAILTRANSFER_SECRET_KEY` to your project settings. These are formatted as: @@ -29,20 +29,19 @@ } WAGTAILTRANSFER_SECRET_KEY = '7cd5de8229be75e1e0c2af8abc2ada7e' - + However, it is best to store the `SECRET_KEY`s themselves in local environment variables for security. - + `WAGTAILTRANSFER_SOURCES` is a dictionary defining the sites available to import from, and their secret keys. - `WAGTAILTRANSFER_SECRET_KEY` and the per-source `SECRET_KEY` settings are used to authenticate the communication between the - source and destination instances; this prevents unauthorised users from using this API to retrieve sensitive data such - as password hashes. The `SECRET_KEY` for each entry in `WAGTAILTRANSFER_SOURCES` must match that instance's + `WAGTAILTRANSFER_SECRET_KEY` and the per-source `SECRET_KEY` settings are used to authenticate the communication between the + source and destination instances; this prevents unauthorised users from using this API to retrieve sensitive data such + as password hashes. The `SECRET_KEY` for each entry in `WAGTAILTRANSFER_SOURCES` must match that instance's `WAGTAILTRANSFER_SECRET_KEY`. - -Once you've followed these instructions for all your source and destination sites, you can start -[importing content](basic_usage.md). -If you need additional configuration - you want to configure which referenced models are updated, how models are identified +Once you've followed these instructions for all your source and destination sites, you can start +[importing content](basic_usage.md). + +If you need additional configuration - you want to configure which referenced models are updated, how models are identified between Wagtail instances, or which models are pulled in and imported from references on an imported page, you can check out [how mappings and references work](how_it_works.md) and the [settings reference](settings.md). - diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 0000000..6137866 --- /dev/null +++ b/ruff.toml @@ -0,0 +1,54 @@ +target-version = "py38" + +exclude = [ + "vendor", + "dist", + "build", + "venv", + ".venv", + ".tox", + ".git", + "__pycache__", + "node_modules", + "LC_MESSAGES", + "locale", + "migrations", +] + +select = [ + "B", # flake8-bugbear + "BLE", # flake8-blind-except + "C4", # flake8-comprehensions + "DJ", # flake8-django + "E", # pycodestyle errors + "F", # pyflakes + "I", # isort + "PGH", # pygrep-hooks + "RUF100", # unused noqa + "S", # flake8-bandit + "T20", # flake8-print + "UP", # pyupgrade + "W", # pycodestyle warnings + "YTT", # flake8-2020 +] + +fixable = ["C4", "E", "F", "I", "UP"] + +ignore = [ + "E501", # line-too-long (conflicts with formatter) + "W191", # tab-indentation (conflicts with formatter) + "DJ008", # model without __str__ method + "B019", # functools.cache/lru_cache can lead to memory leaks + "S113", # use of requests without timeout +] + +[lint.per-file-ignores] +"**/tests/**/*.py" = [ + "S105", # possible hardcoded password + "S106", # possible hardcoded password + "DJ001", # use of null=True on CharField +] +".circleci/report_nightly_build_failure.py" = ["T201"] # use of print() + +[isort] +known-first-party = ["wagtail_transfer"] diff --git a/runtests.py b/runtests.py index 5c6aa2a..4dc6d8a 100755 --- a/runtests.py +++ b/runtests.py @@ -5,5 +5,5 @@ from django.core.management import execute_from_command_line -os.environ['DJANGO_SETTINGS_MODULE'] = 'tests.settings' -execute_from_command_line([sys.argv[0], 'test'] + sys.argv[1:]) +os.environ["DJANGO_SETTINGS_MODULE"] = "tests.settings" +execute_from_command_line([sys.argv[0], "test"] + sys.argv[1:]) diff --git a/setup.py b/setup.py index 13e0e2a..1e3ae75 100644 --- a/setup.py +++ b/setup.py @@ -3,41 +3,42 @@ from setuptools import find_packages, setup setup( - name='wagtail-transfer', - version='0.9.1', + name="wagtail-transfer", + version="0.9.1", description="Content transfer for Wagtail", - author='Matthew Westcott', - author_email='matthew.westcott@torchbox.com', - url='https://github.com/wagtail/wagtail-transfer', - packages=find_packages(exclude=('tests',)), + author="Matthew Westcott", + author_email="matthew.westcott@torchbox.com", + url="https://github.com/wagtail/wagtail-transfer", + packages=find_packages(exclude=("tests",)), include_package_data=True, - install_requires=[ - 'wagtail>=4.1' - ], + install_requires=["wagtail>=4.1"], extras_require={ - 'docs': [ - 'mkdocs>=1.0,<1.1', - 'mkdocs-material>=4.6,<4.7', + "docs": [ + "mkdocs>=1.0,<1.1", + "mkdocs-material>=4.6,<4.7", + ], + "dev": [ + "ruff>=1.2.0", ], }, python_requires=">=3.7", - license='BSD', + license="BSD", long_description="An extension for Wagtail allowing content to be transferred between multiple instances of a Wagtail project", classifiers=[ - 'Development Status :: 4 - Beta', - 'Environment :: Web Environment', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: BSD License', - 'Operating System :: OS Independent', - 'Programming Language :: Python', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: 3.10', - 'Programming Language :: Python :: 3.11', - 'Framework :: Django', - 'Framework :: Wagtail', - 'Framework :: Wagtail :: 4', + "Development Status :: 4 - Beta", + "Environment :: Web Environment", + "Intended Audience :: Developers", + "License :: OSI Approved :: BSD License", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Framework :: Django", + "Framework :: Wagtail", + "Framework :: Wagtail :: 4", ], ) diff --git a/tests/apps.py b/tests/apps.py index b79f9f5..3e61cda 100644 --- a/tests/apps.py +++ b/tests/apps.py @@ -2,5 +2,5 @@ class WagtailTransferTestsAppConfig(AppConfig): - name = 'tests' - default_auto_field = 'django.db.models.AutoField' + name = "tests" + default_auto_field = "django.db.models.AutoField" diff --git a/tests/blocks.py b/tests/blocks.py index a0f8dfd..51ba31c 100644 --- a/tests/blocks.py +++ b/tests/blocks.py @@ -1,6 +1,12 @@ -from wagtail.blocks import (CharBlock, IntegerBlock, ListBlock, - PageChooserBlock, RichTextBlock, StreamBlock, - StructBlock) +from wagtail.blocks import ( + CharBlock, + IntegerBlock, + ListBlock, + PageChooserBlock, + RichTextBlock, + StreamBlock, + StructBlock, +) from wagtail.documents.blocks import DocumentChooserBlock diff --git a/tests/models.py b/tests/models.py index 4ba8b27..d2040f9 100644 --- a/tests/models.py +++ b/tests/models.py @@ -34,7 +34,7 @@ class Category(models.Model): colour = models.CharField(max_length=255, blank=True, null=True) def __str__(self): - return "{} {}".format(self.colour, self.name) + return f"{self.colour} {self.name}" class SponsoredPage(Page): @@ -59,7 +59,9 @@ class PageWithRichText(Page): class PageWithStreamField(Page): - body = StreamField(BaseStreamBlock(), verbose_name="Page body", blank=True, use_json_field=True) + body = StreamField( + BaseStreamBlock(), verbose_name="Page body", blank=True, use_json_field=True + ) class PageWithParentalManyToMany(Page): diff --git a/tests/settings.py b/tests/settings.py index edd8d60..9b859ca 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -9,137 +9,135 @@ # Application definition INSTALLED_APPS = [ - 'tests', - 'wagtail_transfer', - - 'wagtail.contrib.forms', - 'wagtail.contrib.redirects', - 'wagtail.embeds', - 'wagtail.sites', - 'wagtail.users', - 'wagtail.snippets', - 'wagtail.documents', - 'wagtail.images', - 'wagtail.search', - 'wagtail.admin', - 'wagtail', - 'modelcluster', - 'taggit', - - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', + "tests", + "wagtail_transfer", + "wagtail.contrib.forms", + "wagtail.contrib.redirects", + "wagtail.embeds", + "wagtail.sites", + "wagtail.users", + "wagtail.snippets", + "wagtail.documents", + "wagtail.images", + "wagtail.search", + "wagtail.admin", + "wagtail", + "modelcluster", + "taggit", + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", ] MIDDLEWARE = [ - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', - 'django.middleware.security.SecurityMiddleware', - - 'wagtail.contrib.redirects.middleware.RedirectMiddleware', + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", + "django.middleware.security.SecurityMiddleware", + "wagtail.contrib.redirects.middleware.RedirectMiddleware", ] -ROOT_URLCONF = 'tests.urls' +ROOT_URLCONF = "tests.urls" TEMPLATES = [ { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', + "BACKEND": "django.template.backends.django.DjangoTemplates", + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", ], }, }, ] -WSGI_APPLICATION = 'tests.wsgi.application' +WSGI_APPLICATION = "tests.wsgi.application" # Database # https://docs.djangoproject.com/en/{{ docs_version }}/ref/settings/#databases DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": os.path.join(BASE_DIR, "db.sqlite3"), } } AUTH_PASSWORD_VALIDATORS = [ { - 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", }, ] PASSWORD_HASHERS = ( - 'django.contrib.auth.hashers.MD5PasswordHasher', # don't use the intentionally slow default password hasher + "django.contrib.auth.hashers.MD5PasswordHasher", # don't use the intentionally slow default password hasher ) -LANGUAGE_CODE = 'en-us' -TIME_ZONE = 'UTC' +LANGUAGE_CODE = "en-us" +TIME_ZONE = "UTC" USE_I18N = True USE_L10N = True USE_TZ = True STATICFILES_FINDERS = [ - 'django.contrib.staticfiles.finders.FileSystemFinder', - 'django.contrib.staticfiles.finders.AppDirectoriesFinder', + "django.contrib.staticfiles.finders.FileSystemFinder", + "django.contrib.staticfiles.finders.AppDirectoriesFinder", ] -STATIC_ROOT = os.path.join(BASE_DIR, 'static') -STATIC_URL = '/static/' +STATIC_ROOT = os.path.join(BASE_DIR, "static") +STATIC_URL = "/static/" -MEDIA_ROOT = os.path.join(BASE_DIR, 'test-media') -MEDIA_URL = 'http://media.example.com/media/' +MEDIA_ROOT = os.path.join(BASE_DIR, "test-media") +MEDIA_URL = "http://media.example.com/media/" -SECRET_KEY = 'not needed' +SECRET_KEY = "not needed" # Wagtail settings WAGTAIL_SITE_NAME = "wagtail-transfer" -WAGTAILADMIN_BASE_URL = 'http://example.com' +WAGTAILADMIN_BASE_URL = "http://example.com" WAGTAILTRANSFER_SOURCES = { - 'staging': { - 'BASE_URL': 'https://www.example.com/wagtail-transfer/', - 'SECRET_KEY': 'i-am-the-staging-example-secret-key', + "staging": { + "BASE_URL": "https://www.example.com/wagtail-transfer/", + "SECRET_KEY": "i-am-the-staging-example-secret-key", }, - 'local': { + "local": { # so that we can use the wagtail_transfer.auth.digest_for_source helper in API tests - 'BASE_URL': 'http://localhost/wagtail-transfer/', - 'SECRET_KEY': 'i-am-the-local-secret-key', - } + "BASE_URL": "http://localhost/wagtail-transfer/", + "SECRET_KEY": "i-am-the-local-secret-key", + }, } -WAGTAILTRANSFER_FOLLOWED_REVERSE_RELATIONS = [('wagtailimages.image', 'tagged_items', True), ('tests.advert', 'tagged_items', True)] +WAGTAILTRANSFER_FOLLOWED_REVERSE_RELATIONS = [ + ("wagtailimages.image", "tagged_items", True), + ("tests.advert", "tagged_items", True), +] -WAGTAILTRANSFER_SECRET_KEY = 'i-am-the-local-secret-key' +WAGTAILTRANSFER_SECRET_KEY = "i-am-the-local-secret-key" -WAGTAILTRANSFER_UPDATE_RELATED_MODELS = ['wagtailimages.Image', 'tests.advert'] +WAGTAILTRANSFER_UPDATE_RELATED_MODELS = ["wagtailimages.Image", "tests.advert"] -WAGTAILTRANSFER_LOOKUP_FIELDS = { - 'tests.category': ['name'] -} +WAGTAILTRANSFER_LOOKUP_FIELDS = {"tests.category": ["name"]} -WAGTAILADMIN_BASE_URL = 'http://example.com' +WAGTAILADMIN_BASE_URL = "http://example.com" diff --git a/tests/tests/test_api.py b/tests/tests/test_api.py index 63cf69e..ee0e22f 100644 --- a/tests/tests/test_api.py +++ b/tests/tests/test_api.py @@ -14,82 +14,98 @@ from wagtail.images.models import Image from wagtail.models import Collection, Page -from tests.models import (Advert, Avatar, Category, LongAdvert, - ModelWithManyToMany, PageWithParentalManyToMany, - PageWithRichText, PageWithStreamField, SectionedPage, - SponsoredPage) +from tests.models import ( + Advert, + Avatar, + Category, + LongAdvert, + ModelWithManyToMany, + PageWithParentalManyToMany, + PageWithRichText, + PageWithStreamField, + SectionedPage, + SponsoredPage, +) from wagtail_transfer.auth import digest_for_source from wagtail_transfer.models import IDMapping # We could use settings.MEDIA_ROOT here, but this way we avoid clobbering a real media folder if we # ever run these tests with non-test settings for any reason -TEST_MEDIA_DIR = os.path.join(os.path.join(settings.BASE_DIR, 'test-media')) -FIXTURES_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'fixtures') +TEST_MEDIA_DIR = os.path.join(os.path.join(settings.BASE_DIR, "test-media")) +FIXTURES_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "fixtures") class TestPageChooserApi(TestCase): def test_incorrect_digest(self): response = self.client.get( - '/wagtail-transfer/api/chooser/pages/?child_of=root&fields=parent%2Cchildren&limit=20&offset=0&digest=4' + "/wagtail-transfer/api/chooser/pages/?child_of=root&fields=parent%2Cchildren&limit=20&offset=0&digest=4" ) self.assertEqual(response.status_code, 403) def test_correct_digest(self): - digest = digest_for_source('local', 'child_of=root&fields=parent%2Cchildren&limit=20&offset=0') + digest = digest_for_source( + "local", "child_of=root&fields=parent%2Cchildren&limit=20&offset=0" + ) response = self.client.get( - f'/wagtail-transfer/api/chooser/pages/?child_of=root&fields=parent%2Cchildren&limit=20&offset=0&digest={digest}' + f"/wagtail-transfer/api/chooser/pages/?child_of=root&fields=parent%2Cchildren&limit=20&offset=0&digest={digest}" ) self.assertEqual(response.status_code, 200) class TestModelsApi(TestCase): - fixtures = ['test.json'] + fixtures = ["test.json"] - def get_parameters(self, initial_get='models=true'): - digest = digest_for_source('local', initial_get) - return f'?{initial_get}&digest={digest}' + def get_parameters(self, initial_get="models=true"): + digest = digest_for_source("local", initial_get) + return f"?{initial_get}&digest={digest}" def test_model_chooser_response(self): - response = self.client.get(f'/wagtail-transfer/api/chooser/models/{self.get_parameters()}') + response = self.client.get( + f"/wagtail-transfer/api/chooser/models/{self.get_parameters()}" + ) self.assertEqual(response.status_code, 200) content = json.loads(response.content.decode("utf-8")) - self.assertEqual(content['meta']['total_count'], 1) + self.assertEqual(content["meta"]["total_count"], 1) - snippet = content['items'][0] - self.assertEqual(snippet['model_label'], 'tests.category') - self.assertEqual(snippet['name'], 'Category') + snippet = content["items"][0] + self.assertEqual(snippet["model_label"], "tests.category") + self.assertEqual(snippet["name"], "Category") def test_model_object_chooser(self): - response = self.client.get(f'/wagtail-transfer/api/chooser/models/tests.category/{self.get_parameters()}') + response = self.client.get( + f"/wagtail-transfer/api/chooser/models/tests.category/{self.get_parameters()}" + ) self.assertEqual(response.status_code, 200) content = json.loads(response.content.decode("utf-8")) - self.assertEqual(content['meta']['total_count'], 1) - self.assertEqual(content['meta']['next'], None) - self.assertEqual(content['meta']['previous'], None) + self.assertEqual(content["meta"]["total_count"], 1) + self.assertEqual(content["meta"]["next"], None) + self.assertEqual(content["meta"]["previous"], None) - snippet = content['items'][0] - self.assertEqual(snippet['model_label'], 'tests.category') - self.assertEqual(snippet['object_name'], 'red Cars') - self.assertEqual(snippet['name'], 'Cars') - self.assertEqual(snippet['colour'], 'red') + snippet = content["items"][0] + self.assertEqual(snippet["model_label"], "tests.category") + self.assertEqual(snippet["object_name"], "red Cars") + self.assertEqual(snippet["name"], "Cars") + self.assertEqual(snippet["colour"], "red") def test_model_object_next_pagination(self): # Create 50 more categories for i in range(50): - name = "Car #{}".format(i) + name = f"Car #{i}" Category.objects.create(name=name, colour="Violet") - response = self.client.get(f'/wagtail-transfer/api/chooser/models/tests.category/{self.get_parameters()}') + response = self.client.get( + f"/wagtail-transfer/api/chooser/models/tests.category/{self.get_parameters()}" + ) self.assertEqual(response.status_code, 200) content = json.loads(response.content.decode("utf-8")) - self.assertEqual(content['meta']['total_count'], 51) - self.assertTrue(bool(content['meta']['next'])) - self.assertFalse(bool(content['meta']['previous'])) + self.assertEqual(content["meta"]["total_count"], 51) + self.assertTrue(bool(content["meta"]["next"])) + self.assertFalse(bool(content["meta"]["previous"])) - items = content['items'] + items = content["items"] self.assertEqual(len(items), 20) # Remove the newly created categories @@ -98,18 +114,20 @@ def test_model_object_next_pagination(self): def test_model_object_previous_and_next_pagination(self): # Create 50 more categories for i in range(50): - name = "Car #{}".format(i) + name = f"Car #{i}" Category.objects.create(name=name, colour="Violet") - response = self.client.get(f'/wagtail-transfer/api/chooser/models/tests.category/{self.get_parameters("page=2")}') + response = self.client.get( + f'/wagtail-transfer/api/chooser/models/tests.category/{self.get_parameters("page=2")}' + ) self.assertEqual(response.status_code, 200) content = json.loads(response.content.decode("utf-8")) - self.assertEqual(content['meta']['total_count'], 51) - self.assertTrue(bool(content['meta']['previous'])) - self.assertTrue(bool(content['meta']['next'])) + self.assertEqual(content["meta"]["total_count"], 51) + self.assertTrue(bool(content["meta"]["previous"])) + self.assertTrue(bool(content["meta"]["next"])) - items = content['items'] + items = content["items"] self.assertEqual(len(items), 20) # Remove the newly created categories @@ -118,20 +136,22 @@ def test_model_object_previous_and_next_pagination(self): def test_model_object_previous_pagination(self): # Create 50 more categories for i in range(50): - name = "Car #{}".format(i) + name = f"Car #{i}" Category.objects.create(name=name, colour="Violet") - response = self.client.get(f'/wagtail-transfer/api/chooser/models/tests.category/{self.get_parameters("page=3")}') + response = self.client.get( + f'/wagtail-transfer/api/chooser/models/tests.category/{self.get_parameters("page=3")}' + ) self.assertEqual(response.status_code, 200) content = json.loads(response.content.decode("utf-8")) - self.assertEqual(content['meta']['total_count'], 51) - self.assertTrue(bool(content['meta']['previous'])) - self.assertFalse(bool(content['meta']['next'])) + self.assertEqual(content["meta"]["total_count"], 51) + self.assertTrue(bool(content["meta"]["previous"])) + self.assertFalse(bool(content["meta"]["next"])) # Pagination happens 20 at a time by default. # Page 3 = 2 pages of 20, with 11 remaining. - items = content['items'] + items = content["items"] self.assertEqual(len(items), 11) # Remove the newly created categories @@ -139,14 +159,17 @@ def test_model_object_previous_pagination(self): class TestPagesApi(TestCase): - fixtures = ['test.json'] + fixtures = ["test.json"] def get(self, page_id, recursive=True): - digest = digest_for_source('local', str(page_id)) - return self.client.get('/wagtail-transfer/api/pages/%d/?digest=%s&recursive=%s' % (page_id, digest, str(recursive).lower())) + digest = digest_for_source("local", str(page_id)) + return self.client.get( + "/wagtail-transfer/api/pages/%d/?digest=%s&recursive=%s" + % (page_id, digest, str(recursive).lower()) + ) def test_incorrect_digest(self): - response = self.client.get('/wagtail-transfer/api/pages/2/?digest=12345678') + response = self.client.get("/wagtail-transfer/api/pages/2/?digest=12345678") self.assertEqual(response.status_code, 403) def test_pages_api(self): @@ -154,23 +177,27 @@ def test_pages_api(self): self.assertEqual(response.status_code, 200) data = json.loads(response.content) - ids_for_import = data['ids_for_import'] - self.assertIn(['wagtailcore.page', 2], ids_for_import) - self.assertNotIn(['wagtailcore.page', 1], ids_for_import) + ids_for_import = data["ids_for_import"] + self.assertIn(["wagtailcore.page", 2], ids_for_import) + self.assertNotIn(["wagtailcore.page", 1], ids_for_import) homepage = None - for obj in data['objects']: - if obj['model'] == 'tests.simplepage' and obj['pk'] == 2: + for obj in data["objects"]: + if obj["model"] == "tests.simplepage" and obj["pk"] == 2: homepage = obj break self.assertTrue(homepage) - self.assertEqual(homepage['parent_id'], 1) - self.assertEqual(homepage['fields']['intro'], "This is the homepage") + self.assertEqual(homepage["parent_id"], 1) + self.assertEqual(homepage["fields"]["intro"], "This is the homepage") - mappings = data['mappings'] - self.assertIn(['wagtailcore.page', 2, "22222222-2222-2222-2222-222222222222"], mappings) - self.assertIn(['tests.advert', 1, "adadadad-1111-1111-1111-111111111111"], mappings) + mappings = data["mappings"] + self.assertIn( + ["wagtailcore.page", 2, "22222222-2222-2222-2222-222222222222"], mappings + ) + self.assertIn( + ["tests.advert", 1, "adadadad-1111-1111-1111-111111111111"], mappings + ) def test_export_root(self): response = self.get(1) @@ -178,16 +205,16 @@ def test_export_root(self): data = json.loads(response.content) root_page = None - for obj in data['objects']: - if obj['model'] == 'wagtailcore.page' and obj['pk'] == 1: + for obj in data["objects"]: + if obj["model"] == "wagtailcore.page" and obj["pk"] == 1: root_page = obj break self.assertTrue(root_page) - self.assertEqual(root_page['parent_id'], None) + self.assertEqual(root_page["parent_id"], None) # check that the child page will also be imported - self.assertIn(['wagtailcore.page', 2], data['ids_for_import']) + self.assertIn(["wagtailcore.page", 2], data["ids_for_import"]) def test_export_nonrecursive(self): response = self.get(1, recursive=False) @@ -195,17 +222,21 @@ def test_export_nonrecursive(self): data = json.loads(response.content) # check that the child page will not be imported - self.assertNotIn(['wagtailcore.page', 2], data['ids_for_import']) + self.assertNotIn(["wagtailcore.page", 2], data["ids_for_import"]) # check that the original page is still listed for import - self.assertIn(['wagtailcore.page', 1], data['ids_for_import']) + self.assertIn(["wagtailcore.page", 1], data["ids_for_import"]) def test_parental_keys(self): - page = SectionedPage(title='How to make a cake', intro="Here is how to make a cake.") - page.sections.create(title="Create the universe", body="First, create the universe") + page = SectionedPage( + title="How to make a cake", intro="Here is how to make a cake." + ) + page.sections.create( + title="Create the universe", body="First, create the universe" + ) page.sections.create(title="Find some eggs", body="Next, find some eggs") - parent_page = Page.objects.get(url_path='/home/existing-child-page/') + parent_page = Page.objects.get(url_path="/home/existing-child-page/") parent_page.add_child(instance=page) response = self.get(parent_page.id) @@ -214,28 +245,32 @@ def test_parental_keys(self): page_data = None section_data = [] - for obj in data['objects']: - if obj['model'] == 'tests.sectionedpage' and obj['pk'] == page.pk: + for obj in data["objects"]: + if obj["model"] == "tests.sectionedpage" and obj["pk"] == page.pk: page_data = obj - if obj['model'] == 'tests.sectionedpagesection': + if obj["model"] == "tests.sectionedpagesection": section_data.append(obj) - self.assertEqual(len(page_data['fields']['sections']), 2) - self.assertEqual(section_data[0]['model'], 'tests.sectionedpagesection') - self.assertTrue(section_data[0]['fields']['title'] == "Create the universe") - section_id = page_data['fields']['sections'][0] + self.assertEqual(len(page_data["fields"]["sections"]), 2) + self.assertEqual(section_data[0]["model"], "tests.sectionedpagesection") + self.assertTrue(section_data[0]["fields"]["title"] == "Create the universe") + section_id = page_data["fields"]["sections"][0] # there should also be a uid mapping for the section matching_uids = [ - uid for model_name, pk, uid in data['mappings'] - if model_name == 'tests.sectionedpagesection' and pk == section_id + uid + for model_name, pk, uid in data["mappings"] + if model_name == "tests.sectionedpagesection" and pk == section_id ] self.assertEqual(len(matching_uids), 1) def test_rich_text_with_page_link(self): - page = PageWithRichText(title="You won't believe how rich this cake was!", body='
But I have a link
') + page = PageWithRichText( + title="You won't believe how rich this cake was!", + body='But I have a link
', + ) - parent_page = Page.objects.get(url_path='/home/existing-child-page/') + parent_page = Page.objects.get(url_path="/home/existing-child-page/") parent_page.add_child(instance=page) response = self.get(page.id) @@ -243,12 +278,18 @@ def test_rich_text_with_page_link(self): self.assertEqual(response.status_code, 200) data = json.loads(response.content) - self.assertIn(['wagtailcore.page', 1, '11111111-1111-1111-1111-111111111111'], data['mappings']) + self.assertIn( + ["wagtailcore.page", 1, "11111111-1111-1111-1111-111111111111"], + data["mappings"], + ) def test_rich_text_with_dead_page_link(self): - page = PageWithRichText(title="You won't believe how rich this cake was!", body='But I have a link
') + page = PageWithRichText( + title="You won't believe how rich this cake was!", + body='But I have a link
', + ) - parent_page = Page.objects.get(url_path='/home/existing-child-page/') + parent_page = Page.objects.get(url_path="/home/existing-child-page/") parent_page.add_child(instance=page) response = self.get(page.id) @@ -256,15 +297,17 @@ def test_rich_text_with_dead_page_link(self): self.assertEqual(response.status_code, 200) data = json.loads(response.content) - self.assertTrue(any( - model == 'wagtailcore.page' and id == 999 - for model, id, uid in data['mappings'] - )) + self.assertTrue( + any( + model == "wagtailcore.page" and id == 999 + for model, id, uid in data["mappings"] + ) + ) def test_null_rich_text(self): page = PageWithRichText(title="I'm lost for words", body=None) - parent_page = Page.objects.get(url_path='/home/existing-child-page/') + parent_page = Page.objects.get(url_path="/home/existing-child-page/") parent_page.add_child(instance=page) response = self.get(page.id) @@ -272,22 +315,21 @@ def test_null_rich_text(self): self.assertEqual(response.status_code, 200) data = json.loads(response.content) - self.assertTrue(any( - obj['pk'] == page.pk - for obj in data['objects'] - )) + self.assertTrue(any(obj["pk"] == page.pk for obj in data["objects"])) def test_rich_text_with_image_embed(self): - with open(os.path.join(FIXTURES_DIR, 'wagtail.jpg'), 'rb') as f: + with open(os.path.join(FIXTURES_DIR, "wagtail.jpg"), "rb") as f: image = Image.objects.create( - title="Wagtail", - file=ImageFile(f, name='wagtail.jpg') + title="Wagtail", file=ImageFile(f, name="wagtail.jpg") ) - body = 'Here is an image
' % image.pk + body = ( + 'Here is an image
' + % image.pk + ) page = PageWithRichText(title="The cake is a lie.", body=body) - parent_page = Page.objects.get(url_path='/home/existing-child-page/') + parent_page = Page.objects.get(url_path="/home/existing-child-page/") parent_page.add_child(instance=page) response = self.get(page.id) @@ -295,130 +337,203 @@ def test_rich_text_with_image_embed(self): self.assertEqual(response.status_code, 200) data = json.loads(response.content) - self.assertTrue(any( - model == 'wagtailimages.image' and pk == image.pk - for model, pk, uid in data['mappings'] - )) + self.assertTrue( + any( + model == "wagtailimages.image" and pk == image.pk + for model, pk, uid in data["mappings"] + ) + ) def test_streamfield_with_page_links_in_new_listblock_format(self): - page = PageWithStreamField(title="I have a streamfield", - body=json.dumps([ - {'type': 'list_of_captioned_pages', - 'value': - [{'type': 'item', - 'value': { - 'page': 5, - 'text': 'a caption' - }, - 'id': '8c0d7de7-4f77-4477-be67-7d990d0bfb82'}], - 'id': '21ffe52a-c0fc-4ecc-92f1-17b356c9cc94'}, - ])) - parent_page = Page.objects.get(url_path='/home/existing-child-page/') + page = PageWithStreamField( + title="I have a streamfield", + body=json.dumps( + [ + { + "type": "list_of_captioned_pages", + "value": [ + { + "type": "item", + "value": {"page": 5, "text": "a caption"}, + "id": "8c0d7de7-4f77-4477-be67-7d990d0bfb82", + } + ], + "id": "21ffe52a-c0fc-4ecc-92f1-17b356c9cc94", + }, + ] + ), + ) + parent_page = Page.objects.get(url_path="/home/existing-child-page/") parent_page.add_child(instance=page) - digest = digest_for_source('local', str(page.id)) - response = self.client.get('/wagtail-transfer/api/pages/%d/?digest=%s' % (page.id, digest)) + digest = digest_for_source("local", str(page.id)) + response = self.client.get( + "/wagtail-transfer/api/pages/%d/?digest=%s" % (page.id, digest) + ) data = json.loads(response.content) # test PageChooserBlock in ListBlock - self.assertIn(['wagtailcore.page', 5, "00017017-5555-5555-5555-555555555555"], data['mappings']) + self.assertIn( + ["wagtailcore.page", 5, "00017017-5555-5555-5555-555555555555"], + data["mappings"], + ) def test_streamfield_with_page_links(self): # Check that page links in a complex nested StreamField - with StreamBlock, StructBlock, and ListBlock - # are all picked up in mappings - page = PageWithStreamField(title="I have a streamfield", - body=json.dumps([{'type': 'link_block', - 'value': - {'page': 1, - 'text': 'Test'}, - 'id': 'fc3b0d3d-d316-4271-9e31-84919558188a'}, - {'type': 'page', - 'value': 2, - 'id': 'c6d07d3a-72d4-445e-8fa5-b34107291176'}, - {'type': 'stream', - 'value': - [{'type': 'page', - 'value': 3, - 'id': '8c0d7de7-4f77-4477-be67-7d990d0bfb82'}], - 'id': '21ffe52a-c0fc-4ecc-92f1-17b356c9cc94'}, - {'type': 'list_of_pages', - 'value': [5], - 'id': '17b972cb-a952-4940-87e2-e4eb00703997'}])) - parent_page = Page.objects.get(url_path='/home/existing-child-page/') + page = PageWithStreamField( + title="I have a streamfield", + body=json.dumps( + [ + { + "type": "link_block", + "value": {"page": 1, "text": "Test"}, + "id": "fc3b0d3d-d316-4271-9e31-84919558188a", + }, + { + "type": "page", + "value": 2, + "id": "c6d07d3a-72d4-445e-8fa5-b34107291176", + }, + { + "type": "stream", + "value": [ + { + "type": "page", + "value": 3, + "id": "8c0d7de7-4f77-4477-be67-7d990d0bfb82", + } + ], + "id": "21ffe52a-c0fc-4ecc-92f1-17b356c9cc94", + }, + { + "type": "list_of_pages", + "value": [5], + "id": "17b972cb-a952-4940-87e2-e4eb00703997", + }, + ] + ), + ) + parent_page = Page.objects.get(url_path="/home/existing-child-page/") parent_page.add_child(instance=page) - digest = digest_for_source('local', str(page.id)) - response = self.client.get('/wagtail-transfer/api/pages/%d/?digest=%s' % (page.id, digest)) + digest = digest_for_source("local", str(page.id)) + response = self.client.get( + "/wagtail-transfer/api/pages/%d/?digest=%s" % (page.id, digest) + ) data = json.loads(response.content) # test PageChooserBlock in StructBlock - self.assertIn(['wagtailcore.page', 1, '11111111-1111-1111-1111-111111111111'], data['mappings']) + self.assertIn( + ["wagtailcore.page", 1, "11111111-1111-1111-1111-111111111111"], + data["mappings"], + ) # test un-nested PageChooserBlock - self.assertIn(['wagtailcore.page', 2, "22222222-2222-2222-2222-222222222222"], data['mappings']) + self.assertIn( + ["wagtailcore.page", 2, "22222222-2222-2222-2222-222222222222"], + data["mappings"], + ) # test PageChooserBlock in StreamBlock - self.assertIn(['wagtailcore.page', 3, "33333333-3333-3333-3333-333333333333"], data['mappings']) + self.assertIn( + ["wagtailcore.page", 3, "33333333-3333-3333-3333-333333333333"], + data["mappings"], + ) # test PageChooserBlock in ListBlock - self.assertIn(['wagtailcore.page', 5, "00017017-5555-5555-5555-555555555555"], data['mappings']) + self.assertIn( + ["wagtailcore.page", 5, "00017017-5555-5555-5555-555555555555"], + data["mappings"], + ) def test_streamfield_with_rich_text(self): # Check that page references within a RichTextBlock in StreamField are found correctly - page = PageWithStreamField(title="My streamfield rich text block has a link", - body=json.dumps([{'type': 'rich_text', - 'value': 'I link to a page.
', - 'id': '7d4ee3d4-9213-4319-b984-45be4ded8853'}])) + page = PageWithStreamField( + title="My streamfield rich text block has a link", + body=json.dumps( + [ + { + "type": "rich_text", + "value": 'I link to a page.
', + "id": "7d4ee3d4-9213-4319-b984-45be4ded8853", + } + ] + ), + ) - parent_page = Page.objects.get(url_path='/home/existing-child-page/') + parent_page = Page.objects.get(url_path="/home/existing-child-page/") parent_page.add_child(instance=page) - digest = digest_for_source('local', str(page.id)) - response = self.client.get('/wagtail-transfer/api/pages/%d/?digest=%s' % (page.id, digest)) + digest = digest_for_source("local", str(page.id)) + response = self.client.get( + "/wagtail-transfer/api/pages/%d/?digest=%s" % (page.id, digest) + ) data = json.loads(response.content) - self.assertIn(['wagtailcore.page', 1, '11111111-1111-1111-1111-111111111111'], data['mappings']) + self.assertIn( + ["wagtailcore.page", 1, "11111111-1111-1111-1111-111111111111"], + data["mappings"], + ) def test_streamfield_with_dead_page_link(self): page = PageWithStreamField( title="I have a streamfield", - body=json.dumps([ - {'type': 'link_block', 'value': {'page': 999, 'text': 'Test'}, 'id': 'fc3b0d3d-d316-4271-9e31-84919558188a'}, - ]) + body=json.dumps( + [ + { + "type": "link_block", + "value": {"page": 999, "text": "Test"}, + "id": "fc3b0d3d-d316-4271-9e31-84919558188a", + }, + ] + ), ) - parent_page = Page.objects.get(url_path='/home/existing-child-page/') + parent_page = Page.objects.get(url_path="/home/existing-child-page/") parent_page.add_child(instance=page) - digest = digest_for_source('local', str(page.id)) - response = self.client.get('/wagtail-transfer/api/pages/%d/?digest=%s' % (page.id, digest)) + digest = digest_for_source("local", str(page.id)) + response = self.client.get( + "/wagtail-transfer/api/pages/%d/?digest=%s" % (page.id, digest) + ) data = json.loads(response.content) - self.assertTrue(any( - model == 'wagtailcore.page' and id == 999 - for model, id, uid in data['mappings'] - )) + self.assertTrue( + any( + model == "wagtailcore.page" and id == 999 + for model, id, uid in data["mappings"] + ) + ) def test_streamfield_with_null_page(self): # We should gracefully handle null values in non-required chooser blocks page = PageWithStreamField( title="I have a streamfield", - body=json.dumps([{ - 'type': 'link_block', - 'value': {'page': None, 'text': 'Empty test'}, - 'id': 'fc3b0d3d-d316-4271-9e31-84919558188a' - },]) + body=json.dumps( + [ + { + "type": "link_block", + "value": {"page": None, "text": "Empty test"}, + "id": "fc3b0d3d-d316-4271-9e31-84919558188a", + }, + ] + ), ) - parent_page = Page.objects.get(url_path='/home/existing-child-page/') + parent_page = Page.objects.get(url_path="/home/existing-child-page/") parent_page.add_child(instance=page) - digest = digest_for_source('local', str(page.id)) - response = self.client.get('/wagtail-transfer/api/pages/%d/?digest=%s' % (page.id, digest)) + digest = digest_for_source("local", str(page.id)) + response = self.client.get( + "/wagtail-transfer/api/pages/%d/?digest=%s" % (page.id, digest) + ) data = json.loads(response.content) # result should have a mapping for the page we just created, and its parent - page_mappings = filter(lambda mapping: mapping[0] == 'wagtailcore.page', data['mappings']) + page_mappings = filter( + lambda mapping: mapping[0] == "wagtailcore.page", data["mappings"] + ) self.assertEqual(len(list(page_mappings)), 2) def test_parental_many_to_many(self): @@ -427,35 +542,43 @@ def test_parental_many_to_many(self): advert_3 = Advert.objects.get(id=3) page.ads = [advert_2, advert_3] - parent_page = Page.objects.get(url_path='/home/existing-child-page/') + parent_page = Page.objects.get(url_path="/home/existing-child-page/") parent_page.add_child(instance=page) - digest = digest_for_source('local', str(page.id)) - response = self.client.get('/wagtail-transfer/api/pages/%d/?digest=%s' % (page.id, digest)) + digest = digest_for_source("local", str(page.id)) + response = self.client.get( + "/wagtail-transfer/api/pages/%d/?digest=%s" % (page.id, digest) + ) data = json.loads(response.content) - self.assertIn(['tests.advert', 2, "adadadad-2222-2222-2222-222222222222"], data['mappings']) - self.assertIn(['tests.advert', 3, "adadadad-3333-3333-3333-333333333333"], data['mappings']) - self.assertEqual({2, 3}, set(data['objects'][0]['fields']['ads'])) + self.assertIn( + ["tests.advert", 2, "adadadad-2222-2222-2222-222222222222"], + data["mappings"], + ) + self.assertIn( + ["tests.advert", 3, "adadadad-3333-3333-3333-333333333333"], + data["mappings"], + ) + self.assertEqual({2, 3}, set(data["objects"][0]["fields"]["ads"])) def test_related_model_with_field_lookup(self): page = SponsoredPage.objects.get(id=5) - page.categories.add(Category.objects.get(name='Cars')) + page.categories.add(Category.objects.get(name="Cars")) page.save() response = self.get(5) self.assertEqual(response.status_code, 200) data = json.loads(response.content) - mappings = data['mappings'] + mappings = data["mappings"] # Category objects in the mappings section should be identified by name, not UUID - self.assertIn(['tests.category', 1, ['Cars']], mappings) + self.assertIn(["tests.category", 1, ["Cars"]], mappings) class TestObjectsApi(TestCase): - fixtures = ['test.json'] + fixtures = ["test.json"] def setUp(self): shutil.rmtree(TEST_MEDIA_DIR, ignore_errors=True) @@ -464,40 +587,49 @@ def tearDown(self): shutil.rmtree(TEST_MEDIA_DIR, ignore_errors=True) def test_incorrect_digest(self): - request_body = json.dumps({ - 'tests.advert': [1] - }) + request_body = json.dumps({"tests.advert": [1]}) response = self.client.post( - '/wagtail-transfer/api/objects/?digest=12345678', request_body, content_type='application/json' + "/wagtail-transfer/api/objects/?digest=12345678", + request_body, + content_type="application/json", ) self.assertEqual(response.status_code, 403) def get(self, request_body): request_json = json.dumps(request_body) - digest = digest_for_source('local', request_json) + digest = digest_for_source("local", request_json) return self.client.post( - '/wagtail-transfer/api/objects/?digest=%s' % digest, request_json, content_type='application/json' + "/wagtail-transfer/api/objects/?digest=%s" % digest, + request_json, + content_type="application/json", ) def test_objects_api(self): - response = self.get({ - 'tests.advert': [1] - }) + response = self.get({"tests.advert": [1]}) self.assertEqual(response.status_code, 200) data = json.loads(response.content) - self.assertEqual(data['ids_for_import'], []) - self.assertEqual(data['objects'][0]['model'], 'tests.advert') - self.assertEqual(data['objects'][0]['fields']['slogan'], "put a tiger in your tank") - self.assertEqual(data['objects'][0]['fields']['run_until'], "2020-12-23T21:00:00Z") - self.assertEqual(data['objects'][0]['fields']['run_from'], None) + self.assertEqual(data["ids_for_import"], []) + self.assertEqual(data["objects"][0]["model"], "tests.advert") + self.assertEqual( + data["objects"][0]["fields"]["slogan"], "put a tiger in your tank" + ) + self.assertEqual( + data["objects"][0]["fields"]["run_until"], "2020-12-23T21:00:00Z" + ) + self.assertEqual(data["objects"][0]["fields"]["run_from"], None) - self.assertEqual(data['mappings'], [['tests.advert', 1, 'adadadad-1111-1111-1111-111111111111']]) + self.assertEqual( + data["mappings"], + [["tests.advert", 1, "adadadad-1111-1111-1111-111111111111"]], + ) def test_objects_api_with_tree_model(self): root_collection = Collection.objects.get() - collection = root_collection.add_child(instance=Collection(name="Test collection")) + collection = root_collection.add_child( + instance=Collection(name="Test collection") + ) collection_uid = uuid.uuid4() collection_content_type = ContentType.objects.get_for_model(Collection) @@ -508,27 +640,25 @@ def test_objects_api_with_tree_model(self): uid=collection_uid, ) - response = self.get({ - 'wagtailcore.collection': [collection.id] - }) + response = self.get({"wagtailcore.collection": [collection.id]}) self.assertEqual(response.status_code, 200) data = json.loads(response.content) - self.assertEqual(data['ids_for_import'], []) - self.assertEqual(data['objects'][0]['model'], 'wagtailcore.collection') - self.assertEqual(data['objects'][0]['fields']['name'], "Test collection") + self.assertEqual(data["ids_for_import"], []) + self.assertEqual(data["objects"][0]["model"], "wagtailcore.collection") + self.assertEqual(data["objects"][0]["fields"]["name"], "Test collection") # mappings should contain entries for the requested collection and its parent self.assertIn( - ['wagtailcore.collection', collection.id, str(collection_uid)], - data['mappings'] + ["wagtailcore.collection", collection.id, str(collection_uid)], + data["mappings"], ) root_collection_uid = IDMapping.objects.get( content_type=collection_content_type, local_id=root_collection.id ).uid self.assertIn( - ['wagtailcore.collection', root_collection.id, str(root_collection_uid)], - data['mappings'] + ["wagtailcore.collection", root_collection.id, str(root_collection_uid)], + data["mappings"], ) def test_many_to_many(self): @@ -538,161 +668,180 @@ def test_many_to_many(self): ad_holder.ads.set([advert_2, advert_3]) ad_holder.save() - response = self.get({ - 'tests.modelwithmanytomany': [1] - }) + response = self.get({"tests.modelwithmanytomany": [1]}) self.assertEqual(response.status_code, 200) data = json.loads(response.content) - self.assertIn(['tests.advert', 2, "adadadad-2222-2222-2222-222222222222"], data['mappings']) - self.assertIn(['tests.advert', 3, "adadadad-3333-3333-3333-333333333333"], data['mappings']) - self.assertEqual({2, 3}, set(data['objects'][0]['fields']['ads'])) + self.assertIn( + ["tests.advert", 2, "adadadad-2222-2222-2222-222222222222"], + data["mappings"], + ) + self.assertIn( + ["tests.advert", 3, "adadadad-3333-3333-3333-333333333333"], + data["mappings"], + ) + self.assertEqual({2, 3}, set(data["objects"][0]["fields"]["ads"])) def test_model_with_field_lookup(self): - response = self.get({ - 'tests.category': [1] - }) + response = self.get({"tests.category": [1]}) self.assertEqual(response.status_code, 200) data = json.loads(response.content) # Category objects in the mappings section should be identified by name, not UUID - self.assertIn(['tests.category', 1, ['Cars']], data['mappings']) + self.assertIn(["tests.category", 1, ["Cars"]], data["mappings"]) def test_model_with_multi_table_inheritance(self): # LongAdvert inherits from Advert. Fetching the base instance over the objects api should # return a LongAdvert model - long_ad = LongAdvert.objects.create(slogan='test', run_until=datetime.now(timezone.utc), description='longertest') + long_ad = LongAdvert.objects.create( + slogan="test", + run_until=datetime.now(timezone.utc), + description="longertest", + ) - response = self.get({ - 'tests.advert': [long_ad.pk] - }) + response = self.get({"tests.advert": [long_ad.pk]}) self.assertEqual(response.status_code, 200) data = json.loads(response.content) - self.assertEqual(data['mappings'][0][0], 'tests.advert') + self.assertEqual(data["mappings"][0][0], "tests.advert") # mappings should be for the base object - self.assertEqual(data['objects'][0]['model'], 'tests.longadvert') + self.assertEqual(data["objects"][0]["model"], "tests.longadvert") # the child object should be serialized def test_model_with_tags(self): # test that a reverse relation such as tagged_items is followed to obtain references to the # tagged_items, if the model and relationship are specified in WAGTAILTRANSFER_FOLLOWED_REVERSE_RELATIONS - ad = Advert.objects.create(slogan='test', run_until=datetime.now(timezone.utc)) - ad.tags.add('test_tag') + ad = Advert.objects.create(slogan="test", run_until=datetime.now(timezone.utc)) + ad.tags.add("test_tag") - response = self.get({ - 'tests.advert': [ad.pk] - }) + response = self.get({"tests.advert": [ad.pk]}) self.assertEqual(response.status_code, 200) data = json.loads(response.content) - mapped_models = {mapping[0] for mapping in data['mappings']} - self.assertIn('taggit.taggeditem', mapped_models) + mapped_models = {mapping[0] for mapping in data["mappings"]} + self.assertIn("taggit.taggeditem", mapped_models) def test_image(self): - with open(os.path.join(FIXTURES_DIR, 'wagtail.jpg'), 'rb') as f: + with open(os.path.join(FIXTURES_DIR, "wagtail.jpg"), "rb") as f: image = Image.objects.create( - title="Wagtail", - file=ImageFile(f, name='wagtail.jpg') + title="Wagtail", file=ImageFile(f, name="wagtail.jpg") ) - response = self.get({ - 'wagtailimages.image': [image.pk] - }) + response = self.get({"wagtailimages.image": [image.pk]}) self.assertEqual(response.status_code, 200) data = json.loads(response.content) - self.assertEqual(len(data['objects']), 1) - obj = data['objects'][0] - self.assertEqual(obj['fields']['file']['download_url'], 'http://media.example.com/media/original_images/wagtail.jpg') - self.assertEqual(obj['fields']['file']['size'], 1160) - self.assertEqual(obj['fields']['file']['hash'], '45c5db99aea04378498883b008ee07528f5ae416') + self.assertEqual(len(data["objects"]), 1) + obj = data["objects"][0] + self.assertEqual( + obj["fields"]["file"]["download_url"], + "http://media.example.com/media/original_images/wagtail.jpg", + ) + self.assertEqual(obj["fields"]["file"]["size"], 1160) + self.assertEqual( + obj["fields"]["file"]["hash"], "45c5db99aea04378498883b008ee07528f5ae416" + ) - @override_settings(MEDIA_URL='/media/') + @override_settings(MEDIA_URL="/media/") def test_image_with_local_media_url(self): """File URLs should use BASE_URL to form an absolute URL if MEDIA_URL is relative""" - with open(os.path.join(FIXTURES_DIR, 'wagtail.jpg'), 'rb') as f: + with open(os.path.join(FIXTURES_DIR, "wagtail.jpg"), "rb") as f: image = Image.objects.create( - title="Wagtail", - file=ImageFile(f, name='wagtail.jpg') + title="Wagtail", file=ImageFile(f, name="wagtail.jpg") ) - response = self.get({ - 'wagtailimages.image': [image.pk] - }) + response = self.get({"wagtailimages.image": [image.pk]}) self.assertEqual(response.status_code, 200) data = json.loads(response.content) - self.assertEqual(len(data['objects']), 1) - obj = data['objects'][0] - self.assertEqual(obj['fields']['file']['download_url'], 'http://example.com/media/original_images/wagtail.jpg') - self.assertEqual(obj['fields']['file']['size'], 1160) - self.assertEqual(obj['fields']['file']['hash'], '45c5db99aea04378498883b008ee07528f5ae416') + self.assertEqual(len(data["objects"]), 1) + obj = data["objects"][0] + self.assertEqual( + obj["fields"]["file"]["download_url"], + "http://example.com/media/original_images/wagtail.jpg", + ) + self.assertEqual(obj["fields"]["file"]["size"], 1160) + self.assertEqual( + obj["fields"]["file"]["hash"], "45c5db99aea04378498883b008ee07528f5ae416" + ) def test_document(self): - with open(os.path.join(FIXTURES_DIR, 'document.txt'), 'rb') as f: + with open(os.path.join(FIXTURES_DIR, "document.txt"), "rb") as f: document = Document.objects.create( - title="Test document", - file=File(f, name='document.txt') + title="Test document", file=File(f, name="document.txt") ) - response = self.get({ - 'wagtaildocs.document': [document.pk] - }) + response = self.get({"wagtaildocs.document": [document.pk]}) self.assertEqual(response.status_code, 200) data = json.loads(response.content) - self.assertEqual(len(data['objects']), 1) - obj = data['objects'][0] - self.assertEqual(obj['fields']['file']['download_url'], 'http://media.example.com/media/documents/document.txt') - self.assertEqual(obj['fields']['file']['size'], 33) - self.assertEqual(obj['fields']['file']['hash'], '9b90daf19b6e1e8a4852c64f9ea7fec5bcc5f7fb') + self.assertEqual(len(data["objects"]), 1) + obj = data["objects"][0] + self.assertEqual( + obj["fields"]["file"]["download_url"], + "http://media.example.com/media/documents/document.txt", + ) + self.assertEqual(obj["fields"]["file"]["size"], 33) + self.assertEqual( + obj["fields"]["file"]["hash"], "9b90daf19b6e1e8a4852c64f9ea7fec5bcc5f7fb" + ) def test_custom_model_with_file_field(self): - with open(os.path.join(FIXTURES_DIR, 'wagtail.jpg'), 'rb') as f: - avatar = Avatar.objects.create( - image=ImageFile(f, name='wagtail.jpg') - ) + with open(os.path.join(FIXTURES_DIR, "wagtail.jpg"), "rb") as f: + avatar = Avatar.objects.create(image=ImageFile(f, name="wagtail.jpg")) - response = self.get({ - 'tests.avatar': [avatar.pk] - }) + response = self.get({"tests.avatar": [avatar.pk]}) self.assertEqual(response.status_code, 200) data = json.loads(response.content) - self.assertEqual(len(data['objects']), 1) - obj = data['objects'][0] - self.assertEqual(obj['fields']['image']['download_url'], 'http://media.example.com/media/avatars/wagtail.jpg') - self.assertEqual(obj['fields']['image']['size'], 1160) - self.assertEqual(obj['fields']['image']['hash'], '45c5db99aea04378498883b008ee07528f5ae416') + self.assertEqual(len(data["objects"]), 1) + obj = data["objects"][0] + self.assertEqual( + obj["fields"]["image"]["download_url"], + "http://media.example.com/media/avatars/wagtail.jpg", + ) + self.assertEqual(obj["fields"]["image"]["size"], 1160) + self.assertEqual( + obj["fields"]["image"]["hash"], "45c5db99aea04378498883b008ee07528f5ae416" + ) -@mock.patch('requests.get') +@mock.patch("requests.get") class TestChooserProxyApi(TestCase): - fixtures = ['test.json'] + fixtures = ["test.json"] def setUp(self): - self.client.login(username='admin', password='password') + self.client.login(username="admin", password="password") def test(self, get): get.return_value.status_code = 200 - get.return_value.content = b'test content' + get.return_value.content = b"test content" - response = self.client.get('/admin/wagtail-transfer/api/chooser-proxy/staging/foo?bar=baz', HTTP_ACCEPT='application/json') + response = self.client.get( + "/admin/wagtail-transfer/api/chooser-proxy/staging/foo?bar=baz", + HTTP_ACCEPT="application/json", + ) - digest = digest_for_source('staging', 'bar=baz') + digest = digest_for_source("staging", "bar=baz") - get.assert_called_once_with(f'https://www.example.com/wagtail-transfer/api/chooser/pages/foo?bar=baz&digest={digest}', headers={'Accept': 'application/json'}, timeout=5) + get.assert_called_once_with( + f"https://www.example.com/wagtail-transfer/api/chooser/pages/foo?bar=baz&digest={digest}", + headers={"Accept": "application/json"}, + timeout=5, + ) self.assertEqual(response.status_code, 200) - self.assertEqual(response.content, b'test content') + self.assertEqual(response.content, b"test content") def test_with_unknown_source(self, get): get.return_value.status_code = 200 - get.return_value.content = b'test content' + get.return_value.content = b"test content" - response = self.client.get('/admin/wagtail-transfer/api/chooser-proxy/production/foo?bar=baz', HTTP_ACCEPT='application/json') + response = self.client.get( + "/admin/wagtail-transfer/api/chooser-proxy/production/foo?bar=baz", + HTTP_ACCEPT="application/json", + ) get.assert_not_called() diff --git a/tests/tests/test_import.py b/tests/tests/test_import.py index 02c1dda..2fed42b 100644 --- a/tests/tests/test_import.py +++ b/tests/tests/test_import.py @@ -9,24 +9,35 @@ from django.core.files.images import ImageFile from django.test import TestCase, override_settings from wagtail.images.models import Image -from wagtail.models import Collection, Comment, Page - -from tests.models import (Advert, Author, Avatar, Category, LongAdvert, - ModelWithManyToMany, PageWithParentalManyToMany, - PageWithRelatedPages, PageWithRichText, - PageWithStreamField, RedirectPage, SectionedPage, - SimplePage, SponsoredPage) +from wagtail.models import Collection, Page + +from tests.models import ( + Advert, + Author, + Avatar, + Category, + LongAdvert, + ModelWithManyToMany, + PageWithParentalManyToMany, + PageWithRelatedPages, + PageWithRichText, + PageWithStreamField, + RedirectPage, + SectionedPage, + SimplePage, + SponsoredPage, +) from wagtail_transfer.models import IDMapping from wagtail_transfer.operations import ImportPlanner # We could use settings.MEDIA_ROOT here, but this way we avoid clobbering a real media folder if we # ever run these tests with non-test settings for any reason -TEST_MEDIA_DIR = os.path.join(os.path.join(settings.BASE_DIR, 'test-media')) -FIXTURES_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'fixtures') +TEST_MEDIA_DIR = os.path.join(os.path.join(settings.BASE_DIR, "test-media")) +FIXTURES_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "fixtures") class TestImport(TestCase): - fixtures = ['test.json'] + fixtures = ["test.json"] def setUp(self): shutil.rmtree(TEST_MEDIA_DIR, ignore_errors=True) @@ -60,12 +71,11 @@ def test_import_model(self): importer.run() cats = Category.objects.all() - self.assertEquals(cats.count(), 2) - + self.assertEqual(cats.count(), 2) def test_import_pages(self): # make a draft edit to the homepage - home = SimplePage.objects.get(slug='home') + home = SimplePage.objects.get(slug="home") home.title = "Draft home" home.save_revision() @@ -112,7 +122,7 @@ def test_import_pages(self): importer.add_json(data) importer.run() - updated_page = SimplePage.objects.get(url_path='/home/') + updated_page = SimplePage.objects.get(url_path="/home/") self.assertEqual(updated_page.intro, "This is the updated homepage") self.assertEqual(updated_page.title, "New home") self.assertEqual(updated_page.draft_title, "New home") @@ -122,12 +132,16 @@ def test_import_pages(self): self.assertEqual(updated_page_revision.intro, "This is the updated homepage") self.assertEqual(updated_page_revision.title, "New home") - created_page = SimplePage.objects.get(url_path='/home/imported-child-page/') - self.assertEqual(created_page.intro, "This page is imported from the source site") + created_page = SimplePage.objects.get(url_path="/home/imported-child-page/") + self.assertEqual( + created_page.intro, "This page is imported from the source site" + ) # An initial page revision should also be created self.assertTrue(created_page.get_latest_revision()) created_page_revision = created_page.get_latest_revision_as_object() - self.assertEqual(created_page_revision.intro, "This page is imported from the source site") + self.assertEqual( + created_page_revision.intro, "This page is imported from the source site" + ) def test_import_pages_with_fk(self): data = """{ @@ -222,19 +236,25 @@ def test_import_pages_with_fk(self): importer.add_json(data) importer.run() - updated_page = SponsoredPage.objects.get(url_path='/home/oil-is-still-great/') + updated_page = SponsoredPage.objects.get(url_path="/home/oil-is-still-great/") self.assertEqual(updated_page.intro, "yay fossil fuels and climate change") # advert is listed in WAGTAILTRANSFER_UPDATE_RELATED_MODELS, so changes to the advert should have been pulled in too self.assertEqual(updated_page.advert.slogan, "put a leopard in your tank") - self.assertEqual(updated_page.advert.run_until, datetime(2020, 12, 23, 21, 5, 43, tzinfo=timezone.utc)) + self.assertEqual( + updated_page.advert.run_until, + datetime(2020, 12, 23, 21, 5, 43, tzinfo=timezone.utc), + ) self.assertEqual(updated_page.advert.run_from, None) # author is not listed in WAGTAILTRANSFER_UPDATE_RELATED_MODELS, so should be left unchanged self.assertEqual(updated_page.author.bio, "Jack Kerouac's car has broken down.") - created_page = SponsoredPage.objects.get(url_path='/home/eggs-are-great-too/') + created_page = SponsoredPage.objects.get(url_path="/home/eggs-are-great-too/") self.assertEqual(created_page.intro, "you can make cakes with them") self.assertEqual(created_page.advert.slogan, "go to work on an egg") - self.assertEqual(created_page.advert.run_until, datetime(2020, 12, 23, 1, 23, 45, tzinfo=timezone.utc)) + self.assertEqual( + created_page.advert.run_until, + datetime(2020, 12, 23, 1, 23, 45, tzinfo=timezone.utc), + ) self.assertEqual(created_page.advert.run_from, None) def test_import_pages_with_orphaned_uid(self): @@ -291,18 +311,22 @@ def test_import_pages_with_orphaned_uid(self): importer.add_json(data) importer.run() - updated_page = SponsoredPage.objects.get(url_path='/home/oil-is-still-great/') + updated_page = SponsoredPage.objects.get(url_path="/home/oil-is-still-great/") # author should be recreated self.assertEqual(updated_page.author.name, "Edgar Allen Poe") - self.assertEqual(updated_page.author.bio, "Edgar Allen Poe has come back from the dead") + self.assertEqual( + updated_page.author.bio, "Edgar Allen Poe has come back from the dead" + ) # make sure it has't just overwritten the old author... self.assertTrue(Author.objects.filter(name="Jack Kerouac").exists()) # there should now be an IDMapping record for the previously orphaned UID, pointing to the # newly created author self.assertEqual( - IDMapping.objects.get(uid="b00cb00c-0000-0000-0000-00000de1e7ed").content_object, - updated_page.author + IDMapping.objects.get( + uid="b00cb00c-0000-0000-0000-00000de1e7ed" + ).content_object, + updated_page.author, ) def test_import_page_with_child_models(self): @@ -357,7 +381,7 @@ def test_import_page_with_child_models(self): importer.add_json(data) importer.run() - page = SectionedPage.objects.get(url_path='/home/how-to-boil-an-egg/') + page = SectionedPage.objects.get(url_path="/home/how-to-boil-an-egg/") self.assertEqual(page.sections.count(), 2) self.assertEqual(page.sections.first().title, "Boil the outside of the egg") @@ -486,7 +510,7 @@ def test_import_page_with_comments(self): importer.add_json(data) importer.run() - page = SimplePage.objects.get(url_path='/home/how-to-boil-an-egg/') + page = SimplePage.objects.get(url_path="/home/how-to-boil-an-egg/") self.assertEqual(page.wagtail_admin_comments.count(), 1) @@ -530,7 +554,9 @@ def test_import_page_with_rich_text_link(self): page = PageWithRichText.objects.get(slug="imported-rich-text-page") # tests that a page link id is changed successfully when imported - self.assertEqual(page.body, 'But I have a link
') + self.assertEqual( + page.body, 'But I have a link
' + ) # TODO: this should include an embed type as well once document/image import is added @@ -604,7 +630,7 @@ def test_do_not_import_pages_outside_of_selected_root(self): page = PageWithRichText.objects.get(slug="imported-rich-text-page") # tests that the page link tag is removed, as the page does not exist on the destination - self.assertEqual(page.body, 'But I have a link
') + self.assertEqual(page.body, "But I have a link
") def test_import_page_with_streamfield_page_links(self): data = """{ @@ -644,7 +670,37 @@ def test_import_page_with_streamfield_page_links(self): imported_streamfield = page.body.stream_block.get_prep_value(page.body) # Check that PageChooserBlock ids are converted correctly to those on the destination site - self.assertEqual(imported_streamfield, [{'type': 'link_block', 'value': {'page': 1, 'text': 'Test'}, 'id': 'fc3b0d3d-d316-4271-9e31-84919558188a'}, {'type': 'page', 'value': 2, 'id': 'c6d07d3a-72d4-445e-8fa5-b34107291176'}, {'type': 'stream', 'value': [{'type': 'page', 'value': 3, 'id': '8c0d7de7-4f77-4477-be67-7d990d0bfb82'}], 'id': '21ffe52a-c0fc-4ecc-92f1-17b356c9cc94'}, {'type': 'list_of_pages', 'value': [5], 'id': '17b972cb-a952-4940-87e2-e4eb00703997'}]) + self.assertEqual( + imported_streamfield, + [ + { + "type": "link_block", + "value": {"page": 1, "text": "Test"}, + "id": "fc3b0d3d-d316-4271-9e31-84919558188a", + }, + { + "type": "page", + "value": 2, + "id": "c6d07d3a-72d4-445e-8fa5-b34107291176", + }, + { + "type": "stream", + "value": [ + { + "type": "page", + "value": 3, + "id": "8c0d7de7-4f77-4477-be67-7d990d0bfb82", + } + ], + "id": "21ffe52a-c0fc-4ecc-92f1-17b356c9cc94", + }, + { + "type": "list_of_pages", + "value": [5], + "id": "17b972cb-a952-4940-87e2-e4eb00703997", + }, + ], + ) def test_import_page_with_document_chooser_block(self): data = """{ @@ -686,14 +742,16 @@ def test_import_page_with_document_chooser_block(self): imported_streamfield, [ { - 'id': '17b972cb-a952-4940-87e2-e4eb00703997', - 'type': 'document', - 'value': 1, + "id": "17b972cb-a952-4940-87e2-e4eb00703997", + "type": "document", + "value": 1, }, ], ) - def test_import_page_with_streamfield_page_links_where_linked_pages_not_imported(self): + def test_import_page_with_streamfield_page_links_where_linked_pages_not_imported( + self + ): data = """{ "ids_for_import": [ ["wagtailcore.page", 6] @@ -731,17 +789,44 @@ def test_import_page_with_streamfield_page_links_where_linked_pages_not_imported imported_streamfield = page.body.stream_block.get_prep_value(page.body) # The PageChooserBlock has required=True, so when its value is removed, the block should also be removed - self.assertNotIn({'type': 'page', 'value': None, 'id': 'c6d07d3a-72d4-445e-8fa5-b34107291176'}, imported_streamfield) + self.assertNotIn( + { + "type": "page", + "value": None, + "id": "c6d07d3a-72d4-445e-8fa5-b34107291176", + }, + imported_streamfield, + ) # Test that 0 values are not removed, only None - self.assertIn({'type': 'integer', 'value': 0, 'id': 'aad07d3a-72d4-445e-8fa5-b34107291199'}, imported_streamfield) + self.assertIn( + { + "type": "integer", + "value": 0, + "id": "aad07d3a-72d4-445e-8fa5-b34107291199", + }, + imported_streamfield, + ) # By contrast, the PageChooserBlock in the link_block has required=False, so just the block's value should be removed instead - self.assertIn({'type': 'link_block', 'value': {'page': None, 'text': 'Test'}, 'id': 'fc3b0d3d-d316-4271-9e31-84919558188a'}, imported_streamfield) + self.assertIn( + { + "type": "link_block", + "value": {"page": None, "text": "Test"}, + "id": "fc3b0d3d-d316-4271-9e31-84919558188a", + }, + imported_streamfield, + ) # The ListBlock should now be empty, as the (required) PageChooserBlocks inside have had their values set to None - self.assertIn({'type': 'list_of_pages', 'value': [], 'id': '17b972cb-a952-4940-87e2-e4eb00703997'}, imported_streamfield) - + self.assertIn( + { + "type": "list_of_pages", + "value": [], + "id": "17b972cb-a952-4940-87e2-e4eb00703997", + }, + imported_streamfield, + ) def test_import_page_with_streamfield_rich_text_block(self): # Check that ids in RichTextBlock within a StreamField are converted properly @@ -751,11 +836,22 @@ def test_import_page_with_streamfield_rich_text_block(self): importer.add_json(data) importer.run() - page = PageWithStreamField.objects.get(slug="my-streamfield-rich-text-block-has-a-link") + page = PageWithStreamField.objects.get( + slug="my-streamfield-rich-text-block-has-a-link" + ) imported_streamfield = page.body.stream_block.get_prep_value(page.body) - self.assertEqual(imported_streamfield, [{'type': 'rich_text', 'value': 'I link to a page.
', 'id': '7d4ee3d4-9213-4319-b984-45be4ded8853'}]) + self.assertEqual( + imported_streamfield, + [ + { + "type": "rich_text", + "value": 'I link to a page.
', + "id": "7d4ee3d4-9213-4319-b984-45be4ded8853", + } + ], + ) def test_import_page_with_new_list_block_format(self): # Check that ids in a ListBlock with the uuid format within a StreamField are converted properly @@ -768,19 +864,34 @@ def test_import_page_with_new_list_block_format(self): imported_streamfield = page.body.stream_block.get_prep_value(page.body) - self.assertEqual(imported_streamfield, [{'type': 'list_of_captioned_pages', 'value': [{'type': 'item', 'value': {'page': 1, 'text': 'a caption'}, 'id': '8c0d7de7-4f77-4477-be67-7d990d0bfb82'}], 'id': '21ffe52a-c0fc-4ecc-92f1-17b356c9cc94'}]) + self.assertEqual( + imported_streamfield, + [ + { + "type": "list_of_captioned_pages", + "value": [ + { + "type": "item", + "value": {"page": 1, "text": "a caption"}, + "id": "8c0d7de7-4f77-4477-be67-7d990d0bfb82", + } + ], + "id": "21ffe52a-c0fc-4ecc-92f1-17b356c9cc94", + } + ], + ) - @mock.patch('requests.get') + @mock.patch("requests.get") def test_import_image_with_file(self, get): get.return_value.status_code = 200 - get.return_value.content = b'my test image file contents' + get.return_value.content = b"my test image file contents" IDMapping.objects.get_or_create( uid="f91cb31c-1751-11ea-8000-0800278dc04d", defaults={ - 'content_type': ContentType.objects.get_for_model(Collection), - 'local_id': Collection.objects.get().id, - } + "content_type": ContentType.objects.get_for_model(Collection), + "local_id": Collection.objects.get().id, + }, ) data = """{ @@ -836,16 +947,16 @@ def test_import_image_with_file(self, get): get.assert_called() image = Image.objects.get() self.assertEqual(image.title, "Lightnin' Hopkins") - self.assertEqual(image.file.read(), b'my test image file contents') + self.assertEqual(image.file.read(), b"my test image file contents") # TODO: We should verify these self.assertEqual(image.file_size, 18521) self.assertEqual(image.file_hash, "e4eab12cc50b6b9c619c9ddd20b61d8e6a961ada") - @mock.patch('requests.get') + @mock.patch("requests.get") def test_import_image_with_file_without_root_collection_mapping(self, get): get.return_value.status_code = 200 - get.return_value.content = b'my test image file contents' + get.return_value.content = b"my test image file contents" data = """{ "ids_for_import": [ @@ -900,7 +1011,7 @@ def test_import_image_with_file_without_root_collection_mapping(self, get): get.assert_called() image = Image.objects.get() self.assertEqual(image.title, "Lightnin' Hopkins") - self.assertEqual(image.file.read(), b'my test image file contents') + self.assertEqual(image.file.read(), b"my test image file contents") # It should be in the existing root collection (no new collection should be created) self.assertEqual(image.collection.name, "Root") @@ -910,7 +1021,7 @@ def test_import_image_with_file_without_root_collection_mapping(self, get): self.assertEqual(image.file_size, 18521) self.assertEqual(image.file_hash, "e4eab12cc50b6b9c619c9ddd20b61d8e6a961ada") - @mock.patch('requests.get') + @mock.patch("requests.get") def test_existing_image_is_not_refetched(self, get): """ If an incoming object has a FileField that reports the same size/hash as the existing @@ -918,20 +1029,19 @@ def test_existing_image_is_not_refetched(self, get): """ get.return_value.status_code = 200 - get.return_value.content = b'my test image file contents' + get.return_value.content = b"my test image file contents" - with open(os.path.join(FIXTURES_DIR, 'wagtail.jpg'), 'rb') as f: + with open(os.path.join(FIXTURES_DIR, "wagtail.jpg"), "rb") as f: image = Image.objects.create( - title="Wagtail", - file=ImageFile(f, name='wagtail.jpg') + title="Wagtail", file=ImageFile(f, name="wagtail.jpg") ) IDMapping.objects.get_or_create( uid="f91debc6-1751-11ea-8001-0800278dc04d", defaults={ - 'content_type': ContentType.objects.get_for_model(Image), - 'local_id': image.id, - } + "content_type": ContentType.objects.get_for_model(Image), + "local_id": image.id, + }, ) data = """{ @@ -990,7 +1100,7 @@ def test_existing_image_is_not_refetched(self, get): # but file is left alone (i.e. it has not been replaced with 'my test image file contents') self.assertEqual(image.file.size, 1160) - @mock.patch('requests.get') + @mock.patch("requests.get") def test_replace_image(self, get): """ If an incoming object has a FileField that reports a different size/hash to the existing @@ -998,20 +1108,19 @@ def test_replace_image(self, get): """ get.return_value.status_code = 200 - get.return_value.content = b'my test image file contents' + get.return_value.content = b"my test image file contents" - with open(os.path.join(FIXTURES_DIR, 'wagtail.jpg'), 'rb') as f: + with open(os.path.join(FIXTURES_DIR, "wagtail.jpg"), "rb") as f: image = Image.objects.create( - title="Wagtail", - file=ImageFile(f, name='wagtail.jpg') + title="Wagtail", file=ImageFile(f, name="wagtail.jpg") ) IDMapping.objects.get_or_create( uid="f91debc6-1751-11ea-8001-0800278dc04d", defaults={ - 'content_type': ContentType.objects.get_for_model(Image), - 'local_id': image.id, - } + "content_type": ContentType.objects.get_for_model(Image), + "local_id": image.id, + }, ) data = """{ @@ -1066,7 +1175,7 @@ def test_replace_image(self, get): get.assert_called() image = Image.objects.get() self.assertEqual(image.title, "A lovely wagtail") - self.assertEqual(image.file.read(), b'my test image file contents') + self.assertEqual(image.file.read(), b"my test image file contents") def test_import_collection(self): root_collection = Collection.objects.get() @@ -1074,17 +1183,20 @@ def test_import_collection(self): IDMapping.objects.get_or_create( uid="f91cb31c-1751-11ea-8000-0800278dc04d", defaults={ - 'content_type': ContentType.objects.get_for_model(Collection), - 'local_id': root_collection.id, - } + "content_type": ContentType.objects.get_for_model(Collection), + "local_id": root_collection.id, + }, ) - data = """{ + data = ( + """{ "ids_for_import": [ ["wagtailcore.collection", 4] ], "mappings": [ - ["wagtailcore.collection", """ + str(root_collection.id) + """, "f91cb31c-1751-11ea-8000-0800278dc04d"], + ["wagtailcore.collection", """ + + str(root_collection.id) + + """, "f91cb31c-1751-11ea-8000-0800278dc04d"], ["wagtailcore.collection", 4, "8a1d3afd-3fa2-4309-9dc7-6d31902174ca"] ], "objects": [ @@ -1094,10 +1206,13 @@ def test_import_collection(self): "fields": { "name": "New collection" }, - "parent_id": """ + str(root_collection.id) + """ + "parent_id": """ + + str(root_collection.id) + + """ } ] }""" + ) importer = ImportPlanner(root_page_source_pk=1, destination_parent_id=None) importer.add_json(data) @@ -1183,8 +1298,13 @@ def test_import_page_with_parental_many_to_many(self): self.assertEqual(set(page.ads.all()), {advert_2, advert_3}) # advert is listed in WAGTAILTRANSFER_UPDATE_RELATED_MODELS, so changes to the advert should have been pulled in too - self.assertEqual(advert_3.slogan, "Buy a half-scale authentically hydrogen-filled replica of the Hindenburg!") - self.assertEqual(advert_3.run_until, datetime(1937, 5, 6, 23, 25, 12, tzinfo=timezone.utc)) + self.assertEqual( + advert_3.slogan, + "Buy a half-scale authentically hydrogen-filled replica of the Hindenburg!", + ) + self.assertEqual( + advert_3.run_until, datetime(1937, 5, 6, 23, 25, 12, tzinfo=timezone.utc) + ) self.assertEqual(advert_3.run_from, None) def test_import_object_with_many_to_many(self): @@ -1220,8 +1340,13 @@ def test_import_object_with_many_to_many(self): self.assertEqual(set(ad_holder.ads.all()), {advert_2, advert_3}) # advert is listed in WAGTAILTRANSFER_UPDATE_RELATED_MODELS, so changes to the advert should have been pulled in too - self.assertEqual(advert_3.slogan, "Buy a half-scale authentically hydrogen-filled replica of the Hindenburg!") - self.assertEqual(advert_3.run_until, datetime(1937, 5, 6, 23, 25, 12, tzinfo=timezone.utc)) + self.assertEqual( + advert_3.slogan, + "Buy a half-scale authentically hydrogen-filled replica of the Hindenburg!", + ) + self.assertEqual( + advert_3.run_until, datetime(1937, 5, 6, 23, 25, 12, tzinfo=timezone.utc) + ) self.assertEqual(advert_3.run_from, None) def test_import_with_field_based_lookup(self): @@ -1285,11 +1410,13 @@ def test_import_with_field_based_lookup(self): importer.add_json(data) importer.run() - updated_page = SponsoredPage.objects.get(url_path='/home/oil-is-still-great/') + updated_page = SponsoredPage.objects.get(url_path="/home/oil-is-still-great/") # The 'Cars' category should have been matched by name to the existing record - self.assertEqual(updated_page.categories.get(name='Cars').colour, "red") + self.assertEqual(updated_page.categories.get(name="Cars").colour, "red") # The 'Environment' category should have been created - self.assertEqual(updated_page.categories.get(name='Environment').colour, "green") + self.assertEqual( + updated_page.categories.get(name="Environment").colour, "green" + ) def test_skip_import_if_hard_dependency_on_non_imported_page(self): data = """{ @@ -1413,22 +1540,36 @@ def test_skip_import_if_hard_dependency_on_non_imported_page(self): importer.run() # A non-nullable FK to an existing page outside the imported root is fine - redirect_to_oil_page = RedirectPage.objects.get(slug='redirect-to-oil-page') - self.assertEqual(redirect_to_oil_page.redirect_to.slug, 'oil-is-great') + redirect_to_oil_page = RedirectPage.objects.get(slug="redirect-to-oil-page") + self.assertEqual(redirect_to_oil_page.redirect_to.slug, "oil-is-great") # A non-nullable FK to a non-existing page outside the imported root will prevent import - self.assertFalse(RedirectPage.objects.filter(slug='redirect-to-unimported-page').exists()) + self.assertFalse( + RedirectPage.objects.filter(slug="redirect-to-unimported-page").exists() + ) # We can also handle FKs to pages being created in the import - redirect_to_redirect_to_oil_page = RedirectPage.objects.get(slug='redirect-to-redirect-to-oil-page') - self.assertEqual(redirect_to_redirect_to_oil_page.redirect_to.slug, 'redirect-to-oil-page') + redirect_to_redirect_to_oil_page = RedirectPage.objects.get( + slug="redirect-to-redirect-to-oil-page" + ) + self.assertEqual( + redirect_to_redirect_to_oil_page.redirect_to.slug, "redirect-to-oil-page" + ) # Failure to create a page will also propagate to pages with a hard dependency on it - self.assertFalse(RedirectPage.objects.filter(slug='redirect-to-redirect-to-unimported-page').exists()) + self.assertFalse( + RedirectPage.objects.filter( + slug="redirect-to-redirect-to-unimported-page" + ).exists() + ) # Circular references will be caught and pages not created - self.assertFalse(RedirectPage.objects.filter(slug='pork-redirecting-to-lamb').exists()) - self.assertFalse(RedirectPage.objects.filter(slug='lamb-redirecting-to-pork').exists()) + self.assertFalse( + RedirectPage.objects.filter(slug="pork-redirecting-to-lamb").exists() + ) + self.assertFalse( + RedirectPage.objects.filter(slug="lamb-redirecting-to-pork").exists() + ) def test_circular_references_in_rich_text(self): data = """{ @@ -1490,14 +1631,17 @@ def test_circular_references_in_rich_text(self): importer.run() # Both pages should have been created - bill_page = PageWithRichText.objects.get(slug='bill') - ben_page = PageWithRichText.objects.get(slug='ben') + bill_page = PageWithRichText.objects.get(slug="bill") + ben_page = PageWithRichText.objects.get(slug="ben") # At least one of them (i.e. the second one to be created) should have a valid link to the other self.assertTrue( - bill_page.body == """Have you met my friend Ben?
""" % ben_page.id - or - ben_page.body == """Have you met my friend Bill?
""" % bill_page.id + bill_page.body + == """Have you met my friend Ben?
""" + % ben_page.id + or ben_page.body + == """Have you met my friend Bill?
""" + % bill_page.id ) def test_omitting_references_in_m2m_relations(self): @@ -1561,13 +1705,15 @@ def test_omitting_references_in_m2m_relations(self): importer.add_json(data) importer.run() - salad_dressing_page = PageWithRelatedPages.objects.get(slug='salad-dressing') - oil_page = Page.objects.get(slug='oil-is-great') - vinegar_page = Page.objects.get(slug='vinegar') + salad_dressing_page = PageWithRelatedPages.objects.get(slug="salad-dressing") + oil_page = Page.objects.get(slug="oil-is-great") + vinegar_page = Page.objects.get(slug="vinegar") # salad_dressing_page's related_pages should include the oil (id=30) and vinegar (id=21) # pages, but not the missing and not-to-be-imported page id=31 - self.assertEqual(set(salad_dressing_page.related_pages.all()), set([oil_page, vinegar_page])) + self.assertEqual( + set(salad_dressing_page.related_pages.all()), {oil_page, vinegar_page} + ) def test_import_with_soft_dependency_on_grandchild(self): # https://github.com/wagtail/wagtail-transfer/issues/84 - @@ -1637,7 +1783,7 @@ def test_import_with_soft_dependency_on_grandchild(self): # iterates over it, it gets back a known 'worst case' ordering as defined by the page # titles. importer.operations = list(importer.operations) - importer.operations.sort(key=lambda op: op.object_data['fields']['title']) + importer.operations.sort(key=lambda op: op.object_data["fields"]["title"]) importer.run() @@ -1648,12 +1794,12 @@ def test_import_with_soft_dependency_on_grandchild(self): # link from homepage has to be broken page = PageWithRichText.objects.get(slug="level-1-page") - self.assertEqual(page.body, 'link to level 3
') + self.assertEqual(page.body, "link to level 3
") - @mock.patch('requests.get') + @mock.patch("requests.get") def test_import_custom_file_field(self, get): get.return_value.status_code = 200 - get.return_value.content = b'my test image file contents' + get.return_value.content = b"my test image file contents" data = """{ "ids_for_import": [ @@ -1684,7 +1830,7 @@ def test_import_custom_file_field(self, get): # Check the db record and file was imported get.assert_called() avatar = Avatar.objects.get() - self.assertEqual(avatar.image.read(), b'my test image file contents') + self.assertEqual(avatar.image.read(), b"my test image file contents") def test_import_multi_table_model(self): # test that importing a model using multi table inheritance correctly imports the child model, not just the parent @@ -1717,7 +1863,10 @@ def test_import_multi_table_model(self): imported_ad = LongAdvert.objects.filter(id=4).first() self.assertIsNotNone(imported_ad) self.assertEqual(imported_ad.slogan, "test") - self.assertEqual(imported_ad.run_until, datetime(2020, 12, 23, 12, 34, 56, tzinfo=timezone.utc)) + self.assertEqual( + imported_ad.run_until, + datetime(2020, 12, 23, 12, 34, 56, tzinfo=timezone.utc), + ) self.assertEqual(imported_ad.description, "longertest") def test_import_model_with_generic_foreign_key(self): @@ -1818,11 +1967,16 @@ def test_import_model_with_deleted_reverse_related_models(self): self.assertIsNotNone(imported_ad) self.assertIsNone(imported_ad.tags.first()) - @override_settings(WAGTAILTRANSFER_FOLLOWED_REVERSE_RELATIONS=[('tests.advert', 'tagged_items', False)]) + @override_settings( + WAGTAILTRANSFER_FOLLOWED_REVERSE_RELATIONS=[ + ("tests.advert", "tagged_items", False) + ] + ) def test_import_model_with_untracked_deleted_reverse_related_models(self): # test re-importing a model where WAGTAILTRANFER_FOLLOWED_REVERSE_RELATIONS is not used to track tag deletions # will not delete tags from wagtail_transfer import field_adapters + importlib.reload(field_adapters) # force reload field adapters as followed/deleted variables are set on module load, so will not get new setting data = """{ diff --git a/tests/tests/test_views.py b/tests/tests/test_views.py index 668f493..d1db6fd 100644 --- a/tests/tests/test_views.py +++ b/tests/tests/test_views.py @@ -14,24 +14,24 @@ class TestChooseView(TestCase): - fixtures = ['test.json'] + fixtures = ["test.json"] def setUp(self): - self.client.login(username='admin', password='password') + self.client.login(username="admin", password="password") def test_get(self): - response = self.client.get('/admin/wagtail-transfer/choose/') + response = self.client.get("/admin/wagtail-transfer/choose/") self.assertEqual(response.status_code, 200) self.assertContains(response, 'data-wagtail-component="content-import-form"') -@mock.patch('requests.post') -@mock.patch('requests.get') +@mock.patch("requests.post") +@mock.patch("requests.get") class TestImportView(TestCase): - fixtures = ['test.json'] + fixtures = ["test.json"] def setUp(self): - self.client.login(username='admin', password='password') + self.client.login(username="admin", password="password") def test_run(self, get, post): get.return_value.status_code = 200 @@ -125,38 +125,51 @@ def test_run(self, get, post): ] }""" - response = self.client.post('/admin/wagtail-transfer/import/', { - 'source': 'staging', - 'source_page_id': '12', - 'dest_page_id': '2', - }) - self.assertRedirects(response, '/admin/pages/2/') + response = self.client.post( + "/admin/wagtail-transfer/import/", + { + "source": "staging", + "source_page_id": "12", + "dest_page_id": "2", + }, + ) + self.assertRedirects(response, "/admin/pages/2/") # Pages API should be called once, with 12 as the root page get.assert_called_once() args, kwargs = get.call_args - self.assertEqual(args[0], 'https://www.example.com/wagtail-transfer/api/pages/12/') - self.assertIn('digest', kwargs['params']) + self.assertEqual( + args[0], "https://www.example.com/wagtail-transfer/api/pages/12/" + ) + self.assertIn("digest", kwargs["params"]) # then the Objects API should be called, requesting adverts with ID 11 and 8 post.assert_called_once() args, kwargs = post.call_args - self.assertEqual(args[0], 'https://www.example.com/wagtail-transfer/api/objects/') - self.assertIn('digest', kwargs['params']) - requested_ids = json.loads(kwargs['data'])['tests.advert'] - self.assertEqual(set(requested_ids), set([8, 11])) + self.assertEqual( + args[0], "https://www.example.com/wagtail-transfer/api/objects/" + ) + self.assertIn("digest", kwargs["params"]) + requested_ids = json.loads(kwargs["data"])["tests.advert"] + self.assertEqual(set(requested_ids), {8, 11}) # Check import results - updated_page = SponsoredPage.objects.get(url_path='/home/oil-is-still-great/') + updated_page = SponsoredPage.objects.get(url_path="/home/oil-is-still-great/") self.assertEqual(updated_page.intro, "yay fossil fuels and climate change") self.assertEqual(updated_page.advert.slogan, "put a leopard in your tank") - self.assertEqual(updated_page.advert.run_until, datetime(2020, 12, 23, 1, 23, 45, tzinfo=timezone.utc)) + self.assertEqual( + updated_page.advert.run_until, + datetime(2020, 12, 23, 1, 23, 45, tzinfo=timezone.utc), + ) self.assertEqual(updated_page.advert.run_from, date(2020, 1, 21)) - created_page = SponsoredPage.objects.get(url_path='/home/eggs-are-great-too/') + created_page = SponsoredPage.objects.get(url_path="/home/eggs-are-great-too/") self.assertEqual(created_page.intro, "you can make cakes with them") self.assertEqual(created_page.advert.slogan, "go to work on an egg") - self.assertEqual(created_page.advert.run_until, datetime(2020, 1, 23, 1, 23, 45, tzinfo=timezone.utc)) + self.assertEqual( + created_page.advert.run_until, + datetime(2020, 1, 23, 1, 23, 45, tzinfo=timezone.utc), + ) self.assertEqual(created_page.advert.run_from, None) def test_missing_related_object(self, get, post): @@ -246,53 +259,65 @@ def test_missing_related_object(self, get, post): ] }""" - response = self.client.post('/admin/wagtail-transfer/import/', { - 'source': 'staging', - 'source_page_id': '12', - 'dest_page_id': '2', - }) - self.assertRedirects(response, '/admin/pages/2/') + response = self.client.post( + "/admin/wagtail-transfer/import/", + { + "source": "staging", + "source_page_id": "12", + "dest_page_id": "2", + }, + ) + self.assertRedirects(response, "/admin/pages/2/") # Pages API should be called once, with 12 as the root page get.assert_called_once() args, kwargs = get.call_args - self.assertEqual(args[0], 'https://www.example.com/wagtail-transfer/api/pages/12/') - self.assertIn('digest', kwargs['params']) + self.assertEqual( + args[0], "https://www.example.com/wagtail-transfer/api/pages/12/" + ) + self.assertIn("digest", kwargs["params"]) # then the Objects API should be called, requesting adverts with ID 11 and 8 post.assert_called_once() args, kwargs = post.call_args - self.assertEqual(args[0], 'https://www.example.com/wagtail-transfer/api/objects/') - self.assertIn('digest', kwargs['params']) - requested_ids = json.loads(kwargs['data'])['tests.advert'] - self.assertEqual(set(requested_ids), set([8, 11])) + self.assertEqual( + args[0], "https://www.example.com/wagtail-transfer/api/objects/" + ) + self.assertIn("digest", kwargs["params"]) + requested_ids = json.loads(kwargs["data"])["tests.advert"] + self.assertEqual(set(requested_ids), {8, 11}) # Check import results - updated_page = SponsoredPage.objects.get(url_path='/home/oil-is-still-great/') + updated_page = SponsoredPage.objects.get(url_path="/home/oil-is-still-great/") self.assertEqual(updated_page.intro, "yay fossil fuels and climate change") self.assertEqual(updated_page.advert.slogan, "put a leopard in your tank") - self.assertEqual(updated_page.advert.run_until, datetime(2020, 12, 23, 1, 23, 45, tzinfo=timezone.utc)) + self.assertEqual( + updated_page.advert.run_until, + datetime(2020, 12, 23, 1, 23, 45, tzinfo=timezone.utc), + ) self.assertEqual(updated_page.advert.run_from, None) # The egg advert was missing in the object-api response, and the FK on SponsoredPage is # nullable, so it should create the egg page without the advert - created_page = SponsoredPage.objects.get(url_path='/home/eggs-are-great-too/') + created_page = SponsoredPage.objects.get(url_path="/home/eggs-are-great-too/") self.assertEqual(created_page.intro, "you can make cakes with them") self.assertEqual(created_page.advert, None) def test_list_snippet_models(self, get, post): # Test the model chooser view. get_params = "models=True" - digest = digest_for_source('local', get_params) - response = self.client.get(f"https://www.example.com/wagtail-transfer/api/chooser/models/?{get_params}&digest={digest}") + digest = digest_for_source("local", get_params) + response = self.client.get( + f"https://www.example.com/wagtail-transfer/api/chooser/models/?{get_params}&digest={digest}" + ) self.assertEqual(response.status_code, 200) content = json.loads(response.content.decode("utf-8")) - self.assertEqual(content['meta']['total_count'], 1) + self.assertEqual(content["meta"]["total_count"], 1) - snippet = content['items'][0] - self.assertEqual(snippet['model_label'], 'tests.category') - self.assertEqual(snippet['name'], 'Category') + snippet = content["items"][0] + self.assertEqual(snippet["model_label"], "tests.category") + self.assertEqual(snippet["name"], "Category") class ImportPermissionsTests(TestCase): @@ -301,11 +326,13 @@ class ImportPermissionsTests(TestCase): def setUp(self): idmapping_content_type = ContentType.objects.get_for_model(IDMapping) can_import_permission = Permission.objects.get( - content_type=idmapping_content_type, codename="wagtailtransfer_can_import", + content_type=idmapping_content_type, + codename="wagtailtransfer_can_import", ) can_access_admin_permission = Permission.objects.get( content_type=ContentType.objects.get( - app_label="wagtailadmin", model="admin", + app_label="wagtailadmin", + model="admin", ), codename="access_admin", ) @@ -317,7 +344,9 @@ def setUp(self): editors = Group.objects.get(name="Editors") self.superuser = User.objects.create_superuser( - username="superuser", email="superuser@example.com", password="password", + username="superuser", + email="superuser@example.com", + password="password", ) self.inactive_superuser = User.objects.create_superuser( username="inactivesuperuser", @@ -378,7 +407,6 @@ def setUp(self): ] def _test_view(self, method, url, data=None, success_url=None): - for user in self.permitted_users: with self.subTest(user=user): self.client.login(username=user.username, password="password") diff --git a/tests/urls.py b/tests/urls.py index 1babbda..63ac19b 100644 --- a/tests/urls.py +++ b/tests/urls.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import, unicode_literals - from django.urls import include, re_path from wagtail import urls as wagtail_urls from wagtail.admin import urls as wagtailadmin_urls @@ -7,10 +5,9 @@ from wagtail_transfer import urls as wagtailtransfer_urls urlpatterns = [ - re_path(r'^admin/', include(wagtailadmin_urls)), - re_path(r'^wagtail-transfer/', include(wagtailtransfer_urls)), - + re_path(r"^admin/", include(wagtailadmin_urls)), + re_path(r"^wagtail-transfer/", include(wagtailtransfer_urls)), # For anything not caught by a more specific rule above, hand over to # Wagtail's serving mechanism - re_path(r'', include(wagtail_urls)), + re_path(r"", include(wagtail_urls)), ] diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..57e7dac --- /dev/null +++ b/tox.ini @@ -0,0 +1,27 @@ +[tox] +skipsdist = True +usedevelop = True +envlist = + python{3.8,3.9,3.10}-django{3.2,4.1}-wagtail{4.1,5.0,5.1} + python3.11-django4.1-wagtail4.1 + python{3.8,3.9,3.10,3.11}-django{4.1,4.2}-wagtail{5.0,5.1} + +[gh-actions] +python = + 3.8: python3.8 + 3.9: python3.9 + 3.10: python3.10 + 3.11: python3.11 + +[testenv] +install_command = pip install -e . -U {opts} {packages} +commands = python runtests.py + +deps = + django3.2: Django>=3.2,<3.3 + django4.1: Django>=4.1,<4.2 + django4.2: Django>=4.2,<5.0 + + wagtail4.1: wagtail>=4.1,<4.2 + wagtail5.0: wagtail>=5.0,<5.1 + wagtail5.1: wagtail>=5.1,<5.2 diff --git a/wagtail_transfer/admin_urls.py b/wagtail_transfer/admin_urls.py index b0fc079..36f3d53 100644 --- a/wagtail_transfer/admin_urls.py +++ b/wagtail_transfer/admin_urls.py @@ -4,14 +4,21 @@ from .vendor.wagtail_api_v2.router import WagtailAPIRouter -chooser_api = WagtailAPIRouter('wagtail_transfer_admin:page_chooser_api') -chooser_api.register_endpoint('pages', views.PageChooserAPIViewSet) +chooser_api = WagtailAPIRouter("wagtail_transfer_admin:page_chooser_api") +chooser_api.register_endpoint("pages", views.PageChooserAPIViewSet) -app_name = 'wagtail_transfer_admin' +app_name = "wagtail_transfer_admin" urlpatterns = [ - re_path(r'^choose/$', views.choose_page, name='choose_page'), - re_path(r'^import/$', views.do_import, name='import'), - re_path(r'^api/chooser-local/', (chooser_api.urls[0], 'page_chooser_api', 'page_chooser_api')), - re_path(r'^api/chooser-proxy/(\w+)/([\w\-/]*)$', views.chooser_api_proxy, name='chooser_api_proxy'), - re_path(r'^api/check_uid/$', views.check_page_existence_for_uid, name='check_uid'), + re_path(r"^choose/$", views.choose_page, name="choose_page"), + re_path(r"^import/$", views.do_import, name="import"), + re_path( + r"^api/chooser-local/", + (chooser_api.urls[0], "page_chooser_api", "page_chooser_api"), + ), + re_path( + r"^api/chooser-proxy/(\w+)/([\w\-/]*)$", + views.chooser_api_proxy, + name="chooser_api_proxy", + ), + re_path(r"^api/check_uid/$", views.check_page_existence_for_uid, name="check_uid"), ] diff --git a/wagtail_transfer/apps.py b/wagtail_transfer/apps.py index 4f3a452..46320c6 100644 --- a/wagtail_transfer/apps.py +++ b/wagtail_transfer/apps.py @@ -2,5 +2,5 @@ class WagtailTransferAppConfig(AppConfig): - name = 'wagtail_transfer' - default_auto_field = 'django.db.models.AutoField' + name = "wagtail_transfer" + default_auto_field = "django.db.models.AutoField" diff --git a/wagtail_transfer/auth.py b/wagtail_transfer/auth.py index 85b519b..b913b10 100644 --- a/wagtail_transfer/auth.py +++ b/wagtail_transfer/auth.py @@ -5,19 +5,23 @@ from django.conf import settings from django.core.exceptions import PermissionDenied -GROUP_QUERY_WITH_DIGEST = re.compile('(?P