- try:
- label, _, _ = msg.split(",")
- _, _, action = label.split(".")
- return action
- except (AttributeError, ValueError) as e:
- self.log(f"Problem getting message action from {msg}: {e}")
- capture_exception(e)
- return None
-
- def _purge_queue(self, queue) -> None:
- """Remove all entries in the queue, without inspecting them
-
- Split out for clarity - there are a few caveats about queue purging from
- https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/sqs.html#SQS.Client.purge_queue
-
- > The message deletion process takes up to 60 seconds.
- > We recommend waiting for 60 seconds regardless of your queue's size.
-
- This is fine, as we run this every 5 mins.
-
- > Messages sent to the queue before you call PurgeQueue might be received
- > but are deleted within the next minute.
-
- This is _probably_ fine - if a "publish" message arrives 'late' and is deleted
- rather than acted on, that's fine, because we only purge after a "go" action,
- so we'll already be updating from Contentful.
-
- > Messages sent to the queue after you call PurgeQueue might be deleted
- > while the queue is being purged.
-
- This one is a bit riskier, so we should keep an eye on real-world behaviour.
- It might be useful to do a forced rebuild a couple of times a day, to be safe.
- """
- self.log("Purging the rest of the queue")
- queue.purge()
-
- def _queue_has_viable_messages(self) -> bool:
- """
- When pages/entries change, Contentful uses a webhook to push a
- message into a queue. Here, we get all enqueued messages and if ANY ONE
- of them has an action of a 'go' type, we count that as enough and
- we re-sync all of our Contentful content.
-
- If we get a 'go' signal, we explicitly drain the queue rather than waste
- network I/O on pointless checking. If we don't get a 'go' signal at all,
- we still effectively have purged the queue, just iteratively.
-
- What action types count as a 'go' signal?
-
- * auto_save -> YES if using preview mode (ie on Dev/Stage)
- * create -> NO, NEVER - not on Prod, Stage or Dev
- * publish -> YES
- * save -> YES if using preview mode
- * unarchive -> YES
- * archive -> YES (and we'll remove the page from bedrock's DB as well)
- * unpublish -> YES (as above)
- * delete -> YES (as above)
- """
-
- poll_queue = True
- may_purge_queue = False
- viable_message_found = False
-
- GO_ACTIONS = {
- ACTION_ARCHIVE,
- ACTION_PUBLISH,
- ACTION_UNARCHIVE,
- ACTION_UNPUBLISH,
- ACTION_DELETE,
- }
- EXTRA_PREVIEW_API_GO_ACTIONS = {
- # Sites that are using the preview API key (Dev and Stage) act upon
- # two more state changes
- ACTION_AUTO_SAVE,
- ACTION_SAVE,
- }
-
- if settings.APP_NAME != "bedrock-prod":
- # See settings.base.get_app_name()
- GO_ACTIONS = GO_ACTIONS.union(EXTRA_PREVIEW_API_GO_ACTIONS)
-
- if not settings.CONTENTFUL_NOTIFICATION_QUEUE_ACCESS_KEY_ID:
- self.log(
- "AWS SQS Credentials not configured for Contentful webhook",
- )
- # We don't want things to block/fail if we don't have SQS config.
- # Instead, in this situation, it's better to just assume we got
- # a 'go' signal and poll Contentful anyway.
- return True
-
- sqs = boto3.resource(
- "sqs",
- region_name=settings.CONTENTFUL_NOTIFICATION_QUEUE_REGION,
- aws_access_key_id=settings.CONTENTFUL_NOTIFICATION_QUEUE_ACCESS_KEY_ID,
- aws_secret_access_key=settings.CONTENTFUL_NOTIFICATION_QUEUE_SECRET_ACCESS_KEY,
- )
- queue = sqs.Queue(settings.CONTENTFUL_NOTIFICATION_QUEUE_URL)
-
- while poll_queue:
- msg_batch = queue.receive_messages(
- WaitTimeSeconds=settings.CONTENTFUL_NOTIFICATION_QUEUE_WAIT_TIME,
- MaxNumberOfMessages=MAX_MESSAGES_PER_QUEUE_POLL,
- )
-
- if len(msg_batch) == 0:
- self.log("No messages in the queue")
- break
-
- for sqs_msg in msg_batch:
- msg_body = sqs_msg.body
- action = self._get_message_action(msg_body)
- if action in GO_ACTIONS:
- # we've found a viable one, so that's great. Drain down the queue and move on
- viable_message_found = True
- self.log(f"Got a viable message: {msg_body}")
- may_purge_queue = True
- poll_queue = False # no need to get more messages
- break
- else:
- # Explicitly delete the message and move on to the next one.
- # Note that we don't purge the entire queue even if all of the
- # current messages are inviable, because we don't want the risk
- # of a viable message being lost during the purge process.
- sqs_msg.delete()
- continue
-
- if not viable_message_found:
- self.log("No viable message found in queue")
-
- if may_purge_queue:
- self._purge_queue(queue)
-
- return viable_message_found
-
- def _detect_and_delete_absent_entries(self, contentful_data_attempted_for_sync) -> int:
- q_obj = Q()
- for ctype, _contentful_id, _locale in contentful_data_attempted_for_sync:
- if ctype == CONTENT_TYPE_CONNECT_HOMEPAGE:
- base_q = Q(contentful_id=_contentful_id)
- else:
- base_q = Q(
- contentful_id=_contentful_id,
- # DANGER: the _locale up till now is a Contentful locale
- # not how we express it in Bedrock, so we need to remap it
- locale=self._remap_locale_for_bedrock(_locale),
- )
- q_obj |= base_q
-
- _entries_to_delete = ContentfulEntry.objects.exclude(q_obj)
- self.log(f"Entries to be deleted: {_entries_to_delete}")
-
- _num_entries_to_delete = _entries_to_delete.count()
-
- res = _entries_to_delete.delete()
- self.log(f"Deleted by _detect_and_delete_absent_entries: {res}")
-
- # if things aren't right, we don't want to block the rest of the sync
- if res[0] != _num_entries_to_delete:
- capture_message(
- message="Deleted more objects than expected based on these ids slated for deletion. Check the Contentful sync!",
- level="warning",
- )
- return res[1]["contentful.ContentfulEntry"] if res[0] > 0 else 0
-
- def _remap_locale_for_bedrock(self, locale: str) -> str:
- return CONTENTFUL_TO_BEDROCK_LOCALE_MAP.get(locale, locale)
-
- def _page_is_syncable(
- self,
- ctype: str,
- page_id: str,
- locale_code: str,
- ) -> bool:
- """Utility method for deliberately blocking the sync of certain pages/entries"""
-
- # Case 1. The EN and DE homepages are modelled using the connectHomepage entry
- # in Contentful, with no explicit locale field, so the DE homepage is actually
- # sent with en-US locale. We currently load these into Bedrock by ID, and we
- # ONLY want to sync them for a single locale (en-US) only, to avoid an error.
-
- if ctype == CONTENT_TYPE_CONNECT_HOMEPAGE and locale_code != "en-US":
- return False
-
- return True
-
- def _get_content_to_sync(
- self,
- available_locales,
- ) -> list(
- (str, str),
- ):
- """Fetches which content types and ids to query, individually, from the Contentful API"""
- content_to_sync = []
-
- for locale in available_locales:
- _locale_code = locale.code
-
- # TODO: Change to syncing only `page` content types when we're in an all-Compose setup
- # TODO: treat the connectHomepage differently because its locale is an overloaded name field
- for ctype in settings.CONTENTFUL_CONTENT_TYPES_TO_SYNC:
- for entry in ContentfulPage.client.entries(
- {
- "content_type": ctype,
- "include": 0,
- "locale": _locale_code,
- }
- ).items:
- if not self._page_is_syncable(ctype, entry.sys["id"], _locale_code):
- self.log(f"Page {ctype}:{entry.sys['id']} deemed not syncable for {_locale_code}")
- else:
- content_to_sync.append((ctype, entry.sys["id"], _locale_code))
-
- return content_to_sync
-
- def _get_value_from_data(self, data: dict, spec: dict) -> str:
- """Extract a single value from `data` based on the provided `spec`,
- which is written as a jq filter directive.
-
- This will get all the strings in the data and return them as a
- single string. As such, this is not intended for extracing
- presentation-ready data, but is useful to check if we have viable
- data at all."""
-
- # jq docs: https://stedolan.github.io/jq/
- # jq.py docs: https://github.com/mwilliamson/jq.py
-
- try:
- values = jq.all(spec, data)
- except TypeError as e:
- capture_exception(e)
- return ""
-
- for i, val in enumerate(values):
- if val:
- values[i] = val.strip()
- elif val is None:
- values[i] = ""
- return " ".join(values).strip()
-
- def _check_localisation_complete(self) -> None:
- """In the context of Contentful-sourced data, we consider localisation
- to be complete for a specific locale if ALL of the required aspects,
- as defined in the config are present. We do NOT check whether
- the content makes sense or contains as much content as other locales,
- just that they exist in a 'truthy' way.
- """
-
- self.log("\n====================================\nChecking localisation completeness")
- seen_count = 0
- viable_for_localisation_count = 0
- localisation_not_configured_count = 0
- localisation_complete_count = 0
-
- for contentful_entry in ContentfulEntry.objects.all():
- completeness_spec = LOCALISATION_COMPLETENESS_CHECK_CONFIG.get(contentful_entry.content_type)
- seen_count += 1
- if completeness_spec:
- viable_for_localisation_count += 1
- entry_localised_fields_found = set()
- data = contentful_entry.data
- collected_values = []
- for step in completeness_spec:
- _value_for_step = self._get_value_from_data(data, step)
- if _value_for_step:
- entry_localised_fields_found.add(step)
- collected_values.append(_value_for_step)
- contentful_entry.localisation_complete = all(collected_values)
- contentful_entry.save()
- if contentful_entry.localisation_complete:
- localisation_complete_count += 1
- else:
- localisation_not_configured_count += 1
- self.log(
- f"\nChecking {contentful_entry.content_type}:{contentful_entry.locale}:{contentful_entry.contentful_id}"
- f"-> Localised? {contentful_entry.localisation_complete}"
- )
- if completeness_spec and not contentful_entry.localisation_complete:
- _missing_fields = set(completeness_spec).difference(entry_localised_fields_found)
- self.log(f"These fields were missing localised content: {_missing_fields}")
-
- self.log(
- "Localisation completeness checked:"
- f"\nFully localised: {localisation_complete_count} of {viable_for_localisation_count} candidates."
- f"\nNot configured for localisation: {localisation_not_configured_count}."
- f"\nTotal entries checked: {seen_count}."
- "\n====================================\n"
- )
-
- def _refresh_from_contentful(self) -> tuple[int, int, int, int]:
- self.log("Pulling from Contentful")
- updated_count = 0
- added_count = 0
- deleted_count = 0
- error_count = 0
- content_missing_localised_version = set()
-
- EMPTY_ENTRY_ATTRIBUTE_STRING = "'Entry' object has no attribute 'content'"
-
- available_locales = ContentfulPage.client.locales()
-
- # 1. Build a lookup of pages to sync by type, ID and locale
- content_to_sync = self._get_content_to_sync(available_locales)
-
- # 2. Pull down each page and store
- # TODO: we may (TBC) be able to do a wholesale refactor and get all the locale variations
- # of a single Page (where entry['myfield'] in a single-locale setup changes to
- # entry['myfield']['en-US'], entry['myfield']['de'], etc. That might be particularly useful
- # when we have a lot of locales in play. For now, the heavier-IO approach should be OK.
-
- for ctype, page_id, locale_code in content_to_sync:
- request = self.rf.get("/")
- request.locale = locale_code
- try:
- page = ContentfulPage(request, page_id)
- page_data = page.get_content()
- except AttributeError as ae:
- # Problem with the page - most likely not-really-a-page-in-this-locale-after-all.
- # (Contentful seems to send back a Compose `page` in en-US for _any_ other locale,
- # even if the page has no child entries. This false positive / absent entry is
- # only apparent when we try to call page.get_content() and find there is none.)
- if str(ae) == EMPTY_ENTRY_ATTRIBUTE_STRING:
- self.log(f"No content for {page_id} for {locale_code} - page will be deleted from DB if it exists")
- # We want to track this explicitly, because we need to do cleanup later on.
- content_missing_localised_version.add((ctype, page_id, locale_code))
- continue
- else:
- raise
- except Exception as ex:
- # Problem with the page, load other pages
- self.log(f"Problem with {ctype}:{page_id} -> {type(ex)}: {ex}")
- capture_exception(ex)
- error_count += 1
- continue
-
- hash = data_hash(page_data)
- _info = page_data["info"]
-
- # Check we're definitely getting the locales we're expecting (with a temporary caveat)
- if (
- locale_code != _info["locale"]
- and
- # Temporary workaround till Homepage moves into Compose from Connect
- page_id not in settings.CONTENTFUL_HOMEPAGE_LOOKUP.values()
- ):
- msg = f"Locale mismatch on {ctype}:{page_id} -> {locale_code} vs {_info['locale']}"
- self.log(msg)
- capture_message(msg)
- error_count += 1
- continue
-
- # Now we've done the check, let's convert any Contentful-specific
- # locale name into one we use in Bedrock before it reaches the database
- _info["locale"] = self._remap_locale_for_bedrock(_info["locale"])
-
- extra_params = dict(
- locale=_info["locale"],
- data_hash=hash,
- data=page_data,
- slug=_info["slug"],
- classification=_info.get("classification", ""),
- tags=_info.get("tags", []),
- category=_info.get("category", ""),
- )
-
- try:
- obj = ContentfulEntry.objects.get(
- contentful_id=page_id,
- locale=_info["locale"],
- )
- except ContentfulEntry.DoesNotExist:
- self.log(f"Creating new ContentfulEntry for {ctype}:{locale_code}:{page_id}")
- ContentfulEntry.objects.create(
- contentful_id=page_id,
- content_type=ctype,
- **extra_params,
- )
- added_count += 1
- else:
- if self.force or hash != obj.data_hash:
- self.log(f"Updating existing ContentfulEntry for {ctype}:{locale_code}:{page_id}")
- for key, value in extra_params.items():
- setattr(obj, key, value)
- obj.last_modified = tz_now()
- obj.save()
- updated_count += 1
-
- try:
- # Even if we failed to sync certain entities that are usually syncable, we
- # should act as if they were synced when we come to look for records to delete.
- # (If it was just a temporary glitch that caused the exception we would not
- # want to unncessarily delete a page, even if the failed sync means its content
- # is potentially stale)
- # HOWEVER, there are some entities which are just not syncable at all - such as
- # a Compose `page` which has no entry for a specific locale, and so is skipped
- # above. For these, we DO want to delete them, so remove them from the list of
- # synced items
-
- entries_processed_in_sync = set(content_to_sync).difference(content_missing_localised_version)
- deleted_count = self._detect_and_delete_absent_entries(entries_processed_in_sync)
- except Exception as ex:
- self.log(ex)
- capture_exception(ex)
-
- self._check_localisation_complete()
-
- return added_count, updated_count, deleted_count, error_count
diff --git a/bedrock/contentful/migrations/0001_initial.py b/bedrock/contentful/migrations/0001_initial.py
deleted file mode 100644
index c0fe3ed7f03..00000000000
--- a/bedrock/contentful/migrations/0001_initial.py
+++ /dev/null
@@ -1,33 +0,0 @@
-# This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at https://mozilla.org/MPL/2.0/.
-
-# Generated by Django 2.2.21 on 2021-08-30 18:38
-
-import django.utils.timezone
-from django.db import migrations, models
-
-import django_extensions.db.fields.json
-
-
-class Migration(migrations.Migration):
- initial = True
-
- dependencies = []
-
- operations = [
- migrations.CreateModel(
- name="ContentfulEntry",
- fields=[
- ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
- ("contentful_id", models.CharField(max_length=20, unique=True)),
- ("content_type", models.CharField(max_length=20)),
- ("language", models.CharField(max_length=5)),
- ("last_modified", models.DateTimeField(default=django.utils.timezone.now)),
- ("url_base", models.CharField(blank=True, max_length=255)),
- ("slug", models.CharField(blank=True, max_length=255)),
- ("data_hash", models.CharField(max_length=64)),
- ("data", django_extensions.db.fields.json.JSONField(default=dict)),
- ],
- ),
- ]
diff --git a/bedrock/contentful/migrations/0002_rename_contentfulentry_language_to_locale.py b/bedrock/contentful/migrations/0002_rename_contentfulentry_language_to_locale.py
deleted file mode 100644
index 226e0a3e20f..00000000000
--- a/bedrock/contentful/migrations/0002_rename_contentfulentry_language_to_locale.py
+++ /dev/null
@@ -1,21 +0,0 @@
-# This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at https://mozilla.org/MPL/2.0/.
-
-# Generated by Django 2.2.24 on 2021-11-04 11:10
-
-from django.db import migrations
-
-
-class Migration(migrations.Migration):
- dependencies = [
- ("contentful", "0001_initial"),
- ]
-
- operations = [
- migrations.RenameField(
- model_name="contentfulentry",
- old_name="language",
- new_name="locale",
- ),
- ]
diff --git a/bedrock/contentful/migrations/0003_add_classification_category_and_tags_fields_to_contentfulentry.py b/bedrock/contentful/migrations/0003_add_classification_category_and_tags_fields_to_contentfulentry.py
deleted file mode 100644
index cace7433c2d..00000000000
--- a/bedrock/contentful/migrations/0003_add_classification_category_and_tags_fields_to_contentfulentry.py
+++ /dev/null
@@ -1,40 +0,0 @@
-# This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at https://mozilla.org/MPL/2.0/.
-
-# Generated by Django 2.2.24 on 2021-11-05 16:52
-
-from django.db import migrations, models
-
-import django_extensions.db.fields.json
-
-
-class Migration(migrations.Migration):
- dependencies = [
- ("contentful", "0002_rename_contentfulentry_language_to_locale"),
- ]
-
- operations = [
- migrations.AddField(
- model_name="contentfulentry",
- name="category",
- field=models.CharField(blank=True, default="", help_text="Some pages may have a category", max_length=255),
- ),
- migrations.AddField(
- model_name="contentfulentry",
- name="classification",
- field=models.CharField(
- blank=True,
- default="",
- help_text=(
- "Some pages may have custom fields on them, distinct from their content type - eg: pagePageResourceCenter has a 'Product' field"
- ),
- max_length=255,
- ),
- ),
- migrations.AddField(
- model_name="contentfulentry",
- name="tags",
- field=django_extensions.db.fields.json.JSONField(blank=True, default=dict, help_text="Some pages may have tags"),
- ),
- ]
diff --git a/bedrock/contentful/migrations/0004_update_unique_constraint_on_contentfulentry.py b/bedrock/contentful/migrations/0004_update_unique_constraint_on_contentfulentry.py
deleted file mode 100644
index 0616c5efa76..00000000000
--- a/bedrock/contentful/migrations/0004_update_unique_constraint_on_contentfulentry.py
+++ /dev/null
@@ -1,25 +0,0 @@
-# This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at https://mozilla.org/MPL/2.0/.
-
-# Generated by Django 2.2.24 on 2021-12-09 18:45
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
- dependencies = [
- ("contentful", "0003_add_classification_category_and_tags_fields_to_contentfulentry"),
- ]
-
- operations = [
- migrations.AlterField(
- model_name="contentfulentry",
- name="contentful_id",
- field=models.CharField(max_length=20),
- ),
- migrations.AlterUniqueTogether(
- name="contentfulentry",
- unique_together={("contentful_id", "locale")},
- ),
- ]
diff --git a/bedrock/contentful/migrations/0005_contentfulentry_localisation_complete.py b/bedrock/contentful/migrations/0005_contentfulentry_localisation_complete.py
deleted file mode 100644
index b2c4080db9e..00000000000
--- a/bedrock/contentful/migrations/0005_contentfulentry_localisation_complete.py
+++ /dev/null
@@ -1,21 +0,0 @@
-# This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at https://mozilla.org/MPL/2.0/.
-
-# Generated by Django 3.2.16 on 2022-11-11 15:55
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
- dependencies = [
- ("contentful", "0004_update_unique_constraint_on_contentfulentry"),
- ]
-
- operations = [
- migrations.AddField(
- model_name="contentfulentry",
- name="localisation_complete",
- field=models.BooleanField(default=False),
- ),
- ]
diff --git a/bedrock/contentful/migrations/0006_mark-en-us-as-localisation-complete.py b/bedrock/contentful/migrations/0006_mark-en-us-as-localisation-complete.py
deleted file mode 100644
index 3a963895354..00000000000
--- a/bedrock/contentful/migrations/0006_mark-en-us-as-localisation-complete.py
+++ /dev/null
@@ -1,25 +0,0 @@
-# This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at https://mozilla.org/MPL/2.0/.
-
-# Generated by Django 3.2.16 on 2022-11-28 10:40
-
-from django.db import migrations
-
-
-def make_en_us_localisation_complete(apps, schema_editor):
- ContentfulEntry = apps.get_model("contentful", "ContentfulEntry")
- ContentfulEntry.objects.filter(locale="en-US").update(localisation_complete=True)
-
-
-class Migration(migrations.Migration):
- dependencies = [
- ("contentful", "0005_contentfulentry_localisation_complete"),
- ]
-
- operations = [
- migrations.RunPython(
- make_en_us_localisation_complete,
- migrations.RunPython.noop,
- ),
- ]
diff --git a/bedrock/contentful/migrations/0007_data_switch_to_local_images.py b/bedrock/contentful/migrations/0007_data_switch_to_local_images.py
deleted file mode 100644
index bba1b45cc6c..00000000000
--- a/bedrock/contentful/migrations/0007_data_switch_to_local_images.py
+++ /dev/null
@@ -1,126 +0,0 @@
-# This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at https://mozilla.org/MPL/2.0/.
-
-# Generated by Django 3.2.23 on 2024-02-08 14:44
-
-import re
-import sys
-from collections import defaultdict
-
-from django.db import migrations
-
-from bedrock.contentful.constants import CONTENT_TYPE_PAGE_RESOURCE_CENTER
-
-# Renames image urls like the following to be local paths:
-# "https://images.ctfassets.net/w5er3c7zdgmd/7cTC9dTpbRjoeNn3qwNbv2/63b89449a26f88b9de617f0ff0fa5acf/image2.png?w=688"
-# "https://images.ctfassets.net/w5er3c7zdgmd/7cTC9dTpbRjoeNn3qwNbv2/63b89449a26f88b9de617f0ff0fa5acf/image2.png?w=1376 1.5x"
-#
-# Note that some of them appear in srcset attributes, hence have the density multiplier,
-# and some have width querystrigs, both of which must be catered for in the renaming.
-#
-# The actual files have been downloaded and added to the Bedrock codebase in the
-# same changeset as this data migration will be immediately available upon deployment
-
-contentful_cdn_images_pattern = re.compile(
- r"\"https:\/\/images\.ctfassets\.net\/" # CDN hostname
- r"\S*" # any path without a space
- r"(?:\s1\.5x){0,1}" # but allow a space at the end IFF it's before '1.5x' (in a non-capturing group)
- r"\"" # and ending in a quote
-)
-
-
-def _print(args):
- sys.stdout.write(args)
- sys.stdout.write("\n")
-
-
-def _url_to_local_filename(url):
- # Temporarily shelve any density info picked up from the src attribute. 1.5x is the only one used.
- dpr_multiplier = " 1.5x"
-
- if dpr_multiplier in url:
- has_dpr = True
- url = url.replace(dpr_multiplier, "")
- else:
- has_dpr = False
-
- base_filename = url.split("/")[-1]
- parts = base_filename.split("?")
- if len(parts) == 1:
- filename = parts[0]
- else:
- split_filename = parts[0].split(".")
- if len(split_filename) == 2:
- name, suffix = split_filename
- else:
- # filename has multiple . in it
- name = ".".join(split_filename[:-1])
- suffix = split_filename[-1]
-
- filename = f"{name}_{''.join(parts[1:])}.{suffix}"
- filename = filename.replace("=", "")
-
- if has_dpr:
- filename = f"{filename}{dpr_multiplier}"
- return filename
-
-
-def switch_image_urls_to_local(apps, schema_editor):
- ContentfulEntry = apps.get_model("contentful", "ContentfulEntry")
-
- resource_center_pages = ContentfulEntry.objects.filter(
- content_type=CONTENT_TYPE_PAGE_RESOURCE_CENTER,
- )
- for page in resource_center_pages:
- # Some safety checks. If these fail, the migration breaks and it gets rolled back
-
- if page.slug != "unknown":
- _print(f"Checking page {page.contentful_id} {page.slug} [{page.locale}]")
- # First the "SEO" images, which are also used on the listing page
- assert "info" in page.data
-
- if "seo" in page.data["info"] and "image" in page.data["info"]["seo"]:
- _print(f"Updating SEO image URL for {page.contentful_id}. Slug: {page.slug}")
-
- image_url = page.data["info"]["seo"]["image"]
- updated_image_url = f"/media/img/products/vpn/resource-center/{_url_to_local_filename(image_url)}"
- page.data["info"]["seo"]["image"] = updated_image_url
-
- _print("Saving SEO info changes\n")
- page.save()
-
- # Now check for images in the body of the page
- assert len(page.data["entries"]) == 1
- assert "body" in page.data["entries"][0]
-
- body = page.data["entries"][0]["body"]
- old_to_new = defaultdict(str)
- matches = re.findall(contentful_cdn_images_pattern, body)
-
- if matches:
- _print(f"Found {len(matches)} URLs to update")
- for link in matches:
- old_to_new[link] = f'"/media/img/products/vpn/resource-center/{_url_to_local_filename(link[1:-1])}"'
-
- for old, new in old_to_new.items():
- body = body.replace(old, new)
-
- page.data["entries"][0]["body"] = body
- _print("Saving page changes\n")
- page.save()
- else:
- if page.slug != "unknown":
- _print("No changes needed to page body\n")
-
-
-class Migration(migrations.Migration):
- dependencies = [
- ("contentful", "0006_mark-en-us-as-localisation-complete"),
- ]
- operations = [
- migrations.RunPython(
- switch_image_urls_to_local,
- migrations.RunPython.noop,
- )
- ]
diff --git a/bedrock/contentful/migrations/0008_alter_contentfulentry_content_type_and_more.py b/bedrock/contentful/migrations/0008_alter_contentfulentry_content_type_and_more.py
deleted file mode 100644
index b832846baed..00000000000
--- a/bedrock/contentful/migrations/0008_alter_contentfulentry_content_type_and_more.py
+++ /dev/null
@@ -1,31 +0,0 @@
-# This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at https://mozilla.org/MPL/2.0/.
-
-# Generated by Django 4.2.11 on 2024-05-08 15:55
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
- dependencies = [
- ("contentful", "0007_data_switch_to_local_images"),
- ]
-
- operations = [
- migrations.AlterField(
- model_name="contentfulentry",
- name="content_type",
- field=models.CharField(max_length=32),
- ),
- migrations.AlterField(
- model_name="contentfulentry",
- name="contentful_id",
- field=models.CharField(max_length=32),
- ),
- migrations.AlterField(
- model_name="contentfulentry",
- name="locale",
- field=models.CharField(max_length=24),
- ),
- ]
diff --git a/bedrock/contentful/migrations/0009_data_amend_article_slug.py b/bedrock/contentful/migrations/0009_data_amend_article_slug.py
deleted file mode 100644
index 5f011825b7e..00000000000
--- a/bedrock/contentful/migrations/0009_data_amend_article_slug.py
+++ /dev/null
@@ -1,33 +0,0 @@
-# This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at https://mozilla.org/MPL/2.0/.
-
-# Generated by Django 4.2.16 on 2024-10-25 09:48
-
-from django.db import migrations
-
-ORIGINAL_SLUG = "no-Logging-vpn-from-mozilla"
-FIXED_SLUG = "no-logging-vpn-from-mozilla"
-
-
-def forwards(apps, schema_editor):
- ContentfulEntry = apps.get_model("contentful.ContentfulEntry")
- ContentfulEntry.objects.filter(slug=ORIGINAL_SLUG).update(slug=FIXED_SLUG)
-
-
-def backwards(apps, schema_editor):
- ContentfulEntry = apps.get_model("contentful.ContentfulEntry")
- ContentfulEntry.objects.filter(slug=FIXED_SLUG).update(slug=ORIGINAL_SLUG)
-
-
-class Migration(migrations.Migration):
- dependencies = [
- ("contentful", "0008_alter_contentfulentry_content_type_and_more"),
- ]
-
- operations = [
- migrations.operations.RunPython(
- code=forwards,
- reverse_code=backwards,
- ),
- ]
diff --git a/bedrock/contentful/migrations/__init__.py b/bedrock/contentful/migrations/__init__.py
deleted file mode 100644
index 448bb8652d6..00000000000
--- a/bedrock/contentful/migrations/__init__.py
+++ /dev/null
@@ -1,3 +0,0 @@
-# This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at https://mozilla.org/MPL/2.0/.
diff --git a/bedrock/contentful/models.py b/bedrock/contentful/models.py
deleted file mode 100644
index f9c689e1128..00000000000
--- a/bedrock/contentful/models.py
+++ /dev/null
@@ -1,179 +0,0 @@
-# This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at https://mozilla.org/MPL/2.0/.
-
-from django.db import models
-from django.db.models.query_utils import Q
-from django.utils.timezone import now
-
-from django_extensions.db.fields.json import JSONField
-
-from bedrock.contentful.constants import CONTENT_TYPE_CONNECT_HOMEPAGE
-
-
-class ContentfulEntryManager(models.Manager):
- """
- The key distinction here is that get_page_* returns a JSONDict of page data, while
- get_entry_* and get_entries_* returns a QuerySet of ContentfulEntry records.
- """
-
- def get_page_by_id(self, content_id, locale=None):
- kwargs = {"contentful_id": content_id}
- if locale:
- kwargs["locale"] = locale
-
- return self.get(**kwargs).data
-
- def get_active_locales_for_slug(
- self,
- slug,
- content_type,
- classification=None,
- ):
- kwargs = dict(
- slug=slug,
- content_type=content_type,
- localisation_complete=True,
- )
- if classification:
- kwargs["classification"] = classification
- return sorted(self.filter(**kwargs).values_list("locale", flat=True))
-
- def get_entry_by_slug(
- self,
- slug,
- locale,
- content_type,
- classification=None,
- localisation_complete=True,
- ):
- kwargs = dict(
- slug=slug,
- locale=locale,
- content_type=content_type,
- localisation_complete=localisation_complete,
- )
- if classification:
- kwargs["classification"] = classification
- return self.get(**kwargs)
-
- def get_page_by_slug(
- self,
- slug,
- locale,
- content_type,
- classification=None,
- localisation_complete=True,
- ):
- # Thin wrapper that gets back only the JSON data
- return self.get_entry_by_slug(
- slug=slug,
- locale=locale,
- content_type=content_type,
- classification=classification,
- localisation_complete=localisation_complete,
- ).data
-
- def get_entries_by_type(
- self,
- content_type,
- locale,
- classification=None,
- localisation_complete=True,
- order_by="last_modified",
- ):
- """Get multiple appropriate ContentfulEntry records, not just the JSON data.
-
- Args:
- content_type (str): the Contentful content type
- locale (str): eg 'fr', 'en-US'
- classification ([str], optional): specific type of content, used when have
- a single content_type used for different areas of the site. Defaults to None.
- order_by (str, optional): Sorting key for the queryset. Defaults to "last_modified".
-
- Returns:
- QuerySet[ContentfulEntry]: the main ContentfulEntry models, not just their JSON data
- """
-
- kwargs = dict(
- content_type=content_type,
- locale=locale,
- localisation_complete=localisation_complete,
- )
- if classification:
- kwargs["classification"] = classification
-
- return self.filter(**kwargs).order_by(order_by)
-
- def get_homepage(self, locale):
- return self.get(
- content_type=CONTENT_TYPE_CONNECT_HOMEPAGE,
- locale=locale,
- ).data
-
-
-class ContentfulEntry(models.Model):
- contentful_id = models.CharField(max_length=32)
- content_type = models.CharField(max_length=32)
- locale = models.CharField(
- max_length=24
- ) # this is longer than the 5 needed because there are a couple of pages that have non-lang-codes set as their locale
- localisation_complete = models.BooleanField(default=False)
- last_modified = models.DateTimeField(default=now)
- url_base = models.CharField(max_length=255, blank=True)
- slug = models.CharField(max_length=255, blank=True)
- data_hash = models.CharField(max_length=64)
- data = JSONField()
- # Fields we may need to query by
- classification = models.CharField(
- max_length=255,
- blank=True,
- default="",
- help_text="Some pages may have custom fields on them, distinct from their content type - eg: pagePageResourceCenter has a 'Product' field",
- )
- category = models.CharField(
- max_length=255,
- blank=True,
- default="",
- help_text="Some pages may have a category",
- )
- tags = JSONField(
- blank=True,
- help_text="Some pages may have tags",
- )
-
- objects = ContentfulEntryManager()
-
- class Meta:
- unique_together = ["contentful_id", "locale"]
-
- def __str__(self) -> str:
- return f"ContentfulEntry {self.content_type}:{self.contentful_id}[{self.locale}]"
-
- def get_related_entries(self, order_by="last_modified", localisation_complete=True):
- """Find ContentfulEntry records that:
- * are for the same content_type
- * are for the same classification
- * share at least one tag with `self`
-
- Returns:
- QuerySet[ContentfulEntry]
- """
-
- if not self.tags:
- return ContentfulEntry.objects.none()
-
- _base_qs = ContentfulEntry.objects.filter(
- locale=self.locale,
- localisation_complete=localisation_complete,
- content_type=self.content_type,
- classification=self.classification, # eg same Product/project/area of the site
- ).exclude(
- id=self.id,
- )
-
- # Tags are stored in a JSONField, but we can query it as text by quoting them
- q_obj = Q()
- for _tag in self.tags:
- q_obj |= Q(tags__contains=f'"{_tag}"')
- return _base_qs.filter(q_obj).order_by(order_by).distinct()
diff --git a/bedrock/contentful/templates/includes/contentful/all.html b/bedrock/contentful/templates/includes/contentful/all.html
deleted file mode 100644
index cf11cf1c59c..00000000000
--- a/bedrock/contentful/templates/includes/contentful/all.html
+++ /dev/null
@@ -1,129 +0,0 @@
-{#
- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at https://mozilla.org/MPL/2.0/.
-#}
-
-
-{% from "macros-protocol.html" import callout_compact, card, split, picto with context %}
-
-{% for entry in entries -%}
- {% if entry.component == 'callout' %}
-
- {% call callout_compact(
- title=entry.title,
- desc=entry.body|external_html,
- class=entry.theme_class + ' ' + entry.product_class,
- ) %}
-
- {{ entry.cta }}
-
- {% endcall %}
-
- {% elif entry.component == 'text' %}
-
-
-
- {{ entry.body|external_html }}
-
-
-
- {% elif entry.component == 'textColumns' %}
-
-
- {% for body in entry.content -%}
-
- {{ body|external_html }}
-
- {% endfor %}
-
-
- {% elif entry.component == 'cardLayout' %}
-
-
-
- {% for card_data in entry.cards -%}
- {% if card_data.component == 'large_card' %}
- {% set size_class = 'mzp-c-card-large' %}
- {% elif entry.layout_class == 'mzp-l-card-quarter' %}
- {% set size_class = 'mzp-c-card-extra-small' %}
- {% else %}
- {% set size_class = 'mzp-c-card-medium' %}
- {% endif %}
-
- {% if card_data.highres_image_url %}
- {% set image = '

' %}
- {% else %}
- {% set image = '

' %}
- {% endif %}
-
- {{ card(
- class=size_class,
- tag_label=card_data.tag,
- title=card_data.heading,
- desc=card_data.body|external_html,
- ga_title='ga_title',
- image=image,
- aspect_ratio=card_data.aspect_ratio,
- link_url=card_data.link,
- youtube_id=card_data.youtube_id,
- ) }}
-
- {% endfor %}
-
-
-
- {% elif entry.component == 'sectionHeading' %}
- {{ entry.heading }}
- {% elif entry.component == 'pictoLayout' %}
-
-
- {% for picto_data in entry.pictos -%}
- {% call picto(
- image=resp_img(
- url=picto_data.image_url,
- optional_attributes={
- 'class': 'mzp-c-picto-image',
- 'width': entry.image_width
- }
- ),
- title=picto_data.heading,
- heading_level=entry.heading_level,
- body=picto_data.body,
- base_el='li'
- ) %}
-
- {{ picto_data.body|external_html }}
-
- {% endcall %}
-
- {% endfor %}
-
-
- {% elif entry.component == 'split' %}
-
- {% if entries.index(entry) == 0: %}
- {% set loading = 'eager' %}
- {% else %}
- {% set loading = 'lazy' %}
- {% endif %}
-
- {% set image = '
' %}
-
- {% call split(
- block_class=entry.block_class,
- body_class=entry.body_class,
- theme_class=entry.theme_class,
- media_class=entry.media_class,
- mobile_class=entry.mobile_class,
- media_after=entry.media_after,
- image=image
- ) %}
- {{ entry.body|external_html }}
-
- {% endcall %}
-
- {% endif %}
-
-
-{% endfor %}
diff --git a/bedrock/contentful/templates/includes/contentful/css.html b/bedrock/contentful/templates/includes/contentful/css.html
deleted file mode 100644
index 824d3557e1c..00000000000
--- a/bedrock/contentful/templates/includes/contentful/css.html
+++ /dev/null
@@ -1,14 +0,0 @@
-{#
- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at https://mozilla.org/MPL/2.0/.
-#}
-
-
-{% for sheet in page_css -%}
-
- {{ css_bundle(info.theme + '-' + sheet) }}
-
-{% endfor %}
-
-{{ css_bundle('c-logo') }}
diff --git a/bedrock/contentful/templates/includes/contentful/cta.html b/bedrock/contentful/templates/includes/contentful/cta.html
deleted file mode 100644
index b511c475c98..00000000000
--- a/bedrock/contentful/templates/includes/contentful/cta.html
+++ /dev/null
@@ -1,47 +0,0 @@
-{#
- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at https://mozilla.org/MPL/2.0/.
-#}
-
-{% set utm_source = request.page_info.utm_source %}
-{% set utm_campaign = request.page_info.utm_campaign %}
-{% set utm_content = label.replace(' ', '-') %}
-{% set referral = '?utm_source=' + utm_source + '&utm_medium=referral&utm_campaign=' + utm_campaign %}
-
-{% if action == 'Download Firefox' %}
-
-
- {{ download_firefox_thanks(dom_id='download-button-primary', alt_copy=label, button_class=button_class) }}
-
-{% elif action == 'Explore Firefox' %}
-
- {{ label }}
-
-{% elif action == 'Create a Firefox Account' %}
-
- {{ fxa_button(
- entrypoint=utm_source,
- button_text=label,
- class_name=button_class,
- optional_parameters={'utm_campaign': utm_campaign, 'utm_content': 'firefox-sync-' + utm_content },
- optional_attributes={'data-cta-text': cta_text, 'data-cta-type': 'fxa-sync'}
- ) }}
-
-{% elif action == 'Get Pocket' %}
-
- {{ label }}
-
-{% elif action == 'Get Mozilla VPN' %}
-
- {{ label }}
-
-{% elif action == 'Try Relay' %}
-
- {{ label }}
-
-{% elif action == 'Get MDN Plus' %}
-
- {{ label }}
-
-{% endif %}
diff --git a/bedrock/contentful/templates/includes/contentful/js.html b/bedrock/contentful/templates/includes/contentful/js.html
deleted file mode 100644
index 967336fe9e4..00000000000
--- a/bedrock/contentful/templates/includes/contentful/js.html
+++ /dev/null
@@ -1,12 +0,0 @@
-{#
- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at https://mozilla.org/MPL/2.0/.
-#}
-
-
-{% for script in page_js -%}
-
- {{ js_bundle(script) }}
-
-{% endfor %}
diff --git a/bedrock/contentful/templates/includes/contentful/logo.html b/bedrock/contentful/templates/includes/contentful/logo.html
deleted file mode 100644
index b2ddbae0987..00000000000
--- a/bedrock/contentful/templates/includes/contentful/logo.html
+++ /dev/null
@@ -1,8 +0,0 @@
-{#
- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at https://mozilla.org/MPL/2.0/.
-#}
-
-
-{{ product_name }}
diff --git a/bedrock/contentful/templates/includes/contentful/wordmark.html b/bedrock/contentful/templates/includes/contentful/wordmark.html
deleted file mode 100644
index 5ee73c2dd65..00000000000
--- a/bedrock/contentful/templates/includes/contentful/wordmark.html
+++ /dev/null
@@ -1,8 +0,0 @@
-{#
- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at https://mozilla.org/MPL/2.0/.
-#}
-
-
-{{ product_name }}
diff --git a/bedrock/contentful/templatetags/__init__.py b/bedrock/contentful/templatetags/__init__.py
deleted file mode 100644
index 448bb8652d6..00000000000
--- a/bedrock/contentful/templatetags/__init__.py
+++ /dev/null
@@ -1,3 +0,0 @@
-# This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at https://mozilla.org/MPL/2.0/.
diff --git a/bedrock/contentful/templatetags/helpers.py b/bedrock/contentful/templatetags/helpers.py
deleted file mode 100644
index ab21140548d..00000000000
--- a/bedrock/contentful/templatetags/helpers.py
+++ /dev/null
@@ -1,63 +0,0 @@
-# This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at https://mozilla.org/MPL/2.0/.
-
-import bleach
-from django_jinja import library
-from markupsafe import Markup
-
-# based on bleach.sanitizer.ALLOWED_TAGS
-ALLOWED_TAGS = {
- "a",
- "abbr",
- "acronym",
- "b",
- "blockquote",
- "button",
- "code",
- "div",
- "em",
- "h1",
- "h2",
- "h3",
- "h4",
- "h5",
- "h6",
- "hr",
- "i",
- "img",
- "li",
- "ol",
- "p",
- "small",
- "span",
- "strike",
- "strong",
- "ul",
-}
-ALLOWED_ATTRS = [
- "alt",
- "class",
- "href",
- "id",
- "src",
- "srcset",
- "rel",
- "title",
-]
-
-
-def _allowed_attrs(tag, name, value):
- if name in ALLOWED_ATTRS:
- return True
-
- if name.startswith("data-"):
- return True
-
- return False
-
-
-@library.filter
-def external_html(content):
- """Clean and mark "safe" HTML content from external data"""
- return Markup(bleach.clean(content, tags=ALLOWED_TAGS, attributes=_allowed_attrs))
diff --git a/bedrock/contentful/tests/data.py b/bedrock/contentful/tests/data.py
deleted file mode 100644
index 91b0f4ce14f..00000000000
--- a/bedrock/contentful/tests/data.py
+++ /dev/null
@@ -1,34 +0,0 @@
-# This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at https://mozilla.org/MPL/2.0/.
-
-resource_center_page_data = {
- "page_type": "pagePageResourceCenter",
- "page_css": [],
- "page_js": [],
- "info": {
- "slug": "the-difference-between-a-vpn-and-a-web-proxy",
- "title": "The difference between a VPN and a web proxy",
- "blurb": "VPNs and proxies are solutions for online privacy and security. Here\u2019s how these protect you and how to choose the best option.",
- "theme": "mozilla",
- "locale": "en-US",
- "utm_source": "www.mozilla.org-the-difference-between-a-vpn-and-a-web-proxy",
- "utm_campaign": "the-difference-between-a-vpn-and-a-web-proxy",
- "seo": {
- "name": "The difference between a VPN and a web proxy - Compose: SEO",
- "description": "VPNs and proxies are solutions for online privacy and security. Here\u2019s how these protect you and how to choose the best option.",
- "no_index": False,
- "no_follow": False,
- "image": "https://images.ctfassets.net/w5er3c7zdgmd/7o6QXGC6BXMq3aB5hu4mmn/d0dc31407051937f56a0a46767e11f6f/vpn-16x9-phoneglobe.png",
- },
- "category": "Category 1",
- "classification": "VPN",
- },
- "entries": [
- {
- "component": "text",
- "body": 'Virtual private networks (VPNs) and secure web proxies are solutions for better privacy and security online, but it can be confusing to figure out which one is right for you. Here\u2019s a look at how these services protect you and how to choose the best option for when you\u2019re online.
\nStop ISPs from spying on you
\nWhen you use Firefox, Enhanced Tracking Protection automatically blocks many third party web trackers from following you around the web. But here\u2019s an interesting fact: your internet service provider (ISP) that you are paying for an internet connection \u2014 can still observe and track you.
\nBecause your internet traffic moves to and from your devices (computer, phone, tv, tablet) through your ISP, they can see where you go online. An ISP can see what sites you visit, how long you\u2019re on them, your location and information about your devices. An ISP may not know the specifics of what you did on those sites (like what you bought, searched for or read) thanks to encryption, but they could make inferences about you based on the sites that you visited. That personal data can be used to create detailed profiles about you. Why would ISPs do that? In short: this data is valuable.
\nISPs can use this information for their own ad targeting or for monetization opportunities that could include sharing your information with third parties interested in data mining, marketing and targeted advertising, which means less privacy and more tracking. Browsing in private mode doesn\u2019t prevent ISPs from seeing where you go online. But sending your web traffic through a web proxy or VPN can make it much harder.
\nWhen should you choose a VPN or a secure proxy?
\nVPNs and secure web proxies have shared goals: they secure connections. They can, and do, mask your original IP address and protect web traffic that you send between you and your VPN or secure proxy provider. But when would you want to use a VPN vs a proxy?
\n
\n\nSecure web proxy: browser-level protection
\nA secure web proxy works for tasks that you might do only in your browser. This can amount to a lot of activity like shopping, paying bills, logging into social media and reading emails. A secure web proxy serves as an intermediary between your browser and the internet. Your web browsing data will pass through a secure tunnel to the internet directly from your browser, masking your IP address, so the web server you are contacting doesn\u2019t know exactly where you are in the world. And that makes you harder to track and target.
\nA proxy is useful when you\u2019re browsing the web on a public WiFi. When a proxy is enabled, it will stop eavesdroppers on the same network from spying on your browsing activity or reading your transactions on unencrypted sites. It sounds harmless, but public WiFi networks can be like a backdoor for hackers.
\nFirefox Private Network is an easy to install browser extension that provides a secure, encrypted tunnel to the web to protect your browser connection anywhere you use Firefox. It\'s fast and easy to turn on whenever you need it for extra security in your browser
\nVPNs: device-level protection
\nVPNs do more than proxies in that a proxy only protects what you do in your browser, whereas a VPN protects all your traffic, including your browser, wherever you have a VPN installed and enabled. VPNs provide added security and privacy for all your online activity \u2014 an important consideration if you want to keep your activity to yourself and make it more difficult for data hungry trackers and ISPs to create a profile of you across all your devices, like your phone, computer and tablet.
\nA VPN works by creating a secure \u201ctunnel\u201d between your device and the internet at large. It protects your privacy in two key ways:
\n- Concealing your IP address, protecting your identity and obscuring your location.
- Encrypting your traffic between you and your VPN provider so that no one on your local network can decipher or modify it.
\nA VPN also offers security on open and public WiFi connections. Open WiFi can be risky, and it\u2019s impossible to be sure that someone else isn\u2019t connecting to the same network to snoop on what you\u2019re doing. Even if your traffic is encrypted, they can still see which sites you are visiting. And if you\u2019re using an app that doesn\u2019t have encryption \u2014 and even today, many don\u2019t \u2014 then they can see everything you are doing in that app. Mozilla VPN is a fast, secure, trustworthy service that can help close the security gap for you. Mozilla VPN also lets you choose your \u201clocation\u201d (where your traffic appears to be coming from) from more than 30 countries.
\nChoose a trustworthy service
\nThe most important thing to consider when picking either a VPN or a proxy service is choosing a trustworthy company. Be sure you understand the terms you\u2019re agreeing to. Many claim to be great and focused on privacy, but a large number of them fall short on their promise. Not all proxy and VPN services are secure and private. Some will log your online activities so they can sell your data and information to marketing firms themselves. Other services will try to convince you to install malware on your devices.
\nWe\u2019ve done the legwork to ensure that both the Mozilla VPN and Firefox Private Network proxy extension actually respect your privacy, and it\u2019s something we\u2019re willing to stake our reputation on. Mozilla has a reputation for building products that help you keep your information safe. We follow our easy to read, no-nonsense Data Privacy Principles which allow us to focus only on the information we need to provide a service. And since we are backed by a mission-driven company, you can trust that the dollars you spend for this product will not only ensure you have top-notch security, but also are making the internet better for everyone.
\n',
- "width_class": "mzp-t-content-md",
- }
- ],
-}
diff --git a/bedrock/contentful/tests/test_contentful_api.py b/bedrock/contentful/tests/test_contentful_api.py
deleted file mode 100644
index ae362162acc..00000000000
--- a/bedrock/contentful/tests/test_contentful_api.py
+++ /dev/null
@@ -1,1484 +0,0 @@
-# This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at https://mozilla.org/MPL/2.0/.
-
-from copy import deepcopy
-from unittest.mock import ANY, Mock, call, patch
-
-from django.conf import settings
-from django.test import override_settings
-
-import pytest
-from rich_text_renderer.block_renderers import ListItemRenderer
-from rich_text_renderer.text_renderers import TextRenderer
-
-from bedrock.contentful.api import (
- DEFAULT_LOCALE,
- AssetBlockRenderer,
- ContentfulPage,
- EmphasisRenderer,
- InlineEntryRenderer,
- LinkRenderer,
- LiRenderer,
- OlRenderer,
- PRenderer,
- StrongRenderer,
- UlRenderer,
- _get_abbr_from_width,
- _get_aspect_ratio_class,
- _get_card_image_url,
- _get_column_class,
- _get_height,
- _get_image_url,
- _get_layout_class,
- _get_product_class,
- _get_width_class,
- _get_youtube_id,
- _make_cta_button,
- _make_logo,
- _make_plain_text,
- _make_wordmark,
- _only_child,
- _render_list,
- contentful_locale,
- get_client,
-)
-from bedrock.contentful.constants import (
- CONTENT_TYPE_CONNECT_HOMEPAGE,
- CONTENT_TYPE_PAGE_RESOURCE_CENTER,
-)
-from bedrock.contentful.models import ContentfulEntry
-
-
-@pytest.mark.parametrize("raw_mode", [True, False])
-@override_settings(
- CONTENTFUL_SPACE_ID="test_space_id",
- CONTENTFUL_SPACE_KEY="test_space_key",
- CONTENTFUL_ENVIRONMENT="test_environment",
- CONTENTFUL_SPACE_API="https://example.com/test/",
- CONTENTFUL_API_TIMEOUT=987654321,
-)
-@patch("bedrock.contentful.api.contentful_api")
-def test_get_client(mock_contentful_api, raw_mode):
- mock_client = Mock()
-
- mock_contentful_api.Client.return_value = mock_client
-
- assert get_client(raw_mode=raw_mode) == mock_client
-
- mock_contentful_api.Client.assert_called_once_with(
- "test_space_id",
- "test_space_key",
- environment="test_environment",
- api_url="https://example.com/test/",
- raw_mode=raw_mode,
- content_type_cache=False,
- timeout_s=987654321,
- )
-
-
-@override_settings(
- CONTENTFUL_SPACE_ID="",
- CONTENTFUL_SPACE_KEY="",
-)
-@pytest.mark.parametrize("raw_mode", [True, False])
-@patch("bedrock.contentful.api.contentful_api")
-def test_get_client__no_credentials(mock_contentful_api, raw_mode):
- mock_client = Mock()
- mock_contentful_api.Client.return_value = mock_client
- assert get_client(raw_mode=raw_mode) is None
- assert not mock_contentful_api.Client.called
-
-
-@pytest.mark.parametrize(
- "bedrock_locale, expected",
- (
- ("en-US", "en-US"),
- ("en-GB", "en-GB"),
- ("de", "de-DE"),
- ("fr", "fr-FR"),
- ("fr-CA", "fr-CA"),
- ("es-ES", "es-ES"),
- ("es-MX", "es-MX"),
- ),
-)
-def test_contentful_locale(bedrock_locale, expected):
- assert contentful_locale(bedrock_locale) == expected
-
-
-@pytest.mark.parametrize(
- "width, aspect, expected",
- (
- (300, "1:1", 300),
- (300, "3:2", 200),
- (300, "16:9", 169),
- ),
-)
-def test__get_height(width, aspect, expected):
- assert _get_height(width, aspect) == expected
-
-
-def test__get_image_url():
- mock_image = Mock()
- mock_image.url.return_value = "//example.com/path/to/image/"
-
- assert _get_image_url(mock_image, 567) == "https://example.com/path/to/image/"
- mock_image.url.assert_called_once_with(w=567)
-
-
-def test__get_card_image_url():
- mock_image = Mock()
- mock_image.url.return_value = "//example.com/path/to/image/"
-
- assert _get_card_image_url(mock_image, 300, "3:2") == "https://example.com/path/to/image/"
- mock_image.url.assert_called_once_with(
- w=300,
- h=200,
- fit="fill",
- f="faces",
- )
-
-
-@pytest.mark.parametrize(
- ("product_name, expected"),
- (
- ("Firefox", "mzp-t-product-family"),
- ("Firefox Browser", "mzp-t-product-firefox"),
- ("Firefox Browser Beta", "mzp-t-product-beta"),
- ("Firefox Browser Developer", "mzp-t-product-developer"),
- ("Firefox Browser Nightly", "mzp-t-product-nightly"),
- ("Firefox Browser Focus", "mzp-t-product-focus"),
- ("Firefox Monitor", "mzp-t-product-monitor"),
- ("Firefox Lockwise", "mzp-t-product-lockwise"),
- ("Firefox Relay", "mzp-t-product-relay"),
- ("Mozilla", "mzp-t-product-mozilla"),
- ("Mozilla VPN", "mzp-t-product-vpn"),
- ("Pocket", "mzp-t-product-pocket"),
- ),
-)
-def test__get_product_class(product_name, expected):
- assert _get_product_class(product_name) == expected
-
-
-@pytest.mark.parametrize(
- "layout,expected",
- (
- ("layout2Cards", "mzp-l-card-half"),
- ("layout3Cards", "mzp-l-card-third"),
- ("layout4Cards", "mzp-l-card-quarter"),
- ("layout5Cards", "mzp-l-card-hero"),
- ),
-)
-def test__get_layout_class(layout, expected):
- assert _get_layout_class(layout) == expected
-
-
-@pytest.mark.parametrize(
- "width,expected",
- (
- ("Extra Small", "xs"),
- ("Small", "sm"),
- ("Medium", "md"),
- ("Large", "lg"),
- ("Extra Large", "xl"),
- ("Max", "max"),
- ),
-)
-def test__get_abbr_from_width(width, expected):
- assert _get_abbr_from_width(width) == expected
-
-
-@pytest.mark.parametrize(
- "ratio,expected",
- (
- ("1:1", "mzp-has-aspect-1-1"),
- ("3:2", "mzp-has-aspect-3-2"),
- ("16:9", "mzp-has-aspect-16-9"),
- ),
-)
-def test__get_aspect_ratio_class(ratio, expected):
- assert _get_aspect_ratio_class(ratio) == expected
-
-
-@pytest.mark.parametrize(
- "width,expected",
- (
- ("Extra Small", "mzp-t-content-xs"),
- ("Small", "mzp-t-content-sm"),
- ("Medium", "mzp-t-content-md"),
- ("Large", "mzp-t-content-lg"),
- ("Extra Large", "mzp-t-content-xl"),
- ("Max", "mzp-t-content-max"),
- ),
-)
-def test__get_width_class(width, expected):
- assert _get_width_class(width) == expected
-
-
-@pytest.mark.parametrize(
- "url",
- (
- "https://www.youtube.com/watch?v=qldxyjEjjBQ",
- "https://www.youtube.com/watch?v=qldxyjEjjBQ&some=querystring-here",
- "https://www.youtube.com/watch?v=qldxyjEjjBQ&v=BAD_SECOND_VIDEO_ID",
- ),
-)
-def test__get_youtube_id(url):
- assert _get_youtube_id(url) == "qldxyjEjjBQ"
-
-
-@pytest.mark.parametrize(
- "classname,expected",
- (
- ("1", ""),
- ("2", "mzp-l-columns mzp-t-columns-two"),
- ("3", "mzp-l-columns mzp-t-columns-three"),
- ("4", "mzp-l-columns mzp-t-columns-four"),
- ),
-)
-def test__get_column_class(classname, expected):
- assert _get_column_class(classname) == expected
-
-
-@pytest.mark.parametrize(
- "product_icon, icon_size, expected_data",
- (
- (
- "Firefox",
- "Small",
- {
- "product_name": "Firefox",
- "product_icon": "family",
- "icon_size": "sm",
- },
- ),
- ("", "Medium", None),
- (
- None,
- "Medium",
- None,
- ),
- (
- "Mozilla",
- None,
- {
- "product_name": "Mozilla",
- "product_icon": "mozilla",
- "icon_size": "md",
- },
- ),
- ),
-)
-@patch("bedrock.contentful.api.render_to_string")
-def test__make_logo(
- mock_render_to_string,
- product_icon,
- icon_size,
- expected_data,
-):
- mock_entry = Mock()
- data_dict = {}
-
- if product_icon:
- data_dict.update({"product_icon": product_icon})
- if icon_size:
- data_dict.update({"icon_size": icon_size})
- mock_entry.fields.return_value = data_dict
-
- _make_logo(mock_entry)
-
- if expected_data:
- mock_render_to_string.assert_called_once_with(
- "includes/contentful/logo.html",
- expected_data,
- ANY,
- )
- else:
- assert mock_render_to_string.call_count == 0
-
-
-@pytest.mark.parametrize(
- "product_icon, icon_size, expected_data",
- (
- (
- "Firefox",
- "Small",
- {
- "product_name": "Firefox",
- "product_icon": "family",
- "icon_size": "sm",
- },
- ),
- ("", "Medium", None),
- (
- None,
- "Medium",
- None,
- ),
- (
- "Mozilla",
- None,
- {
- "product_name": "Mozilla",
- "product_icon": "mozilla",
- "icon_size": "md",
- },
- ),
- ),
-)
-@patch("bedrock.contentful.api.render_to_string")
-def test__make_wordmark(
- mock_render_to_string,
- product_icon,
- icon_size,
- expected_data,
-):
- mock_entry = Mock()
- data_dict = {}
-
- if product_icon:
- data_dict.update({"product_icon": product_icon})
- if icon_size:
- data_dict.update({"icon_size": icon_size})
- mock_entry.fields.return_value = data_dict
-
- _make_wordmark(mock_entry)
-
- if expected_data:
- mock_render_to_string.assert_called_once_with(
- "includes/contentful/wordmark.html",
- expected_data,
- ANY,
- )
- else:
- assert mock_render_to_string.call_count == 0
-
-
-@pytest.mark.parametrize(
- "action, label, theme, size, expected_data",
- (
- (
- "Test action",
- "Test button label",
- "Primary",
- "Small",
- {
- "action": "Test action",
- "label": "Test button label",
- "button_class": "mzp-t-product mzp-t-sm",
- "location": "",
- "cta_text": "Test button label",
- },
- ),
- (
- "Get Mozilla VPN",
- "Test button label",
- "Secondary",
- "Large",
- {
- "action": "Get Mozilla VPN",
- "label": "Test button label",
- "button_class": "mzp-t-secondary mzp-t-lg",
- "location": "",
- "cta_text": "Test button label",
- },
- ),
- (
- "Minimal content test",
- "Test button label",
- "irrelevant",
- "unsupported size",
- {
- "action": "Minimal content test",
- "label": "Test button label",
- "button_class": "mzp-t-product mzp-t-", # broken class, but looks intentional in source
- "location": "",
- "cta_text": "Test button label",
- },
- ),
- ),
-)
-@patch("bedrock.contentful.api.render_to_string")
-def test__make_cta_button(
- mock_render_to_string,
- action,
- label,
- theme,
- size,
- expected_data,
-):
- mock_entry = Mock()
- data_dict = {}
-
- if action:
- data_dict.update({"action": action})
- if label:
- data_dict.update({"label": label})
- if theme:
- data_dict.update({"theme": theme})
- if size:
- data_dict.update({"size": size})
-
- mock_entry.fields.return_value = data_dict
-
- _make_cta_button(mock_entry)
-
- if expected_data:
- mock_render_to_string.assert_called_once_with(
- "includes/contentful/cta.html",
- expected_data,
- ANY,
- )
- else:
- assert mock_render_to_string.call_count == 0
-
-
-def test__make_plain_text():
- # Note this test will need a fixup when we add unidecode() support
- node = {
- "content": [
- {"value": "one"},
- {"value": "two"},
- {"value": "three"},
- ]
- }
-
- assert _make_plain_text(node) == "onetwothree"
-
-
-def test__only_child():
- # TODO: Broaden this out a bit - these aren't sufficient as-is, but are a start.
- # Written as based purely on the source code, not the data from Contentful
- node = {
- "content": [
- {
- "nodeType": "text",
- "value": "some text",
- },
- {
- "nodeType": "dummy-other",
- "value": "",
- },
- {
- "nodeType": "dummy-other",
- "value": "",
- },
- {
- "nodeType": "text",
- "value": "more text",
- },
- {
- "nodeType": "dummy-extra",
- "value": "",
- },
- ]
- }
-
- assert not _only_child(node, "text")
- assert not _only_child(node, "dummy-other")
- assert not _only_child(node, "dummy-extra")
-
- node = {
- "content": [
- {
- "nodeType": "text",
- "value": "some text",
- },
- ]
- }
- assert _only_child(node, "text")
- assert not _only_child(node, "dummy-other")
-
-
-# These are what the rich_text_renderer library use for its own tests
-mock_simple_node = {"value": "foo"}
-mock_node = {"content": [{"value": "foo", "nodeType": "text"}]}
-mock_list_node = {"content": [{"content": [{"value": "foo", "nodeType": "text"}], "nodeType": "list-item"}]}
-mock_hyperlink_node = {
- "data": {"uri": "https://example.com"},
- "content": [{"value": "Example", "nodeType": "text", "marks": []}],
-}
-
-
-def test_StrongRenderer():
- assert StrongRenderer().render(mock_simple_node) == "foo"
-
-
-def test_EmphasisRenderer():
- assert EmphasisRenderer().render(mock_simple_node) == "foo"
-
-
-@patch("bedrock.contentful.api.get_current_request")
-def test_LinkRenderer__mozilla_link(mock_get_current_request):
- mock_request = Mock()
- mock_request.page_info = {"utm_campaign": "TEST"}
- mock_get_current_request.return_value = mock_request
- mozilla_mock_hyperlink_node = deepcopy(mock_hyperlink_node)
- # Here we're assuming that we need to set a subdomain, because if we
- # used a naked mozilla.org domain we'd get 30Xed to www.mozilla.org
- mozilla_mock_hyperlink_node["data"]["uri"] = "https://subdomain.mozilla.org/test/page/"
- output = LinkRenderer({"text": TextRenderer}).render(mozilla_mock_hyperlink_node)
- expected = (
- 'Example'
- )
-
- assert output == expected
-
-
-@patch("bedrock.contentful.api.get_current_request")
-def test_LinkRenderer__mozilla_link__existing_utm(mock_get_current_request):
- mock_request = Mock()
- mock_request.page_info = {"utm_campaign": "TEST"}
- mock_get_current_request.return_value = mock_request
- mozilla_mock_hyperlink_node = deepcopy(mock_hyperlink_node)
- mozilla_mock_hyperlink_node["data"]["uri"] = "https://mozilla.org/test/page/?utm_source=UTMTEST"
- output = LinkRenderer({"text": TextRenderer}).render(mozilla_mock_hyperlink_node)
- expected = 'Example'
-
- assert output == expected
-
-
-def test_LinkRenderer__non_mozilla():
- assert (
- LinkRenderer(
- {
- "text": TextRenderer,
- }
- ).render(mock_hyperlink_node)
- == 'Example'
- )
-
-
-def test_UlRenderer():
- assert (
- UlRenderer(
- {
- "text": TextRenderer,
- "list-item": ListItemRenderer,
- }
- ).render(mock_list_node)
- == ""
- )
-
-
-def test_OlRenderer():
- assert (
- OlRenderer(
- {
- "text": TextRenderer,
- "list-item": ListItemRenderer,
- }
- ).render(mock_list_node)
- == "- foo
"
- )
-
-
-@patch("bedrock.contentful.api._only_child")
-def test__LiRenderer(mock__only_child):
- li_renderer = LiRenderer()
-
- li_renderer._render_content = Mock("mocked__render_content")
- li_renderer._render_content.return_value = "rendered_content"
-
- mock__only_child.return_value = True
-
- output = li_renderer.render(mock_node)
- assert output == "rendered_content"
- mock__only_child.assert_called_once_with(mock_node, "text")
-
- mock__only_child.reset_mock()
- mock__only_child.return_value = False
-
- output = li_renderer.render(mock_node)
- assert output == "rendered_content"
- mock__only_child.assert_called_once_with(mock_node, "text")
-
-
-def test_PRenderer():
- assert PRenderer({"text": TextRenderer}).render(mock_node) == "foo
"
-
-
-def test_PRenderer__empty():
- empty_node = deepcopy(mock_node)
- empty_node["content"][0]["value"] = ""
- assert PRenderer({"text": TextRenderer}).render(empty_node) == ""
-
-
-@pytest.mark.parametrize(
- "content_type_label",
- (
- "componentLogo",
- "componentWordmark",
- "componentCtaButton",
- "somethingElse",
- ),
-)
-@patch("bedrock.contentful.api.ContentfulPage.client")
-@patch("bedrock.contentful.api._make_logo")
-@patch("bedrock.contentful.api._make_wordmark")
-@patch("bedrock.contentful.api._make_cta_button")
-def test_InlineEntryRenderer(
- mock_make_cta_button,
- mock_make_wordmark,
- mock_make_logo,
- mock_client,
- content_type_label,
-):
- mock_entry = Mock()
- mock_content_type = Mock()
- mock_content_type.id = content_type_label
- mock_entry.sys = {"content_type": mock_content_type}
- mock_client.entry.return_value = mock_entry
-
- node = {"data": {"target": {"sys": {"id": mock_entry}}}}
-
- output = InlineEntryRenderer().render(node)
-
- if content_type_label == "componentLogo":
- mock_make_logo.assert_called_once_with(mock_entry)
- elif content_type_label == "componentWordmark":
- mock_make_wordmark.assert_called_once_with(mock_entry)
- elif content_type_label == "componentCtaButton":
- mock_make_cta_button.assert_called_once_with(mock_entry)
- else:
- assert output == content_type_label
-
-
-@patch("bedrock.contentful.api._get_image_url")
-@patch("bedrock.contentful.api.ContentfulPage.client")
-def test_AssetBlockRenderer(mock_client, mock__get_image_url):
- mock_asset = Mock()
- mock_asset.title = "test_title.png"
- mock_asset.description = "Test Description"
- mock_client.asset.return_value = mock_asset
-
- node = {"data": {"target": {"sys": {"id": mock_asset}}}}
- mock__get_image_url.side_effect = [
- "https://example.com/image.png",
- "https://example.com/image-hires.png",
- ]
- output = AssetBlockRenderer().render(node)
- expected = '
'
-
- assert mock__get_image_url.call_args_list[0][0] == (mock_asset, 688)
- assert mock__get_image_url.call_args_list[1][0] == (mock_asset, 1376)
-
- assert output == expected
-
-
-def test__render_list():
- assert _render_list("ol", "test content here") == "test content here
"
- assert _render_list("ul", "test content here") == ""
-
-
-@pytest.fixture
-def basic_contentful_page(rf):
- """Naive reusable fixture for setting up a ContentfulPage
- Note that it does NOTHING with set_current_request / thread-locals
- """
- with patch("bedrock.contentful.api.set_current_request"):
- with patch("bedrock.contentful.api.get_locale") as mock_get_locale:
- mock_get_locale.return_value = settings.LANGUAGE_CODE # use the fallback
- mock_request = rf.get("/")
- page = ContentfulPage(mock_request, "test-page-id")
-
- return page
-
-
-@pytest.mark.parametrize("locale_to_patch", ("fr", "de-DE", "es-MX"))
-@patch("bedrock.contentful.api.set_current_request")
-@patch("bedrock.contentful.api.get_locale")
-def test_ContentfulPage__init(
- mock_get_locale,
- mock_set_current_request,
- locale_to_patch,
- rf,
-):
- mock_get_locale.side_effect = lambda x: x.locale
- mock_request = rf.get("/")
- mock_request.locale = locale_to_patch
- page = ContentfulPage(mock_request, "test-page-id")
-
- mock_get_locale.assert_called_once_with(mock_request)
- mock_set_current_request.assert_called_once_with(mock_request)
- assert page.request == mock_request
- assert page.page_id == "test-page-id"
- assert page.locale == locale_to_patch
-
-
-@pytest.mark.parametrize("locale_to_patch", ("fr", "de-DE", "es-MX"))
-def test_ContentfulPage__page_property(basic_contentful_page, locale_to_patch):
- page = basic_contentful_page
- page.locale = locale_to_patch
- page.client = Mock()
- page.client.entry.return_value = "fake page data"
-
- output = page.page
- assert output == "fake page data"
- page.client.entry.assert_called_once_with(
- "test-page-id",
- {
- "include": 10,
- "locale": locale_to_patch,
- },
- )
-
-
-def test_ContentfulPage__render_rich_text(basic_contentful_page):
- # The actual/underlying RichTextRenderer is tested in its own package
- # - this test just checks our usage
-
- basic_contentful_page._renderer.render = Mock()
- basic_contentful_page._renderer.render.return_value = "mock rendered rich text"
- output = basic_contentful_page.render_rich_text(mock_node)
- assert output == "mock rendered rich text"
- basic_contentful_page._renderer.render.assert_called_once_with(mock_node)
-
- basic_contentful_page._renderer.render.reset_mock()
- output = basic_contentful_page.render_rich_text(None)
- assert output == ""
- assert not basic_contentful_page._renderer.render.called
-
- basic_contentful_page._renderer.render.reset_mock()
- output = basic_contentful_page.render_rich_text("")
- assert output == ""
- assert not basic_contentful_page._renderer.render.called
-
-
-def test_ContentfulPage___get_preview_image_from_fields__data_present(basic_contentful_page):
- mock_image = Mock(name="preview_image")
- mock_image.fields.return_value = {
- "file": {
- "url": "//example.com/image.png",
- }
- }
-
- fields = {"preview_image": mock_image}
-
- output = basic_contentful_page._get_preview_image_from_fields(fields)
- assert output == "https://example.com/image.png"
-
-
-def test_ContentfulPage___get_preview_image_from_fields__no_data(
- basic_contentful_page,
-):
- assert basic_contentful_page._get_preview_image_from_fields({}) is None
-
-
-def test_ContentfulPage___get_preview_image_from_fields__bad_data(
- basic_contentful_page,
-):
- mock_image = Mock(name="preview_image")
- mock_image.fields.return_value = {
- # no file key
- }
- fields = {"preview_image": mock_image}
- output = basic_contentful_page._get_preview_image_from_fields(fields)
- assert output is None
-
- mock_image = Mock(name="preview_image")
- mock_image.fields.return_value = {"file": {}} # no url key
- fields = {"preview_image": mock_image}
- output = basic_contentful_page._get_preview_image_from_fields(fields)
- assert output is None
-
-
-@pytest.mark.django_db
-def test_ContentfulPage___get_image_from_default_locale_seo_object__happy_path(
- basic_contentful_page,
-):
- assert DEFAULT_LOCALE == "en-US"
-
- ContentfulEntry.objects.create(
- content_type=CONTENT_TYPE_PAGE_RESOURCE_CENTER,
- contentful_id="test_id",
- locale=DEFAULT_LOCALE,
- data={
- "entries": ["dummy"],
- "info": {
- "seo": {
- "image": "https://example.com/test.webp",
- },
- },
- },
- )
-
- assert basic_contentful_page._get_image_from_default_locale_seo_object("test_id") == "https://example.com/test.webp"
-
-
-@pytest.mark.django_db
-def test_ContentfulPage___get_image_from_default_locale_seo_object__no_match_for_id(
- basic_contentful_page,
-):
- ContentfulEntry.objects.create(
- content_type=CONTENT_TYPE_PAGE_RESOURCE_CENTER,
- contentful_id="test_id",
- locale=DEFAULT_LOCALE,
- data={
- "entries": ["dummy"],
- "info": {
- "seo": {
- "image": "https://example.com/test.webp",
- },
- },
- },
- )
-
- assert basic_contentful_page._get_image_from_default_locale_seo_object("NOT_THE_test_id") == ""
-
-
-@pytest.mark.django_db
-def test_ContentfulPage___get_image_from_default_locale_seo_object__no_relevant_data(
- basic_contentful_page,
-):
- ContentfulEntry.objects.create(
- content_type=CONTENT_TYPE_PAGE_RESOURCE_CENTER,
- contentful_id="test_id",
- locale=DEFAULT_LOCALE,
- data={
- "entries": ["dummy"],
- "info": {
- "bobbins": { # replacing `seo` key
- "image": "https://example.com/test.webp",
- },
- },
- },
- )
-
- assert basic_contentful_page._get_image_from_default_locale_seo_object("test_id") == ""
-
-
-@pytest.mark.parametrize(
- "entry_fields, expected",
- (
- (
- {"folder": "firefox"},
- {"theme": "firefox", "campaign": "firefox-test-test-test"},
- ),
- (
- {"folder": "mentions-firefox-in-title"},
- {"theme": "firefox", "campaign": "firefox-test-test-test"},
- ),
- (
- {"folder": "other-thing"},
- {"theme": "mozilla", "campaign": "test-test-test"},
- ),
- (
- {"folder": ""},
- {"theme": "mozilla", "campaign": "test-test-test"},
- ),
- (
- {},
- {"theme": "mozilla", "campaign": "test-test-test"},
- ),
- ),
-)
-def test_ContentfulPage__get_info_data__theme_campaign(
- basic_contentful_page,
- entry_fields,
- expected,
-):
- slug = "test-test-test"
-
- output = basic_contentful_page._get_info_data__theme_campaign(entry_fields, slug)
-
- assert output == expected
-
-
-@pytest.mark.parametrize(
- "page_type, entry_fields, entry_obj__sys__locale, expected",
- (
- (
- "pageHome",
- {
- "name": "locale temporarily in overridden name field",
- },
- "Not used",
- {"locale": "locale temporarily in overridden name field"},
- ),
- (
- "pagePageResourceCenter",
- {"name": "NOT USED"},
- "fr-CA",
- {"locale": "fr-CA"},
- ),
- ),
-)
-def test_ContentfulPage__get_info_data__locale(
- basic_contentful_page,
- page_type,
- entry_fields,
- entry_obj__sys__locale,
- expected,
-):
- entry_obj = Mock()
- entry_obj.sys = {"locale": entry_obj__sys__locale}
- output = basic_contentful_page._get_info_data__locale(
- page_type,
- entry_fields,
- entry_obj,
- )
- assert output == expected
-
-
-@pytest.mark.parametrize(
- "page_type, legacy_connect_page_fields, entry_fields, seo_fields, expected",
- (
- (
- CONTENT_TYPE_PAGE_RESOURCE_CENTER,
- {}, # Connect-type fields
- {
- "slug": "vrc-main-page-slug",
- "title": "test page one",
- }, # fields from the page itself
- {}, # SEO object's fields
- {
- "slug": "vrc-main-page-slug",
- "title": "test page one",
- "blurb": "",
- },
- ),
- (
- CONTENT_TYPE_PAGE_RESOURCE_CENTER,
- {},
- {
- "slug": "vrc-main-page-slug",
- "title": "",
- },
- {},
- {
- "slug": "vrc-main-page-slug",
- "title": "",
- "blurb": "",
- },
- ),
- (
- CONTENT_TYPE_PAGE_RESOURCE_CENTER,
- {},
- {
- "preview_title": "preview title",
- "preview_blurb": "preview blurb",
- "slug": "vrc-main-page-slug",
- "title": "",
- },
- {},
- {
- "slug": "vrc-main-page-slug",
- "title": "preview title",
- "blurb": "preview blurb",
- },
- ),
- (
- CONTENT_TYPE_PAGE_RESOURCE_CENTER,
- {},
- {
- "preview_title": "preview title",
- "preview_blurb": "preview blurb",
- "slug": "vrc-main-page-slug",
- "title": "",
- },
- {"description": "seo description"},
- {
- "slug": "vrc-main-page-slug",
- "title": "preview title",
- "blurb": "seo description",
- },
- ),
- (
- CONTENT_TYPE_CONNECT_HOMEPAGE,
- {
- "slug": "homepage-slug", # This will be ignored
- },
- {
- "preview_title": "preview title",
- "preview_blurb": "preview blurb",
- },
- {}, # SEO fields not present for non-Compose pages
- {
- "slug": "home", # ie, there is no way to set the slug using Connect:Homepage
- "title": "preview title",
- "blurb": "preview blurb",
- },
- ),
- (
- CONTENT_TYPE_CONNECT_HOMEPAGE,
- {
- # no slug field, so will fall back to default of 'home'
- },
- {
- "preview_title": "preview title",
- "preview_blurb": "preview blurb",
- },
- {}, # SEO fields not present for non-Compose pages
- {
- "slug": "home",
- "title": "preview title",
- "blurb": "preview blurb",
- },
- ),
- ),
- ids=[
- "compose page with slug, title, no blurb",
- "compose page with slug, no title, no blurb",
- "compose page with slug, title from entry, blurb from entry",
- "compose page with slug, no title, blurb from seo",
- "Non-Compose page with title, blurb from entry + PROOF SLUG IS NOT SET",
- "Non-Compose page with default slug, title, blurb from entry",
- ],
-)
-def test_ContentfulPage__get_info_data__slug_title_blurb(
- basic_contentful_page,
- page_type,
- legacy_connect_page_fields,
- entry_fields,
- seo_fields,
- expected,
-):
- basic_contentful_page.page = Mock()
- basic_contentful_page.page.content_type.id = page_type
- basic_contentful_page.page.fields = Mock(return_value=legacy_connect_page_fields)
-
- assert (
- basic_contentful_page._get_info_data__slug_title_blurb(
- entry_fields,
- seo_fields,
- )
- == expected
- )
-
-
-@pytest.mark.parametrize(
- "entry_fields, page_type, expected",
- (
- (
- {
- "category": "test category",
- "tags": [
- "test tag1",
- "test tag2",
- ],
- "product": "test product",
- },
- CONTENT_TYPE_PAGE_RESOURCE_CENTER,
- {
- "category": "test category",
- "tags": [
- "test tag1",
- "test tag2",
- ],
- "classification": "test product",
- },
- ),
- (
- {
- "category": "test category",
- "tags": [
- "test tag1",
- "test tag2",
- ],
- "product": "test product",
- },
- "NOT A CONTENT_TYPE_PAGE_RESOURCE_CENTER",
- {}, # no data expected if it's not a VRC page
- ),
- ),
-)
-def test_ContentfulPage__get_info_data__category_tags_classification(
- basic_contentful_page,
- entry_fields,
- page_type,
- expected,
-):
- assert basic_contentful_page._get_info_data__category_tags_classification(entry_fields, page_type) == expected
-
-
-@pytest.mark.parametrize(
- "entry_obj__fields, seo_obj__fields, expected",
- (
- (
- {
- "dummy": "entry fields",
- "preview_image": "https://example.com/test-entry.png",
- },
- {
- "dummy": "seo fields",
- "preview_image": "https://example.com/test-seo.png",
- "description": "Test SEO description comes through",
- },
- {
- "title": "test title",
- "blurb": "test blurb",
- "slug": "test-slug",
- "locale": "fr-CA",
- "theme": "test-theme",
- "utm_source": "www.mozilla.org-test-campaign",
- "utm_campaign": "test-campaign",
- "image": "https://example.com/test-entry.png",
- "category": "test category",
- "tags": [
- "test tag1",
- "test tag2",
- ],
- "product": "test product",
- "seo": {
- "dummy": "seo fields",
- "image": "https://example.com/test-seo.png",
- "description": "Test SEO description comes through",
- },
- },
- ),
- (
- {
- "dummy": "entry fields",
- "preview_image": "https://example.com/test-entry.png",
- },
- None, # no SEO fields
- {
- "title": "test title",
- "blurb": "test blurb",
- "slug": "test-slug",
- "locale": "fr-CA",
- "theme": "test-theme",
- "utm_source": "www.mozilla.org-test-campaign",
- "utm_campaign": "test-campaign",
- "image": "https://example.com/test-entry.png",
- "category": "test category",
- "tags": [
- "test tag1",
- "test tag2",
- ],
- "product": "test product",
- },
- ),
- ),
- ids=["entry_obj and seo_obj", "entry_obj, no seo_obj"],
-)
-@patch("bedrock.contentful.api.ContentfulPage._get_preview_image_from_fields")
-@patch("bedrock.contentful.api.ContentfulPage._get_info_data__locale")
-@patch("bedrock.contentful.api.ContentfulPage._get_info_data__theme_campaign")
-@patch("bedrock.contentful.api.ContentfulPage._get_info_data__slug_title_blurb")
-@patch("bedrock.contentful.api.ContentfulPage._get_info_data__category_tags_classification")
-def test_ContentfulPage__get_info_data(
- mock__get_info_data__category_tags_classification,
- mock__get_info_data__slug_title_blurb,
- mock__get_info_data__theme_campaign,
- mock__get_info_data__locale,
- mock__get_preview_image_from_fields,
- basic_contentful_page,
- entry_obj__fields,
- seo_obj__fields,
- expected,
-):
- mock__get_preview_image_from_fields.side_effect = [
- "https://example.com/test-entry.png",
- "https://example.com/test-seo.png",
- ]
- mock__get_info_data__theme_campaign.return_value = {
- "theme": "test-theme",
- "campaign": "test-campaign",
- }
- mock__get_info_data__locale.return_value = {
- "locale": "fr-CA",
- }
- mock__get_info_data__slug_title_blurb.return_value = {
- "slug": "test-slug",
- "title": "test title",
- "blurb": "test blurb",
- }
- mock__get_info_data__category_tags_classification.return_value = {
- "category": "test category",
- "tags": [
- "test tag1",
- "test tag2",
- ],
- "product": "test product",
- }
-
- mock_entry_obj = Mock()
- mock_entry_obj.fields.return_value = entry_obj__fields
- mock_entry_obj.content_type.id = "mock-page-type"
-
- if seo_obj__fields:
- mock_seo_obj = Mock()
- mock_seo_obj.fields.return_value = seo_obj__fields
- else:
- mock_seo_obj = None
-
- output = basic_contentful_page.get_info_data(mock_entry_obj, mock_seo_obj)
-
- assert output == expected
-
- if seo_obj__fields:
- assert mock__get_preview_image_from_fields.call_count == 2
- assert (
- call(
- {
- "dummy": "entry fields",
- "preview_image": "https://example.com/test-entry.png",
- }
- )
- in mock__get_preview_image_from_fields.call_args_list
- )
- assert (
- call(
- {
- "dummy": "seo fields",
- "preview_image": "https://example.com/test-seo.png",
- "description": "Test SEO description comes through",
- },
- )
- in mock__get_preview_image_from_fields.call_args_list
- )
- else:
- assert mock__get_preview_image_from_fields.call_count == 1
- mock__get_preview_image_from_fields.assert_called_once_with(entry_obj__fields)
-
- mock__get_info_data__category_tags_classification.assert_called_once_with(
- entry_obj__fields,
- "mock-page-type",
- )
- mock__get_info_data__slug_title_blurb.assert_called_once_with(
- entry_obj__fields,
- seo_obj__fields,
- )
- mock__get_info_data__theme_campaign.assert_called_once_with(
- entry_obj__fields,
- "test-slug",
- )
- mock__get_info_data__locale.assert_called_once_with(
- "mock-page-type",
- entry_obj__fields,
- mock_entry_obj,
- )
-
-
-@patch("bedrock.contentful.api._get_image_url")
-def test_ContentfulPage__get_split_data(mock__get_image_url, basic_contentful_page):
- # mock self and entry data
- basic_contentful_page.page = Mock()
- basic_contentful_page.page.content_type.id = "mockPage"
- basic_contentful_page.render_rich_text = Mock()
- mock_entry_obj = Mock()
- # only set required and default fields
- mock_entry_obj.fields.return_value = {
- "name": "Split Test",
- "image": "Stub image",
- "body": "Stub body",
- "mobile_media_after": False,
- }
- mock_entry_obj.content_type.id = "mock-split-type"
-
- output = basic_contentful_page.get_split_data(mock_entry_obj)
-
- def is_empty_string(string):
- return len(string.strip()) == 0
-
- assert output["component"] == "split"
- assert is_empty_string(output["block_class"])
- assert is_empty_string(output["theme_class"])
- assert is_empty_string(output["body_class"])
- basic_contentful_page.render_rich_text.assert_called_once()
- assert is_empty_string(output["media_class"])
- assert output["media_after"] is False
- mock__get_image_url.assert_called_once()
- assert is_empty_string(output["mobile_class"])
-
-
-@pytest.mark.parametrize(
- "split_class_fields, expected",
- (
- (None, ""),
- ({"image_side": "Right"}, ""),
- ({"image_side": "Left"}, "mzp-l-split-reversed"),
- ({"body_width": "Even"}, ""),
- ({"body_width": "Narrow"}, "mzp-l-split-body-narrow"),
- ({"body_width": "Wide"}, "mzp-l-split-body-wide"),
- ({"image_pop": "None"}, ""),
- ({"image_pop": "Both"}, "mzp-l-split-pop"),
- ({"image_pop": "Top"}, "mzp-l-split-pop-top"),
- ({"image_pop": "Bottom"}, "mzp-l-split-pop-bottom"),
- ({"image_side": "Left", "body_width": "Narrow", "image_pop": "Both"}, "mzp-l-split-reversed mzp-l-split-body-narrow mzp-l-split-pop"),
- ),
-)
-@patch("bedrock.contentful.api._get_image_url")
-def test_ContentfulPage__get_split_data__get_split_class(
- mock__get_image_url,
- basic_contentful_page,
- split_class_fields,
- expected,
-):
- # mock self and entry data
- basic_contentful_page.page = Mock()
- basic_contentful_page.page.content_type.id = "mockPage"
- basic_contentful_page.render_rich_text = Mock()
- mock_entry_obj = Mock()
- mock_entry_obj.fields.return_value = {
- "name": "Split Test",
- "image": "Stub image",
- "body": "Stub body",
- "mobile_media_after": False,
- }
- if split_class_fields:
- mock_entry_obj.fields.return_value.update(split_class_fields)
-
- mock_entry_obj.content_type.id = "mock-split-type"
-
- output = basic_contentful_page.get_split_data(mock_entry_obj)
-
- assert output["block_class"].strip() == expected
-
-
-@pytest.mark.parametrize(
- "page_id, body_class_fields, expected",
- (
- ("mockPage", None, ""),
- ("pageHome", None, "c-home-body"),
- ("mockPage", {"body_vertical_alignment": "Top"}, "mzp-l-split-v-start"),
- ("mockPage", {"body_vertical_alignment": "Center"}, "mzp-l-split-v-center"),
- ("mockPage", {"body_vertical_alignment": "Bottom"}, "mzp-l-split-v-end"),
- ("mockPage", {"body_horizontal_alignment": "Left"}, "mzp-l-split-h-start"),
- ("mockPage", {"body_horizontal_alignment": "Center"}, "mzp-l-split-h-center"),
- ("mockPage", {"body_horizontal_alignment": "Right"}, "mzp-l-split-h-end"),
- (
- "pageHome",
- {"body_vertical_alignment": "Top", "body_horizontal_alignment": "Center"},
- "mzp-l-split-v-start mzp-l-split-h-center c-home-body",
- ),
- ),
-)
-@patch("bedrock.contentful.api._get_image_url")
-def test_ContentfulPage__get_split_data__get_body_class(
- mock__get_image_url,
- basic_contentful_page,
- page_id,
- body_class_fields,
- expected,
-):
- # mock self and entry data
- basic_contentful_page.page = Mock()
- basic_contentful_page.page.content_type.id = page_id
- basic_contentful_page.render_rich_text = Mock()
- mock_entry_obj = Mock()
- mock_entry_obj.fields.return_value = {
- "name": "Split Test",
- "image": "Stub image",
- "body": "Stub body",
- "mobile_media_after": False,
- }
- if body_class_fields:
- mock_entry_obj.fields.return_value.update(body_class_fields)
-
- mock_entry_obj.content_type.id = "mock-split-type"
-
- output = basic_contentful_page.get_split_data(mock_entry_obj)
-
- assert output["body_class"].strip() == expected
-
-
-@pytest.mark.parametrize(
- "media_class_fields, expected",
- (
- (None, ""),
- ({"image_width": "Fill available width"}, ""),
- ({"image_width": "Fill available height"}, "mzp-l-split-media-constrain-height"),
- ({"image_width": "Overflow container"}, "mzp-l-split-media-overflow"),
- ),
-)
-@patch("bedrock.contentful.api._get_image_url")
-def test_ContentfulPage__get_split_data__get_media_class(mock__get_image_url, basic_contentful_page, media_class_fields, expected):
- # mock self and entry data
- basic_contentful_page.page = Mock()
- basic_contentful_page.page.content_type.id = "mockPage"
- basic_contentful_page.render_rich_text = Mock()
- mock_entry_obj = Mock()
- mock_entry_obj.fields.return_value = {
- "name": "Split Test",
- "image": "Stub image",
- "body": "Stub body",
- "mobile_media_after": False,
- }
- if media_class_fields:
- mock_entry_obj.fields.return_value.update(media_class_fields)
-
- mock_entry_obj.content_type.id = "mock-split-type"
-
- output = basic_contentful_page.get_split_data(mock_entry_obj)
-
- assert output["media_class"].strip() == expected
-
-
-@pytest.mark.parametrize(
- "mobile_class_fields, expected",
- (
- (None, ""),
- ({"mobile_display": "Center content"}, "mzp-l-split-center-on-sm-md"),
- ({"mobile_display": "Hide image"}, "mzp-l-split-hide-media-on-sm-md"),
- ),
-)
-@patch("bedrock.contentful.api._get_image_url")
-def test_ContentfulPage__get_split_data__get_mobile_class(mock__get_image_url, basic_contentful_page, mobile_class_fields, expected):
- # mock self and entry data
- basic_contentful_page.page = Mock()
- basic_contentful_page.page.content_type.id = "mockPage"
- basic_contentful_page.render_rich_text = Mock()
- mock_entry_obj = Mock()
- mock_entry_obj.fields.return_value = {
- "name": "Split Test",
- "image": "Stub image",
- "body": "Stub body",
- "mobile_media_after": False,
- }
- if mobile_class_fields:
- mock_entry_obj.fields.return_value.update(mobile_class_fields)
-
- mock_entry_obj.content_type.id = "mock-split-type"
-
- output = basic_contentful_page.get_split_data(mock_entry_obj)
-
- assert output["mobile_class"].strip() == expected
-
-
-# FURTHER TESTS TO COME
-# def test_ContentfulPage__get_content():
-# assert False, "WRITE ME"
-
-
-# def test_ContentfulPage__get_content__proc():
-# assert False, "WRITE ME"
-
-
-# def test_ContentfulPage__get_text_data():
-# assert False, "WRITE ME"
-
-
-# def test_ContentfulPage__get_section_data():
-# assert False, "WRITE ME"
-
-
-# def test_ContentfulPage__get_callout_data():
-# assert False, "WRITE ME"
-
-
-# def test_ContentfulPage__get_card_data():
-# assert False, "WRITE ME"
-
-
-# def test_ContentfulPage__get_large_card_data():
-# assert False, "WRITE ME"
-
-
-# def test_ContentfulPage__get_card_layout_data():
-# assert False, "WRITE ME"
-
-
-# def test_ContentfulPage__get_picto_data():
-# assert False, "WRITE ME"
-
-
-# def test_ContentfulPage__get_picto_layout_data():
-# assert False, "WRITE ME"
-
-
-# def test_ContentfulPage__get_text_column_data():
-# assert False, "WRITE ME"
diff --git a/bedrock/contentful/tests/test_contentful_commands.py b/bedrock/contentful/tests/test_contentful_commands.py
deleted file mode 100644
index 3de4e02aec0..00000000000
--- a/bedrock/contentful/tests/test_contentful_commands.py
+++ /dev/null
@@ -1,860 +0,0 @@
-# This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at https://mozilla.org/MPL/2.0/.
-
-from unittest import mock
-
-from django.conf import settings
-from django.test import override_settings
-
-import pytest
-
-from bedrock.contentful.constants import (
- ACTION_ARCHIVE,
- ACTION_AUTO_SAVE,
- ACTION_CREATE,
- ACTION_DELETE,
- ACTION_PUBLISH,
- ACTION_SAVE,
- ACTION_UNARCHIVE,
- ACTION_UNPUBLISH,
- CONTENT_TYPE_CONNECT_HOMEPAGE,
- CONTENT_TYPE_PAGE_RESOURCE_CENTER,
-)
-from bedrock.contentful.management.commands.update_contentful import (
- MAX_MESSAGES_PER_QUEUE_POLL,
- Command as UpdateContentfulCommand,
-)
-from bedrock.contentful.models import ContentfulEntry
-from bedrock.contentful.tests.data import resource_center_page_data
-
-
-@pytest.fixture
-def command_instance():
- command = UpdateContentfulCommand()
- command.quiet = False
- command.log = mock.Mock(name="log")
- return command
-
-
-@pytest.mark.parametrize(
- "space_id, space_key, run_expected",
- (
- ("", "a_key", False),
- ("an_id", "", False),
- ("an_id", "a_key", True),
- ),
-)
-def test_handle__no_contentful_configuration_results_in_pass_but_no_exception(
- space_id,
- space_key,
- run_expected,
- command_instance,
-):
- # If Contentful is not set up, we should just return gracefully, not blow up
- with mock.patch("builtins.print") as mock_print:
- with override_settings(
- CONTENTFUL_SPACE_ID=space_id,
- CONTENTFUL_SPACE_KEY=space_key,
- ):
- command_instance.refresh = mock.Mock(
- name="mock_refresh",
- return_value=(True, 0, 0, 0, 0),
- )
- command_instance.handle(quiet=True, force=False)
-
- if run_expected:
- command_instance.refresh.assert_called_once()
- else:
- command_instance.refresh.assert_not_called()
- mock_print.assert_called_once_with("Contentful credentials not configured")
-
-
-@override_settings(CONTENTFUL_SPACE_ID="space_id", CONTENTFUL_SPACE_KEY="space_key")
-def test_handle__message_logging__forced__and__successful(command_instance):
- command_instance.refresh = mock.Mock(
- name="mock_refresh",
- return_value=(True, 1, 2, 3, 4),
- )
- command_instance.handle(quiet=False, force=True)
- assert command_instance.log.call_count == 2
- command_instance.log.call_args_list[0][0] == ("Running forced update from Contentful data",)
- assert command_instance.log.call_args_list[1][0] == ("Done. Added: 1. Updated: 2. Deleted: 3. Errors: 4",)
-
-
-@override_settings(CONTENTFUL_SPACE_ID="space_id", CONTENTFUL_SPACE_KEY="space_key")
-def test_handle__message_logging__not_forced__and__nothing_changed(command_instance):
- command_instance.refresh = mock.Mock(
- name="mock_refresh",
- return_value=(False, 0, 0, 0, 0),
- )
- command_instance.handle(quiet=False, force=True)
- assert command_instance.log.call_count == 2
- command_instance.log.call_args_list[0][0] == ("Checking for updated Contentful data",)
- assert command_instance.log.call_args_list[1][0] == ("Nothing to pull from Contentful",)
-
-
-@pytest.mark.parametrize(
- "param,expected",
- (
- (
- "ContentManagement.Entry.create,123abcdef123abcdef,abcdefabcdefabcdef",
- ACTION_CREATE,
- ),
- (
- "ContentManagement.Entry.publish,123abcdef123abcdef,abcdefabcdefabcdef",
- ACTION_PUBLISH,
- ),
- (
- "ContentManagement.Entry.unpublish,123abcdef123abcdef,abcdefabcdefabcdef",
- ACTION_UNPUBLISH,
- ),
- (
- "ContentManagement.Entry.archive,123abcdef123abcdef,abcdefabcdefabcdef",
- ACTION_ARCHIVE,
- ),
- (
- "ContentManagement.Entry.unarchive,123abcdef123abcdef,abcdefabcdefabcdef",
- ACTION_UNARCHIVE,
- ),
- (
- "ContentManagement.Entry.save,123abcdef123abcdef,abcdefabcdefabcdef",
- ACTION_SAVE,
- ),
- (
- "ContentManagement.Entry.auto_save,123abcdef123abcdef,abcdefabcdefabcdef",
- ACTION_AUTO_SAVE,
- ),
- (
- "ContentManagement.Entry.delete,123abcdef123abcdef,abcdefabcdefabcdef",
- ACTION_DELETE,
- ),
- (
- "ContentManagement.Entry,123abcdef123abcdef,abcdefabcdefabcdef",
- None,
- ),
- (
- "ContentManagement,123abcdef123abcdef,abcdefabcdefabcdef",
- None,
- ),
- (
- "abcdefabcdefabcdef",
- None,
- ),
- (
- "",
- None,
- ),
- (
- None,
- None,
- ),
- ),
-)
-@mock.patch("bedrock.contentful.management.commands.update_contentful.capture_exception")
-def test_update_contentful__get_message_action(
- mock_capture_exception,
- param,
- expected,
- command_instance,
-):
- assert command_instance._get_message_action(param) == expected
-
- if expected is None:
- assert mock_capture_exception.call_count == 1
-
-
-def test_update_contentful__purge_queue(command_instance):
- mock_queue = mock.Mock(name="mock-queue")
- command_instance._purge_queue(mock_queue)
- assert mock_queue.purge.call_count == 1
-
-
-def _build_mock_messages(actions: list) -> list[list]:
- messages = []
- for i, action in enumerate(actions):
- msg = mock.Mock(name=f"msg-{i}-{action}")
- msg.body = f"ContentManagement.Entry.{action},123abc,123abc"
- messages.append(msg)
-
- # Split the messages into sublists, if need be
- batched_messages = []
- for idx in range(0, len(messages), MAX_MESSAGES_PER_QUEUE_POLL):
- offset = idx + MAX_MESSAGES_PER_QUEUE_POLL
- batched_messages.append(messages[idx:offset])
- return batched_messages
-
-
-def _establish_mock_queue(batched_messages: list[list]) -> tuple[mock.Mock, mock.Mock]:
- mock_queue = mock.Mock(name="mock_queue")
-
- def _receive_messages(*args, **kwargs):
- # Doing it this way to avoid side_effect raising StopIteration when the batch is exhausted
- if batched_messages:
- return batched_messages.pop(0)
- return []
-
- mock_queue.receive_messages = _receive_messages
-
- mock_sqs = mock.Mock(name="mock_sqs")
- mock_sqs.Queue.return_value = mock_queue
- return (mock_sqs, mock_queue)
-
-
-@override_settings(
- CONTENTFUL_NOTIFICATION_QUEUE_ACCESS_KEY_ID="dummy",
- APP_NAME="bedrock-dev",
-)
-@mock.patch("bedrock.contentful.management.commands.update_contentful.boto3")
-@pytest.mark.parametrize(
- "message_actions_sequence",
- (
- # In Dev mode, all Contentful actions apart from `create` are ones which
- # should trigger polling the queue
- [ACTION_AUTO_SAVE],
- [ACTION_SAVE],
- [ACTION_ARCHIVE],
- [ACTION_UNARCHIVE],
- [ACTION_PUBLISH],
- [ACTION_UNPUBLISH],
- [ACTION_DELETE],
- [ACTION_DELETE, ACTION_AUTO_SAVE, ACTION_PUBLISH],
- ["dummy_for_test", ACTION_AUTO_SAVE, "dummy_for_test"],
- ["dummy_for_test", "dummy_for_test", ACTION_PUBLISH],
- [ACTION_PUBLISH, "dummy_for_test", "dummy_for_test"],
- ),
- ids=[
- "single auto-save message",
- "single save message",
- "single archive message",
- "single unarchive message",
- "single publish message",
- "single unpublish message",
- "single delete message",
- "multiple messages, all are triggers",
- "multiple messages with fake extra non-trigger states, go-signal is in middle",
- "multiple messages with fake extra non-trigger states, go-signal is last",
- "multiple messages with fake extra non-trigger states, go-signal is first",
- ],
-)
-def test_update_contentful__queue_has_viable_messages__viable_message_found__dev_mode(
- mock_boto_3,
- message_actions_sequence,
- command_instance,
-):
- messages_for_queue = _build_mock_messages(message_actions_sequence)
- mock_sqs, mock_queue = _establish_mock_queue(messages_for_queue)
-
- mock_boto_3.resource.return_value = mock_sqs
-
- assert command_instance._queue_has_viable_messages() is True
- mock_queue.purge.assert_called_once()
-
-
-@override_settings(
- CONTENTFUL_NOTIFICATION_QUEUE_ACCESS_KEY_ID="dummy",
- APP_NAME="bedrock-prod",
-)
-@mock.patch("bedrock.contentful.management.commands.update_contentful.boto3")
-@pytest.mark.parametrize(
- "message_actions_sequence",
- (
- # One message in the queue
- [ACTION_PUBLISH],
- [ACTION_UNPUBLISH],
- [ACTION_ARCHIVE],
- [ACTION_UNARCHIVE],
- [ACTION_DELETE],
- # Multiple messages in the queue
- [ACTION_AUTO_SAVE, ACTION_DELETE, ACTION_AUTO_SAVE],
- [ACTION_SAVE, ACTION_AUTO_SAVE, ACTION_PUBLISH],
- [ACTION_ARCHIVE, ACTION_AUTO_SAVE, ACTION_AUTO_SAVE],
- [ACTION_ARCHIVE, ACTION_PUBLISH, ACTION_DELETE, ACTION_UNPUBLISH, ACTION_UNARCHIVE],
- ),
- ids=[
- "single publish message",
- "single unpublish message",
- "single archive message",
- "single unarchive message",
- "single delete message",
- "multiple messages, go-signal is in middle",
- "multiple messages, go-signal is last",
- "multiple messages, go-signal is first",
- "multiple messages, all are go-signals",
- ],
-)
-def test_update_contentful__queue_has_viable_messages__viable_message_found__prod_mode(
- mock_boto_3,
- message_actions_sequence,
- command_instance,
-):
- messages_for_queue = _build_mock_messages(message_actions_sequence)
- mock_sqs, mock_queue = _establish_mock_queue(messages_for_queue)
-
- mock_boto_3.resource.return_value = mock_sqs
-
- assert command_instance._queue_has_viable_messages() is True
- mock_queue.purge.assert_called_once()
-
-
-@override_settings(
- CONTENTFUL_NOTIFICATION_QUEUE_ACCESS_KEY_ID="dummy",
- APP_NAME="bedrock-dev",
-)
-@mock.patch("bedrock.contentful.management.commands.update_contentful.boto3")
-@pytest.mark.parametrize(
- "message_actions_sequence",
- (
- # One message in the queue
- [ACTION_CREATE],
- # Multiple messages in the queue
- [ACTION_CREATE, ACTION_CREATE, ACTION_CREATE],
- ),
- ids=[
- "single create message",
- "multiple create messages",
- ],
-)
-def test_update_contentful__queue_has_viable_messages__no_viable_message_found__dev_mode(
- mock_boto_3,
- message_actions_sequence,
- command_instance,
-):
- # Create is the only message that will not trigger a Contentful poll in Dev
- assert settings.APP_NAME == "bedrock-dev"
- messages_for_queue = _build_mock_messages(message_actions_sequence)
- mock_sqs, mock_queue = _establish_mock_queue(messages_for_queue)
-
- mock_boto_3.resource.return_value = mock_sqs
-
- assert command_instance._queue_has_viable_messages() is False
- mock_queue.purge.assert_not_called()
-
-
-@override_settings(
- CONTENTFUL_NOTIFICATION_QUEUE_ACCESS_KEY_ID="dummy",
- APP_NAME="bedrock-prod",
-)
-@mock.patch("bedrock.contentful.management.commands.update_contentful.boto3")
-@pytest.mark.parametrize(
- "message_actions_sequence",
- (
- # One message in the queue
- [ACTION_SAVE],
- [ACTION_AUTO_SAVE],
- [ACTION_CREATE],
- # Multiple messages in the queue
- [ACTION_AUTO_SAVE, ACTION_SAVE, ACTION_CREATE],
- ),
- ids=[
- "single save message",
- "single auto-save message",
- "single create message",
- "multiple messages",
- ],
-)
-def test_update_contentful__queue_has_viable_messages__no_viable_message_found__prod_mode(
- mock_boto_3,
- message_actions_sequence,
- command_instance,
-):
- # In prod mode we don't want creation or draft editing to trigger a
- # re-poll of the API because it's unnecessary.
- assert settings.APP_NAME == "bedrock-prod"
- messages_for_queue = _build_mock_messages(message_actions_sequence)
- mock_sqs, mock_queue = _establish_mock_queue(messages_for_queue)
-
- mock_boto_3.resource.return_value = mock_sqs
-
- assert command_instance._queue_has_viable_messages() is False
- mock_queue.purge.assert_not_called()
-
-
-@pytest.mark.parametrize("unconfigured_value", ("", None))
-def test_queue_has_viable_messages__no_sqs_configured(
- unconfigured_value,
- command_instance,
-):
- # If SQS is not set up, we should just poll as if --force was used
- with override_settings(
- CONTENTFUL_NOTIFICATION_QUEUE_ACCESS_KEY_ID=unconfigured_value,
- ):
- assert settings.CONTENTFUL_NOTIFICATION_QUEUE_ACCESS_KEY_ID == unconfigured_value
- assert command_instance._queue_has_viable_messages() is True
-
-
-@override_settings(
- CONTENTFUL_NOTIFICATION_QUEUE_ACCESS_KEY_ID="dummy",
- APP_NAME="bedrock-dev",
-)
-@mock.patch("bedrock.contentful.management.commands.update_contentful.boto3")
-@pytest.mark.parametrize(
- "message_actions_sequence",
- (
- [ACTION_CREATE] * 10 + [ACTION_PUBLISH],
- [ACTION_CREATE] * 9 + [ACTION_PUBLISH],
- [ACTION_CREATE] * 8 + [ACTION_PUBLISH],
- [ACTION_CREATE] * 56 + [ACTION_PUBLISH],
- ),
- ids=[
- "Eleven messages",
- "Ten messages",
- "Nine messages",
- "57 messages",
- ],
-)
-def test_update_contentful__iteration_through_message_batch_thresholds(
- mock_boto_3,
- message_actions_sequence,
- command_instance,
-):
- # ie, show we handle less and more than 10 messages in the queue.
- # Only the last message in each test case is viable, because
- # ACTION_CREATE should NOT trigger anything in Dev, Stage or Prod
- messages_for_queue = _build_mock_messages(message_actions_sequence)
- mock_sqs, mock_queue = _establish_mock_queue(messages_for_queue)
-
- mock_boto_3.resource.return_value = mock_sqs
-
- assert command_instance._queue_has_viable_messages() is True
- mock_queue.purge.assert_called_once()
-
-
-@override_settings(
- CONTENTFUL_NOTIFICATION_QUEUE_ACCESS_KEY_ID="dummy",
- APP_NAME="bedrock-dev",
-)
-@mock.patch("bedrock.contentful.management.commands.update_contentful.boto3")
-def test_update_contentful__queue_has_viable_messages__no_messages(
- mock_boto_3,
- command_instance,
-):
- messages_for_queue = _build_mock_messages([])
- mock_sqs, mock_queue = _establish_mock_queue(messages_for_queue)
-
- mock_boto_3.resource.return_value = mock_sqs
-
- assert command_instance._queue_has_viable_messages() is False
- mock_queue.purge.assert_not_called()
-
-
-@pytest.mark.parametrize(
- "must_force,has_viable_messages,expected",
- (
- (False, False, (False, -1, -1, -1, -1)),
- (True, False, (True, 3, 2, 1, 0)),
- (False, True, (True, 3, 2, 1, 0)),
- ),
- ids=(
- "Not forced, no viable messages in queue",
- "Forced, no viable messages in queue",
- "Not forced, queue has viable messages",
- ),
-)
-def test_update_contentful__refresh(
- must_force,
- has_viable_messages,
- expected,
- command_instance,
-):
- command_instance._queue_has_viable_messages = mock.Mock(
- name="_queue_has_viable_messages",
- return_value=has_viable_messages,
- )
-
- command_instance._refresh_from_contentful = mock.Mock(
- name="_refresh_from_contentful",
- return_value=(
- 3, # 'added'
- 2, # 'updated'
- 1, # 'deleted'
- 0, # 'errors'
- ),
- )
-
- command_instance.force = must_force
-
- retval = command_instance.refresh()
- assert retval == expected
-
-
-def _build_mock_entries(mock_entry_data: list[dict]) -> list[mock.Mock]:
- output = []
- for datum_dict in mock_entry_data:
- mock_entry = mock.Mock()
- for attr, val in datum_dict.items():
- setattr(mock_entry, attr, val)
-
- output.append(mock_entry)
- return output
-
-
-@override_settings(CONTENTFUL_CONTENT_TYPES_TO_SYNC=["type_one", "type_two"])
-@mock.patch("bedrock.contentful.management.commands.update_contentful.ContentfulPage")
-def test_update_contentful__get_content_to_sync(
- mock_contentful_page,
- command_instance,
-):
- mock_en_us_locale = mock.Mock()
- mock_en_us_locale.code = "en-US"
- mock_de_locale = mock.Mock()
- mock_de_locale.code = "de"
-
- available_locales = [
- mock_en_us_locale,
- mock_de_locale,
- ]
-
- _first_batch = _build_mock_entries(
- [
- {"sys": {"id": "one"}},
- {"sys": {"id": "two"}},
- {"sys": {"id": "three"}},
- {"sys": {"id": "four"}},
- ],
- )
-
- _second_batch = _build_mock_entries(
- [
- {"sys": {"id": "1"}},
- {"sys": {"id": "2"}},
- {"sys": {"id": "3"}},
- {"sys": {"id": "4"}},
- ],
- )
-
- _third_batch = _build_mock_entries(
- # These will not be used/requested
- [
- {"sys": {"id": "X"}},
- {"sys": {"id": "Y"}},
- {"sys": {"id": "Z"}},
- ],
- )
-
- mock_retval_1 = mock.Mock()
- mock_retval_1.items = _first_batch
- mock_retval_2 = mock.Mock()
- mock_retval_2.items = _second_batch
- mock_retval_3 = mock.Mock()
- mock_retval_3.items = _first_batch
- mock_retval_4 = mock.Mock()
- mock_retval_4.items = _second_batch
- mock_retval_5 = mock.Mock()
- mock_retval_5.items = _third_batch
-
- mock_contentful_page.client.entries.side_effect = [
- mock_retval_1,
- mock_retval_2,
- mock_retval_3,
- mock_retval_4,
- mock_retval_5, # will not be called for
- ]
-
- output = command_instance._get_content_to_sync(available_locales)
-
- assert output == [
- ("type_one", "one", "en-US"),
- ("type_one", "two", "en-US"),
- ("type_one", "three", "en-US"),
- ("type_one", "four", "en-US"),
- ("type_two", "1", "en-US"),
- ("type_two", "2", "en-US"),
- ("type_two", "3", "en-US"),
- ("type_two", "4", "en-US"),
- ("type_one", "one", "de"),
- ("type_one", "two", "de"),
- ("type_one", "three", "de"),
- ("type_one", "four", "de"),
- ("type_two", "1", "de"),
- ("type_two", "2", "de"),
- ("type_two", "3", "de"),
- ("type_two", "4", "de"),
- # and deliberately nothing from the third batch
- ]
-
- assert mock_contentful_page.client.entries.call_count == 4
-
- assert mock_contentful_page.client.entries.call_args_list[0][0] == (
- {
- "content_type": "type_one",
- "include": 0,
- "locale": "en-US",
- },
- )
- assert mock_contentful_page.client.entries.call_args_list[1][0] == (
- {
- "content_type": "type_two",
- "include": 0,
- "locale": "en-US",
- },
- )
- assert mock_contentful_page.client.entries.call_args_list[2][0] == (
- {
- "content_type": "type_one",
- "include": 0,
- "locale": "de",
- },
- )
- assert mock_contentful_page.client.entries.call_args_list[3][0] == (
- {
- "content_type": "type_two",
- "include": 0,
- "locale": "de",
- },
- )
-
-
-@pytest.mark.django_db
-@pytest.mark.parametrize(
- "total_to_create_per_locale, locales_to_use, entries_processed_in_sync, expected_deletion_count",
- (
- (
- 3,
- ["en-US"],
- [
- (CONTENT_TYPE_PAGE_RESOURCE_CENTER, "entry_1", "en-US"),
- (CONTENT_TYPE_PAGE_RESOURCE_CENTER, "entry_2", "en-US"),
- (CONTENT_TYPE_PAGE_RESOURCE_CENTER, "entry_3", "en-US"),
- ],
- 0,
- ),
- (
- 3,
- ["en-US"],
- [
- (CONTENT_TYPE_PAGE_RESOURCE_CENTER, "entry_1", "en-US"),
- (CONTENT_TYPE_PAGE_RESOURCE_CENTER, "entry_2", "en-US"),
- ],
- 1,
- ),
- (
- 5,
- ["en-US"],
- [
- (CONTENT_TYPE_PAGE_RESOURCE_CENTER, "entry_2", "en-US"),
- (CONTENT_TYPE_PAGE_RESOURCE_CENTER, "entry_3", "en-US"),
- (CONTENT_TYPE_PAGE_RESOURCE_CENTER, "entry_4", "en-US"),
- ],
- 2,
- ),
- (
- 3,
- ["en-US"],
- [],
- 3,
- ),
- (
- 3,
- ["en-US", "de", "fr", "it"],
- [
- (CONTENT_TYPE_PAGE_RESOURCE_CENTER, "entry_1", "en-US"),
- # (CONTENT_TYPE_PAGE_RESOURCE_CENTER, "entry_1", "de"), # simulating deletion/absence from sync
- (CONTENT_TYPE_PAGE_RESOURCE_CENTER, "entry_1", "fr"),
- # (CONTENT_TYPE_PAGE_RESOURCE_CENTER, "entry_1", "it"),
- # (CONTENT_TYPE_PAGE_RESOURCE_CENTER, "entry_2", "en-US"),
- (CONTENT_TYPE_PAGE_RESOURCE_CENTER, "entry_2", "de"),
- (CONTENT_TYPE_PAGE_RESOURCE_CENTER, "entry_2", "fr"),
- (CONTENT_TYPE_PAGE_RESOURCE_CENTER, "entry_2", "it"),
- (CONTENT_TYPE_PAGE_RESOURCE_CENTER, "entry_3", "en-US"),
- # (CONTENT_TYPE_PAGE_RESOURCE_CENTER, "entry_3", "de"),
- # (CONTENT_TYPE_PAGE_RESOURCE_CENTER, "entry_3", "fr"),
- # (CONTENT_TYPE_PAGE_RESOURCE_CENTER, "entry_3", "it"),
- ],
- 6,
- ),
- ),
- ids=[
- "All ids attempted, so none deleted",
- "First two ids attempted, so one deleted",
- "Middle three of five ids attempted,, so two deleted",
- "No ids attempted, so all deleted",
- "Pages remain but some locales zapped, reducing entries",
- ],
-)
-def test_update_contentful__detect_and_delete_absent_entries(
- total_to_create_per_locale,
- locales_to_use,
- entries_processed_in_sync,
- expected_deletion_count,
- command_instance,
-):
- for locale in locales_to_use:
- for idx in range(total_to_create_per_locale):
- ContentfulEntry.objects.create(
- content_type=CONTENT_TYPE_PAGE_RESOURCE_CENTER,
- contentful_id=f"entry_{idx + 1}",
- locale=locale,
- )
-
- retval = command_instance._detect_and_delete_absent_entries(entries_processed_in_sync)
- assert retval == expected_deletion_count
-
-
-@pytest.mark.django_db
-def test_update_contentful__detect_and_delete_absent_entries__homepage_involved(command_instance):
- # Make two homepages, with en-US locales (because that's how it rolls for now)
- ContentfulEntry.objects.create(
- content_type=CONTENT_TYPE_CONNECT_HOMEPAGE,
- contentful_id="home_1",
- locale="en-US",
- )
- ContentfulEntry.objects.create(
- content_type=CONTENT_TYPE_CONNECT_HOMEPAGE,
- contentful_id="home_2",
- locale="en-US",
- )
-
- # Make some other pages
- for locale in ["en-US", "fr", "it"]:
- for idx in range(3):
- ContentfulEntry.objects.create(
- content_type=CONTENT_TYPE_PAGE_RESOURCE_CENTER,
- contentful_id=f"entry_{idx + 1}",
- locale=locale,
- )
-
- # Let's pretend the second homepage and some others have been deleted
- entries_processed_in_sync = [
- (CONTENT_TYPE_CONNECT_HOMEPAGE, "home_1", "en-US"),
- # (CONTENT_TYPE_CONNECT_HOMEPAGE, "home_2", "en-US"),
- (CONTENT_TYPE_PAGE_RESOURCE_CENTER, "entry_1", "en-US"),
- (CONTENT_TYPE_PAGE_RESOURCE_CENTER, "entry_1", "fr"),
- (CONTENT_TYPE_PAGE_RESOURCE_CENTER, "entry_1", "it"),
- # (CONTENT_TYPE_PAGE_RESOURCE_CENTER, "entry_2", "en-US"),
- (CONTENT_TYPE_PAGE_RESOURCE_CENTER, "entry_2", "fr"),
- # (CONTENT_TYPE_PAGE_RESOURCE_CENTER, "entry_2", "it"),
- (CONTENT_TYPE_PAGE_RESOURCE_CENTER, "entry_3", "en-US"),
- # (CONTENT_TYPE_PAGE_RESOURCE_CENTER, "entry_3", "fr"),
- (CONTENT_TYPE_PAGE_RESOURCE_CENTER, "entry_3", "it"),
- ]
- retval = command_instance._detect_and_delete_absent_entries(entries_processed_in_sync)
- assert retval == 4
-
- for ctype, contentful_id, locale in [
- (CONTENT_TYPE_CONNECT_HOMEPAGE, "home_1", "en-US"),
- (CONTENT_TYPE_PAGE_RESOURCE_CENTER, "entry_1", "en-US"),
- (CONTENT_TYPE_PAGE_RESOURCE_CENTER, "entry_1", "fr"),
- (CONTENT_TYPE_PAGE_RESOURCE_CENTER, "entry_1", "it"),
- (CONTENT_TYPE_PAGE_RESOURCE_CENTER, "entry_2", "fr"),
- (CONTENT_TYPE_PAGE_RESOURCE_CENTER, "entry_3", "en-US"),
- (CONTENT_TYPE_PAGE_RESOURCE_CENTER, "entry_3", "it"),
- ]:
- assert ContentfulEntry.objects.get(
- content_type=ctype,
- contentful_id=contentful_id,
- locale=locale,
- )
-
- for ctype, contentful_id, locale in [
- (CONTENT_TYPE_CONNECT_HOMEPAGE, "home_2", "en-US"),
- (CONTENT_TYPE_PAGE_RESOURCE_CENTER, "entry_2", "en-US"),
- (CONTENT_TYPE_PAGE_RESOURCE_CENTER, "entry_2", "it"),
- (CONTENT_TYPE_PAGE_RESOURCE_CENTER, "entry_3", "fr"),
- ]:
- assert not ContentfulEntry.objects.filter(
- content_type=ctype,
- contentful_id=contentful_id,
- locale=locale,
- ).exists()
-
-
-def test_log():
- command_instance = UpdateContentfulCommand()
- command_instance.quiet = False
- with mock.patch("builtins.print") as mock_print:
- command_instance.log("This SHALL be printed")
- mock_print.assert_called_once_with("This SHALL be printed")
-
- mock_print.reset_mock()
-
- command_instance.quiet = True
- with mock.patch("builtins.print") as mock_print:
- command_instance.log("This shall not be printed")
- assert not mock_print.called
-
-
-_dummy_completeness_spec = {
- "test-content-type": [
- # just three of anything, as it's not acted upon in this test
- {
- "type": list,
- "key": "fake1",
- },
- {
- "type": dict,
- "key": "fake2",
- },
- {
- "type": list,
- "key": "fake3",
- },
- ]
-}
-
-
-@pytest.mark.django_db
-@pytest.mark.parametrize(
- "mock_localised_values, expected_completion_flag",
- (
- (["one", "two", "three"], True),
- (["", "", ""], False),
- (["one", None, ""], False),
- ),
-)
-def test_update_contentful__check_localisation_complete(
- mock_localised_values,
- expected_completion_flag,
- command_instance,
-):
- entry = ContentfulEntry.objects.create(
- content_type=CONTENT_TYPE_PAGE_RESOURCE_CENTER,
- contentful_id="test_1",
- locale="en-US",
- )
- assert entry.localisation_complete is False
-
- command_instance._get_value_from_data = mock.Mock(side_effect=mock_localised_values)
- command_instance._check_localisation_complete()
-
- entry.refresh_from_db()
- assert entry.localisation_complete == expected_completion_flag
-
-
-@pytest.mark.parametrize(
- "spec, expected_string",
- (
- (".entries[].body", "Virtual private networks (VPNs) and secure web proxies are solutions "),
- (
- ".info.seo.description",
- "VPNs and proxies are solutions for online privacy and security. Here’s how these protect you and how to choose the best option.",
- ),
- (
- ".info.seo.image",
- "https://images.ctfassets.net/w5er3c7zdgmd/7o6QXGC6BXMq3aB5hu4mmn/d0dc31407051937f56a0a46767e11f6f/vpn-16x9-phoneglobe.png",
- ),
- ),
-)
-def test_update_contentful__get_value_from_data(spec, expected_string, command_instance):
- assert expected_string in command_instance._get_value_from_data(
- resource_center_page_data,
- spec,
- )
-
-
-@pytest.mark.parametrize(
- "jq_all_mocked_output, expected",
- (
- ([" ", "", None], ""),
- ([" Hello, World! ", "test", None], "Hello, World! test"),
- ),
-)
-@mock.patch("bedrock.contentful.management.commands.update_contentful.jq.all")
-def test__get_value_from_data__no_false_positives(
- mocked_jq_all,
- jq_all_mocked_output,
- expected,
- command_instance,
-):
- mocked_jq_all.return_value = jq_all_mocked_output
- assert command_instance._get_value_from_data(data=None, spec=None) == expected
diff --git a/bedrock/contentful/tests/test_contentful_models.py b/bedrock/contentful/tests/test_contentful_models.py
deleted file mode 100644
index 0ba7f49ba0d..00000000000
--- a/bedrock/contentful/tests/test_contentful_models.py
+++ /dev/null
@@ -1,522 +0,0 @@
-# This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at https://mozilla.org/MPL/2.0/.
-
-from unittest.mock import Mock, patch
-
-import pytest
-
-from bedrock.contentful.constants import CONTENT_TYPE_CONNECT_HOMEPAGE
-from bedrock.contentful.models import ContentfulEntry
-
-pytestmark = pytest.mark.django_db
-
-
-@pytest.fixture
-def dummy_homepages():
- ContentfulEntry.objects.create(
- contentful_id="home000001",
- content_type=CONTENT_TYPE_CONNECT_HOMEPAGE,
- classification="",
- data={"dummy": "Homepage FR"},
- locale="fr",
- slug="home",
- category="",
- tags=[],
- )
- ContentfulEntry.objects.create(
- contentful_id="home000002",
- content_type=CONTENT_TYPE_CONNECT_HOMEPAGE,
- classification="",
- data={"dummy": "Homepage DE"},
- locale="de",
- slug="home",
- category="",
- tags=[],
- )
-
-
-@pytest.fixture
-def dummy_entries():
- ContentfulEntry.objects.create(
- contentful_id="abcdef000001",
- content_type="test_type_1",
- classification="classification_1",
- data={"dummy": "Type 1, Classification 1, category one, tags 1-3"},
- locale="en-US",
- localisation_complete=True,
- slug="test-one",
- category="category one",
- tags=["tag 1", "tag 2", "tag 3"],
- )
- ContentfulEntry.objects.create(
- contentful_id="abcdef000002",
- content_type="test_type_1",
- classification="classification_2", # nb, different from above
- data={"dummy": "Type 1, Classification 2, category one, tag 10 only"},
- locale="en-US",
- localisation_complete=True,
- slug="test-two",
- category="category one", # nb, same as above
- tags=["tag 10"],
- )
- ContentfulEntry.objects.create(
- contentful_id="abcdef000003",
- content_type="test_type_2",
- classification="classification_1",
- data={"dummy": "Type 2, Classification 1, category one, tags 1-3"},
- locale="en-US",
- localisation_complete=True,
- slug="test-three",
- category="category one", # nb, same as above
- tags=["tag 1", "tag 2", "tag 3"],
- )
- ContentfulEntry.objects.create(
- # identical to above, save for the classification
- contentful_id="abcdef000004",
- content_type="test_type_2",
- classification="classification_2",
- data={"dummy": "Type 2, Classification 2, category one, tags 2 and 3"},
- locale="en-US",
- localisation_complete=True,
- slug="test-four",
- category="category one", # nb, same as above
- tags=[
- "tag 2",
- ],
- )
- ContentfulEntry.objects.create(
- contentful_id="abcdef000005",
- content_type="test_type_1",
- classification="classification_2",
- data={"dummy": "Type 1, Classification 2, category two, tags 2 and 10 only"},
- locale="en-US",
- localisation_complete=True,
- slug="test-five",
- category="category two", # nb, same as above
- tags=["tag 10", "tag 2"],
- )
- ContentfulEntry.objects.create(
- contentful_id="abcdef000006",
- content_type="test_type_1",
- classification="classification_2",
- data={"dummy": "Type 1, Classification 2, category three, tags 2 and 7 only - French locale"},
- locale="fr",
- localisation_complete=True,
- slug="test-six",
- category="category three", # nb, same as above
- tags=["tag 2", "tag 7"],
- )
- ContentfulEntry.objects.create(
- contentful_id="abcdef000007",
- content_type="test_type_1",
- classification="",
- data={"dummy": "Type 1, no classification, category three, tags 2 and 7 only - French locale"},
- locale="de",
- localisation_complete=True,
- slug="test-seven",
- category="category three", # nb, same as above
- tags=["tag 2", "tag 7"],
- )
-
-
-@pytest.mark.parametrize(
- "id,expected",
- (
- (
- "abcdef000001",
- {"dummy": "Type 1, Classification 1, category one, tags 1-3"},
- ),
- (
- "abcdef000002",
- {"dummy": "Type 1, Classification 2, category one, tag 10 only"},
- ),
- (
- "abcdef000005",
- {"dummy": "Type 1, Classification 2, category two, tags 2 and 10 only"},
- ),
- ),
-)
-def test_contentfulentrymanager__get_page_by_id(id, expected, dummy_entries):
- assert ContentfulEntry.objects.get_page_by_id(id) == expected
-
-
-@pytest.mark.parametrize(
- "slug,locale,content_type,classification,expected_contentful_id",
- (
- (
- "test-one",
- "en-US",
- "test_type_1",
- "classification_1",
- "abcdef000001",
- ),
- (
- "test-two",
- "en-US",
- "test_type_1",
- "classification_2",
- "abcdef000002",
- ),
- (
- "test-four",
- "en-US",
- "test_type_2",
- "classification_2",
- "abcdef000004",
- ),
- (
- "test-six",
- "fr",
- "test_type_1",
- "classification_2",
- "abcdef000006",
- ),
- (
- "test-seven",
- "de",
- "test_type_1",
- None,
- "abcdef000007",
- ),
- ),
-)
-def test_contentfulentrymanager__get_entry_by_slug__happy_paths(
- slug,
- locale,
- content_type,
- classification,
- expected_contentful_id,
- dummy_entries,
-):
- assert (
- ContentfulEntry.objects.get_entry_by_slug(
- slug=slug,
- locale=locale,
- content_type=content_type,
- classification=classification,
- ).contentful_id
- == expected_contentful_id
- )
-
-
-@pytest.mark.parametrize(
- "slug, locale, content_type, classification",
- (
- (
- "test-one",
- "fr",
- "test_type_1",
- "classification_1",
- ),
- (
- "test-one",
- "en-US",
- "test_type_2",
- "classification_1",
- ),
- (
- "test-one",
- "en-US",
- "test_type_1",
- "classification_3",
- ),
- (
- "test-one-bad-slug",
- "en-US",
- "test_type_1",
- "classification_1",
- ),
- ),
- ids=[
- "locale mismatch",
- "content_type mismatch",
- "classification mismatch",
- "slug mismatch",
- ],
-)
-def test_contentfulentrymanager__get_entry_by_slug__unhappy_paths(
- slug,
- locale,
- content_type,
- classification,
-):
- try:
- ContentfulEntry.objects.get_entry_by_slug(
- slug=slug,
- locale=locale,
- content_type=content_type,
- classification=classification,
- )
- assert False, "Should not have found a ContentfulEntry"
- except ContentfulEntry.DoesNotExist:
- pass
-
-
-@patch("bedrock.contentful.models.ContentfulEntry.objects.get_entry_by_slug")
-def test_contentfulentrymanager__get_page_by_slug(mock_get_entry_by_slug):
- mock_retval = Mock(name="mock_retval")
- mock_retval.data = {"mocked": "data"}
- mock_get_entry_by_slug.return_value = mock_retval
-
- res = ContentfulEntry.objects.get_page_by_slug(
- slug="test_slug",
- locale="test_locale",
- content_type="test_content_type",
- classification="test_classification",
- )
- assert res == {"mocked": "data"}
- mock_get_entry_by_slug.assert_called_once_with(
- slug="test_slug",
- locale="test_locale",
- content_type="test_content_type",
- classification="test_classification",
- localisation_complete=True,
- )
-
-
-def test_contentfulentrymanager__get_active_locales_for_slug():
- for i in range(3):
- for locale in [
- "de",
- "ja",
- "it",
- ]:
- ContentfulEntry.objects.create(
- contentful_id=f"{locale}-locale00{i}",
- content_type="test_type_1",
- classification="",
- data={"dummy": "dummy"},
- locale=locale,
- localisation_complete=True,
- slug=f"locale00{i}",
- )
-
- for i in range(3):
- for locale in [
- "pt",
- "zh-CN",
- ]:
- ContentfulEntry.objects.create(
- contentful_id=f"{locale}-locale00{i}",
- content_type="test_type_1",
- classification="test-classification",
- data={"dummy": "dummy"},
- locale=locale,
- localisation_complete=True,
- slug=f"locale00{i}",
- )
-
- for locale in [
- "en-US",
- "fr",
- "es-ES",
- ]:
- ContentfulEntry.objects.create(
- contentful_id=f"{locale}-locale00{i}",
- content_type="test_type_1",
- classification="",
- data={"dummy": "dummy"},
- locale=locale,
- localisation_complete=False,
- slug=f"locale00{i}",
- )
-
- # no match for slug
- assert (
- ContentfulEntry.objects.get_active_locales_for_slug(
- slug="non-matching-slug",
- content_type="test_type_1",
- )
- == []
- )
-
- # no match for content type
- assert (
- ContentfulEntry.objects.get_active_locales_for_slug(
- slug="locale001",
- content_type="test_type_DOES_NOT_EXIST",
- )
- == []
- )
-
- # match for slug and content type, but only on those with localisation complete
- assert ContentfulEntry.objects.get_active_locales_for_slug(
- slug="locale001",
- content_type="test_type_1",
- ) == ["de", "it", "ja", "pt", "zh-CN"]
-
- # match for slug and content type and classifcation,
- # but only on those with localisation complete
- assert ContentfulEntry.objects.get_active_locales_for_slug(
- slug="locale001",
- content_type="test_type_1",
- classification="test-classification",
- ) == ["pt", "zh-CN"]
-
-
-@pytest.mark.parametrize(
- "content_type, locale, classification, expected_ids",
- (
- (
- "test_type_1",
- "en-US",
- "classification_1",
- ["abcdef000001"],
- ),
- (
- "test_type_1",
- "en-US",
- "classification_2",
- ["abcdef000002", "abcdef000005"],
- ),
- (
- "test_type_1",
- "fr",
- "classification_2",
- ["abcdef000006"],
- ),
- (
- "test_type_1",
- "en-US",
- None,
- ["abcdef000001", "abcdef000002", "abcdef000005"],
- ),
- (
- "test_type_2",
- "en-US",
- None,
- ["abcdef000003", "abcdef000004"],
- ),
- (
- "test_type_1",
- "es",
- "classification_1",
- [],
- ),
- ),
-)
-def test_contentfulentrymanager__get_entries_by_type(
- locale,
- content_type,
- classification,
- expected_ids,
- dummy_entries,
-):
- res = ContentfulEntry.objects.get_entries_by_type(
- locale=locale,
- content_type=content_type,
- classification=classification,
- )
- assert [x.contentful_id for x in res] == expected_ids
-
-
-def test_contentfulentrymanager__get_entries_by_type__ordering(dummy_entries):
- kwargs = dict(content_type="test_type_1", locale="en-US", classification=None)
-
- # default ordering: last_modified
- res_1 = [x.contentful_id for x in ContentfulEntry.objects.get_entries_by_type(**kwargs)]
- assert res_1 == ["abcdef000001", "abcdef000002", "abcdef000005"]
-
- res_2 = [x.contentful_id for x in ContentfulEntry.objects.get_entries_by_type(order_by="-last_modified", **kwargs)]
- assert res_2 == ["abcdef000005", "abcdef000002", "abcdef000001"]
- assert [x for x in reversed(res_1)] == res_2
-
-
-@pytest.mark.parametrize(
- "locale, expected_data",
- (
- ("fr", {"dummy": "Homepage FR"}),
- ("de", {"dummy": "Homepage DE"}),
- ),
-)
-def test_contentfulentrymanager__get_homepage(locale, expected_data, dummy_homepages):
- assert ContentfulEntry.objects.get_homepage(locale) == expected_data
-
-
-@pytest.mark.parametrize(
- "source_id, expected_related_ids",
- (
- (
- "abcdef000001",
- [
- "abcdef000003", # tag 1
- "abcdef000004", # tag 2
- "abcdef000005", # tag 2
- ],
- ),
- (
- "abcdef000005",
- [
- "abcdef000001", # tag 2
- "abcdef000002", # tag 10
- "abcdef000003", # tag 2
- "abcdef000004", # tag 2
- # "abcdef000006", # tag 2, but NOT same locale
- # "abcdef000007", # tag 2, but NOT same locale
- ],
- ),
- (
- "abcdef000006",
- [
- # "abcdef000007", # tag 2, and tag 7, but NOT same locale
- ],
- ),
- ),
-)
-def test_contentfulentry__get_related_entries(
- source_id,
- expected_related_ids,
- dummy_entries,
-):
- # update all the classifications and content_types to be the same, for the sake of this test
- ContentfulEntry.objects.all().update(
- classification="relations-test",
- content_type="relation-test-type",
- )
-
- entry = ContentfulEntry.objects.get(contentful_id=source_id)
- assert [x.contentful_id for x in entry.get_related_entries()] == expected_related_ids
-
-
-@pytest.mark.parametrize(
- "source_id, expected_related_ids",
- (
- (
- "abcdef000001",
- [],
- ),
- (
- "abcdef000005",
- ["abcdef000002"],
- ),
- (
- "abcdef000006",
- [],
- ),
- ),
-)
-def test_contentfulentry__get_related_entries__show_content_type_and_classification_matter(
- source_id,
- expected_related_ids,
- dummy_entries,
-):
- "Similar to above, but without standardising the content_type or classification"
-
- entry = ContentfulEntry.objects.get(contentful_id=source_id)
- assert [x.contentful_id for x in entry.get_related_entries()] == expected_related_ids
-
-
-def test_contentfulentry__get_related_entries__no_tags(dummy_entries):
- entry = ContentfulEntry.objects.get(contentful_id="abcdef000005")
-
- assert entry.tags and len(entry.get_related_entries()) != 0 # initially related items are shown
-
- entry.tags = {}
- entry.save()
- assert not entry.tags and len(entry.get_related_entries()) == 0 # Because not tags, no related items
-
-
-def test_contentfulentry__str__method(dummy_entries):
- entry = ContentfulEntry.objects.get(contentful_id="abcdef000005")
- assert f"{entry}" == "ContentfulEntry test_type_1:abcdef000005[en-US]"
diff --git a/bedrock/contentful/tests/test_contentful_utils.py b/bedrock/contentful/tests/test_contentful_utils.py
deleted file mode 100644
index e6e5b7e2616..00000000000
--- a/bedrock/contentful/tests/test_contentful_utils.py
+++ /dev/null
@@ -1,83 +0,0 @@
-# This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at https://mozilla.org/MPL/2.0/.
-
-from django.test import override_settings
-
-import pytest
-
-from bedrock.contentful.constants import CONTENT_TYPE_PAGE_RESOURCE_CENTER
-from bedrock.contentful.models import ContentfulEntry
-from bedrock.contentful.utils import locales_with_available_content
-
-
-@pytest.mark.django_db
-@pytest.mark.parametrize(
- "creation_params, threshold_setting, function_params, expected",
- (
- (
- [
- [3, "en-US", "classification_one", CONTENT_TYPE_PAGE_RESOURCE_CENTER],
- [1, "fr", "classification_one", CONTENT_TYPE_PAGE_RESOURCE_CENTER],
- [2, "de", "classification_one", CONTENT_TYPE_PAGE_RESOURCE_CENTER],
- [4, "jp", "classification_one", CONTENT_TYPE_PAGE_RESOURCE_CENTER],
- [1, "it", "classification_one", CONTENT_TYPE_PAGE_RESOURCE_CENTER],
- [3, "ru", "classification_one", CONTENT_TYPE_PAGE_RESOURCE_CENTER],
- ],
- float(66),
- [CONTENT_TYPE_PAGE_RESOURCE_CENTER, "classification_one"],
- ["de", "en-US", "jp", "ru"], # sorted
- ),
- (
- [],
- float(66),
- [CONTENT_TYPE_PAGE_RESOURCE_CENTER, "classification_one"],
- [], # sorted
- ),
- (
- [
- [3, "en-US", "classification_one", CONTENT_TYPE_PAGE_RESOURCE_CENTER],
- [3, "fr", "classification_one", CONTENT_TYPE_PAGE_RESOURCE_CENTER],
- [3, "de", "classification_one", CONTENT_TYPE_PAGE_RESOURCE_CENTER],
- [4, "jp", "classification_one", CONTENT_TYPE_PAGE_RESOURCE_CENTER],
- [3, "it", "classification_one", CONTENT_TYPE_PAGE_RESOURCE_CENTER],
- [3, "ru", "classification_one", CONTENT_TYPE_PAGE_RESOURCE_CENTER],
- ],
- float(66),
- [CONTENT_TYPE_PAGE_RESOURCE_CENTER, "classification_one"],
- ["de", "en-US", "fr", "it", "jp", "ru"], # sorted
- ),
- ),
- ids=[
- "Various locales, some active, some not",
- "no locales",
- "all locales active",
- ],
-)
-def test_locales_with_available_content(
- creation_params,
- threshold_setting,
- function_params,
- expected,
-):
- for count, locale, classification, content_type in creation_params:
- for i in range(count):
- ContentfulEntry.objects.create(
- content_type=content_type,
- contentful_id=f"entry_{i + 1}",
- classification=classification,
- locale=locale,
- localisation_complete=True,
- )
- # Add some with incomplete localisation as control
- ContentfulEntry.objects.create(
- content_type=content_type,
- contentful_id=f"entry_{i + 100}",
- classification=classification,
- locale=locale,
- localisation_complete=False,
- )
-
- with override_settings(CONTENTFUL_LOCALE_SUFFICIENT_CONTENT_PERCENTAGE=threshold_setting):
- active_locales = locales_with_available_content(*function_params)
- assert active_locales == expected
diff --git a/bedrock/contentful/tests/test_templatetags_helpers.py b/bedrock/contentful/tests/test_templatetags_helpers.py
deleted file mode 100644
index b951363e875..00000000000
--- a/bedrock/contentful/tests/test_templatetags_helpers.py
+++ /dev/null
@@ -1,106 +0,0 @@
-# This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at https://mozilla.org/MPL/2.0/.
-
-import pytest
-from markupsafe import Markup
-
-from bedrock.contentful.templatetags.helpers import (
- ALLOWED_ATTRS,
- ALLOWED_TAGS,
- _allowed_attrs,
- external_html,
-)
-
-
-@pytest.mark.parametrize(
- "attr, allowed",
- (
- ("alt", True),
- ("class", True),
- ("href", True),
- ("id", True),
- ("src", True),
- ("srcset", True),
- ("rel", True),
- ("title", True),
- ("data-foo", True),
- ("data-bar", True),
- ("custom", False),
- ),
-)
-def test__allowed_attrs(attr, allowed):
- assert _allowed_attrs(None, attr, None) == allowed
-
-
-@pytest.mark.parametrize(
- "content,expected",
- (
- (
- '',
- '<script>alert("boo!")</script>',
- ),
- (
- 'test',
- 'test',
- ),
- ),
- ids=[
- "disallowed tag, so is escaped",
- "allowed tag but disallowed attr",
- ],
-)
-def test_external_html(content, expected):
- # light test of our bleaching
- output = external_html(content)
-
- assert isinstance(content, str)
- assert output == Markup(expected)
-
-
-def test_allowed_attrs_const__remains_what_we_expect():
- # Regression safety net so that any amendment to ALLOWED_ATTRS is 100% deliberate
-
- assert ALLOWED_ATTRS == [
- "alt",
- "class",
- "href",
- "id",
- "src",
- "srcset",
- "rel",
- "title",
- ]
-
-
-def test_allowed_tags_const__remains_what_we_expect():
- # Regression safety net so that any amendment to ALLOWED_TAGS is 100% deliberate
-
- assert ALLOWED_TAGS == {
- "a",
- "abbr",
- "acronym",
- "b",
- "blockquote",
- "button",
- "code",
- "div",
- "em",
- "h1",
- "h2",
- "h3",
- "h4",
- "h5",
- "h6",
- "hr",
- "i",
- "img",
- "li",
- "ol",
- "p",
- "small",
- "span",
- "strike",
- "strong",
- "ul",
- }
diff --git a/bedrock/contentful/utils.py b/bedrock/contentful/utils.py
deleted file mode 100644
index 80bbca37514..00000000000
--- a/bedrock/contentful/utils.py
+++ /dev/null
@@ -1,61 +0,0 @@
-# This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at https://mozilla.org/MPL/2.0/.
-
-from collections import defaultdict
-
-from django.conf import settings
-
-from bedrock.contentful.models import ContentfulEntry
-
-
-# TODO? Cache this
-def locales_with_available_content(
- content_type: str,
- classification: str,
- default_locale: str = "en-US",
-) -> list[str]:
- """
- Returns a list of locale names for which we have 'enough' content in that
- locale to merit showing a listing page.
-
- Why? We don't want a resource center with only one article available in it;
- better to wait a few days till translations for the rest of the content
- lands in Bedrock.
-
- Note that this is not about working how much of a _single_ Entry has been
- localised - it's focused on the number of localised Entries in each
- non-default locale compared to the number of entries in the default locale.
- """
- threshold_pc = settings.CONTENTFUL_LOCALE_SUFFICIENT_CONTENT_PERCENTAGE
-
- kwargs = dict(
- content_type=content_type,
- classification=classification,
- localisation_complete=True,
- )
- # Get all the locales _with at least one complete localisation each_
- all_locales = ContentfulEntry.objects.filter(**kwargs).values_list("locale", flat=True)
-
- # Build a Counter-like lookup table that simply tracks the total number of viable
- # (i.e. complete, localised) articles in each locale
- locale_counts = defaultdict(int)
- for locale in all_locales:
- locale_counts[locale] += 1
-
- default_locale_count = locale_counts.pop(default_locale, 0)
- active_locales = set()
-
- if default_locale_count > 0:
- active_locales.add(default_locale)
- else:
- # no default locale count, and also not possible to say the other
- # locales are more active relative to it, because of division by zero
- return []
-
- for locale, count in locale_counts.items():
- relative_pc = (count / default_locale_count) * 100
- if relative_pc >= threshold_pc:
- active_locales.add(locale)
-
- return sorted(active_locales)
diff --git a/bedrock/firefox/templates/firefox/contentful-all.html b/bedrock/firefox/templates/firefox/contentful-all.html
deleted file mode 100644
index a4edda1ac93..00000000000
--- a/bedrock/firefox/templates/firefox/contentful-all.html
+++ /dev/null
@@ -1,45 +0,0 @@
-{#
- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at https://mozilla.org/MPL/2.0/.
-#}
-
-{% extends "firefox/base/base-protocol.html" %}
-
-{# analytics #}
-{% set _utm_source = 'mozilla.org-firefox-' + info.slug %}
-{% set _utm_campaign = 'features' %}
-{% set referrals = '?utm_source=www.mozilla.org&utm_medium=referral&utm_campaign=' + _utm_campaign %}
-
-{% set show_send_to_device = LANG in settings.SEND_TO_DEVICE_LOCALES %}
-{% set android_url = play_store_url('firefox', 'mobile-page') %}
-{% set ios_url = app_store_url('firefox', 'mobile-page') %}
-
-{# meta data #}
-{% block page_title %}{{ info.title }}{% endblock %}
-{% block page_desc %}{{ info.blurb }}{% endblock %}
-{% if info.image != '' %}
-{% block page_image %}{{ info.image }}{% endblock %}
-{% endif %}
-{% block page_og_title %}{{ info.title }}{% endblock %}
-{% block page_og_desc %}{{ info.blurb }}{% endblock %}
-
-{% block page_css %}
- {% include 'includes/contentful/css.html' %}
-{% endblock %}
-
-{% block body_class %}mzp-t-firefox wmo-contentful{% endblock %}
-
-
-{% block content %}
-
-
- {% include 'includes/contentful/all.html' %}
-
-
-{% endblock %}
-
-
-{% block js %}
- {% include 'includes/contentful/js.html' %}
-{% endblock %}
diff --git a/bedrock/firefox/urls.py b/bedrock/firefox/urls.py
index 59b4350accc..72ffef68768 100644
--- a/bedrock/firefox/urls.py
+++ b/bedrock/firefox/urls.py
@@ -1,7 +1,6 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
-from django.conf import settings
from django.urls import path, re_path
import bedrock.releasenotes.views
@@ -271,7 +270,3 @@
active_locales=["de", "fr", "en-US", "en-CA", "en-GB"],
),
)
-
-# Contentful
-if settings.DEV:
- urlpatterns += (path("firefox/more//", views.FirefoxContentful.as_view()),)
diff --git a/bedrock/firefox/views.py b/bedrock/firefox/views.py
index b6d6ebf664f..cb749b12a47 100644
--- a/bedrock/firefox/views.py
+++ b/bedrock/firefox/views.py
@@ -21,7 +21,6 @@
from bedrock.base.templatetags.helpers import urlparams
from bedrock.base.urlresolvers import reverse
from bedrock.base.waffle import switch
-from bedrock.contentful.api import ContentfulPage
from bedrock.firefox.firefox_details import (
firefox_android,
firefox_desktop,
@@ -974,20 +973,3 @@ def get_template_names(self):
template_name = "firefox/features/pdf-editor.html"
return [template_name]
-
-
-class FirefoxContentful(L10nTemplateView):
- def get_context_data(self, **kwargs):
- ctx = super().get_context_data(**kwargs)
- content_id = ctx["content_id"]
- locale = l10n_utils.get_locale(self.request)
- page = ContentfulPage(content_id, locale)
- content = page.get_content()
- self.request.page_info = content["info"]
- ctx.update(content)
- return ctx
-
- def render_to_response(self, context, **response_kwargs):
- template = "firefox/contentful-all.html"
-
- return l10n_utils.render(self.request, template, context, **response_kwargs)
diff --git a/bedrock/mozorg/dev_urls.py b/bedrock/mozorg/dev_urls.py
deleted file mode 100644
index 8922b897f69..00000000000
--- a/bedrock/mozorg/dev_urls.py
+++ /dev/null
@@ -1,16 +0,0 @@
-# This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at https://mozilla.org/MPL/2.0/.
-
-# URLS only available with settings.DEV is enabled
-from django.urls import path
-
-from bedrock.mozorg import views
-
-urlpatterns = (
- path(
- "contentful-preview//",
- views.ContentfulPreviewView.as_view(),
- name="contentful.preview",
- ),
-)
diff --git a/bedrock/mozorg/templates/mozorg/contentful-all.html b/bedrock/mozorg/templates/mozorg/contentful-all.html
deleted file mode 100644
index c79f0931253..00000000000
--- a/bedrock/mozorg/templates/mozorg/contentful-all.html
+++ /dev/null
@@ -1,45 +0,0 @@
-{#
- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at https://mozilla.org/MPL/2.0/.
-#}
-
-{% extends "base-protocol-mozilla.html" %}
-
-{# analytics #}
-{% set _utm_source = info.utm_source %}
-{% set _utm_campaign = info.utm_campaign %}
-{% set referrals = '?utm_source=www.mozilla.org&utm_medium=referral&utm_campaign=' + _utm_campaign %}
-
-{% set show_send_to_device = LANG in settings.SEND_TO_DEVICE_LOCALES %}
-{% set android_url = play_store_url('firefox', _utm_campaign) %}
-{% set ios_url = app_store_url('firefox', _utm_campaign) %}
-
-{# meta data #}
-{% block page_title %}{{ info.title }}{% endblock %}
-{% block page_desc %}{{ info.blurb }}{% endblock %}
-{% if info.image != '' %}
-{% block page_image %}{{ info.image }}{% endblock %}
-{% endif %}
-{% block page_og_title %}{{ info.title }}{% endblock %}
-{% block page_og_desc %}{{ info.blurb }}{% endblock %}
-
-{% block page_css %}
- {% include 'includes/contentful/css.html' %}
-{% endblock %}
-
-{% block body_class %}wmo-contentful{% endblock %}
-
-
-{% block content %}
-
-
- {% include 'includes/contentful/all.html' %}
-
-
-{% endblock %}
-
-
-{% block js %}
- {% include 'includes/contentful/js.html' %}
-{% endblock %}
diff --git a/bedrock/mozorg/tests/contentful_test_urlconf.py b/bedrock/mozorg/tests/contentful_test_urlconf.py
deleted file mode 100644
index 1981995062d..00000000000
--- a/bedrock/mozorg/tests/contentful_test_urlconf.py
+++ /dev/null
@@ -1,20 +0,0 @@
-# This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at https://mozilla.org/MPL/2.0/.
-
-# A test-friendly version of dev_urls that includes them as i18n_pattern-ed URLs
-# like they would be in production
-
-# URLS only available with settings.DEV is enabled
-from django.urls import path
-
-from bedrock.base.i18n import bedrock_i18n_patterns
-from bedrock.mozorg import views
-
-urlpatterns = bedrock_i18n_patterns(
- path(
- "contentful-preview//",
- views.ContentfulPreviewView.as_view(),
- name="contentful.preview",
- ),
-)
diff --git a/bedrock/mozorg/tests/test_views.py b/bedrock/mozorg/tests/test_views.py
index 71331430cd1..1c7240ee34e 100644
--- a/bedrock/mozorg/tests/test_views.py
+++ b/bedrock/mozorg/tests/test_views.py
@@ -3,15 +3,13 @@
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
import json
-from unittest.mock import Mock, patch
+from unittest.mock import patch
from django.core import mail
from django.http.response import HttpResponse
from django.test import override_settings
from django.test.client import RequestFactory
-import pytest
-
from bedrock.base.urlresolvers import reverse
from bedrock.mozorg import views
from bedrock.mozorg.models import WebvisionDoc
@@ -104,60 +102,6 @@ def test_no_post(self, render_mock):
self.assertEqual(resp.status_code, 405)
-@pytest.mark.django_db
-@pytest.mark.parametrize(
- "content_id, page_data, expected_template",
- (
- (
- "abc",
- {"page_type": "pagePageResourceCenter", "info": {"theme": "mozilla"}},
- "products/vpn/resource-center/article.html",
- ),
- (
- "def",
- {"page_type": "OTHER", "info": {"theme": "firefox"}},
- "firefox/contentful-all.html",
- ),
- (
- "ghi",
- {"page_type": "OTHER", "info": {"theme": "mozilla"}},
- "mozorg/contentful-all.html",
- ),
- (
- "jkl",
- {"page_type": "OTHER", "info": {"theme": "OTHER"}},
- "mozorg/contentful-all.html",
- ),
- ),
-)
-@patch("bedrock.mozorg.views.l10n_utils.render")
-@patch("bedrock.mozorg.views.ContentfulPage")
-# Trying to hot-reload the URLconf with settings.DEV = True was not
-# viable when the tests were being run in CI or via Makefile, so
-# instead we're explicitly including the urlconf that is loaded
-# when settings.DEV is True
-@pytest.mark.urls("bedrock.mozorg.tests.contentful_test_urlconf")
-def test_contentful_preview_view(
- contentfulpage_mock,
- render_mock,
- client,
- content_id,
- page_data,
- expected_template,
-):
- mock_page_data = Mock(name="mock_page_data")
- mock_page_data.get_content.return_value = page_data
- contentfulpage_mock.return_value = mock_page_data
-
- render_mock.return_value = HttpResponse("dummy")
-
- url = reverse("contentful.preview", kwargs={"content_id": content_id})
-
- client.get(url, follow=True)
- assert render_mock.call_count == 1
- assert render_mock.call_args_list[0][0][1] == expected_template
-
-
class TestWebvisionDocView(TestCase):
def test_doc(self):
WebvisionDoc.objects.create(name="summary", content={"title": "Summary
"})
diff --git a/bedrock/mozorg/urls.py b/bedrock/mozorg/urls.py
index df7295badd2..9ed82cf43fc 100644
--- a/bedrock/mozorg/urls.py
+++ b/bedrock/mozorg/urls.py
@@ -12,11 +12,9 @@
"""
-from django.conf import settings
from django.urls import path
from . import views
-from .dev_urls import urlpatterns as dev_only_urlpatterns
from .util import page
urlpatterns = [
@@ -145,6 +143,3 @@
path("antiharassment-tool/", views.anti_harassment_tool_view, name="mozorg.antiharassment-tool"),
page("rise25/nominate/", "mozorg/rise25/landing.html"),
]
-
-if settings.DEV:
- urlpatterns += dev_only_urlpatterns
diff --git a/bedrock/mozorg/views.py b/bedrock/mozorg/views.py
index b8f446a01b9..fd9137a3f6f 100644
--- a/bedrock/mozorg/views.py
+++ b/bedrock/mozorg/views.py
@@ -9,8 +9,6 @@
from django.http import Http404
from django.shortcuts import render as django_render
from django.template.loader import render_to_string
-from django.utils.decorators import method_decorator
-from django.views.decorators.cache import never_cache
from django.views.decorators.http import require_safe
from django.views.generic import TemplateView
@@ -18,7 +16,6 @@
from product_details import product_details
from bedrock.base.waffle import switch
-from bedrock.contentful.api import ContentfulPage
from bedrock.mozorg.credits import CreditsFile
from bedrock.mozorg.forms import MiecoEmailForm
from bedrock.mozorg.models import WebvisionDoc
@@ -173,28 +170,6 @@ def get_template_names(self):
return [self.template_name]
-@method_decorator(never_cache, name="dispatch")
-class ContentfulPreviewView(L10nTemplateView):
- def get_context_data(self, **kwargs):
- ctx = super().get_context_data(**kwargs)
- content_id = ctx["content_id"]
- page = ContentfulPage(self.request, content_id)
- ctx.update(page.get_content())
- return ctx
-
- def render_to_response(self, context, **response_kwargs):
- page_type = context["page_type"]
- theme = context["info"]["theme"]
- if page_type == "pagePageResourceCenter":
- template = "products/vpn/resource-center/article.html"
- elif theme == "firefox":
- template = "firefox/contentful-all.html"
- else:
- template = "mozorg/contentful-all.html"
-
- return l10n_utils.render(self.request, template, context, **response_kwargs)
-
-
class WebvisionDocView(RequireSafeMixin, TemplateView):
"""
Generic view for loading a webvision doc and displaying it with a template.
diff --git a/bedrock/products/redirects.py b/bedrock/products/redirects.py
index 46500911b18..f4ba89aebd5 100644
--- a/bedrock/products/redirects.py
+++ b/bedrock/products/redirects.py
@@ -43,4 +43,12 @@ def mobile_app(request, *args, **kwargs):
redirect(r"^products/vpn/mobile/app/?$", mobile_app, cache_timeout=0, query=False),
# Issue 15386
redirect(r"^products/vpn/resource-center/no-Logging-vpn-from-mozilla/$", "/products/vpn/resource-center/no-logging-vpn-from-mozilla/"),
+ # Issue 15843
+ redirect("/products/vpn/more/what-is-an-ip-address/", "/products/vpn/resource-center/what-is-an-ip-address/"),
+ redirect(
+ "/products/vpn/more/the-difference-between-a-vpn-and-a-web-proxy/",
+ "/products/vpn/resource-center/the-difference-between-a-vpn-and-a-web-proxy/",
+ ),
+ redirect("/products/vpn/more/what-is-a-vpn/", "/products/vpn/resource-center/what-is-a-vpn/"),
+ redirect("/products/vpn/more/5-reasons-you-should-use-a-vpn/", "/products/vpn/resource-center/5-reasons-you-should-use-a-vpn/"),
)
diff --git a/bedrock/products/tests/test_views.py b/bedrock/products/tests/test_views.py
index 6c9a5e83666..0d4006a615c 100644
--- a/bedrock/products/tests/test_views.py
+++ b/bedrock/products/tests/test_views.py
@@ -2,22 +2,16 @@
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
-from unittest.mock import Mock, patch
+from unittest.mock import patch
from django.conf import settings
from django.http import HttpResponse
from django.test import override_settings
from django.test.client import RequestFactory
-from django.urls import reverse
import pytest
from waffle.testutils import override_switch
-from bedrock.contentful.constants import (
- CONTENT_CLASSIFICATION_VPN,
- CONTENT_TYPE_PAGE_RESOURCE_CENTER,
-)
-from bedrock.contentful.models import ContentfulEntry
from bedrock.mozorg.tests import TestCase
from bedrock.products import views
@@ -256,296 +250,6 @@ def test_vpn_downoad_page_links(self, render_mock):
self.assertEqual(ctx["linux_download_url"], "https://vpn.mozilla.org/r/vpn/download/linux")
-class TestVPNResourceCenterHelpers(TestCase):
- def _build_mock_entry(self, entry_category_name):
- entry = Mock(name=entry_category_name)
- entry.category = entry_category_name
- return entry
-
- def test__filter_articles(self):
- articles = {
- "a": self._build_mock_entry("Category one"),
- "b": self._build_mock_entry("category TWO"),
- "c": self._build_mock_entry("Category three"),
- "d": self._build_mock_entry("category TWO"),
- "e": self._build_mock_entry("category TWO"),
- "f": self._build_mock_entry("category TWO"),
- }
-
- article_list = articles.values()
- self.assertEqual(
- views._filter_articles(article_list, "category TWO"),
- [
- articles["b"],
- articles["d"],
- articles["e"],
- articles["f"],
- ],
- )
-
- self.assertEqual(
- views._filter_articles(article_list, "Category one"),
- [
- articles["a"],
- ],
- )
- self.assertEqual(
- views._filter_articles(article_list, ""),
- article_list,
- )
-
- self.assertEqual(
- views._filter_articles(article_list, None),
- article_list,
- )
-
- def test__build_category_list(self):
- root_url = reverse("products.vpn.resource-center.landing")
- cases = [
- {
- "entry_list": [
- self._build_mock_entry("Category one"),
- self._build_mock_entry("category TWO"),
- self._build_mock_entry("Category three"),
- ],
- "expected": [
- # Alphabetical based on category name
- {
- "name": "Category one",
- "url": f"{root_url}?category=Category+one",
- },
- {
- "name": "Category three",
- "url": f"{root_url}?category=Category+three",
- },
- {
- "name": "category TWO",
- "url": f"{root_url}?category=category+TWO",
- },
- ],
- },
- {
- "entry_list": [
- self._build_mock_entry("Category one"),
- self._build_mock_entry("category TWO"),
- self._build_mock_entry("Category three"),
- self._build_mock_entry("category TWO"),
- self._build_mock_entry("Category three"),
- ],
- "expected": [
- {
- "name": "Category one",
- "url": f"{root_url}?category=Category+one",
- },
- {
- "name": "Category three",
- "url": f"{root_url}?category=Category+three",
- },
- {
- "name": "category TWO",
- "url": f"{root_url}?category=category+TWO",
- },
- ],
- },
- {
- "entry_list": [
- self._build_mock_entry("Category one"),
- self._build_mock_entry("Category one"),
- self._build_mock_entry("Category one"),
- self._build_mock_entry("Category one"),
- self._build_mock_entry("Category one"),
- self._build_mock_entry("Category one"),
- ],
- "expected": [
- {
- "name": "Category one",
- "url": f"{root_url}?category=Category+one",
- },
- ],
- },
- {
- "entry_list": [],
- "expected": [],
- },
- ]
- for case in cases:
- with self.subTest(case=case):
- output = views._build_category_list(
- case["entry_list"],
- )
- self.assertEqual(output, case["expected"])
-
-
-@override_settings(CONTENTFUL_LOCALE_SUFFICIENT_CONTENT_PERCENTAGE=60)
-@patch("bedrock.products.views.l10n_utils.render", return_value=HttpResponse())
-class TestVPNResourceListingView(TestCase):
- def setUp(self):
- for i in range(8):
- for locale in ["en-US", "fr", "ja"]:
- ContentfulEntry.objects.create(
- content_type=CONTENT_TYPE_PAGE_RESOURCE_CENTER,
- category="Test Category",
- classification=CONTENT_CLASSIFICATION_VPN,
- locale=locale,
- localisation_complete=True,
- contentful_id=f"entry_{i + 1}",
- slug=f"slug-{i + 1}",
- # We only get back the .data field, so let's put something useful in here to look for
- data={"slug_for_test": f"slug-{i + 1}-{locale}"},
- )
-
- def _request(
- self,
- locale,
- querystring=None,
- expected_status=200,
- ):
- querystring = querystring if querystring else {}
- with self.activate_locale(locale):
- dest = reverse("products.vpn.resource-center.landing")
- resp = self.client.get(dest, querystring, headers={"accept-language": locale}, follow=True)
- assert resp.status_code == expected_status
- return resp
-
- def test_simple_get__for_valid_locale_with_enough_content(self, render_mock):
- self._request(locale="fr")
- passed_context = render_mock.call_args_list[0][0][2]
-
- self.assertEqual(passed_context["active_locales"], ["en-US", "fr", "ja"])
- self.assertEqual(
- passed_context["category_list"],
- [{"name": "Test Category", "url": "/fr/products/vpn/resource-center/?category=Test+Category"}],
- )
- self.assertEqual(passed_context["selected_category"], "")
- self.assertEqual(
- [x["slug_for_test"] for x in passed_context["first_article_group"]],
- [
- "slug-1-fr",
- "slug-2-fr",
- "slug-3-fr",
- "slug-4-fr",
- "slug-5-fr",
- "slug-6-fr",
- ],
- )
- self.assertEqual(
- [x["slug_for_test"] for x in passed_context["second_article_group"]],
- [
- "slug-7-fr",
- "slug-8-fr",
- ],
- )
-
- def test_simple_get__for_unavailable_locale(self, render_mock):
- resp = self._request(locale="sk")
- self.assertEqual(resp.redirect_chain, [("/en-US/products/vpn/resource-center/", 302)])
-
- @override_settings(CONTENTFUL_LOCALE_SUFFICIENT_CONTENT_PERCENTAGE=95)
- def test_simple_get__for_valid_locale_WITHOUT_enough_content(self, render_mock):
- # ie, if you go to the VRC for a language we're working on but which
- # isn't active yet because it doesn't meet the activation threshold
- # percentage, we should send you to the default locale by calling render() early
- ContentfulEntry.objects.filter(locale="fr").last().delete()
- assert ContentfulEntry.objects.filter(locale="fr").count() < ContentfulEntry.objects.filter(locale="en-US").count()
-
- resp = self._request(locale="fr")
- self.assertEqual(resp.redirect_chain, [("/en-US/products/vpn/resource-center/", 302)])
-
- def test_category_filtering(self, render_mock):
- first = ContentfulEntry.objects.filter(locale="en-US").first()
- first.category = "other category"
- first.save()
-
- last = ContentfulEntry.objects.filter(locale="en-US").last()
- last.category = "other category"
- last.save()
-
- self._request(locale="en-US", querystring={"category": "other+category"})
- passed_context = render_mock.call_args_list[0][0][2]
-
- self.assertEqual(passed_context["active_locales"], ["en-US", "fr", "ja"])
- self.assertEqual(
- passed_context["category_list"],
- [
- {"name": "Test Category", "url": "/en-US/products/vpn/resource-center/?category=Test+Category"},
- {"name": "other category", "url": "/en-US/products/vpn/resource-center/?category=other+category"},
- ],
- )
- self.assertEqual(passed_context["selected_category"], "other category")
- self.assertEqual(
- [x["slug_for_test"] for x in passed_context["first_article_group"]],
- [
- "slug-1-en-US",
- "slug-8-en-US",
- ],
- )
- self.assertEqual(
- [x["slug_for_test"] for x in passed_context["second_article_group"]],
- [],
- )
-
- def test_active_locales_is_in_context(self, render_mock):
- self._request(locale="en-US")
- passed_context = render_mock.call_args_list[0][0][2]
- self.assertEqual(passed_context["active_locales"], ["en-US", "fr", "ja"])
-
-
-@override_settings(CONTENTFUL_LOCALE_SUFFICIENT_CONTENT_PERCENTAGE=60)
-class TestVPNResourceArticleView(TestCase):
- def setUp(self):
- for i in range(8):
- for locale in ["en-US", "fr", "ja"]:
- ContentfulEntry.objects.create(
- content_type=CONTENT_TYPE_PAGE_RESOURCE_CENTER,
- category="Test Category",
- classification=CONTENT_CLASSIFICATION_VPN,
- locale=locale,
- localisation_complete=True,
- contentful_id=f"entry_{i + 1}",
- slug=f"slug-{i + 1}",
- # We only get back the .data field, so let's put something useful in here to look for
- data={"slug_for_test": f"slug-{i + 1}-{locale}"},
- )
-
- @patch("bedrock.products.views.l10n_utils.render", return_value=HttpResponse())
- def test_appropriate_active_locales_is_in_context(self, render_mock):
- # ie, not the full set of available locales, but the ones specific to this page
-
- # Zap an entry and show that it's not available as a locale option for its locale siblings
- ContentfulEntry.objects.get(locale="fr", slug="slug-4").delete()
- self.client.get("/en-US/products/vpn/resource-center/slug-4/")
- passed_context = render_mock.call_args_list[0][0][2]
- self.assertEqual(passed_context["active_locales"], ["en-US", "ja"])
-
- # Show that it is for other pages where all three locales are active
- render_mock.reset_mock()
- self.client.get("/en-US/products/vpn/resource-center/slug-2/")
- passed_context = render_mock.call_args_list[0][0][2]
- self.assertEqual(passed_context["active_locales"], ["en-US", "fr", "ja"])
-
- def test_simple_get__no_active_locales_for_slug_at_all__gives_404(self):
- # change all entries so that get_active_locales helper will return []
- ContentfulEntry.objects.all().update(classification="something-that-will-not-match-query")
- resp = self.client.get("/en-US/products/vpn/resource-center/slug-4/")
- assert resp.status_code == 404
-
- @patch("bedrock.products.views.l10n_utils.render", return_value=HttpResponse())
- def test_simple_get(self, render_mock):
- resp = self.client.get("/ja/products/vpn/resource-center/slug-2/")
- assert resp.status_code == 200 # From the Mock, but still not a 30x/40x
- passed_context = render_mock.call_args_list[0][0][2]
- self.assertEqual(passed_context["active_locales"], ["en-US", "fr", "ja"])
- self.assertEqual(passed_context["slug_for_test"], "slug-2-ja")
- self.assertEqual(passed_context["related_articles"], []) # TODO: test independently
-
- @patch("bedrock.products.views.l10n_utils.render", return_value=HttpResponse())
- def test_simple_get__for_unavailable_locale(self, render_mock):
- resp = self.client.get("/de/products/vpn/resource-center/slug-2/")
- # Which will 302 as expected
- self.assertEqual(resp.headers["location"], "/en-US/products/vpn/resource-center/slug-2/")
- render_mock.assert_not_called()
-
-
@patch("bedrock.products.views.l10n_utils.render", return_value=HttpResponse())
class TestMonitorScanWaitlistPage(TestCase):
@override_settings(DEV=False)
@@ -569,39 +273,3 @@ def test_monitor_plus_waitlist_template(self, render_mock):
self.assertEqual(ctx["newsletter_id"], "monitor-waitlist")
template = render_mock.call_args[0][1]
assert template == "products/monitor/waitlist/plus.html"
-
-
-@patch("bedrock.products.views.l10n_utils.render", return_value=HttpResponse())
-class TestVPNMorePages(TestCase):
- @override_settings(DEV=True)
- @patch.object(views, "ftl_file_is_active", lambda *x: False)
- @patch.object(views, "active_locale_available", lambda *x: False)
- def test_vpn_more_resource_center_redirect_rc_en_us(self, render_mock):
- req = RequestFactory().get("/products/vpn/more/what-is-an-ip-address/")
- req.locale = "en-US"
- view = views.vpn_resource_center_redirect
- resp = view(req, "what-is-an-ip-address")
- # should redirect to en-US/vpn/resource-center/what-is-an-ip-address
- assert resp.status_code == 302 and resp.url == "/en-US/products/vpn/resource-center/what-is-an-ip-address/"
-
- @override_settings(DEV=True)
- @patch.object(views, "ftl_file_is_active", lambda *x: True)
- @patch.object(views, "active_locale_available", lambda *x: False)
- def test_vpn_more_resource_center_redirect_ftl_active(self, render_mock):
- req = RequestFactory().get("/products/vpn/more/what-is-an-ip-address/")
- req.locale = "ar"
- view = views.vpn_resource_center_redirect
- resp = view(req, "what-is-an-ip-address")
- # should 200 as expected
- assert resp.status_code == 200
-
- @override_settings(DEV=True)
- @patch.object(views, "ftl_file_is_active", lambda *x: False)
- @patch.object(views, "active_locale_available", lambda *x: True)
- def test_vpn_more_resource_center_redirect_rc_active(self, render_mock):
- req = RequestFactory().get("/products/vpn/more/what-is-an-ip-address/")
- req.locale = "fr"
- view = views.vpn_resource_center_redirect
- resp = view(req, "what-is-an-ip-address")
- # should redirect to fr/vpn/resource-center/what-is-an-ip-address
- assert resp.status_code == 302 and resp.url == "/fr/products/vpn/resource-center/what-is-an-ip-address/"
diff --git a/bedrock/products/urls.py b/bedrock/products/urls.py
index f5bc611a1da..eb232b8e6d0 100644
--- a/bedrock/products/urls.py
+++ b/bedrock/products/urls.py
@@ -4,7 +4,6 @@
from django.urls import path
-from bedrock.cms.decorators import prefer_cms
from bedrock.mozorg.util import page
from bedrock.products import views
@@ -26,29 +25,6 @@
page("vpn/mobile/ios/", "products/vpn/platforms/ios.html", ftl_files=["products/vpn/platforms/ios_v2", "products/vpn/shared"]),
page("vpn/mobile/android/", "products/vpn/platforms/android.html", ftl_files=["products/vpn/platforms/android_v2", "products/vpn/shared"]),
page("vpn/ipad/", "products/vpn/platforms/ipad.html", ftl_files=["products/vpn/platforms/ipad", "products/vpn/shared"]),
- # Evergreen SEO articles (issue #10224)
- path(
- "vpn/more//",
- views.vpn_resource_center_redirect,
- name="products.vpn.more.redirect",
- ),
- # VPN Resource Center
- path(
- "vpn/resource-center/",
- prefer_cms(
- views.resource_center_landing_view,
- fallback_lang_codes=["de", "en-US", "es-ES", "fr", "it", "ja", "nl", "pl", "pt-BR", "ru", "zh-CN"],
- ),
- name="products.vpn.resource-center.landing",
- ),
- path(
- "vpn/resource-center//",
- prefer_cms(
- views.resource_center_article_view,
- fallback_callable=views.resource_center_article_available_locales_lookup,
- ),
- name="products.vpn.resource-center.article",
- ),
path("monitor/waitlist-plus/", views.monitor_waitlist_plus_page, name="products.monitor.waitlist-plus"),
path("monitor/waitlist-scan/", views.monitor_waitlist_scan_page, name="products.monitor.waitlist-scan"),
)
diff --git a/bedrock/products/views.py b/bedrock/products/views.py
index 81c52dce9fe..72d2ea15fcb 100644
--- a/bedrock/products/views.py
+++ b/bedrock/products/views.py
@@ -1,29 +1,14 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
-from html import escape
-from urllib.parse import quote_plus, unquote_plus
from django.conf import settings
-from django.http import Http404
-from django.shortcuts import redirect
-from django.urls import reverse
from django.views.decorators.http import require_safe
-from sentry_sdk import capture_exception
-
from bedrock.base.geo import get_country_from_request
from bedrock.base.waffle import switch
-from bedrock.contentful.constants import (
- ARTICLE_CATEGORY_LABEL,
- CONTENT_CLASSIFICATION_VPN,
- CONTENT_TYPE_PAGE_RESOURCE_CENTER,
-)
-from bedrock.contentful.models import ContentfulEntry
-from bedrock.contentful.utils import locales_with_available_content
from bedrock.products.forms import VPNWaitlistForm
from lib import l10n_utils
-from lib.l10n_utils.fluent import ftl_file_is_active
def vpn_available(request):
@@ -63,15 +48,6 @@ def vpn_available_android_sub_only(request):
return country in country_list
-def active_locale_available(slug, locale):
- active_locales_for_this_article = ContentfulEntry.objects.get_active_locales_for_slug(
- classification=CONTENT_CLASSIFICATION_VPN,
- content_type=CONTENT_TYPE_PAGE_RESOURCE_CENTER,
- slug=slug,
- )
- return locale in active_locales_for_this_article
-
-
@require_safe
def vpn_landing_page(request):
ftl_files = ["products/vpn/landing-2023", "products/vpn/shared", "products/vpn/pricing-2023"]
@@ -222,163 +198,6 @@ def vpn_invite_page(request):
return l10n_utils.render(request, "products/vpn/invite.html", ctx, ftl_files=ftl_files)
-def _build_category_list(entry_list):
- # Template is expecting this format:
- # category_list = [
- # {"name": "Cat1", "url": "/full/path/to/category"}, ...
- # ]
- categories_seen = set()
- category_list = []
- root_url = reverse("products.vpn.resource-center.landing")
- for entry in entry_list:
- category = entry.category
- if category and category not in categories_seen:
- category_list.append(
- {
- "name": category,
- "url": f"{root_url}?{ARTICLE_CATEGORY_LABEL}={quote_plus(category)}",
- }
- )
- categories_seen.add(category)
-
- category_list = sorted(category_list, key=lambda x: x["name"])
- return category_list
-
-
-def _filter_articles(articles_list, category):
- if not category:
- return articles_list
-
- return [article for article in articles_list if article.category == category]
-
-
-@require_safe
-def resource_center_landing_view(request):
- ARTICLE_GROUP_SIZE = 6
- template_name = "products/vpn/resource-center/landing.html"
- vpn_available_in_country = vpn_available(request)
- mobile_sub_only = vpn_available_mobile_sub_only(request)
- active_locales = locales_with_available_content(
- classification=CONTENT_CLASSIFICATION_VPN,
- content_type=CONTENT_TYPE_PAGE_RESOURCE_CENTER,
- )
- requested_locale = l10n_utils.get_locale(request)
-
- if requested_locale not in active_locales:
- return l10n_utils.redirect_to_best_locale(
- request,
- translations=active_locales,
- )
-
- resource_articles = ContentfulEntry.objects.get_entries_by_type(
- locale=requested_locale,
- classification=CONTENT_CLASSIFICATION_VPN,
- content_type=CONTENT_TYPE_PAGE_RESOURCE_CENTER,
- )
- category_list = _build_category_list(resource_articles)
- selected_category = unquote_plus(request.GET.get(ARTICLE_CATEGORY_LABEL, ""))
-
- filtered_articles = _filter_articles(
- resource_articles,
- category=selected_category,
- )
-
- # The resource_articles are ContentfulEntry objects at the moment, but
- # we really only need their JSON data from here on
- filtered_article_data = [x.data for x in filtered_articles]
-
- first_article_group, second_article_group = (
- filtered_article_data[:ARTICLE_GROUP_SIZE],
- filtered_article_data[ARTICLE_GROUP_SIZE:],
- )
-
- ctx = {
- "active_locales": active_locales,
- "vpn_available": vpn_available_in_country,
- "mobile_sub_only": mobile_sub_only,
- "category_list": category_list,
- "first_article_group": first_article_group,
- "second_article_group": second_article_group,
- "selected_category": escape(selected_category),
- }
- return l10n_utils.render(
- request,
- template_name,
- ctx,
- ftl_files=["products/vpn/resource-center", "products/vpn/shared"],
- )
-
-
-@require_safe
-def resource_center_article_view(request, slug):
- """Individual detail pages for the VPN Resource Center"""
-
- template_name = "products/vpn/resource-center/article.html"
- requested_locale = l10n_utils.get_locale(request)
- vpn_available_in_country = vpn_available(request)
-
- active_locales_for_this_article = ContentfulEntry.objects.get_active_locales_for_slug(
- classification=CONTENT_CLASSIFICATION_VPN,
- content_type=CONTENT_TYPE_PAGE_RESOURCE_CENTER,
- slug=slug,
- )
-
- if not active_locales_for_this_article:
- # ie, this article just isn't available in any locale
- raise Http404()
-
- if requested_locale not in active_locales_for_this_article:
- # Calling render() early will redirect the user to the most
- # appropriate default/alternative locale for their browser
- return l10n_utils.redirect_to_best_locale(
- request,
- translations=active_locales_for_this_article,
- )
- ctx = {}
- try:
- article = ContentfulEntry.objects.get_entry_by_slug(
- slug=slug,
- locale=requested_locale,
- classification=CONTENT_CLASSIFICATION_VPN,
- content_type=CONTENT_TYPE_PAGE_RESOURCE_CENTER,
- )
- ctx.update(article.data)
- except ContentfulEntry.DoesNotExist as ex:
- # We shouldn't get this far, given active_locales_for_this_article,
- # should only show up viable locales etc, so log it loudly before 404ing.
- capture_exception(ex)
- raise Http404()
-
- ctx.update(
- {
- "active_locales": active_locales_for_this_article,
- "vpn_available": vpn_available_in_country,
- "related_articles": [x.data for x in article.get_related_entries()],
- }
- )
-
- return l10n_utils.render(
- request,
- template_name,
- ctx,
- ftl_files=[
- "products/vpn/resource-center",
- "products/vpn/shared",
- ],
- )
-
-
-def resource_center_article_available_locales_lookup(*, slug: str) -> list[str]:
- # Helper func to get a list of language codes available for the given
- # Contentful-powered VPN RC slug
- return list(
- ContentfulEntry.objects.filter(
- localisation_complete=True,
- slug=slug,
- ).values_list("locale", flat=True)
- )
-
-
@require_safe
def monitor_waitlist_scan_page(request):
template_name = "products/monitor/waitlist/scan.html"
@@ -395,42 +214,3 @@ def monitor_waitlist_plus_page(request):
ctx = {"newsletter_id": newsletter_id}
return l10n_utils.render(request, template_name, ctx)
-
-
-@require_safe
-def vpn_resource_center_redirect(request, slug):
- # When a /more url is requested the user should be forwarded to the /resource-centre url
- # If the rc article is not available in their requested language bedrock should display the /more/ article if it is available in their language.
- # 2. If neither is available in their language bedrock should forward to the English rc article.
- VPNRC_SLUGS = {
- "what-is-an-ip-address": {
- "slug": "what-is-an-ip-address",
- "old_template": "products/vpn/more/ip-address.html",
- "ftl_files": ["products/vpn/more/ip-address", "products/vpn/shared"],
- },
- "vpn-or-proxy": {
- "slug": "the-difference-between-a-vpn-and-a-web-proxy",
- "old_template": "products/vpn/more/vpn-or-proxy.html",
- "ftl_files": ["products/vpn/more/vpn-or-proxy", "products/vpn/shared"],
- },
- "what-is-a-vpn": {
- "slug": "what-is-a-vpn",
- "old_template": "products/vpn/more/what-is-a-vpn.html",
- "ftl_files": ["products/vpn/more/what-is-a-vpn", "products/vpn/shared"],
- },
- "when-to-use-a-vpn": {
- "slug": "5-reasons-you-should-use-a-vpn",
- "old_template": "products/vpn/more/when-to-use.html",
- "ftl_files": ["products/vpn/more/when-to-use-a-vpn", "products/vpn/shared"],
- },
- }
- locale = l10n_utils.get_locale(request)
- curr_page = VPNRC_SLUGS[slug]
- rc_slug = curr_page["slug"]
- redirect_link = f"/{locale}/products/vpn/resource-center/{rc_slug}/"
- if active_locale_available(rc_slug, locale):
- return redirect(redirect_link)
- elif ftl_file_is_active(curr_page["ftl_files"][0], locale):
- return l10n_utils.render(request, template=curr_page["old_template"], ftl_files=curr_page["ftl_files"])
- else:
- return redirect(redirect_link)
diff --git a/bedrock/settings/base.py b/bedrock/settings/base.py
index 510c1399b61..8887cd9ce63 100644
--- a/bedrock/settings/base.py
+++ b/bedrock/settings/base.py
@@ -25,9 +25,6 @@
from sentry_sdk.integrations.rq import RqIntegration
from bedrock.base.config_manager import config
-from bedrock.contentful.constants import (
- DEFAULT_CONTENT_TYPES as CONTENTFUL_DEFAULT_CONTENT_TYPES,
-)
# ROOT path of the project. A pathlib.Path object.
DATA_PATH = config("DATA_PATH", default="data")
@@ -768,7 +765,6 @@ def get_app_name(hostname):
"bedrock.security",
"bedrock.releasenotes",
"bedrock.contentcards",
- "bedrock.contentful",
"bedrock.utils",
"bedrock.wordpress",
"bedrock.sitemaps",
@@ -1034,35 +1030,6 @@ def _is_bedrock_custom_app(app_name):
CONTENT_CARDS_BRANCH = config("CONTENT_CARDS_BRANCH", default=content_cards_default_branch)
CONTENT_CARDS_URL = config("CONTENT_CARDS_URL", default=STATIC_URL)
-CONTENTFUL_SPACE_ID = config("CONTENTFUL_SPACE_ID", raise_error=False)
-CONTENTFUL_SPACE_KEY = config("CONTENTFUL_SPACE_KEY", raise_error=False)
-CONTENTFUL_ENVIRONMENT = config("CONTENTFUL_ENVIRONMENT", default="master")
-CONTENTFUL_SPACE_API = ("preview" if DEV else "cdn") + ".contentful.com"
-CONTENTFUL_API_TIMEOUT = config("CONTENTFUL_API_TIMEOUT", default="5", parser=int)
-CONTENTFUL_CONTENT_TYPES_TO_SYNC = config(
- "CONTENTFUL_CONTENT_TYPES_TO_SYNC",
- default=CONTENTFUL_DEFAULT_CONTENT_TYPES,
- parser=ListOf(str),
-)
-
-CONTENTFUL_NOTIFICATION_QUEUE_URL = config("CONTENTFUL_NOTIFICATION_QUEUE_URL", default="", raise_error=False)
-CONTENTFUL_NOTIFICATION_QUEUE_REGION = config("CONTENTFUL_NOTIFICATION_QUEUE_REGION", default="", raise_error=False)
-CONTENTFUL_NOTIFICATION_QUEUE_ACCESS_KEY_ID = config("CONTENTFUL_NOTIFICATION_QUEUE_ACCESS_KEY_ID", default="", raise_error=False)
-CONTENTFUL_NOTIFICATION_QUEUE_SECRET_ACCESS_KEY = config("CONTENTFUL_NOTIFICATION_QUEUE_SECRET_ACCESS_KEY", default="", raise_error=False)
-CONTENTFUL_NOTIFICATION_QUEUE_WAIT_TIME = config("CONTENTFUL_NOTIFICATION_QUEUE_WAIT_TIME", default="10", parser=int, raise_error=False)
-
-CONTENTFUL_HOMEPAGE_LOOKUP = {
- # TEMPORARY lookup table for which Contentful `connectHomepage` page ID to get for which locale
- "en-US": "58YIvwDmzSDjtvpSqstDcL",
- "de": "4k3CxqZGjxXOjR1I0dhyto",
-}
-
-CONTENTFUL_LOCALE_SUFFICIENT_CONTENT_PERCENTAGE = config(
- "CONTENTFUL_LOCALE_SUFFICIENT_CONTENT_PERCENTAGE",
- default="1" if DEV is True else "10",
- parser=float,
-)
-
RELEASE_NOTES_PATH = config("RELEASE_NOTES_PATH", default=data_path("release_notes"))
RELEASE_NOTES_REPO = config("RELEASE_NOTES_REPO", default="https://github.com/mozilla/release-notes.git")
RELEASE_NOTES_BRANCH = config("RELEASE_NOTES_BRANCH", default="master")
diff --git a/bedrock/sitemaps/tests/test_utils.py b/bedrock/sitemaps/tests/test_utils.py
index a0223530cea..06bbfa7d6a1 100644
--- a/bedrock/sitemaps/tests/test_utils.py
+++ b/bedrock/sitemaps/tests/test_utils.py
@@ -8,15 +8,8 @@
from wagtail.models import Locale, Page, PageViewRestriction, Site
from bedrock.cms.tests.factories import LocaleFactory, SimpleRichTextPageFactory, StructuralPageFactory
-from bedrock.contentful.constants import (
- CONTENT_CLASSIFICATION_VPN,
- CONTENT_TYPE_PAGE_RESOURCE_CENTER,
-)
-from bedrock.contentful.models import ContentfulEntry
from bedrock.sitemaps.utils import (
- _get_vrc_urls,
_path_for_cms_url,
- get_contentful_urls,
get_wagtail_urls,
update_sitemaps,
)
@@ -24,49 +17,6 @@
pytestmark = pytest.mark.django_db
-@pytest.fixture
-def dummy_vrc_pages():
- # No content, just the bare data we need
- for idx in range(5):
- ContentfulEntry.objects.create(
- contentful_id=f"DUMMY-{idx}",
- slug=f"test-slug-{idx}",
- # TODO: support different locales
- locale="en-US",
- localisation_complete=True,
- content_type=CONTENT_TYPE_PAGE_RESOURCE_CENTER,
- classification=CONTENT_CLASSIFICATION_VPN,
- data={},
- data_hash="dummy",
- )
-
-
-def test__get_vrc_urls(dummy_vrc_pages):
- # TODO: support different locales
- output = _get_vrc_urls()
- assert output == {
- "/products/vpn/resource-center/test-slug-0/": ["en-US"],
- "/products/vpn/resource-center/test-slug-1/": ["en-US"],
- "/products/vpn/resource-center/test-slug-2/": ["en-US"],
- "/products/vpn/resource-center/test-slug-3/": ["en-US"],
- "/products/vpn/resource-center/test-slug-4/": ["en-US"],
- }
-
-
-def test__get_vrc_urls__no_content():
- output = _get_vrc_urls()
- assert output == {}
-
-
-@patch("bedrock.sitemaps.utils._get_vrc_urls")
-def test_get_contentful_urls(mock__get_vrc_urls):
- mock__get_vrc_urls.return_value = {"vrc-urls": "dummy-here"}
-
- output = get_contentful_urls()
- assert output == {"vrc-urls": "dummy-here"}
- mock__get_vrc_urls.assert_called_once_with()
-
-
@pytest.fixture
def dummy_wagtail_pages():
en_us_locale = Locale.objects.get(language_code="en-US")
@@ -271,13 +221,11 @@ def test_get_wagtail_urls__ensure_locale_codes_not_stripped(dummy_wagtail_pages)
@patch("bedrock.sitemaps.utils.get_static_urls")
@patch("bedrock.sitemaps.utils.get_release_notes_urls")
@patch("bedrock.sitemaps.utils.get_security_urls")
-@patch("bedrock.sitemaps.utils.get_contentful_urls")
@patch("bedrock.sitemaps.utils.get_wagtail_urls")
@patch("bedrock.sitemaps.utils.output_json")
def test_update_sitemaps(
mock_output_json,
mock_get_wagtail_urls,
- mock_get_contentful_urls,
mock_get_security_urls,
mock_get_release_notes_urls,
mock_get_static_urls,
@@ -285,7 +233,6 @@ def test_update_sitemaps(
"Light check to ensure we've not added _new_ things we haven't added tests for"
mock_get_wagtail_urls.return_value = {"wagtail": "dummy1"}
- mock_get_contentful_urls.return_value = {"contentful": "dummy2"}
mock_get_security_urls.return_value = {"security": "dummy3"}
mock_get_release_notes_urls.return_value = {"release_notes": "dummy4"}
mock_get_static_urls.return_value = {"static_urls": "dummy5"}
@@ -293,7 +240,6 @@ def test_update_sitemaps(
update_sitemaps()
expected = {
"wagtail": "dummy1",
- "contentful": "dummy2",
"security": "dummy3",
"release_notes": "dummy4",
"static_urls": "dummy5",
diff --git a/bedrock/sitemaps/utils.py b/bedrock/sitemaps/utils.py
index 65fff87a817..06154b01862 100644
--- a/bedrock/sitemaps/utils.py
+++ b/bedrock/sitemaps/utils.py
@@ -14,12 +14,6 @@
from wagtail.models import Page
-from bedrock.contentful.constants import (
- CONTENT_CLASSIFICATION_VPN,
- CONTENT_TYPE_PAGE_RESOURCE_CENTER,
- VRC_ROOT_PATH,
-)
-from bedrock.contentful.models import ContentfulEntry
from bedrock.releasenotes.models import ProductRelease
from bedrock.security.models import SecurityAdvisory
@@ -165,29 +159,6 @@ def get_static_urls():
return urls
-def _get_vrc_urls():
- # URLs for individual VRC articles - the listing/landing page is declared
- # separately in bedrock/products/urls.py so we don't need to include it here
-
- urls = defaultdict(list)
-
- for entry in ContentfulEntry.objects.filter(
- localisation_complete=True,
- content_type=CONTENT_TYPE_PAGE_RESOURCE_CENTER,
- classification=CONTENT_CLASSIFICATION_VPN,
- ):
- _path = f"{VRC_ROOT_PATH}{entry.slug}/"
- urls[_path].append(entry.locale) # One slug may support multiple locales
-
- return urls
-
-
-def get_contentful_urls():
- urls = {}
- urls.update(_get_vrc_urls())
- return urls
-
-
def _path_for_cms_url(page_url, lang_code):
# If possible, drop the leading slash + lang code from the URL
# so that we get a locale-agnostic path that we can include in the
@@ -231,7 +202,6 @@ def update_sitemaps():
urls = get_static_urls()
urls.update(get_release_notes_urls())
urls.update(get_security_urls())
- urls.update(get_contentful_urls())
urls.update(get_wagtail_urls())
# Output static files
diff --git a/bin/export-db-to-sqlite.sh b/bin/export-db-to-sqlite.sh
index a29a1288960..3b436f03ced 100755
--- a/bin/export-db-to-sqlite.sh
+++ b/bin/export-db-to-sqlite.sh
@@ -172,7 +172,6 @@ python manage.py dumpdata \
security.MitreCVE \
releasenotes.ProductRelease \
contentcards.ContentCard \
- contentful.ContentfulEntry \
utils.GitRepoState \
wordpress.BlogPost \
sitemaps.SitemapURL \
diff --git a/bin/fill-empty-postgres-database.sh b/bin/fill-empty-postgres-database.sh
index 24597a56d2d..f166effa188 100755
--- a/bin/fill-empty-postgres-database.sh
+++ b/bin/fill-empty-postgres-database.sh
@@ -24,11 +24,3 @@ PROD_DETAILS_STORAGE=product_details.storage.PDFileStorage ./manage.py migrate
# 2. Run db update scripts to pull down the usual data sources
./bin/run-db-update.sh
-
-# 3. Port the Contentful data (for the VPN Resource Center pages) from sqlite
-# (Contentful sync is not enabled and doesn't work any more, but
-# we can get the data from the sqlite db in the bedrock install)
-DATABASE_URL=sqlite://./data/bedrock.db ./manage.py dumpdata contentful.contentfulentry -o /tmp/contentful_data.json
-
-# ...and then load it in to postgres
-./manage.py loaddata /tmp/contentful_data.json
diff --git a/docs/contentful.rst b/docs/contentful.rst
deleted file mode 100644
index b5ea55059ad..00000000000
--- a/docs/contentful.rst
+++ /dev/null
@@ -1,786 +0,0 @@
-.. This Source Code Form is subject to the terms of the Mozilla Public
-.. License, v. 2.0. If a copy of the MPL was not distributed with this
-.. file, You can obtain one at https://mozilla.org/MPL/2.0/.
-
-.. _contentful:
-
-===========================================================================
-Contentful :abbr:`CMS (Content Management System)` Integration (Deprecated)
-===========================================================================
-
-.. important::
-
- We are no longer syncing content from Contentful, but we still hold
- that content frozen in our database and use it to render pages.
-
- Pages previously managed with Contentful will be the first pages to
- be (re)implemented using our upcoming part-of-Bedrock CMS system. At that
- point, we will remove all Contentful-related code from the codebase.
-
- In the meantime, if content changes are needed to pages formerly managed via
- Contentful, we can do this via data migration – just ask the backend team.
-
- **Please do not add new pages to Bedrock using Contentful.**
-
-Overview
---------
-
-Contentful is a headless :abbr:`CMS (Content Management System)`. It stores content for our website in a structured
-format. We request the content from Contentful using an API. Then the content
-gets made into Protocol components for display on the site.
-
-We define the structure Contentful uses to store the data in **content models**.
-The content models are used to create a form for editors to fill out when they want
-to enter new content. Each chunk of content is called an **entry**.
-
-For example: we have a content model for our "card" component. That model creates a
-form with fields like heading, link, blurb, and image. Each card that is created from
-the model is its own entry.
-
-We have created a few different types of content models. Most are components that
-correspond to components in our design system. The smallest create little bits of code
-like buttons. The larger ones group together several entries for the smaller components
-into a bigger component or an entire page.
-
-For example: The *Page: General* model allows editors to include a hero entry, body
-entry, and callout entry. The callout layout entry, in turn, includes a :abbr:`CTA (Call To Action)`
-entry.
-
-One advantage of storing the content in small chunks like this is that is can be
-reused in many different pages. A callout which focuses on the privacy related reasons
-to download Firefox could end up on the Private Browsing, Ad Tracker Blocking, and
-Fingerprinter Blocking pages. If our privacy focused tagline changes from "Keep it
-secret with Firefox" to "Keep it private with Firefox" it only needs to be updated in
-one entry.
-
-So, when looking at a page on the website that comes from Contentful you are typically
-looking at several different entries combined together.
-
-On the bedrock side, the data for all entries is periodically requested from the API
-and stored in a database.
-
-When a Contentful page is requested the code in `api.py` transforms the information
-from the database into a group of Python dictionaries (these are like key/value pairs
-or an object in JS).
-
-This data is then passed to the page template (either Mozilla or for Firefox themed
-as appropriate). The page template includes some files which take the data and feed
-it into macros to create Protocol components. These are the same macros we use on
-non-Contentful pages. There are also includes which will import the appropriate JS and
-CSS files to support the components.
-
-Once rendered the pages get cached on the :abbr:`CDN (Content Delivery Network)` as usual.
-
-Contentful Apps
----------------
-
-.. important::
-
- We are no longer syncing content from Contentful – see the note at the top of this page.
-
- **Please do not add new pages to Bedrock using Contentful.**
-
-
-Installed on Environment level. Make sure you are in the environment you want to edit before accessing an app.
-Use *Apps* link in top navigation of Contentful Web App to find an environment's installed apps.
-
-Compose
-~~~~~~~
-
-`Compose `_ provides a nicer editing experience.
-It creates a streamlined view of pages by combining multiple entries into a single edit screen and allowing field
-groups for better organization.
-
-Any changes made to Compose page entries in a specific environment are limited to that
-environment. If you are in a sandbox environment, you should see an ``/environments/sandbox-name`` path at the end
-of your Compose URL.
-
-Known Limitations
-^^^^^^^^^^^^^^^^^
-* Comments are not available on Compose entries
-* It is not possible to edit embedded entries in Rich Text fields in Compose app. Selecting the "edit" option in the dropdown opens the entry in the Contentful web app.
-
-Merge
-~~~~~
-
-`Merge `_ provides a UI for comparing the state of Content Models across two environments. You can select what changes you would like to migrate to a new environment.
-
-Known Limitations
-^^^^^^^^^^^^^^^^^
-* Does not migrate Help Text (under Appearance Tab)
-* Does not migrate any apps used with those Content Models
-* Does not migrate Content Entries or Assets
-* It can identify when Content Models should be available in Compose, but it cannot migrate the field groups
-
-Others
-~~~~~~
-* `Launch `_ allows creation of "releases", which can help coordinate publishing of multiple entries
-* `Workflows `_ standardizes process for a specific Content Model. You can specify steps and permissions to regulate how content moves from draft to published.
-
-Content Models
---------------
-
-Emoji legend for content models
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-* 📄 this component is a page, it will include meta data for the page, a folder, and slug
-* 🎁 this is a layout wrapper for another component
-* ✏️ this component includes editable content, not just layout config
-* ♟ this component is suitable for inclusion as an inline entry in a rich text field
-* ➡️ this component can be embedded without a layout wrapper
-
-
-Naming conventions for content models
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-.. note::
-
- For some fields it is important to be consistent because of how they are processed in
- bedrock. For all it is important to make the editor's jobs easier.
-
-Name
- This is for the internal name of the entry. It should be set as the **Entry title**,
- required, and unique.
-
-Preview (and Preview Title, Preview Blurb, Preview Image)
- These will be used in search results and social media sites. There's also the
- potential to use them for aggregate pages on our own sites. Copy configuration and
- validation from an existing page.
-
-Heading (and Heading Level)
- Text on a page which provides context for information that follows it. Usually made
- into a H1-H4 in bedrock. Not: header, title, or name.
-
-Image (and Image Size, Image Width)
- Not: picture, photo, logo, or icon (unless we are specifically talking about a logo or icon.)
-
-Content
- Multi-reference
-
-Product Icon
- Copy configuration and validation from an existing page.
-
-Theme
- Copy configuration and validation from an existing page.
-
-Body (Body Width, Body Vertical Alignment, Body Horizontal Alignment)
- Rich text field in a Component. Do not use this for multi reference fields, even if the only content on the page is other content entries.
- Do not use MarkDown for body fields, we can’t restrict the markup. Copy configuration and validation from an existing page.
-
-Rich Text Content
- Rich text field in a Compose Page
-
-:abbr:`CTA (Call To Action)`
- The button/link/dropdown that we want a user to interact with following some content. Most often appearing in Split and Callout components.
-
-
-
-📄 Page
-~~~~~~~
-
-Pages in bedrock are created from page entries in Contentful's `Compose`_ App.
-
-Homepage
- The homepage needs to be connected to bedrock using a Connect component (see `Legacy`_) and page meta
- data like title, blurb, image, etc come from bedrock.
-
-General
- Includes hero, text, and callout. The simplified list and order of
- components is intended to make it easier for editors to put a page together.
-
-Versatile
- No pre-defined template. These pages can be constructed from any combination of layout and
- component entries.
-
-Resource Center
- Includes product, category, tags, and a rich text editor. These pages follow a recognizable
- format that will help orient users looking for more general product information (i.e. VPN).
-
-
-The versatile and general templates do not need bedrock configuration to be displayed.
-Instead, they should appear automatically at the folder and slug specified in the entry.
-These templates do include fields for meta data.
-
-🎁 Layout
-~~~~~~~~~
-
-These entries bring a group of components together. For example: 3 picto blocks in
-a picto block layout. They also include layout and theme options which are applied to
-all of the components they bring together. For example: centering the icons in all 3
-picto blocks.
-
-These correspond roughly to Protocol templates.
-
-The one exception to the above is the Layout: Large Card, which exists to attach a large
-display image to a regular card entry. The large card must still be included in the
-Layout: 5 Cards.
-
-✏️ Component
-~~~~~~~~~~~~
-
-We're using this term pretty loosely. It corresponds roughly to a Protocol atom,
-molecule, or organism.
-
-These entries include the actual content, the bits that people write and the images that
-go with it.
-
-If they do not require a layout wrapper there may also be some layout and theme options.
-For example, the text components include options for width and alignment.
-
-♟ Embed
-~~~~~~~~~~~
-
-These pre-configured content pieces can go in rich text editors when allowed (picto, split, multi column text...).
-
-Embeds are things like logos, where we want tightly coupled style and content that will be consistent across entries.
-If a logo design changes, we only need to update it in one place, and all uses of that embed will be updated.
-
-Adding a new 📄 Page
-~~~~~~~~~~~~~~~~~~~~
-* Create the content model
-
- * Ensure the content model name starts with page (i.e. pageProductJournalismStory)
-
- * Add an SEO reference field which requires the **SEO Metadata** content type
-
- * In Compose, go to Page Types and click “Manage Page Types” to make your new content model available to the Compose editor.
-
- * If you have referenced components, you can choose whether they will be displayed as expanded by default.
-
- * Select “SEO” field for “Page Settings” field
-
- * If the page is meant to be localised, ensure all fields that need localisation have the “Enable localization of this field” checkbox checked in content model field settings
-
-* Update ``bedrock/contentful/constants``
-
- * Add content type constant
-
- * Add constant to default array
-
- * If page is for a single locale only, add to SINGLE_LOCALE_CONTENT_TYPES
-
- * If page is localised, add to LOCALISATION_COMPLETENESS_CHECK_CONFIG with an array of localised fields that need to be checked before the page’s translation can be considered complete
-
-* Update ``bedrock/contentful/api.py``
-
- * If you’re adding new embeddable content types, expand list of renderer helpers configured for the RichTextRenderer in the ``ContentfulAPIWrapper``
-
- * Update ``ContentfulAPIWrapper.get_content()`` to have a clause to handle the new page type
-
-* Create a `custom view `_ to pass the Contentful data to a template
-
-Adding a new ✏️ Component
-~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Example: Picto
-
-#. Create the content model in Contentful.
-
- * *Follow the naming conventions*.
- * You may need two models if you are configuring layout separately.
-
-#. Add the new content model to the list of allowed references in other content models (At the moment this is just the "content" reference field on pages).
-#. In bedrock create CSS and JS entries in static-bundles for the new component.
-#. In api.py write a def for the component.
-#. In api.py add the component name, def, and bundles to the CONTENT_TYPE_MAP.
-#. Find or add the macro to macros-protocol.
-#. Import the macro into all.html and add a call to it in the entries loop.
-
-.. note::
-
- Tips:
-
- * can't define defaults in Contentful, so set those in your Python def.
- * for any optional fields make sure you check the field exists before referencing the content.
-
-
-Adding a new ♟ Embed
-~~~~~~~~~~~~~~~~~~~~~~~~
-
-Example: Wordmark.
-
-#. Create the content model in Contentful.
-
- * *Follow the naming conventions*.
-
-#. Add the new content model to rich text fields (like split and text).
-#. In bedrock include the CSS in the Sass file for any component which may use it (yeah, this is not ideal, hopefully we will have better control in the future).
-#. Add a def to api.py to render the piece (like ``_make_wordmark``).
-
-.. note::
-
- Tips:
-
- * can't define defaults in Contentful, so set those in your Python def.
- * for any optional fields make sure you check the field exists before referencing the content.
-
-Adding a rich text field in a component
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Disable everything then enable: B, I, UL, OL, Link to URL, and Inline entry. You will
-want to enable some some Headings as well, H1 should be enabled very rarely. Enable
-H2-H4 using your best judgement.
-
-
-Adding support for a new product icon, size, folder
----------------------------------------------------
-
-Many content models have drop downs with identical content. For example: the Hero, Callout,
-and Wordmark models all include a "product icon". Other common fields are width and folder.
-
-There are two ways to keep these lists up to date to reflect Protocol updates:
-
-#. By opening and editing the content models individually in Contentful
-#. Scripting updates using the API
-
-At the moment it's not too time consuming to do by hand, just make sure you are copy and
-pasting to avoid introducing spelling errors.
-
-We have not tried scripting updates with the API yet. One thing to keep in mind if
-attempting this is that not all widths are available on all components. For example: the
-"Text: Four columns" component cannot be displayed in small content widths.
-
-Rich Text Rendering
--------------------
-
-Contentful provides a helper library to transform the rich text fields in the API into
-HTML content.
-
-In places were we disagree with the rendering or want to enhance the rendering we can
-provide our own renderers on the bedrock side. They can be as simple as changing `` tags
-to `` tags or as complex as inserting a component.
-
-A list of our custom renderers is passed to the `RichTextRenderer` helper at the start of
-the `ContentfulPage` class in api.py. The renderers themselves are also defined in api.py
-
-.. note::
-
- * Built-in nodes cannot be extended or customized: *Custom node types and marks are not allowed*. Embed entry types are required to extend rich text functionality. (i.e. if you need more than one style of blockquote)
-
-L10N
-----
-
-.. important::
-
- We are no longer syncing content from Contentful – see the note at the top of this page.
-
- **Please do not add new pages to Bedrock using Contentful.**
-
-
-Smartling - our selected approach
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-When setting up a content model in Contentful, fields can be designated as available for
-translation.
-
-Individual users can be associated with different languages, so when they edit
-entries they see duplicate fields for each language they can translate into.
-In addition - and in the most common case - these fields are automatically sent to
-Smartling to be translated there.
-
-Once text for translation lands in Smartling, it is batched up into jobs for
-human translation. When the work is complete, Smartling automatically updates
-the relevant Contentful entries with the translations, in the appropriate fields.
-
-Note that those translations are only visible in Contentful if you select to view
-that locale's fields, but if they are present in Contentful's datastore (and
-that locale is enabled in the API response) they will be synced down by Bedrock.
-
-On the Bedrock side, the translated content is pulled down the same way as the
-default locale's content is, and is stored in a locale-specific ContentfulEntry
-in the database.
-
-In terms of 'activation', or "Do we have all the parts to show this
-Contentful content"?, Contentful content is not evaluated in the same way as
-Fluent strings (where we will show a page in a given locale if 80% of its
-Fluent strings have been translated, falling back to en-US where not).
-
-Instead, we check that all of the required fields present in the translated
-Entry have non-null data, and if so, then the entire page is viable to show in the
-given locale. (ie, we look at fields, not strings. It's a coarser level of
-granularity compared to Fluent, because the data is organised differently -
-most of Contentful-sourced content will be rich text, not individual strings).
-
-The check about whether or not a Contentful entry is 'active' or 'localisation complete'
-happens during the main sync from Contentful. Note that there is no fallback
-locale for Contentful content other than a redirect to the en-US version of the
-page - either the page is definitely available in a locale, or it's not at all
-available in that locale.
-
-Notes:
-
- * The batching of jobs in Smartling is still manual, even though the data flow is automated. We need to keep an eye on how onerous this is, plus what the cost exposure could be like if we fully automate it.
- * The Smartling integration is currently only set to use Mozilla.org's 10 most popular locales, in addition to en-US.
- * No localisation of Contentful content happens via Pontoon.
- * The Smartling setup is most effectively leveraged with Compose-based pages rather than Connect-based components, and the latter may require some code tweaks.
- * Our Compose: SEO field in Contentful is configured for translation (and in use on the VPN Resource Center). All Compose pages require this field. If a Compose page type is *not* meant to be localised, we need to stop these SEO-related fields from going on to Smartling.
-
-
-Fluent
-~~~~~~
-
-**NB: Not selected for use, but notes retained for reference**
-
-Instead of using the language translation fields in Contentful to store translations we
-could designate one of the locales to contain a fluent string ID. Bedrock could then
-use the string IDs and the English content to create Fluent files for submission into our
-current translation system.
-
-Creation of the string IDs could be automated using Contentful's write API.
-
-To give us the ability to use fallback strings the Contentful field could accept a comma
-separated list of values.
-
-This approach requires significant integration code on the bedrock side but comes with
-the benefit of using our current translation system, including community contributions.
-
-No English Equivalent
-~~~~~~~~~~~~~~~~~~~~~
-
-**NB: Not selected for use, but notes retained for reference**
-
-Components could be created in the language they are intended to display in. The localized
-content would be written in the English content fields.
-
-The down sides of this are that we do not know what language the components are written in
-and could accidentally display the wrong language on any page. It also means that localized
-content cannot be created automatically by English editors and translations would have to
-be manually associated with URLs.
-
-This is the approach that will likely be used for the German and French homepages since
-that content is not going to be used on English pages and creating a separate homepage
-with different components is valuable to the German and French teams.
-
-Assets
-------
-
-.. important::
-
- We are no longer syncing content from Contentful – see the note at the top of this page.
-
- **Please do not add new pages to Bedrock using Contentful.**
-
-Images that are uploaded in Contentful will be served to site visitors from the Contentful
-:abbr:`CDN (Content Delivery Network)`. The cost of using the CDN are not by request so we
-don't have to worry about how many times an image will be requested.
-
-Using the Contentful :abbr:`CDN (Content Delivery Network)` lets us use their
-`Images API `_
-to format our images.
-
-In theory, a large high quality image is uploaded in Contentful and then bedrock inserts
-links to the :abbr:`CDN (Content Delivery Network)` for images which are cropped to fit their
-component and resized to fit their place on the page.
-
-Because we cannot rely on the dimensions of the image uploaded to Contentful as a guide
-for displaying the image - bedrock needs to be opinionated about what size images it requests
-based on the component and its configuration. For example, hero images are fixed at 800px
-wide. In the future this could be a user configurable option.
-
-
-Preview
--------
-
-.. important::
-
- We are no longer syncing content from Contentful – see the note at the top of this page.
-
- **Please do not add new pages to Bedrock using Contentful.**
-
-Content previews are configured under *Settings* > *Content preview* on a per-content model
-basis. At the moment previews are only configured for pages, and display on demo5.
-
-Once the code is merged into bedrock they should be updated to use the dev server.
-
-Specific URLs will only update every 5 minutes as the data is pulled from the API but pages
-can be previewed up to the second at the `contentful-preview` URL. This preview will include
-"changed" and "draft" changes (even if there is an error in the data) not just published changes.
-
-For previewing on localhost, see Development Practices, below.
-
-
-Roles/Permissions
------------------
-
-In general we are trusting people to check their work before publishing and very few
-guard rails have been installed. We have a few roles with different permissions.
-
-Admin
- Organization
-
- * Define roles and permission
- * Manage users
- * Change master and sandbox environment aliases
- * Create new environments
-
- Master environment
-
- * Edit content model
- * Create, Edit, Publish, Archive, Delete content
- * Install/Uninstall apps
-
-Developer
- Organization
-
- * Create new environments
-
- Master environment
-
- * Create, Edit, Publish, Archive content
-
- Sandbox environments (any non-master environment)
-
- * Edit content model
- * Create, Edit, Publish, Archive, Delete content
- * Install/Uninstall apps
-
-Editor (WIP)
- Master environment (through Compose)
-
- * Create, Edit, Publish, Archive content
-
-
-Development practices
----------------------
-
-.. important::
-
- We are no longer syncing content from Contentful – see the note at the top of this page.
-
- **Please do not add new pages to Bedrock using Contentful.**
-
-
-This section outlines tasks generally required if developing features against Contentful.
-
-Get bedrock set up locally to work with Contentful
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-In your ``.env`` file for Bedrock, make sure you have the followign environment variables
-set up.
-
-* ``CONTENTFUL_SPACE_ID`` - this is the ID of our Contentful integration
-* ``CONTENTFUL_SPACE_KEY`` - this is the API key that allows you access to our space. Note that two types of key are available: a Preview key allows you to load in draft content; the Delivery key only loads published contnet. For local dev, you want a Preview key.
-* ``SWITCH_CONTENTFUL_HOMEPAGE_DE`` should be set to ``True`` if you are working on the German Contentful-powered homepage
-* ``CONTENTFUL_ENVIRONMENT`` Contentful has 'branches' which it calls environments. `master` is what we use in production, and `sandbox` is generally what we use in development. It's also possible to reference a specific environment - e.g. ``CONTENTFUL_ENVIRONMENT=sandbox-2021-11-02``
-
-To get values for these vars, please check with someone on the backend team.
-
-If you are working on the Contentful Sync backed by the message-queue (and if you don't know what this is, you don't need it for local dev), you will also need to set the following env vars:
-
-* ``CONTENTFUL_NOTIFICATION_QUEUE_URL``
-* ``CONTENTFUL_NOTIFICATION_QUEUE_REGION``
-* ``CONTENTFUL_NOTIFICATION_QUEUE_ACCESS_KEY_ID``
-* ``CONTENTFUL_NOTIFICATION_QUEUE_SECRET_ACCESS_KEY``
-
-
-How to preview your changes on localhost
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-When viewing a page in Contentful, it's possible to trigger a preview of the draft page. This is typically rendered on www-dev.allizom.org. However, that's only useful for code that's already in ``main``.
-If you want to preview Contentful content on your local machine - e.g. you're working on a feature branch that isn't ready for merging - do the following:
-
-Existing (master) Content Types
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-In the right-hand sidebar of the editor page in Contentful:
-
-* Find the Preview section
-* Select ``Change`` and pick ``Localhost Preview``
-* Click ``Open preview``
-
-New (non-master) Content Types
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-In bedrock:
-
-* Update ``class ContentfulPreviewView(L10nTemplateView)`` in `Mozorg Views `_ with a render case for your new content type
-
-In the right-hand sidebar of the editor page in Contentful:
-
-* Click Info tab
-* Find ``Entry ID`` section and copy the value
-
-Manually create preview URL in browser:
-
-* `http://localhost:8000/en-US/contentful-preview/{entry_id}/`
-
-Note that previewing a page will require it to be pulled from Contentful's API, so you will need ``CONTENTFUL_SPACE_ID`` and ``CONTENTFUL_SPACE_KEY`` set in your ``.env``. It may take a few seconds to get the data.
-
-Also note that when you select ``Localhost preview``, the choice sticks, so you should set it back to ``Preview on web`` when you're done.
-
-
-How to update/refresh the sandbox environment
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-It helps to think of Contentful 'environments' as simply branches of a git-like repo full of content. You can take a particular environment and branch off it to make a new environment for :abbr:`WIP (Work in Progress)` or experimental content, using the original one as your starting point.
-On top of this, Contentful has the concept of aliases for environments and we use two aliases in our setup:
-
-* ``master`` is used for production and is an alias currently pointing to the `V1` environment. It is pretty stable and access to it is limited.
-* ``sandbox`` is used for development and more team members have access to edit content. Again, it's an alias and is pointed at an environment (think, branch) with a name in the format ``sandbox-YYYY-MM-DD``.
-
-
-While updating ``master`` is something that we generally don't do (at the moment only a product owner and/or admin would do this), updating the sandbox happens more often, typically to populate it with data more recently added to master.
-To do this:
-
-* Go to ``Settings > Environments``
-* Ensure we have at least one spare environment slot. If we don't delete the oldest ``sandbox-XXXX-XX-XX`` environment.
-* Click the blue Add Environment button, to the right. Name it using the ``sandbox-YYYY-MM-DD`` pattern and base it on whatever environment is aliased to ``master`` - this will basically create a new 'branch' with the content currently in master.
-* In the Environment Aliases section of the main page, find `sandbox` and click Change alias target, then select the ``sandbox-XXXX-XX-XX`` environment you just made.
-
-Which environment is connected to where?
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-``master`` is the environment used in Bedrock production, stage, dev and test
-``sandbox`` may, in the future, be made the default environment for dev. It's also the one we should use for local development.
-
-If you develop a new feature that adds to Contentful (e.g. page or component) and you author it in the sandbox, you will need to re-create it in master before the corresponding bedrock changes hit production.
-
-
-Troubleshooting
-~~~~~~~~~~~~~~~
-
-If you run into trouble on an issue, be sure to check in these places first and include the relevant information in requests for help (i.e. environment).
-
-Contentful Content Model & Entries
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-* What environment are you using?
-* Do you have the necessary permissions to make changes?
-* Do you see all the entry fields you need? Do those fields have the correct value options?
-
-`Bedrock API (api.py) `_
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-* What environment are you using?
-* Can you find a Python function definition for the content type you need?
-* Does it structure data as expected?
-
-.. code-block:: python
-
- # example content type def
-
-
- def get_section_data(self, entry_obj):
- fields = entry_obj.fields()
- # run `print(fields)` here to verify field values from Contentful
-
- data = {
- "component": "sectionHeading",
- "heading": fields.get("heading"),
- }
-
- # run `print(data)` here to verify data values from Bedrock API
- return data
-
-`Bedrock Render (all.html) `_
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-* Can you find a render condition for the component you need?
-
-.. code-block:: jinja
-
- /* example component condition */
-
- {% elif entry.component == 'sectionHeading' %}
-
-* If the component calls a macro:
- * Does it have all the necessary parameters?
- * Is it passing the expected values as arguments?
-* If the component is custom HTML:
- * Is the HTML structure correct?
- * Are Protocol-specific class names spelled correctly?
-* Is the component `CSS `_ available?
-* Is the component JS available?
-
-.. note::
-
- Component CSS and JS are defined in a ``CONTENT_TYPE_MAP`` from the Bedrock API (``api.py``).
-
-Bedrock Database
-^^^^^^^^^^^^^^^^
-
-Once content is synced into your local database, it can be found in the contentful_contentfulentry table. All the dependencies to explore the data are installed by default for local development.
-
-Using sqlite (with an example query to get some info about en-US pages):
-
-.. code-block:: bash
-
- ./manage.py dbshell
-
-.. code-block::
-
- select id, slug, data from contentful_contentfulentry where locale='en-US';
-
-Close the sqlite shell with ``.exit``
-
-Using Django shell (with an example query to get data from first entry of "pageProductJournalismStory" type):
-
-.. code-block:: bash
-
- ./manage.py shell
-
-.. code-block:: python
-
- from bedrock.contentful.models import ContentfulEntry
-
- product_stories = ContentfulEntry.objects.filter(
- content_type="pageProductJournalismStory",
- localisation_complete=True,
- locale="en-US",
- )
-
- product_stories[0].data # to see the data stored for the first story in the results
-
-Close the Djanjo shell with ``exit()`` or ``CTRL+D``
-
-Useful Contentful Docs
-----------------------
-
-.. important::
-
- We are no longer syncing content from Contentful – see the note at the top of this page.
-
- **Please do not add new pages to Bedrock using Contentful.**
-
-https://www.contentful.com/developers/docs/references/images-api/#/reference/resizing-&-cropping/specify-focus-area
-
-https://www.contentful.com/developers/docs/references/content-delivery-api/
-
-https://contentful.github.io/contentful.py/#filtering-options
-
-https://github.com/contentful/rich-text-renderer.py
-https://github.com/contentful/rich-text-renderer.py/blob/a1274a11e65f3f728c278de5d2bac89213b7470e/rich_text_renderer/block_renderers.py
-
-
-
-
-
-Assumptions we still need to deal with
---------------------------------------
-
- - image sizes
-
-
-Legacy
-------
-
-Since we decided to move forward the the Compose App, we no longer need the Connect content model.
-The EN-US homepage is currently still using Connect. Documentation is here for reference.
-
-* 🔗 this component is referenced by ID in bedrock (at the moment that is just the homepage but could be used to connect single components for display on non-contentful pages. For example: the latest feature box on /new)
-
-🔗 Connect
-~~~~~~~~~~
-
-These are the highest level component. They should be just a name and entry reference.
-
-The purpose of the connect is to create a stable ID that can be referenced in bedrock
-to be included in a jinja template. Right now we only do this for the homepage. This
-is because the homepage has some conditional content above and below the Contentful
-content.
-
-Using a connect component to create the link between jinja template and the Contentful
-Page entry means an entire new page can be created and proofed in Contentful before
-the bedrock homepage begins pulling that content in.
-
-In other contexts a connect content model could be created to link to entries where the
-ID may change. For example: the "Latest Firefox Features: section of /new could be
-moved to Contentful using a connect component which references 3 picto blocks.
-
-Because the ID must be added to a bedrock by a dev, only devs should be able to make new
-connect entries.
diff --git a/docs/index.rst b/docs/index.rst
index 8657bb539b2..627431aa3d3 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -30,7 +30,6 @@ Contents
testing
redirects
newsletters
- contentful
cms
sitemap
legal-docs
diff --git a/eslint.config.js b/eslint.config.js
index 173312ad77f..abedc715e33 100644
--- a/eslint.config.js
+++ b/eslint.config.js
@@ -84,7 +84,6 @@ module.exports = [
eslintConfigPrettier,
{
ignores: [
- 'contentful_migrations/**/*.cjs',
'docs/_build/**/*.js',
'media/js/ie/libs/**/*.js',
'media/js/libs/**/*.js',
diff --git a/media/css/contentful/c-logo.scss b/media/css/contentful/c-logo.scss
deleted file mode 100644
index 5ffb4f7f259..00000000000
--- a/media/css/contentful/c-logo.scss
+++ /dev/null
@@ -1,34 +0,0 @@
-// This Source Code Form is subject to the terms of the Mozilla Public
-// License, v. 2.0. If a copy of the MPL was not distributed with this
-// file, You can obtain one at https://mozilla.org/MPL/2.0/.
-
-$image-path: '/media/protocol/img';
-
-@import '~@mozilla-protocol/core/protocol/css/includes/lib';
-
-@import '~@mozilla-protocol/core/protocol/css/components/logos/logo';
-@import '~@mozilla-protocol/core/protocol/css/components/logos/logo-product-family';
-@import '~@mozilla-protocol/core/protocol/css/components/logos/logo-product-firefox';
-@import '~@mozilla-protocol/core/protocol/css/components/logos/logo-product-beta';
-@import '~@mozilla-protocol/core/protocol/css/components/logos/logo-product-developer';
-@import '~@mozilla-protocol/core/protocol/css/components/logos/logo-product-nightly';
-@import '~@mozilla-protocol/core/protocol/css/components/logos/logo-product-focus';
-@import '~@mozilla-protocol/core/protocol/css/components/logos/logo-product-monitor';
-@import '~@mozilla-protocol/core/protocol/css/components/logos/logo-product-lockwise';
-@import '~@mozilla-protocol/core/protocol/css/components/logos/logo-product-mozilla';
-@import '~@mozilla-protocol/core/protocol/css/components/logos/logo-product-vpn';
-@import '~@mozilla-protocol/core/protocol/css/components/logos/logo-product-pocket';
-@import '~@mozilla-protocol/core/protocol/css/components/logos/logo-product-relay';
-@import '~@mozilla-protocol/core/protocol/css/components/logos/wordmark';
-@import '~@mozilla-protocol/core/protocol/css/components/logos/wordmark-product-family';
-@import '~@mozilla-protocol/core/protocol/css/components/logos/wordmark-product-firefox';
-@import '~@mozilla-protocol/core/protocol/css/components/logos/wordmark-product-beta';
-@import '~@mozilla-protocol/core/protocol/css/components/logos/wordmark-product-developer';
-@import '~@mozilla-protocol/core/protocol/css/components/logos/wordmark-product-nightly';
-@import '~@mozilla-protocol/core/protocol/css/components/logos/wordmark-product-focus';
-@import '~@mozilla-protocol/core/protocol/css/components/logos/wordmark-product-monitor';
-@import '~@mozilla-protocol/core/protocol/css/components/logos/wordmark-product-lockwise';
-@import '~@mozilla-protocol/core/protocol/css/components/logos/wordmark-product-mozilla';
-@import '~@mozilla-protocol/core/protocol/css/components/logos/wordmark-product-vpn';
-@import '~@mozilla-protocol/core/protocol/css/components/logos/wordmark-product-pocket';
-@import '~@mozilla-protocol/core/protocol/css/components/logos/wordmark-product-relay';
diff --git a/media/css/contentful/firefox/c-article.scss b/media/css/contentful/firefox/c-article.scss
deleted file mode 100644
index d0d40ebbbc2..00000000000
--- a/media/css/contentful/firefox/c-article.scss
+++ /dev/null
@@ -1,10 +0,0 @@
-// This Source Code Form is subject to the terms of the Mozilla Public
-// License, v. 2.0. If a copy of the MPL was not distributed with this
-// file, You can obtain one at https://mozilla.org/MPL/2.0/.
-
-$font-path: '/media/protocol/fonts';
-$image-path: '/media/protocol/img';
-$brand-theme: 'firefox';
-
-@import '~@mozilla-protocol/core/protocol/css/includes/lib';
-@import '~@mozilla-protocol/core/protocol/css/components/article';
diff --git a/media/css/contentful/firefox/c-call-out.scss b/media/css/contentful/firefox/c-call-out.scss
deleted file mode 100644
index 842d7c7167b..00000000000
--- a/media/css/contentful/firefox/c-call-out.scss
+++ /dev/null
@@ -1,10 +0,0 @@
-// This Source Code Form is subject to the terms of the Mozilla Public
-// License, v. 2.0. If a copy of the MPL was not distributed with this
-// file, You can obtain one at https://mozilla.org/MPL/2.0/.
-
-$font-path: '/media/protocol/fonts';
-$image-path: '/media/protocol/img';
-$brand-theme: 'firefox';
-
-@import '~@mozilla-protocol/core/protocol/css/includes/lib';
-@import '~@mozilla-protocol/core/protocol/css/components/callout';
diff --git a/media/css/contentful/firefox/c-card.scss b/media/css/contentful/firefox/c-card.scss
deleted file mode 100644
index 28ab5addbf0..00000000000
--- a/media/css/contentful/firefox/c-card.scss
+++ /dev/null
@@ -1,11 +0,0 @@
-// This Source Code Form is subject to the terms of the Mozilla Public
-// License, v. 2.0. If a copy of the MPL was not distributed with this
-// file, You can obtain one at https://mozilla.org/MPL/2.0/.
-
-$font-path: '/media/protocol/fonts';
-$image-path: '/media/protocol/img';
-$brand-theme: 'firefox';
-
-@import '~@mozilla-protocol/core/protocol/css/includes/lib';
-@import '~@mozilla-protocol/core/protocol/css/components/card';
-@import '~@mozilla-protocol/core/protocol/css/templates/card-layout';
diff --git a/media/css/contentful/firefox/c-picto.scss b/media/css/contentful/firefox/c-picto.scss
deleted file mode 100644
index 220b8720711..00000000000
--- a/media/css/contentful/firefox/c-picto.scss
+++ /dev/null
@@ -1,10 +0,0 @@
-// This Source Code Form is subject to the terms of the Mozilla Public
-// License, v. 2.0. If a copy of the MPL was not distributed with this
-// file, You can obtain one at https://mozilla.org/MPL/2.0/.
-
-$font-path: '/media/protocol/fonts';
-$image-path: '/media/protocol/img';
-$brand-theme: 'firefox';
-
-@import '~@mozilla-protocol/core/protocol/css/includes/lib';
-@import '~@mozilla-protocol/core/protocol/css/components/picto';
diff --git a/media/css/contentful/firefox/c-section-heading.scss b/media/css/contentful/firefox/c-section-heading.scss
deleted file mode 100644
index e2c679b8ba9..00000000000
--- a/media/css/contentful/firefox/c-section-heading.scss
+++ /dev/null
@@ -1,10 +0,0 @@
-// This Source Code Form is subject to the terms of the Mozilla Public
-// License, v. 2.0. If a copy of the MPL was not distributed with this
-// file, You can obtain one at https://mozilla.org/MPL/2.0/.
-
-$font-path: '/media/protocol/fonts';
-$image-path: '/media/protocol/img';
-$brand-theme: 'firefox';
-
-@import '~@mozilla-protocol/core/protocol/css/includes/lib';
-@import '~@mozilla-protocol/core/protocol/css/components/section-heading';
diff --git a/media/css/contentful/firefox/c-split.scss b/media/css/contentful/firefox/c-split.scss
deleted file mode 100644
index 574b853f4b5..00000000000
--- a/media/css/contentful/firefox/c-split.scss
+++ /dev/null
@@ -1,18 +0,0 @@
-// This Source Code Form is subject to the terms of the Mozilla Public
-// License, v. 2.0. If a copy of the MPL was not distributed with this
-// file, You can obtain one at https://mozilla.org/MPL/2.0/.
-
-$font-path: '/media/protocol/fonts';
-$image-path: '/media/protocol/img';
-$brand-theme: 'firefox';
-
-@import '~@mozilla-protocol/core/protocol/css/includes/lib';
-@import '~@mozilla-protocol/core/protocol/css/components/split';
-
-@import '../patch';
-
-// Override until MDN plus logo is added to Protocol.
-// https://github.com/mozilla/protocol-assets/issues/83
-.mzp-c-wordmark.mzp-t-product-mdn-plus {
- background-image: url('/media/img/logos/mdn/mdn-plus-wordmark.svg');
-}
diff --git a/media/css/contentful/firefox/t-multi-column.scss b/media/css/contentful/firefox/t-multi-column.scss
deleted file mode 100644
index 0e6d30c4c54..00000000000
--- a/media/css/contentful/firefox/t-multi-column.scss
+++ /dev/null
@@ -1,12 +0,0 @@
-// This Source Code Form is subject to the terms of the Mozilla Public
-// License, v. 2.0. If a copy of the MPL was not distributed with this
-// file, You can obtain one at https://mozilla.org/MPL/2.0/.
-
-$font-path: '/media/protocol/fonts';
-$image-path: '/media/protocol/img';
-$brand-theme: 'firefox';
-
-@import '~@mozilla-protocol/core/protocol/css/includes/lib';
-@import '~@mozilla-protocol/core/protocol/css/templates/multi-column';
-
-@import '../patch';
diff --git a/media/css/contentful/mozilla/c-article.scss b/media/css/contentful/mozilla/c-article.scss
deleted file mode 100644
index ca44cadfebb..00000000000
--- a/media/css/contentful/mozilla/c-article.scss
+++ /dev/null
@@ -1,10 +0,0 @@
-// This Source Code Form is subject to the terms of the Mozilla Public
-// License, v. 2.0. If a copy of the MPL was not distributed with this
-// file, You can obtain one at https://mozilla.org/MPL/2.0/.
-
-$font-path: '/media/protocol/fonts';
-$image-path: '/media/protocol/img';
-$brand-theme: 'mozilla';
-
-@import '~@mozilla-protocol/core/protocol/css/includes/lib';
-@import '~@mozilla-protocol/core/protocol/css/components/article';
diff --git a/media/css/contentful/mozilla/c-call-out.scss b/media/css/contentful/mozilla/c-call-out.scss
deleted file mode 100644
index 1ae9b29aa99..00000000000
--- a/media/css/contentful/mozilla/c-call-out.scss
+++ /dev/null
@@ -1,10 +0,0 @@
-// This Source Code Form is subject to the terms of the Mozilla Public
-// License, v. 2.0. If a copy of the MPL was not distributed with this
-// file, You can obtain one at https://mozilla.org/MPL/2.0/.
-
-$font-path: '/media/protocol/fonts';
-$image-path: '/media/protocol/img';
-$brand-theme: 'mozilla';
-
-@import '~@mozilla-protocol/core/protocol/css/includes/lib';
-@import '~@mozilla-protocol/core/protocol/css/components/callout';
diff --git a/media/css/contentful/mozilla/c-card.scss b/media/css/contentful/mozilla/c-card.scss
deleted file mode 100644
index 9ea7db1f261..00000000000
--- a/media/css/contentful/mozilla/c-card.scss
+++ /dev/null
@@ -1,11 +0,0 @@
-// This Source Code Form is subject to the terms of the Mozilla Public
-// License, v. 2.0. If a copy of the MPL was not distributed with this
-// file, You can obtain one at https://mozilla.org/MPL/2.0/.
-
-$font-path: '/media/protocol/fonts';
-$image-path: '/media/protocol/img';
-$brand-theme: 'mozilla';
-
-@import '~@mozilla-protocol/core/protocol/css/includes/lib';
-@import '~@mozilla-protocol/core/protocol/css/components/card';
-@import '~@mozilla-protocol/core/protocol/css/templates/card-layout';
diff --git a/media/css/contentful/mozilla/c-picto.scss b/media/css/contentful/mozilla/c-picto.scss
deleted file mode 100644
index 4b4070fd8e9..00000000000
--- a/media/css/contentful/mozilla/c-picto.scss
+++ /dev/null
@@ -1,10 +0,0 @@
-// This Source Code Form is subject to the terms of the Mozilla Public
-// License, v. 2.0. If a copy of the MPL was not distributed with this
-// file, You can obtain one at https://mozilla.org/MPL/2.0/.
-
-$font-path: '/media/protocol/fonts';
-$image-path: '/media/protocol/img';
-$brand-theme: 'mozilla';
-
-@import '~@mozilla-protocol/core/protocol/css/includes/lib';
-@import '~@mozilla-protocol/core/protocol/css/components/picto';
diff --git a/media/css/contentful/mozilla/c-section-heading.scss b/media/css/contentful/mozilla/c-section-heading.scss
deleted file mode 100644
index 8ef028c5761..00000000000
--- a/media/css/contentful/mozilla/c-section-heading.scss
+++ /dev/null
@@ -1,10 +0,0 @@
-// This Source Code Form is subject to the terms of the Mozilla Public
-// License, v. 2.0. If a copy of the MPL was not distributed with this
-// file, You can obtain one at https://mozilla.org/MPL/2.0/.
-
-$font-path: '/media/protocol/fonts';
-$image-path: '/media/protocol/img';
-$brand-theme: 'mozilla';
-
-@import '~@mozilla-protocol/core/protocol/css/includes/lib';
-@import '~@mozilla-protocol/core/protocol/css/components/section-heading';
diff --git a/media/css/contentful/mozilla/c-split.scss b/media/css/contentful/mozilla/c-split.scss
deleted file mode 100644
index 2bfbd65dbd2..00000000000
--- a/media/css/contentful/mozilla/c-split.scss
+++ /dev/null
@@ -1,34 +0,0 @@
-// This Source Code Form is subject to the terms of the Mozilla Public
-// License, v. 2.0. If a copy of the MPL was not distributed with this
-// file, You can obtain one at https://mozilla.org/MPL/2.0/.
-
-$font-path: '/media/protocol/fonts';
-$image-path: '/media/protocol/img';
-$brand-theme: 'mozilla';
-
-@import '~@mozilla-protocol/core/protocol/css/includes/lib';
-@import '~@mozilla-protocol/core/protocol/css/components/split';
-
-@import '../patch';
-
-// Temporary fix to center the wordmark and logo on mobile when the split component also has the class "mzp-l-split-center-on-sm-md"
-.mzp-c-split.mzp-l-split-center-on-sm-md {
- .mzp-c-wordmark,
- .mzp-c-logo {
- background-position: center;
- margin-left: auto;
- margin-right: auto;
-
- @media #{$mq-md} {
- background-position: top left;
- margin-left: 0;
- margin-right: 0;
- }
- }
-}
-
-// Override until MDN plus logo is added to Protocol.
-// https://github.com/mozilla/protocol-assets/issues/83
-.mzp-c-wordmark.mzp-t-product-mdn-plus {
- background-image: url('/media/img/logos/mdn/mdn-plus-wordmark.svg');
-}
diff --git a/media/css/contentful/mozilla/t-multi-column.scss b/media/css/contentful/mozilla/t-multi-column.scss
deleted file mode 100644
index 94e1c39811c..00000000000
--- a/media/css/contentful/mozilla/t-multi-column.scss
+++ /dev/null
@@ -1,12 +0,0 @@
-// This Source Code Form is subject to the terms of the Mozilla Public
-// License, v. 2.0. If a copy of the MPL was not distributed with this
-// file, You can obtain one at https://mozilla.org/MPL/2.0/.
-
-$font-path: '/media/protocol/fonts';
-$image-path: '/media/protocol/img';
-$brand-theme: 'mozilla';
-
-@import '~@mozilla-protocol/core/protocol/css/includes/lib';
-@import '~@mozilla-protocol/core/protocol/css/templates/multi-column';
-
-@import '../patch';
diff --git a/media/css/contentful/patch.scss b/media/css/contentful/patch.scss
deleted file mode 100644
index e7dfef1b06b..00000000000
--- a/media/css/contentful/patch.scss
+++ /dev/null
@@ -1,12 +0,0 @@
-// This Source Code Form is subject to the terms of the Mozilla Public
-// License, v. 2.0. If a copy of the MPL was not distributed with this
-// file, You can obtain one at https://mozilla.org/MPL/2.0/.
-
-.mzp-u-center {
- text-align: center;
-
- .mzp-c-logo,
- .mzp-c-wordmark {
- margin: 0 auto;
- }
-}
diff --git a/media/static-bundles.json b/media/static-bundles.json
index 863e8488944..d09b477f100 100644
--- a/media/static-bundles.json
+++ b/media/static-bundles.json
@@ -952,96 +952,6 @@
],
"name": "mozilla-vpn-article"
},
- {
- "files": [
- "css/contentful/mozilla/c-article.scss"
- ],
- "name": "mozilla-c-article"
- },
- {
- "files": [
- "css/contentful/mozilla/c-call-out.scss"
- ],
- "name": "mozilla-c-call-out"
- },
- {
- "files": [
- "css/contentful/mozilla/c-picto.scss"
- ],
- "name": "mozilla-c-picto"
- },
- {
- "files": [
- "css/contentful/mozilla/c-section-heading.scss"
- ],
- "name": "mozilla-c-section-heading"
- },
- {
- "files": [
- "css/contentful/mozilla/c-split.scss"
- ],
- "name": "mozilla-c-split"
- },
- {
- "files": [
- "css/contentful/mozilla/c-card.scss"
- ],
- "name": "mozilla-c-card"
- },
- {
- "files": [
- "css/contentful/mozilla/t-multi-column.scss"
- ],
- "name": "mozilla-t-multi-column"
- },
- {
- "files": [
- "css/contentful/firefox/c-article.scss"
- ],
- "name": "firefox-c-article"
- },
- {
- "files": [
- "css/contentful/firefox/c-call-out.scss"
- ],
- "name": "firefox-c-call-out"
- },
- {
- "files": [
- "css/contentful/firefox/c-picto.scss"
- ],
- "name": "firefox-c-picto"
- },
- {
- "files": [
- "css/contentful/firefox/c-section-heading.scss"
- ],
- "name": "firefox-c-section-heading"
- },
- {
- "files": [
- "css/contentful/firefox/c-split.scss"
- ],
- "name": "firefox-c-split"
- },
- {
- "files": [
- "css/contentful/c-logo.scss"
- ],
- "name": "c-logo"
- },
- {
- "files": [
- "css/contentful/firefox/c-card.scss"
- ],
- "name": "firefox-c-card"
- },
- {
- "files": [
- "css/contentful/firefox/t-multi-column.scss"
- ],
- "name": "firefox-t-multi-column"
- },
{
"files": [
"css/products/vpn/resource-center.scss"
@@ -1139,13 +1049,13 @@
"name": "firefox-built-for-you"
},
{
- "files": [
+ "files": [
"css/m24/root.scss"
],
"name": "m24-root"
},
{
- "files": [
+ "files": [
"css/m24/base.scss"
],
"name": "m24-base"
@@ -1237,7 +1147,7 @@
],
"name": "firefox_welcome_page16"
},
- {
+ {
"files": [
"js/firefox/welcome/welcome19-to-24.js"
],
diff --git a/pyproject.toml b/pyproject.toml
index 2504a89d3f4..05240f6ebd2 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -21,7 +21,6 @@ select = [
[tool.ruff.lint.per-file-ignores]
"bedrock/settings/__init__.py" = ["F405"]
-"bedrock/contentful/tests/data.py" = ["E501"]
[tool.ruff.lint.isort]
known-first-party = ["bedrock", "lib", "pages"]
diff --git a/requirements/dev.txt b/requirements/dev.txt
index 984917f8525..1e2cc6b3a16 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -292,10 +292,6 @@ compare-locales==9.0.4 \
# via
# -r requirements/dev.in
# cl-ext-lang
-contentful==2.2.0 \
- --hash=sha256:11a63bbb7963fd905a3408aed2064b0468cd44969c6e99ab43923073113b80af \
- --hash=sha256:c181c94f9d9d7fa3a9124b0538492f9c8cc01a1be9550598b170ce43402d89c0
- # via -r requirements/prod.txt
contextlib2==21.6.0 \
--hash=sha256:3fbdb64466afd23abaf6c977627b75b6139a5a3e8ce38405c5b413aed7a0471f \
--hash=sha256:ab1e2bfe1d01d968e1b7e8d9023bc51ef3509bba217bb730cee3827e1ee82869
@@ -1718,7 +1714,6 @@ python-dateutil==2.9.0.post0 \
# via
# -r requirements/prod.txt
# botocore
- # contentful
# faker
# freezegun
pytz==2024.2 \
@@ -1914,7 +1909,6 @@ requests==2.32.3 \
# -r requirements/prod.txt
# basket-client
# bpython
- # contentful
# datadog
# django-mozilla-product-details
# google-api-core
diff --git a/requirements/prod.in b/requirements/prod.in
index 691a9859175..3ca0d80e60c 100644
--- a/requirements/prod.in
+++ b/requirements/prod.in
@@ -5,7 +5,6 @@ bleach[css]==6.1.0
boto3==1.35.99
chardet==5.2.0
commonware==0.6.0
-contentful==2.2.0
contextlib2==21.6.0
dirsync==2.2.5
dj-database-url==2.3.0
diff --git a/requirements/prod.txt b/requirements/prod.txt
index 36722cbe6b6..116507562c8 100644
--- a/requirements/prod.txt
+++ b/requirements/prod.txt
@@ -245,10 +245,6 @@ commonware==0.6.0 \
--hash=sha256:0e9520986e292f2bf8cdf80b32f21ef01e4058fd7baa61d2d282d21ed7085b1f \
--hash=sha256:f596962fd11bc53b5453ffa766dc99f297895021946096d3e6b4826f9ae075ea
# via -r requirements/prod.in
-contentful==2.2.0 \
- --hash=sha256:11a63bbb7963fd905a3408aed2064b0468cd44969c6e99ab43923073113b80af \
- --hash=sha256:c181c94f9d9d7fa3a9124b0538492f9c8cc01a1be9550598b170ce43402d89c0
- # via -r requirements/prod.in
contextlib2==21.6.0 \
--hash=sha256:3fbdb64466afd23abaf6c977627b75b6139a5a3e8ce38405c5b413aed7a0471f \
--hash=sha256:ab1e2bfe1d01d968e1b7e8d9023bc51ef3509bba217bb730cee3827e1ee82869
@@ -1252,7 +1248,6 @@ python-dateutil==2.9.0.post0 \
--hash=sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427
# via
# botocore
- # contentful
pytz==2024.2 \
--hash=sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a \
--hash=sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725
@@ -1341,7 +1336,6 @@ requests==2.32.3 \
# via
# -r requirements/prod.in
# basket-client
- # contentful
# datadog
# django-mozilla-product-details
# google-api-core
diff --git a/tests/redirects/map_globalconf.py b/tests/redirects/map_globalconf.py
index 87e7faa15e1..0c863e321fd 100644
--- a/tests/redirects/map_globalconf.py
+++ b/tests/redirects/map_globalconf.py
@@ -1328,5 +1328,13 @@
),
# Issue 15386
url_test("/products/vpn/resource-center/no-Logging-vpn-from-mozilla/", "/products/vpn/resource-center/no-logging-vpn-from-mozilla/"),
+ # Issue 15843
+ url_test("/products/vpn/more/what-is-an-ip-address/", "/products/vpn/resource-center/what-is-an-ip-address/"),
+ url_test(
+ "/products/vpn/more/the-difference-between-a-vpn-and-a-web-proxy/",
+ "/products/vpn/resource-center/the-difference-between-a-vpn-and-a-web-proxy/",
+ ),
+ url_test("/products/vpn/more/what-is-a-vpn/", "/products/vpn/resource-center/what-is-a-vpn/"),
+ url_test("/products/vpn/more/5-reasons-you-should-use-a-vpn/", "/products/vpn/resource-center/5-reasons-you-should-use-a-vpn/"),
)
)