From cd63baaf63f29be1eab5c3f4f42ef0924b0dddba Mon Sep 17 00:00:00 2001 From: Phillip Stanley Date: Tue, 24 Sep 2024 10:39:34 +0100 Subject: [PATCH] CDD-2169: New landing page --- .../management/commands/build_cms_site.py | 6 + .../build_cms_site_helpers/__init__.py | 1 + .../build_cms_site_helpers/landing_page.py | 135 ++ .../commands/build_cms_site_helpers/pages.py | 23 + .../cms_starting_pages/landing_page.json | 30 + cms/dynamic_content/access.py | 7 + cms/dynamic_content/blocks.py | 9 + cms/dynamic_content/cards.py | 11 +- cms/dynamic_content/elements.py | 6 + cms/dynamic_content/help_texts.py | 6 +- cms/dynamic_content/sections.py | 13 + cms/landing_page/__init__.py | 0 cms/landing_page/apps.py | 6 + cms/landing_page/managers.py | 35 + cms/landing_page/migrations/0001_initial.py | 1220 +++++++++++++++++ cms/landing_page/migrations/__init__.py | 0 cms/landing_page/models.py | 75 + .../field_choices_callables.py | 21 + cms/metrics_interface/interface.py | 12 + metrics/api/settings/default.py | 1 + metrics/domain/common/utils.py | 12 + .../factories/cms/landing_page_factory.py | 20 + tests/fakes/models/cms/landing_page.py | 17 + tests/fakes/models/cms/page_link_chooser.py | 11 + .../cms/dynamic_content/__init__.py | 0 .../dynamic_content/test_page_link_chooser.py | 47 + .../integration/cms/landing_page/__init__.py | 0 .../cms/landing_page/test_managers.py | 37 + .../dynamic_content/test_page_link_chooser.py | 21 + tests/unit/cms/landing_page/__init__.py | 0 tests/unit/cms/landing_page/test_models.py | 103 ++ .../test_field_choices_callables.py | 21 + .../cms/metrics_interface/test_interface.py | 20 + .../unit/metrics/domain/common/test_utils.py | 15 + 34 files changed, 1935 insertions(+), 6 deletions(-) create mode 100644 cms/dashboard/management/commands/build_cms_site_helpers/landing_page.py create mode 100644 cms/dashboard/templates/cms_starting_pages/landing_page.json create mode 100644 cms/landing_page/__init__.py create mode 100644 cms/landing_page/apps.py create mode 100644 cms/landing_page/managers.py create mode 100644 cms/landing_page/migrations/0001_initial.py create mode 100644 cms/landing_page/migrations/__init__.py create mode 100644 cms/landing_page/models.py create mode 100644 tests/fakes/factories/cms/landing_page_factory.py create mode 100644 tests/fakes/models/cms/landing_page.py create mode 100644 tests/fakes/models/cms/page_link_chooser.py create mode 100644 tests/integration/cms/dynamic_content/__init__.py create mode 100644 tests/integration/cms/dynamic_content/test_page_link_chooser.py create mode 100644 tests/integration/cms/landing_page/__init__.py create mode 100644 tests/integration/cms/landing_page/test_managers.py create mode 100644 tests/unit/cms/dynamic_content/test_page_link_chooser.py create mode 100644 tests/unit/cms/landing_page/__init__.py create mode 100644 tests/unit/cms/landing_page/test_models.py diff --git a/cms/dashboard/management/commands/build_cms_site.py b/cms/dashboard/management/commands/build_cms_site.py index 1385be100..23d48cd8a 100644 --- a/cms/dashboard/management/commands/build_cms_site.py +++ b/cms/dashboard/management/commands/build_cms_site.py @@ -57,6 +57,7 @@ def handle(self, *args, **options): is_default_site=True, ) + # Deprecated: to be removed after migration to landing page version two. landing_dashboard_page: HomePage = ( build_cms_site_helpers.create_landing_dashboard_page(parent_page=root_page) ) @@ -121,6 +122,11 @@ def handle(self, *args, **options): name="cold_health_alerts", parent_page=weather_health_alerts_page ) + # landing page version two + build_cms_site_helpers.create_landing_dashboard_page_version_two( + parent_page=root_page + ) + build_cms_site_helpers.create_menu_snippet() @staticmethod diff --git a/cms/dashboard/management/commands/build_cms_site_helpers/__init__.py b/cms/dashboard/management/commands/build_cms_site_helpers/__init__.py index 76f1f051e..37f91d499 100644 --- a/cms/dashboard/management/commands/build_cms_site_helpers/__init__.py +++ b/cms/dashboard/management/commands/build_cms_site_helpers/__init__.py @@ -4,6 +4,7 @@ create_composite_page, create_bulk_downloads_page, create_landing_dashboard_page, + create_landing_dashboard_page_version_two, create_whats_new_parent_page, create_whats_new_child_entry, ) diff --git a/cms/dashboard/management/commands/build_cms_site_helpers/landing_page.py b/cms/dashboard/management/commands/build_cms_site_helpers/landing_page.py new file mode 100644 index 000000000..03360ca6e --- /dev/null +++ b/cms/dashboard/management/commands/build_cms_site_helpers/landing_page.py @@ -0,0 +1,135 @@ +from cms.composite.models import CompositePage +from cms.topic.models import TopicPage + + +def create_landing_page_body_wih_page_links() -> list[dict]: + covid_page = TopicPage.objects.get(slug="covid-19") + influenza_page = TopicPage.objects.get(slug="influenza") + other_respiratory_viruses_page = TopicPage.objects.get( + slug="other-respiratory-viruses" + ) + weather_health_alerts_page = CompositePage.objects.get(slug="weather-health-alerts") + + return [ + { + "type": "section", + "value": { + "heading": "Respiratory viruses", + "page_link": None, + "content": [ + { + "type": "chart_row_card", + "value": { + "columns": [ + { + "type": "simplified_chart_with_link", + "value": { + "title": "COVID-19", + "sub_title": "Cases reported", + "tag_manager_event_id": "", + "topic_page": covid_page.id, + "x_axis": "date", + "y_axis": "metric", + "chart": [ + { + "type": "plot", + "value": { + "topic": "COVID-19", + "metric": "COVID-19_cases_countRollingMean", + "geography": "England", + "geography_type": "Nation", + "sex": "all", + "age": "all", + "stratum": "default", + "chart_type": "line_single_simplified", + }, + "id": "0cb2a953-8737-4978-9886-d3943b76820a", + } + ], + }, + "id": "8b2ca8aa-7bdb-4c47-835c-dc1c09f767cf", + }, + { + "type": "simplified_chart_with_link", + "value": { + "title": "Influenza", + "sub_title": "Healthcare admission rates", + "tag_manager_event_id": "", + "topic_page": influenza_page.id, + "x_axis": "date", + "y_axis": "metric", + "chart": [ + { + "type": "plot", + "value": { + "topic": "Influenza", + "metric": "influenza_healthcare_ICUHDUadmissionRateByWeek", + "geography": "England", + "geography_type": "Nation", + "sex": "all", + "age": "all", + "stratum": "default", + "chart_type": "line_single_simplified", + }, + "id": "7423460c-aa0c-482d-8fd5-ab9c62396657", + } + ], + }, + "id": "b7b37be5-8bc0-4f88-8310-7a91430b7993", + }, + { + "type": "simplified_chart_with_link", + "value": { + "title": "RSV", + "sub_title": "Healthcare admission rates", + "tag_manager_event_id": "", + "topic_page": other_respiratory_viruses_page.id, + "x_axis": "date", + "y_axis": "metric", + "chart": [ + { + "type": "plot", + "value": { + "topic": "RSV", + "metric": "RSV_healthcare_admissionRateByWeek", + "geography": "England", + "geography_type": "Nation", + "sex": "all", + "age": "all", + "stratum": "default", + "chart_type": "line_single_simplified", + }, + "id": "f9eb94ff-0d92-4265-a88b-d52bf73532a5", + } + ], + }, + "id": "0a830ab5-b232-47f1-a5af-f140e0f07c23", + }, + ] + }, + "id": "40d17855-71e3-424e-a662-2f6930073e59", + } + ], + }, + "id": "c3ec156e-e58b-4976-b0f2-c08c1e933467", + }, + { + "type": "section", + "value": { + "heading": "Weather health alerts", + "page_link": weather_health_alerts_page.id, + "content": [ + { + "type": "weather_health_alert_card", + "value": { + "title": "weather health alerts", + "sub_title": "Across England", + "alert_type": "heat", + }, + "id": "d01c65cb-4cd2-4e07-bd6e-71ea0ec04594", + } + ], + }, + "id": "be533e25-ba91-4e86-8a45-1314ed395fb9", + }, + ] diff --git a/cms/dashboard/management/commands/build_cms_site_helpers/pages.py b/cms/dashboard/management/commands/build_cms_site_helpers/pages.py index f19609430..8e265f8cc 100644 --- a/cms/dashboard/management/commands/build_cms_site_helpers/pages.py +++ b/cms/dashboard/management/commands/build_cms_site_helpers/pages.py @@ -5,7 +5,11 @@ from cms.common.models import CommonPage, CommonPageRelatedLink from cms.composite.models import CompositePage, CompositeRelatedLink +from cms.dashboard.management.commands.build_cms_site_helpers.landing_page import ( + create_landing_page_body_wih_page_links, +) from cms.home.models import HomePage, HomePageRelatedLink +from cms.landing_page.models import LandingPage from cms.snippets.data_migrations.operations import ( get_or_create_download_button_internal_button_snippet, ) @@ -41,6 +45,7 @@ def _add_page_to_parent(*, page: Page, parent_page: Page) -> None: page.save_revision().publish() +# Deprecated: to be removed after version two migration. def create_landing_dashboard_page(*, parent_page: Page) -> HomePage: data = open_example_page_response(page_name="ukhsa_data_dashboard") @@ -63,6 +68,24 @@ def create_landing_dashboard_page(*, parent_page: Page) -> HomePage: return page +def create_landing_dashboard_page_version_two(*, parent_page: Page) -> LandingPage: + data = open_example_page_response(page_name="landing_page") + + landing_page_body = create_landing_page_body_wih_page_links() + + page = LandingPage( + title=data["title"], + sub_title=data["sub_title"], + body=landing_page_body, + slug=data["meta"]["slug"], + seo_title=data["meta"]["seo_title"], + search_description=data["meta"]["search_description"], + ) + _add_page_to_parent(page=page, parent_page=parent_page) + + return page + + def create_topic_page(*, name: str, parent_page: Page) -> TopicPage: data = open_example_page_response(page_name=name) diff --git a/cms/dashboard/templates/cms_starting_pages/landing_page.json b/cms/dashboard/templates/cms_starting_pages/landing_page.json new file mode 100644 index 000000000..d73274033 --- /dev/null +++ b/cms/dashboard/templates/cms_starting_pages/landing_page.json @@ -0,0 +1,30 @@ +{ + "id": 79, + "meta": { + "search_description": "", + "type": "landing_page.LandingPage", + "detail_url": "https://http:/api/pages/79/", + "html_url": "https://http://localhost:3000/landing-page/", + "slug": "landing-page", + "show_in_menus": false, + "seo_title": "Home | UKHSA data dashboard", + "first_published_at": "2024-09-20T13:06:42.106160+01:00", + "alias_of": null, + "parent": { + "id": 3, + "meta": { + "type": "home.UKHSARootPage", + "detail_url": "https://http:/api/pages/3/", + "html_url": null + }, + "title": "UKHSA Dashboard Root" + } + }, + "title": "Landing page", + "sub_title": "showing public health data across England", + "body": "", + "last_published_at": "2024-09-20T14:04:30.332316+01:00", + "last_updated_at": "2024-09-24T16:40:55.228390+01:00", + "seo_change_frequency": 5, + "seo_priority": "0.5" +} diff --git a/cms/dynamic_content/access.py b/cms/dynamic_content/access.py index 78937252f..ec1884c41 100644 --- a/cms/dynamic_content/access.py +++ b/cms/dynamic_content/access.py @@ -15,6 +15,13 @@ use_json_field=True, ) +ALLOWABLE_BODY_CONTENT_SECTION_LINK = StreamField( + [ + ("section", sections.SectionWithLink()), + ], + use_json_field=True, +) + ALLOWABLE_BODY_CONTENT_TEXT_SECTION = StreamField( [ ("section", sections.TextSection()), diff --git a/cms/dynamic_content/blocks.py b/cms/dynamic_content/blocks.py index 23b945a21..c29cc453c 100644 --- a/cms/dynamic_content/blocks.py +++ b/cms/dynamic_content/blocks.py @@ -117,3 +117,12 @@ def get_api_representation(cls, value, context=None) -> dict | None: "geography_code": value.geography_code, } return None + + +class PageLinkChooserBlock(blocks.PageChooserBlock): + @classmethod + def get_api_representation(cls, value, context=None) -> str | None: + if value: + return value.full_url + + return None diff --git a/cms/dynamic_content/cards.py b/cms/dynamic_content/cards.py index d2523e658..54402b3f8 100644 --- a/cms/dynamic_content/cards.py +++ b/cms/dynamic_content/cards.py @@ -6,6 +6,7 @@ from cms.dynamic_content.blocks import ( HeadlineNumberBlockTypes, MetricNumberBlock, + PageLinkChooserBlock, ) from cms.dynamic_content.components import ( ChartComponent, @@ -21,7 +22,7 @@ MAXIMUM_HEADLINES_IN_CHART_CARD_COLUMN_COUNT: int = 2 MINIMUM_COLUMNS_CHART_COLUMNS_COUNT: int = 1 -MAXIMUM_COLUMNS_CHART_COLUMNS_COUNT: int = 2 +MAXIMUM_COLUMNS_CHART_COLUMNS_COUNT: int = 3 MAXIMUM_TOPIC_TREND_CARD_CHARTS: int = 1 MAXIMUM_TREND_NUMBER: int = 1 @@ -40,8 +41,8 @@ class Meta: class WHAlerts(models.TextChoices): - HEAT = "Heat" - COLD = "Cold" + HEAT = "heat" + COLD = "cold" @classmethod def get_alerts(cls) -> tuple[tuple[str, str]]: @@ -108,13 +109,13 @@ class Meta: class SimplifiedChartWithLink(blocks.StructBlock): title = blocks.TextBlock(required=True, help_text=help_texts.TITLE_FIELD) - body = blocks.TextBlock(required=False, help_text=help_texts.OPTIONAL_BODY_FIELD) + sub_title = blocks.CharBlock(required=False, help_text=help_texts.SUB_TITLE_FIELD) tag_manager_event_id = blocks.CharBlock( required=False, help_text=help_texts.TAG_MANAGER_EVENT_ID_FIELD, label="Tag manager event ID", ) - topic_page = blocks.PageChooserBlock( + topic_page = PageLinkChooserBlock( page_type="topic.TopicPage", required=True, help_text=help_texts.TOPIC_PAGE_FIELD, diff --git a/cms/dynamic_content/elements.py b/cms/dynamic_content/elements.py index ed6bccdf1..5825474e4 100644 --- a/cms/dynamic_content/elements.py +++ b/cms/dynamic_content/elements.py @@ -15,6 +15,7 @@ get_chart_types, get_colours, get_headline_chart_types, + get_simplified_chart_types, ) DEFAULT_GEOGRAPHY = "England" @@ -151,3 +152,8 @@ class SimplifiedChartPlotElement(BaseMetricsElement): choices=get_all_timeseries_metric_names, help_text=help_texts.METRIC_FIELD, ) + chart_type = blocks.ChoiceBlock( + required=True, + choices=get_simplified_chart_types, + default=DEFAULT_SIMPLIFIED_CHART_TYPE, + ) diff --git a/cms/dynamic_content/help_texts.py b/cms/dynamic_content/help_texts.py index ef5910097..774466ff7 100644 --- a/cms/dynamic_content/help_texts.py +++ b/cms/dynamic_content/help_texts.py @@ -28,7 +28,11 @@ """ TOPIC_PAGE_FIELD: str = """ -The related topic page you want to link to. +The related topic page you want to link to. Eg: `COVID-19` +""" + +INDEX_PAGE_FIELD: str = """ +The related index page you want to link to. Eg: `Respiratory viruses` or `Outbreaks` """ HEADLINE_BLOCK_FIELD: str = """ diff --git a/cms/dynamic_content/sections.py b/cms/dynamic_content/sections.py index 0a6fdbe23..a2406d1e5 100644 --- a/cms/dynamic_content/sections.py +++ b/cms/dynamic_content/sections.py @@ -23,6 +23,19 @@ class Meta: icon = "thumbtack" +class SectionWithLink(StructBlock): + heading = TextBlock(help_text=help_texts.HEADING_BLOCK, required=True) + page_link = blocks.PageLinkChooserBlock( + page_type=["composite.CompositePage"], + required=False, + help_text=help_texts.INDEX_PAGE_FIELD, + ) + content = ContentCards(help_text=help_texts.CONTENT_ROW_CARDS) + + class Meta: + icon = "thumbtack" + + class TextSection(StructBlock): title = TextBlock(help_text=help_texts.HEADING_BLOCK, required=True) body = RichTextBlock(help_text=help_texts.REQUIRED_BODY_FIELD, required=True) diff --git a/cms/landing_page/__init__.py b/cms/landing_page/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/cms/landing_page/apps.py b/cms/landing_page/apps.py new file mode 100644 index 000000000..dca74deff --- /dev/null +++ b/cms/landing_page/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class LandingPageConfig(AppConfig): + default_auto_fields = "django.db.models.BigAutoField" + name = "cms.landing_page" diff --git a/cms/landing_page/managers.py b/cms/landing_page/managers.py new file mode 100644 index 000000000..487281dfa --- /dev/null +++ b/cms/landing_page/managers.py @@ -0,0 +1,35 @@ +from django.db import models +from wagtail.models import PageManager +from wagtail.query import PageQuerySet + +EXPECTED_LANDING_PAGE_SLUG = "data_dashboard" + + +class LandingPageQuerySet(PageQuerySet): + """Custom queryset which can be used by the `LandingPageManager`""" + + def get_landing_page(self) -> models.QuerySet: + """Gets the designated landing page. + + Returns: + QuerySet: A queryset of the individual landing page: + Examples: + `]>` + """ + return self.filter(slug=EXPECTED_LANDING_PAGE_SLUG) + + +class LandingPageManager(PageManager): + """Custom model manager class for the `LandingPage` model.""" + + def get_queryset(self) -> LandingPageQuerySet: + return LandingPageQuerySet(model=self.model, using=self.db) + + def get_landing_page(self) -> "LandingPage": + """Gets the designated landing page. + + Returns: + The designated landing page object + which has the slug of `data-dashboard` + """ + return self.get_queryset().get_landing_page().last() diff --git a/cms/landing_page/migrations/0001_initial.py b/cms/landing_page/migrations/0001_initial.py new file mode 100644 index 000000000..3b01d14aa --- /dev/null +++ b/cms/landing_page/migrations/0001_initial.py @@ -0,0 +1,1220 @@ +# Generated by Django 5.0.6 on 2024-09-25 14:44 + +import cms.dynamic_content.blocks +import cms.dynamic_content.cards +import cms.metrics_interface.field_choices_callables +import django.core.validators +import django.db.models.deletion +import wagtail.blocks +import wagtail.fields +from decimal import Decimal +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ("wagtailcore", "0093_uploadedfile"), + ] + + operations = [ + migrations.CreateModel( + name="LandingPage", + fields=[ + ( + "page_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="wagtailcore.page", + ), + ), + ( + "seo_change_frequency", + models.IntegerField( + choices=[ + (1, "Always"), + (2, "Hourly"), + (3, "Daily"), + (4, "Weekly"), + (5, "Monthly"), + (6, "Yearly"), + (7, "Never"), + ], + default=5, + help_text="

This value tells search engines how often a page’s content updates, offering a hint for crawling prioritization.

\n

Always: This means the page is constantly changing with important, up-to-the-minute updates. \nA subreddit index page, a stock market data page, and the index page of a major news site might use this tag.

\n

Hourly: The page is updated on an hourly basis or thereabouts.\n Major news sites, weather sites, and active web forums might use this tag.

\n

Daily: The page is updated with new content on average once a day. \nSmall web forums, classified ad pages, daily newspapers, and daily blogs might use this tag for their homepage.

\n

Weekly: The page is updated around once a week with new content. \nProduct info pages with daily pricing information, small blogs, and website directories use this tag.

\n

Monthly: The page is updated around once a month; maybe more, maybe less. \nCategory pages, evergreen guides with updated information, and FAQs often use this tag.

\n

Yearly: The page is rarely updated but may receive updates once or twice a year. \nMany static pages, such as registration pages, About pages, and privacy policies, fall into this category.

\n

Never: The page is never going to be updated. \nOld blog entries, old news stories, and completely static pages fall into this category.

", + verbose_name="SEO change frequency", + ), + ), + ( + "seo_priority", + models.DecimalField( + decimal_places=1, + default=0.5, + help_text="\nThis value signals the importance of a page to search engines. \nAssigning accurate priority values to key pages of your site can help search engines understand \nthe structure and hierarchy of your content.\nThis must be a number between 0.1 - 1.0.\n", + max_digits=2, + validators=[ + django.core.validators.MaxValueValidator(Decimal("1.0")), + django.core.validators.MinValueValidator(Decimal("0.1")), + ], + verbose_name="SEO priority", + ), + ), + ( + "body", + wagtail.fields.StreamField( + [ + ( + "section", + wagtail.blocks.StructBlock( + [ + ( + "heading", + wagtail.blocks.TextBlock( + help_text="\nThe text you add here will be used as the heading for this section. \n", + required=True, + ), + ), + ( + "page_link", + cms.dynamic_content.blocks.PageLinkChooserBlock( + help_text="\nThe related index page you want to link to. Eg: `Respiratory viruses` or `Outbreaks`\n", + page_type=["composite.CompositePage"], + required=False, + ), + ), + ( + "content", + wagtail.blocks.StreamBlock( + [ + ( + "text_card", + wagtail.blocks.StructBlock( + [ + ( + "body", + wagtail.blocks.RichTextBlock( + features=[ + "h2", + "h3", + "h4", + "bold", + "ul", + "link", + ], + help_text="\nThis section of text will comprise this card. \nNote that this card will span the length of the available page width if sufficient text content is provided.\n", + ), + ) + ] + ), + ), + ( + "chart_row_card", + wagtail.blocks.StructBlock( + [ + ( + "columns", + wagtail.blocks.StreamBlock( + [ + ( + "chart_card", + wagtail.blocks.StructBlock( + [ + ( + "title", + wagtail.blocks.TextBlock( + help_text="\nThe title to display for this component. \nNote that this will be shown in the hex colour #505A5F\n", + required=True, + ), + ), + ( + "body", + wagtail.blocks.TextBlock( + help_text="\nAn optional body of text to accompany this block.\n", + required=False, + ), + ), + ( + "tag_manager_event_id", + wagtail.blocks.CharBlock( + help_text="\nThe ID to associate with this component. \nThis allows for tracking of events when users interact with this component.\nNote that changing this multiple times will result in the recording of different groups of events.\n", + label="Tag manager event ID", + required=False, + ), + ), + ( + "x_axis", + wagtail.blocks.ChoiceBlock( + choices=cms.metrics_interface.field_choices_callables.get_possible_axis_choices, + help_text="\nAn optional choice of what to display along the x-axis of the chart.\nIf nothing is provided, `dates` will be used by default.\nDates are used by default\n", + required=False, + ), + ), + ( + "y_axis", + wagtail.blocks.ChoiceBlock( + choices=cms.metrics_interface.field_choices_callables.get_possible_axis_choices, + help_text="\nAn optional choice of what to display along the y-axis of the chart.\nIf nothing is provided, `metric value` will be used by default.\n", + required=False, + ), + ), + ( + "chart", + wagtail.blocks.StreamBlock( + [ + ( + "plot", + wagtail.blocks.StructBlock( + [ + ( + "topic", + wagtail.blocks.ChoiceBlock( + choices=cms.metrics_interface.field_choices_callables.get_all_topic_names, + help_text="The name of the topic to pull data e.g. COVID-19.", + ), + ), + ( + "metric", + wagtail.blocks.ChoiceBlock( + choices=cms.metrics_interface.field_choices_callables.get_all_timeseries_metric_names, + help_text='\nThe name of the metric to pull data for e.g. "COVID-19_deaths_ONSByDay".\n', + ), + ), + ( + "geography", + wagtail.blocks.ChoiceBlock( + choices=cms.metrics_interface.field_choices_callables.get_all_geography_names, + help_text="\nThe name of the geography associated with this particular piece of data.\nIf nothing is provided, then no filtering will be applied for this field.\n", + ), + ), + ( + "geography_type", + wagtail.blocks.ChoiceBlock( + choices=cms.metrics_interface.field_choices_callables.get_all_geography_type_names, + help_text="\nThe type of geographical categorisation to apply any data filtering to.\nIf nothing is provided, then no filtering will be applied for this field.\n", + ), + ), + ( + "sex", + wagtail.blocks.ChoiceBlock( + choices=cms.metrics_interface.field_choices_callables.get_all_sex_names, + help_text="\nThe gender to filter for, if any.\nThe only options available are `M`, `F` and `ALL`.\nBy default, no filtering will be applied to the underlying query if no selection is made.\n", + ), + ), + ( + "age", + wagtail.blocks.ChoiceBlock( + choices=cms.metrics_interface.field_choices_callables.get_all_age_names, + help_text="\nThe age band to filter for, if any.\nBy default, no filtering will be applied to the underlying query if no selection is made.\n", + ), + ), + ( + "stratum", + wagtail.blocks.ChoiceBlock( + choices=cms.metrics_interface.field_choices_callables.get_all_stratum_names, + help_text="\nThe smallest subgroup a piece of data can be broken down into.\nFor example, this could be broken down by ethnicity or testing pillar.\nIf nothing is provided, then no filtering will be applied for this field.\n", + ), + ), + ( + "chart_type", + wagtail.blocks.ChoiceBlock( + choices=cms.metrics_interface.field_choices_callables.get_chart_types, + help_text="\nThe name of the type of chart which you want to create e.g. waffle\n", + ), + ), + ( + "date_from", + wagtail.blocks.DateBlock( + help_text="\nThe date from which to begin the supporting plot data. \nNote that if nothing is provided, a default of 1 year ago from the current date will be applied.\n", + required=False, + ), + ), + ( + "date_to", + wagtail.blocks.DateBlock( + help_text="\nThe date to which to end the supporting plot data. \nNote that if nothing is provided, a default of the current date will be applied.\n", + required=False, + ), + ), + ( + "label", + wagtail.blocks.TextBlock( + help_text="\nThe label to assign on the legend for this individual plot.\nE.g. `15 to 44 years old`\n", + required=False, + ), + ), + ( + "line_colour", + wagtail.blocks.ChoiceBlock( + choices=cms.metrics_interface.field_choices_callables.get_colours, + help_text='\nThe colour to apply to this individual line plot. The colours conform to the GDS specification.\nCurrently, only the `line_multi_coloured` chart type supports different line colours.\nFor all other chart types, this field will be ignored.\nNote that if nothing is provided, a default of "BLACK" will be applied.\nE.g. `GREEN`\n', + ), + ), + ( + "line_type", + wagtail.blocks.ChoiceBlock( + choices=cms.metrics_interface.field_choices_callables.get_chart_line_types, + help_text='\nThe line type to apply to this individual line plot.\nCurrently, only the `line_multi_coloured` chart type supports different line types.\nFor all other chart types, this field will be ignored.\nNote that if nothing is provided, a default of "SOLID" will be applied.\nE.g. `DASH`\n', + required=False, + ), + ), + ( + "use_markers", + wagtail.blocks.BooleanBlock( + default=False, + help_text="\nIf set to true, markers are drawn on each individual data point.\nIf set to false, markers are not drawn at all.\nThis is only applicable to line-type charts.\n", + required=False, + ), + ), + ( + "use_smooth_lines", + wagtail.blocks.BooleanBlock( + default=True, + help_text="\nIf set to true, draws the plot as a spline line, resulting in smooth curves between data points.\nIf set to false, draws the plot as a linear line, \nresulting in linear point-to-point lines being drawn between data points.\nThis is only applicable to line-type charts.\n", + required=False, + ), + ), + ] + ), + ) + ], + help_text="\nAdd the plots required for your chart. \nWithin each plot, you will be required to add a set of fields which will be used to fetch the supporting data \nfor that plot.\n", + ), + ), + ] + ), + ), + ( + "headline_chart_card", + wagtail.blocks.StructBlock( + [ + ( + "title", + wagtail.blocks.TextBlock( + help_text="\nThe title to display for this component. \nNote that this will be shown in the hex colour #505A5F\n", + required=True, + ), + ), + ( + "body", + wagtail.blocks.TextBlock( + help_text="\nAn optional body of text to accompany this block.\n", + required=False, + ), + ), + ( + "tag_manager_event_id", + wagtail.blocks.CharBlock( + help_text="\nThe ID to associate with this component. \nThis allows for tracking of events when users interact with this component.\nNote that changing this multiple times will result in the recording of different groups of events.\n", + label="Tag manager event ID", + required=False, + ), + ), + ( + "x_axis", + wagtail.blocks.ChoiceBlock( + choices=cms.metrics_interface.field_choices_callables.get_possible_axis_choices, + help_text="\nA required choice of what to display along the x-axis of the chart.\n", + ), + ), + ( + "y_axis", + wagtail.blocks.ChoiceBlock( + choices=cms.metrics_interface.field_choices_callables.get_possible_axis_choices, + help_text="\nAn optional choice of what to display along the y-axis of the chart.\nIf nothing is provided, `metric value` will be used by default.\n", + required=False, + ), + ), + ( + "chart", + wagtail.blocks.StreamBlock( + [ + ( + "plot", + wagtail.blocks.StructBlock( + [ + ( + "topic", + wagtail.blocks.ChoiceBlock( + choices=cms.metrics_interface.field_choices_callables.get_all_topic_names, + help_text="The name of the topic to pull data e.g. COVID-19.", + ), + ), + ( + "metric", + wagtail.blocks.ChoiceBlock( + choices=cms.metrics_interface.field_choices_callables.get_all_headline_metric_names, + help_text='\nThe name of the metric to pull data for e.g. "COVID-19_deaths_ONSByDay".\n', + ), + ), + ( + "geography", + wagtail.blocks.ChoiceBlock( + choices=cms.metrics_interface.field_choices_callables.get_all_geography_names, + help_text="\nThe name of the geography associated with this particular piece of data.\nIf nothing is provided, then no filtering will be applied for this field.\n", + ), + ), + ( + "geography_type", + wagtail.blocks.ChoiceBlock( + choices=cms.metrics_interface.field_choices_callables.get_all_geography_type_names, + help_text="\nThe type of geographical categorisation to apply any data filtering to.\nIf nothing is provided, then no filtering will be applied for this field.\n", + ), + ), + ( + "sex", + wagtail.blocks.ChoiceBlock( + choices=cms.metrics_interface.field_choices_callables.get_all_sex_names, + help_text="\nThe gender to filter for, if any.\nThe only options available are `M`, `F` and `ALL`.\nBy default, no filtering will be applied to the underlying query if no selection is made.\n", + ), + ), + ( + "age", + wagtail.blocks.ChoiceBlock( + choices=cms.metrics_interface.field_choices_callables.get_all_age_names, + help_text="\nThe age band to filter for, if any.\nBy default, no filtering will be applied to the underlying query if no selection is made.\n", + ), + ), + ( + "stratum", + wagtail.blocks.ChoiceBlock( + choices=cms.metrics_interface.field_choices_callables.get_all_stratum_names, + help_text="\nThe smallest subgroup a piece of data can be broken down into.\nFor example, this could be broken down by ethnicity or testing pillar.\nIf nothing is provided, then no filtering will be applied for this field.\n", + ), + ), + ( + "chart_type", + wagtail.blocks.ChoiceBlock( + choices=cms.metrics_interface.field_choices_callables.get_headline_chart_types, + help_text="\nThe name of the type of chart which you want to create e.g. waffle\n", + ), + ), + ( + "line_colour", + wagtail.blocks.ChoiceBlock( + choices=cms.metrics_interface.field_choices_callables.get_colours, + help_text='\nThe colour to apply to this individual line plot. The colours conform to the GDS specification.\nCurrently, only the `line_multi_coloured` chart type supports different line colours.\nFor all other chart types, this field will be ignored.\nNote that if nothing is provided, a default of "BLACK" will be applied.\nE.g. `GREEN`\n', + ), + ), + ( + "label", + wagtail.blocks.TextBlock( + help_text="\nThe label to assign on the legend for this individual plot.\nE.g. `15 to 44 years old`\n", + required=False, + ), + ), + ] + ), + ) + ], + help_texts="\nAdd the plots required for your chart. \nWithin each plot, you will be required to add a set of fields which will be used to fetch the supporting data \nfor that plot.\n", + ), + ), + ] + ), + ), + ( + "chart_with_headline_and_trend_card", + wagtail.blocks.StructBlock( + [ + ( + "title", + wagtail.blocks.TextBlock( + help_text="\nThe title to display for this component. \nNote that this will be shown in the hex colour #505A5F\n", + required=True, + ), + ), + ( + "body", + wagtail.blocks.TextBlock( + help_text="\nAn optional body of text to accompany this block.\n", + required=False, + ), + ), + ( + "tag_manager_event_id", + wagtail.blocks.CharBlock( + help_text="\nThe ID to associate with this component. \nThis allows for tracking of events when users interact with this component.\nNote that changing this multiple times will result in the recording of different groups of events.\n", + label="Tag manager event ID", + required=False, + ), + ), + ( + "x_axis", + wagtail.blocks.ChoiceBlock( + choices=cms.metrics_interface.field_choices_callables.get_possible_axis_choices, + help_text="\nAn optional choice of what to display along the x-axis of the chart.\nIf nothing is provided, `dates` will be used by default.\nDates are used by default\n", + required=False, + ), + ), + ( + "y_axis", + wagtail.blocks.ChoiceBlock( + choices=cms.metrics_interface.field_choices_callables.get_possible_axis_choices, + help_text="\nAn optional choice of what to display along the y-axis of the chart.\nIf nothing is provided, `metric value` will be used by default.\n", + required=False, + ), + ), + ( + "chart", + wagtail.blocks.StreamBlock( + [ + ( + "plot", + wagtail.blocks.StructBlock( + [ + ( + "topic", + wagtail.blocks.ChoiceBlock( + choices=cms.metrics_interface.field_choices_callables.get_all_topic_names, + help_text="The name of the topic to pull data e.g. COVID-19.", + ), + ), + ( + "metric", + wagtail.blocks.ChoiceBlock( + choices=cms.metrics_interface.field_choices_callables.get_all_timeseries_metric_names, + help_text='\nThe name of the metric to pull data for e.g. "COVID-19_deaths_ONSByDay".\n', + ), + ), + ( + "geography", + wagtail.blocks.ChoiceBlock( + choices=cms.metrics_interface.field_choices_callables.get_all_geography_names, + help_text="\nThe name of the geography associated with this particular piece of data.\nIf nothing is provided, then no filtering will be applied for this field.\n", + ), + ), + ( + "geography_type", + wagtail.blocks.ChoiceBlock( + choices=cms.metrics_interface.field_choices_callables.get_all_geography_type_names, + help_text="\nThe type of geographical categorisation to apply any data filtering to.\nIf nothing is provided, then no filtering will be applied for this field.\n", + ), + ), + ( + "sex", + wagtail.blocks.ChoiceBlock( + choices=cms.metrics_interface.field_choices_callables.get_all_sex_names, + help_text="\nThe gender to filter for, if any.\nThe only options available are `M`, `F` and `ALL`.\nBy default, no filtering will be applied to the underlying query if no selection is made.\n", + ), + ), + ( + "age", + wagtail.blocks.ChoiceBlock( + choices=cms.metrics_interface.field_choices_callables.get_all_age_names, + help_text="\nThe age band to filter for, if any.\nBy default, no filtering will be applied to the underlying query if no selection is made.\n", + ), + ), + ( + "stratum", + wagtail.blocks.ChoiceBlock( + choices=cms.metrics_interface.field_choices_callables.get_all_stratum_names, + help_text="\nThe smallest subgroup a piece of data can be broken down into.\nFor example, this could be broken down by ethnicity or testing pillar.\nIf nothing is provided, then no filtering will be applied for this field.\n", + ), + ), + ( + "chart_type", + wagtail.blocks.ChoiceBlock( + choices=cms.metrics_interface.field_choices_callables.get_chart_types, + help_text="\nThe name of the type of chart which you want to create e.g. waffle\n", + ), + ), + ( + "date_from", + wagtail.blocks.DateBlock( + help_text="\nThe date from which to begin the supporting plot data. \nNote that if nothing is provided, a default of 1 year ago from the current date will be applied.\n", + required=False, + ), + ), + ( + "date_to", + wagtail.blocks.DateBlock( + help_text="\nThe date to which to end the supporting plot data. \nNote that if nothing is provided, a default of the current date will be applied.\n", + required=False, + ), + ), + ( + "label", + wagtail.blocks.TextBlock( + help_text="\nThe label to assign on the legend for this individual plot.\nE.g. `15 to 44 years old`\n", + required=False, + ), + ), + ( + "line_colour", + wagtail.blocks.ChoiceBlock( + choices=cms.metrics_interface.field_choices_callables.get_colours, + help_text='\nThe colour to apply to this individual line plot. The colours conform to the GDS specification.\nCurrently, only the `line_multi_coloured` chart type supports different line colours.\nFor all other chart types, this field will be ignored.\nNote that if nothing is provided, a default of "BLACK" will be applied.\nE.g. `GREEN`\n', + ), + ), + ( + "line_type", + wagtail.blocks.ChoiceBlock( + choices=cms.metrics_interface.field_choices_callables.get_chart_line_types, + help_text='\nThe line type to apply to this individual line plot.\nCurrently, only the `line_multi_coloured` chart type supports different line types.\nFor all other chart types, this field will be ignored.\nNote that if nothing is provided, a default of "SOLID" will be applied.\nE.g. `DASH`\n', + required=False, + ), + ), + ( + "use_markers", + wagtail.blocks.BooleanBlock( + default=False, + help_text="\nIf set to true, markers are drawn on each individual data point.\nIf set to false, markers are not drawn at all.\nThis is only applicable to line-type charts.\n", + required=False, + ), + ), + ( + "use_smooth_lines", + wagtail.blocks.BooleanBlock( + default=True, + help_text="\nIf set to true, draws the plot as a spline line, resulting in smooth curves between data points.\nIf set to false, draws the plot as a linear line, \nresulting in linear point-to-point lines being drawn between data points.\nThis is only applicable to line-type charts.\n", + required=False, + ), + ), + ] + ), + ) + ], + help_text="\nAdd the plots required for your chart. \nWithin each plot, you will be required to add a set of fields which will be used to fetch the supporting data \nfor that plot.\n", + ), + ), + ( + "headline_number_columns", + wagtail.blocks.StreamBlock( + [ + ( + "headline_number", + wagtail.blocks.StructBlock( + [ + ( + "topic", + wagtail.blocks.ChoiceBlock( + choices=cms.metrics_interface.field_choices_callables.get_all_topic_names, + help_text="The name of the topic to pull data e.g. COVID-19.", + ), + ), + ( + "metric", + wagtail.blocks.ChoiceBlock( + choices=cms.metrics_interface.field_choices_callables.get_all_unique_metric_names, + help_text='\nThe name of the metric to pull data for e.g. "COVID-19_deaths_ONSByDay".\n', + ), + ), + ( + "geography", + wagtail.blocks.ChoiceBlock( + choices=cms.metrics_interface.field_choices_callables.get_all_geography_names, + help_text="\nThe name of the geography associated with this particular piece of data.\nIf nothing is provided, then no filtering will be applied for this field.\n", + ), + ), + ( + "geography_type", + wagtail.blocks.ChoiceBlock( + choices=cms.metrics_interface.field_choices_callables.get_all_geography_type_names, + help_text="\nThe type of geographical categorisation to apply any data filtering to.\nIf nothing is provided, then no filtering will be applied for this field.\n", + ), + ), + ( + "sex", + wagtail.blocks.ChoiceBlock( + choices=cms.metrics_interface.field_choices_callables.get_all_sex_names, + help_text="\nThe gender to filter for, if any.\nThe only options available are `M`, `F` and `ALL`.\nBy default, no filtering will be applied to the underlying query if no selection is made.\n", + ), + ), + ( + "age", + wagtail.blocks.ChoiceBlock( + choices=cms.metrics_interface.field_choices_callables.get_all_age_names, + help_text="\nThe age band to filter for, if any.\nBy default, no filtering will be applied to the underlying query if no selection is made.\n", + ), + ), + ( + "stratum", + wagtail.blocks.ChoiceBlock( + choices=cms.metrics_interface.field_choices_callables.get_all_stratum_names, + help_text="\nThe smallest subgroup a piece of data can be broken down into.\nFor example, this could be broken down by ethnicity or testing pillar.\nIf nothing is provided, then no filtering will be applied for this field.\n", + ), + ), + ( + "body", + wagtail.blocks.TextBlock( + help_text="\nAn optional body of text to accompany this block.\n", + required=False, + ), + ), + ], + help_text='\nThis component will display a key headline number type metric.\nYou can also optionally add a body of text to accompany that headline number.\nE.g. "Patients admitted"\n', + ), + ), + ( + "trend_number", + wagtail.blocks.StructBlock( + [ + ( + "topic", + wagtail.blocks.ChoiceBlock( + choices=cms.metrics_interface.field_choices_callables.get_all_topic_names, + help_text="The name of the topic to pull data e.g. COVID-19.", + ), + ), + ( + "metric", + wagtail.blocks.ChoiceBlock( + choices=cms.metrics_interface.field_choices_callables.get_all_unique_change_type_metric_names, + help_text="\nThe name of the trend type metric to pull data e.g. \"COVID-19_headline_ONSdeaths_7daychange\". \nNote that only 'change' type metrics are available for selection for this field type.\n", + ), + ), + ( + "geography", + wagtail.blocks.ChoiceBlock( + choices=cms.metrics_interface.field_choices_callables.get_all_geography_names, + help_text="\nThe name of the geography associated with this particular piece of data.\nIf nothing is provided, then no filtering will be applied for this field.\n", + ), + ), + ( + "geography_type", + wagtail.blocks.ChoiceBlock( + choices=cms.metrics_interface.field_choices_callables.get_all_geography_type_names, + help_text="\nThe type of geographical categorisation to apply any data filtering to.\nIf nothing is provided, then no filtering will be applied for this field.\n", + ), + ), + ( + "sex", + wagtail.blocks.ChoiceBlock( + choices=cms.metrics_interface.field_choices_callables.get_all_sex_names, + help_text="\nThe gender to filter for, if any.\nThe only options available are `M`, `F` and `ALL`.\nBy default, no filtering will be applied to the underlying query if no selection is made.\n", + ), + ), + ( + "age", + wagtail.blocks.ChoiceBlock( + choices=cms.metrics_interface.field_choices_callables.get_all_age_names, + help_text="\nThe age band to filter for, if any.\nBy default, no filtering will be applied to the underlying query if no selection is made.\n", + ), + ), + ( + "stratum", + wagtail.blocks.ChoiceBlock( + choices=cms.metrics_interface.field_choices_callables.get_all_stratum_names, + help_text="\nThe smallest subgroup a piece of data can be broken down into.\nFor example, this could be broken down by ethnicity or testing pillar.\nIf nothing is provided, then no filtering will be applied for this field.\n", + ), + ), + ( + "body", + wagtail.blocks.TextBlock( + help_text="\nAn optional body of text to accompany this block.\n", + required=False, + ), + ), + ( + "percentage_metric", + wagtail.blocks.ChoiceBlock( + choices=cms.metrics_interface.field_choices_callables.get_all_unique_percent_change_type_names, + help_text="\nThe name of the accompanying percentage trend type metric to pull data \ne.g. \"COVID-19_headline_ONSdeaths_7daypercentchange\". \nNote that only 'percent' type metrics are available for selection for this field type.\n", + ), + ), + ], + help_text='\nThis component will display a trend number type metric.\nThis will display an arrow pointing in the direction of the metric change \nas well as colouring of the block to indicate the context of the change.\nYou can also optionally add a body of text to accompany that headline number.\nE.g. "Last 7 days"\n', + ), + ), + ( + "percentage_number", + wagtail.blocks.StructBlock( + [ + ( + "topic", + wagtail.blocks.ChoiceBlock( + choices=cms.metrics_interface.field_choices_callables.get_all_topic_names, + help_text="The name of the topic to pull data e.g. COVID-19.", + ), + ), + ( + "metric", + wagtail.blocks.ChoiceBlock( + choices=cms.metrics_interface.field_choices_callables.get_all_unique_metric_names, + help_text='\nThe name of the metric to pull data for e.g. "COVID-19_deaths_ONSByDay".\n', + ), + ), + ( + "geography", + wagtail.blocks.ChoiceBlock( + choices=cms.metrics_interface.field_choices_callables.get_all_geography_names, + help_text="\nThe name of the geography associated with this particular piece of data.\nIf nothing is provided, then no filtering will be applied for this field.\n", + ), + ), + ( + "geography_type", + wagtail.blocks.ChoiceBlock( + choices=cms.metrics_interface.field_choices_callables.get_all_geography_type_names, + help_text="\nThe type of geographical categorisation to apply any data filtering to.\nIf nothing is provided, then no filtering will be applied for this field.\n", + ), + ), + ( + "sex", + wagtail.blocks.ChoiceBlock( + choices=cms.metrics_interface.field_choices_callables.get_all_sex_names, + help_text="\nThe gender to filter for, if any.\nThe only options available are `M`, `F` and `ALL`.\nBy default, no filtering will be applied to the underlying query if no selection is made.\n", + ), + ), + ( + "age", + wagtail.blocks.ChoiceBlock( + choices=cms.metrics_interface.field_choices_callables.get_all_age_names, + help_text="\nThe age band to filter for, if any.\nBy default, no filtering will be applied to the underlying query if no selection is made.\n", + ), + ), + ( + "stratum", + wagtail.blocks.ChoiceBlock( + choices=cms.metrics_interface.field_choices_callables.get_all_stratum_names, + help_text="\nThe smallest subgroup a piece of data can be broken down into.\nFor example, this could be broken down by ethnicity or testing pillar.\nIf nothing is provided, then no filtering will be applied for this field.\n", + ), + ), + ( + "body", + wagtail.blocks.TextBlock( + help_text="\nAn optional body of text to accompany this block.\n", + required=False, + ), + ), + ], + help_text='\nThis component will display a percentage number type metric.\nThis will display the value of the metric appended with a % character.\nYou can also optionally add a body of text to accompany this percentage number.\nE.g. "Virus tests positivity".\n', + ), + ), + ], + help_text="\nAdd up to 2 headline or trend number column components within this space.\nNote that these figures will be displayed within the card, and above the chart itself.\n", + max_num=2, + min_num=0, + required=False, + ), + ), + ] + ), + ), + ( + "simplified_chart_with_link", + wagtail.blocks.StructBlock( + [ + ( + "title", + wagtail.blocks.TextBlock( + help_text="\nThe title to display for this component. \nNote that this will be shown in the hex colour #505A5F\n", + required=True, + ), + ), + ( + "sub_title", + wagtail.blocks.CharBlock( + help_text="\nThe sub title to display for this component.\n", + required=False, + ), + ), + ( + "tag_manager_event_id", + wagtail.blocks.CharBlock( + help_text="\nThe ID to associate with this component. \nThis allows for tracking of events when users interact with this component.\nNote that changing this multiple times will result in the recording of different groups of events.\n", + label="Tag manager event ID", + required=False, + ), + ), + ( + "topic_page", + cms.dynamic_content.blocks.PageLinkChooserBlock( + help_text="\nThe related topic page you want to link to. Eg: `COVID-19`\n", + page_type=[ + "topic.TopicPage" + ], + required=True, + ), + ), + ( + "x_axis", + wagtail.blocks.ChoiceBlock( + choices=cms.metrics_interface.field_choices_callables.get_possible_axis_choices, + help_text="\nA required choice of what to display along the x-axis of the chart.\n", + ready_only=True, + ), + ), + ( + "y_axis", + wagtail.blocks.ChoiceBlock( + choices=cms.metrics_interface.field_choices_callables.get_possible_axis_choices, + help_text="\nA required choice of what to display along the y-axis of the chart.\n", + ), + ), + ( + "chart", + wagtail.blocks.StreamBlock( + [ + ( + "plot", + wagtail.blocks.StructBlock( + [ + ( + "topic", + wagtail.blocks.ChoiceBlock( + choices=cms.metrics_interface.field_choices_callables.get_all_topic_names, + help_text="The name of the topic to pull data e.g. COVID-19.", + ), + ), + ( + "metric", + wagtail.blocks.ChoiceBlock( + choices=cms.metrics_interface.field_choices_callables.get_all_timeseries_metric_names, + help_text='\nThe name of the metric to pull data for e.g. "COVID-19_deaths_ONSByDay".\n', + ), + ), + ( + "geography", + wagtail.blocks.ChoiceBlock( + choices=cms.metrics_interface.field_choices_callables.get_all_geography_names, + help_text="\nThe name of the geography associated with this particular piece of data.\nIf nothing is provided, then no filtering will be applied for this field.\n", + ), + ), + ( + "geography_type", + wagtail.blocks.ChoiceBlock( + choices=cms.metrics_interface.field_choices_callables.get_all_geography_type_names, + help_text="\nThe type of geographical categorisation to apply any data filtering to.\nIf nothing is provided, then no filtering will be applied for this field.\n", + ), + ), + ( + "sex", + wagtail.blocks.ChoiceBlock( + choices=cms.metrics_interface.field_choices_callables.get_all_sex_names, + help_text="\nThe gender to filter for, if any.\nThe only options available are `M`, `F` and `ALL`.\nBy default, no filtering will be applied to the underlying query if no selection is made.\n", + ), + ), + ( + "age", + wagtail.blocks.ChoiceBlock( + choices=cms.metrics_interface.field_choices_callables.get_all_age_names, + help_text="\nThe age band to filter for, if any.\nBy default, no filtering will be applied to the underlying query if no selection is made.\n", + ), + ), + ( + "stratum", + wagtail.blocks.ChoiceBlock( + choices=cms.metrics_interface.field_choices_callables.get_all_stratum_names, + help_text="\nThe smallest subgroup a piece of data can be broken down into.\nFor example, this could be broken down by ethnicity or testing pillar.\nIf nothing is provided, then no filtering will be applied for this field.\n", + ), + ), + ( + "chart_type", + wagtail.blocks.ChoiceBlock( + choices=cms.metrics_interface.field_choices_callables.get_simplified_chart_types + ), + ), + ] + ), + ) + ], + help_text="\nAdd the plots required for your chart. \nWithin each plot, you will be required to add a set of fields which will be used to fetch the supporting data \nfor that plot.\n", + max_num=1, + required=True, + ), + ), + ] + ), + ), + ], + help_text="\nHere you can add 1 or 2 columns to contain a particular chart card.\nIf you add the 1 column, then the chart card will spread across the available width.\nIf you add 2 columns, then the cards will be split across 2 columns within the available width.\n", + max_num=3, + min_num=1, + ), + ) + ] + ), + ), + ( + "headline_numbers_row_card", + wagtail.blocks.StructBlock( + [ + ( + "columns", + wagtail.blocks.StreamBlock( + [ + ( + "column", + wagtail.blocks.StructBlock( + [ + ( + "title", + wagtail.blocks.TextBlock( + help_text="\nThe title to display for this component. \nNote that this will be shown in the hex colour #505A5F\n", + required=True, + ), + ), + ( + "rows", + wagtail.blocks.StreamBlock( + [ + ( + "headline_number", + wagtail.blocks.StructBlock( + [ + ( + "topic", + wagtail.blocks.ChoiceBlock( + choices=cms.metrics_interface.field_choices_callables.get_all_topic_names, + help_text="The name of the topic to pull data e.g. COVID-19.", + ), + ), + ( + "metric", + wagtail.blocks.ChoiceBlock( + choices=cms.metrics_interface.field_choices_callables.get_all_unique_metric_names, + help_text='\nThe name of the metric to pull data for e.g. "COVID-19_deaths_ONSByDay".\n', + ), + ), + ( + "geography", + wagtail.blocks.ChoiceBlock( + choices=cms.metrics_interface.field_choices_callables.get_all_geography_names, + help_text="\nThe name of the geography associated with this particular piece of data.\nIf nothing is provided, then no filtering will be applied for this field.\n", + ), + ), + ( + "geography_type", + wagtail.blocks.ChoiceBlock( + choices=cms.metrics_interface.field_choices_callables.get_all_geography_type_names, + help_text="\nThe type of geographical categorisation to apply any data filtering to.\nIf nothing is provided, then no filtering will be applied for this field.\n", + ), + ), + ( + "sex", + wagtail.blocks.ChoiceBlock( + choices=cms.metrics_interface.field_choices_callables.get_all_sex_names, + help_text="\nThe gender to filter for, if any.\nThe only options available are `M`, `F` and `ALL`.\nBy default, no filtering will be applied to the underlying query if no selection is made.\n", + ), + ), + ( + "age", + wagtail.blocks.ChoiceBlock( + choices=cms.metrics_interface.field_choices_callables.get_all_age_names, + help_text="\nThe age band to filter for, if any.\nBy default, no filtering will be applied to the underlying query if no selection is made.\n", + ), + ), + ( + "stratum", + wagtail.blocks.ChoiceBlock( + choices=cms.metrics_interface.field_choices_callables.get_all_stratum_names, + help_text="\nThe smallest subgroup a piece of data can be broken down into.\nFor example, this could be broken down by ethnicity or testing pillar.\nIf nothing is provided, then no filtering will be applied for this field.\n", + ), + ), + ( + "body", + wagtail.blocks.TextBlock( + help_text="\nAn optional body of text to accompany this block.\n", + required=False, + ), + ), + ], + help_text='\nThis component will display a key headline number type metric.\nYou can also optionally add a body of text to accompany that headline number.\nE.g. "Patients admitted"\n', + ), + ), + ( + "trend_number", + wagtail.blocks.StructBlock( + [ + ( + "topic", + wagtail.blocks.ChoiceBlock( + choices=cms.metrics_interface.field_choices_callables.get_all_topic_names, + help_text="The name of the topic to pull data e.g. COVID-19.", + ), + ), + ( + "metric", + wagtail.blocks.ChoiceBlock( + choices=cms.metrics_interface.field_choices_callables.get_all_unique_change_type_metric_names, + help_text="\nThe name of the trend type metric to pull data e.g. \"COVID-19_headline_ONSdeaths_7daychange\". \nNote that only 'change' type metrics are available for selection for this field type.\n", + ), + ), + ( + "geography", + wagtail.blocks.ChoiceBlock( + choices=cms.metrics_interface.field_choices_callables.get_all_geography_names, + help_text="\nThe name of the geography associated with this particular piece of data.\nIf nothing is provided, then no filtering will be applied for this field.\n", + ), + ), + ( + "geography_type", + wagtail.blocks.ChoiceBlock( + choices=cms.metrics_interface.field_choices_callables.get_all_geography_type_names, + help_text="\nThe type of geographical categorisation to apply any data filtering to.\nIf nothing is provided, then no filtering will be applied for this field.\n", + ), + ), + ( + "sex", + wagtail.blocks.ChoiceBlock( + choices=cms.metrics_interface.field_choices_callables.get_all_sex_names, + help_text="\nThe gender to filter for, if any.\nThe only options available are `M`, `F` and `ALL`.\nBy default, no filtering will be applied to the underlying query if no selection is made.\n", + ), + ), + ( + "age", + wagtail.blocks.ChoiceBlock( + choices=cms.metrics_interface.field_choices_callables.get_all_age_names, + help_text="\nThe age band to filter for, if any.\nBy default, no filtering will be applied to the underlying query if no selection is made.\n", + ), + ), + ( + "stratum", + wagtail.blocks.ChoiceBlock( + choices=cms.metrics_interface.field_choices_callables.get_all_stratum_names, + help_text="\nThe smallest subgroup a piece of data can be broken down into.\nFor example, this could be broken down by ethnicity or testing pillar.\nIf nothing is provided, then no filtering will be applied for this field.\n", + ), + ), + ( + "body", + wagtail.blocks.TextBlock( + help_text="\nAn optional body of text to accompany this block.\n", + required=False, + ), + ), + ( + "percentage_metric", + wagtail.blocks.ChoiceBlock( + choices=cms.metrics_interface.field_choices_callables.get_all_unique_percent_change_type_names, + help_text="\nThe name of the accompanying percentage trend type metric to pull data \ne.g. \"COVID-19_headline_ONSdeaths_7daypercentchange\". \nNote that only 'percent' type metrics are available for selection for this field type.\n", + ), + ), + ], + help_text='\nThis component will display a trend number type metric.\nThis will display an arrow pointing in the direction of the metric change \nas well as colouring of the block to indicate the context of the change.\nYou can also optionally add a body of text to accompany that headline number.\nE.g. "Last 7 days"\n', + ), + ), + ( + "percentage_number", + wagtail.blocks.StructBlock( + [ + ( + "topic", + wagtail.blocks.ChoiceBlock( + choices=cms.metrics_interface.field_choices_callables.get_all_topic_names, + help_text="The name of the topic to pull data e.g. COVID-19.", + ), + ), + ( + "metric", + wagtail.blocks.ChoiceBlock( + choices=cms.metrics_interface.field_choices_callables.get_all_unique_metric_names, + help_text='\nThe name of the metric to pull data for e.g. "COVID-19_deaths_ONSByDay".\n', + ), + ), + ( + "geography", + wagtail.blocks.ChoiceBlock( + choices=cms.metrics_interface.field_choices_callables.get_all_geography_names, + help_text="\nThe name of the geography associated with this particular piece of data.\nIf nothing is provided, then no filtering will be applied for this field.\n", + ), + ), + ( + "geography_type", + wagtail.blocks.ChoiceBlock( + choices=cms.metrics_interface.field_choices_callables.get_all_geography_type_names, + help_text="\nThe type of geographical categorisation to apply any data filtering to.\nIf nothing is provided, then no filtering will be applied for this field.\n", + ), + ), + ( + "sex", + wagtail.blocks.ChoiceBlock( + choices=cms.metrics_interface.field_choices_callables.get_all_sex_names, + help_text="\nThe gender to filter for, if any.\nThe only options available are `M`, `F` and `ALL`.\nBy default, no filtering will be applied to the underlying query if no selection is made.\n", + ), + ), + ( + "age", + wagtail.blocks.ChoiceBlock( + choices=cms.metrics_interface.field_choices_callables.get_all_age_names, + help_text="\nThe age band to filter for, if any.\nBy default, no filtering will be applied to the underlying query if no selection is made.\n", + ), + ), + ( + "stratum", + wagtail.blocks.ChoiceBlock( + choices=cms.metrics_interface.field_choices_callables.get_all_stratum_names, + help_text="\nThe smallest subgroup a piece of data can be broken down into.\nFor example, this could be broken down by ethnicity or testing pillar.\nIf nothing is provided, then no filtering will be applied for this field.\n", + ), + ), + ( + "body", + wagtail.blocks.TextBlock( + help_text="\nAn optional body of text to accompany this block.\n", + required=False, + ), + ), + ], + help_text='\nThis component will display a percentage number type metric.\nThis will display the value of the metric appended with a % character.\nYou can also optionally add a body of text to accompany this percentage number.\nE.g. "Virus tests positivity".\n', + ), + ), + ], + help_text="\nHere you can add up to 2 rows within this column component.\nEach row can be used to add a number block. \nThis can be a headline number, a trend number or a percentage number.\nIf you only add 1 row, then that block will be rendered on the upper half of the column.\nAnd the bottom row of the column will remain empty.\n", + max_num=2, + min_num=1, + required=True, + ), + ), + ] + ), + ) + ], + help_text="\nAdd up to 5 number column components within this row. \nThe columns are ordered from left to right, top to bottom respectively. \nSo by moving 1 column component above the other, that component will be rendered in the column left of the other. \n", + max_num=5, + min_num=1, + ), + ) + ] + ), + ), + ( + "weather_health_alert_card", + wagtail.blocks.StructBlock( + [ + ( + "title", + wagtail.blocks.TextBlock( + help_text="\nThe title to display for this component. \nNote that this will be shown in the hex colour #505A5F\n", + required=True, + ), + ), + ( + "sub_title", + wagtail.blocks.TextBlock( + help_text="\nThe sub title to display for this component.\n", + required=True, + ), + ), + ( + "alert_type", + wagtail.blocks.ChoiceBlock( + choices=cms.dynamic_content.cards.WHAlerts.get_alerts, + help_text="\nThis is used to select the current weather health alert type Eg: Heat or Cold alert season.\n", + ), + ), + ] + ), + ), + ], + help_text="\nHere you can add any number of content row cards for this section.\nNote that these cards will be displayed across the available width.\n", + ), + ), + ] + ), + ) + ] + ), + ), + ("sub_title", models.CharField(max_length=255)), + ], + options={ + "abstract": False, + }, + bases=("wagtailcore.page",), + ), + ] diff --git a/cms/landing_page/migrations/__init__.py b/cms/landing_page/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/cms/landing_page/models.py b/cms/landing_page/models.py new file mode 100644 index 000000000..b16ff49c7 --- /dev/null +++ b/cms/landing_page/models.py @@ -0,0 +1,75 @@ +from django.db import models +from wagtail.admin.panels import FieldPanel, ObjectList, TabbedInterface +from wagtail.api import APIField +from wagtail.models import Page + +from cms.dashboard.models import UKHSAPage +from cms.dynamic_content.access import ALLOWABLE_BODY_CONTENT_SECTION_LINK +from cms.landing_page.managers import LandingPageManager + + +class LandingPage(UKHSAPage): + is_creatable = False + max_count = 1 + sub_title = models.CharField(max_length=255) + body = ALLOWABLE_BODY_CONTENT_SECTION_LINK + + content_panels = Page.content_panels + [FieldPanel("sub_title"), FieldPanel("body")] + + api_fields = [ + APIField("title"), + APIField("sub_title"), + APIField("body"), + APIField("last_updated_at"), + APIField("last_published_at"), + APIField("search_description"), + APIField("seo_title"), + APIField("seo_change_frequency"), + APIField("seo_priority"), + ] + + edit_handler = TabbedInterface( + [ + ObjectList(content_panels, heading="Content"), + ObjectList(UKHSAPage.promote_panels, heading="Promote"), + ] + ) + + objects = LandingPageManager() + + @classmethod + def is_previewable(cls): + """Returns False. This is a headline CMS, preview panel is not supported .""" + return False + + def get_url_parts(self, request=None) -> tuple[int, str, str]: + """Builds the full URL for the home page + + Notes: + Page url parts are returned as a tuple of + (site_id, site_root_url, page_url_relative_to_site_root) + The base implementation of this method assumes Wagtail + is running in full app mode i.e not in headless, + because the building of page paths is handed off to + the `wagtail-serve` route which does not exist in headless mode. + Hence, the need to override and provide the url here. + + Args: + `request`: Optional request object which is not to + be used for our implementation. + + Returns: + Tuple containing the URL parts: + 1) ID of the corresponding `Site` record + 2) The root URL of the site + e.g. `https://ukhsa-dashboard.data.gov.uk` + 3) The path of the current page. + This always returns an empty string + e.g. `""` + This is because the homepage is assumed + to be at the root of the visible page tree. + + """ + site_id, root_url, page_path = super().get_url_parts(request=request) + page_path = "" + return site_id, root_url, page_path diff --git a/cms/metrics_interface/field_choices_callables.py b/cms/metrics_interface/field_choices_callables.py index f6f53b77b..0892fbd20 100644 --- a/cms/metrics_interface/field_choices_callables.py +++ b/cms/metrics_interface/field_choices_callables.py @@ -152,6 +152,27 @@ def get_headline_chart_types() -> LIST_OF_TWO_STRING_ITEM_TUPLES: return MetricsAPIInterface.get_headline_chart_types() +def get_simplified_chart_types() -> LIST_OF_TWO_STRING_ITEM_TUPLES: + """Callable for the `choices` on the `chart_type` fields of the CMS blocks + for simplified chart types. + + Notes: + This callable wraps the `MetricsAPIInterface` + and is passed to a migration for the CMS blocks. + This means that we don't need to create a new migration + whenever a new chart type is added. + Instead, the 1-off migration is pointed at this callable. + So Wagtail will pull the choices by invoking this function. + + Returns: + A list of 2-item tuples of chart_types. + Examples: + [("line_single_simplified", "line_single_simplified"), ...] + + """ + return MetricsAPIInterface.get_simplified_chart_types() + + def get_chart_line_types() -> LIST_OF_TWO_STRING_ITEM_TUPLES: """Callable for the `choices` on the `line_type` fields of the CMS blocks. diff --git a/cms/metrics_interface/interface.py b/cms/metrics_interface/interface.py index 9e320af1a..21000d459 100644 --- a/cms/metrics_interface/interface.py +++ b/cms/metrics_interface/interface.py @@ -98,6 +98,18 @@ def get_headline_chart_types() -> tuple[tuple[str, str], ...]: """ return ChartTypes.selectable_headline_choices() + @staticmethod + def get_simplified_chart_types() -> tuple[tuple[str, str], ...]: + """Gets all available chart type choices for headline charts as a nested tuple of 2-item tuples. + Note this is achived by delegating the call to the `ChartTypes` enum from the metrics API + + Returns: + Nested tuples of 2 item tuples as expected by the form blocks. + Examples: + (("line_single_simplified", "line_single_simplified"), ...) + """ + return ChartTypes.selectable_simplified_chart_choices() + @staticmethod def get_chart_axis_choices() -> list[tuple[str, str]]: """Gets all available axis choices for a chart as a list of 2-item tuples. diff --git a/metrics/api/settings/default.py b/metrics/api/settings/default.py index 31b9c1d61..1bfff35b8 100644 --- a/metrics/api/settings/default.py +++ b/metrics/api/settings/default.py @@ -58,6 +58,7 @@ "cms.composite", "cms.whats_new", "cms.metrics_documentation", + "cms.landing_page", "cms.snippets", "wagtail.api.v2", "wagtail.contrib.forms", diff --git a/metrics/domain/common/utils.py b/metrics/domain/common/utils.py index 626466771..1ed80ae94 100644 --- a/metrics/domain/common/utils.py +++ b/metrics/domain/common/utils.py @@ -63,6 +63,18 @@ def selectable_headline_choices(cls) -> tuple[tuple[str, str], ...]: selectable = (cls.bar,) return tuple((chart_type.value, chart_type.value) for chart_type in selectable) + @classmethod + def selectable_simplified_chart_choices(cls) -> tuple[tuple[str, str], ...]: + """Returns chart types which are selectable from the CMS as nested tuple of 2-item tuples. + + Returns: + Nested tuples of 2 item tuples as expected by the form blocks. + Examples: + (("line_single_simplified", "line_single_simplified"), ...) + """ + selectable = (cls.line_single_simplified,) + return tuple((chart_type.value, chart_type.value) for chart_type in selectable) + @classmethod def values(cls) -> list[str]: return [chart_type.value for chart_type in cls] diff --git a/tests/fakes/factories/cms/landing_page_factory.py b/tests/fakes/factories/cms/landing_page_factory.py new file mode 100644 index 000000000..5c2892e75 --- /dev/null +++ b/tests/fakes/factories/cms/landing_page_factory.py @@ -0,0 +1,20 @@ +import factory + +from cms.dashboard.management.commands.build_cms_site_helpers.pages import ( + open_example_page_response, +) + +from tests.fakes.models.cms.landing_page import FakeLandingPage + + +class FakeLandingPageFactory(factory.Factory): + """ + Factory for creating `FakeHomePage` instances for tests + """ + + class Meta: + model = FakeLandingPage + + @classmethod + def build_blank_page(cls, **kwargs): + return cls.build(**kwargs) diff --git a/tests/fakes/models/cms/landing_page.py b/tests/fakes/models/cms/landing_page.py new file mode 100644 index 000000000..59d021c55 --- /dev/null +++ b/tests/fakes/models/cms/landing_page.py @@ -0,0 +1,17 @@ +from cms.landing_page.models import LandingPage +from tests.fakes.models.fake_model_meta import FakeMeta + + +class FakeLandingPage(LandingPage): + """ + A fake version of the Django model `LandingPage` + which has had its dependencies altered so that it does not interact with the database + """ + + Meta = FakeMeta + + def __init__(self, **kwargs): + """ + Constructor takes the same arguments as a normal `LandingPage` model. + """ + super().__init__(content_type_id=1, **kwargs) diff --git a/tests/fakes/models/cms/page_link_chooser.py b/tests/fakes/models/cms/page_link_chooser.py new file mode 100644 index 000000000..0203d070b --- /dev/null +++ b/tests/fakes/models/cms/page_link_chooser.py @@ -0,0 +1,11 @@ +class FakePageLinkChooser: + """ + A fake version of the `PageLinkChooserBlock` + which has had its dependencies altered so that it does not interact with the database. + """ + + def __init__(self, **kwargs): + """ + Constructor takes the same arguments as a normal `PageChooserBlock`. + """ + super().__init__(**kwargs) diff --git a/tests/integration/cms/dynamic_content/__init__.py b/tests/integration/cms/dynamic_content/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/integration/cms/dynamic_content/test_page_link_chooser.py b/tests/integration/cms/dynamic_content/test_page_link_chooser.py new file mode 100644 index 000000000..4c8f4165f --- /dev/null +++ b/tests/integration/cms/dynamic_content/test_page_link_chooser.py @@ -0,0 +1,47 @@ +from unittest import mock +import pytest +import datetime +from django.utils import timezone + +from cms.dashboard.models import UKHSAPage +from cms.dynamic_content.blocks import PageLinkChooserBlock +from cms.topic.models import TopicPage + + +class TestPageChooser: + @pytest.mark.django_db + @mock.patch.object(UKHSAPage, "get_url_parts") + def test_page_chooser_returns_full_url( + self, mocked_url_parts: tuple[int, str, str] + ): + """ + Given a `TopicPage` with a mocked `full_url` + When the page is chosen via the `PageChooser` block + Then the `full_url` is returned from the `get_api_representation()` method. + """ + # Given + url_parts = ["http://my-prefix", "/topics/abc"] + + mocked_url_parts.return_value = (1, url_parts[0], url_parts[1]) + + date_posted: datetime.datetime = timezone.make_aware( + value=datetime.datetime(year=2023, month=1, day=1) + ) + TopicPage.objects.create( + path="abc", + depth=1, + title="abc", + date_posted=date_posted, + live=True, + seo_title="ABC", + ) + page_link_chooser = PageLinkChooserBlock(page_type="topic.TopicPage") + retrieved_topic_page = TopicPage.objects.get_live_pages().first() + + # When + api_representation_result = page_link_chooser.get_api_representation( + value=retrieved_topic_page + ) + + # Then + assert api_representation_result == f"{url_parts[0]}{url_parts[1]}" diff --git a/tests/integration/cms/landing_page/__init__.py b/tests/integration/cms/landing_page/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/integration/cms/landing_page/test_managers.py b/tests/integration/cms/landing_page/test_managers.py new file mode 100644 index 000000000..26ec36e36 --- /dev/null +++ b/tests/integration/cms/landing_page/test_managers.py @@ -0,0 +1,37 @@ +import pytest + +from cms.landing_page.managers import EXPECTED_LANDING_PAGE_SLUG +from cms.landing_page.models import LandingPage + + +class TestLandingPageManager: + @pytest.mark.django_db + def test_get_landing_page(self): + """ + Given 2 `LandingPage` records of which has a slug of "dashboard" + When `get_landing_page()` is called from the `LandingPageManager` + Then the correct `LandingPage` record is returned + """ + # Given + landing_page = LandingPage.objects.create( + path="abc", + depth=1, + title="abc", + sub_title="xyz", + slug=EXPECTED_LANDING_PAGE_SLUG, + seo_title="ABC", + ) + invalid_landing_page = LandingPage.objects.create( + path="def", + depth=1, + title="def", + sub_title="uvw", + slug="invalid_slug", + seo_title="DEF", + ) + + # When + retrieved_landing_page = LandingPage.objects.get_landing_page() + + # Then + assert retrieved_landing_page == landing_page != invalid_landing_page diff --git a/tests/unit/cms/dynamic_content/test_page_link_chooser.py b/tests/unit/cms/dynamic_content/test_page_link_chooser.py new file mode 100644 index 000000000..61ad77657 --- /dev/null +++ b/tests/unit/cms/dynamic_content/test_page_link_chooser.py @@ -0,0 +1,21 @@ +from cms.dynamic_content.blocks import PageLinkChooserBlock +from tests.fakes.models.cms.page_link_chooser import FakePageLinkChooser +from tests.fakes.factories.cms.composite_page_factory import FakeCompositePageFactory + + +class TestPageLinkChooser: + def test_page_link_chooser_returns_none(self): + """ + Given a `PageLinkChooser` instance + When the `PageLinkChooser.get_api_representation()` is called with + None instead of a page link instance. + Then None is returned. + """ + # Given + page_link_chooser = PageLinkChooserBlock() + + # When + api_representation_result = page_link_chooser.get_api_representation(value=None) + + # Then + assert api_representation_result is None diff --git a/tests/unit/cms/landing_page/__init__.py b/tests/unit/cms/landing_page/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/unit/cms/landing_page/test_models.py b/tests/unit/cms/landing_page/test_models.py new file mode 100644 index 000000000..cfa9e6c12 --- /dev/null +++ b/tests/unit/cms/landing_page/test_models.py @@ -0,0 +1,103 @@ +from unittest import mock + +import pytest +from wagtail.admin.panels.field_panel import FieldPanel +from wagtail.api.conf import APIField + +from cms.dashboard.models import UKHSAPage +from metrics.domain.common.utils import ChartTypes +from tests.fakes.factories.cms.landing_page_factory import FakeLandingPageFactory + + +class TestBlankLandingPage: + @pytest.mark.parametrize( + "expected_api_field_name", + ( + "title", + "sub_title", + "body", + "last_published_at", + "seo_title", + "search_description", + ), + ) + def test_has_correct_api_fields( + self, + expected_api_field_name: str, + ): + """ + Given a blank `LandingPage` model + When `api_fields` is called + Then the expected names are on teh returned `APIField` objects + """ + # Given + blank_page = FakeLandingPageFactory.build_blank_page() + + # When + api_fields: list[APIField] = blank_page.api_fields + + # Then + api_field_names: set[str] = {api_field.name for api_field in api_fields} + assert expected_api_field_name in api_field_names + + @pytest.mark.parametrize("expected_content_panel", ["title", "sub_title", "body"]) + def test_has_correct_content_panels( + self, + expected_content_panel: str, + ): + """ + Given a blank `LandingPage` model + When `content_panels` is called + Then the expected names are on the returned `FieldPanel` objects + """ + # Given + blank_page = FakeLandingPageFactory.build_blank_page() + + # When + content_panels: list[FieldPanel] = blank_page.content_panels + + # Then + content_panels_names: set[str] = {p.field_name for p in content_panels} + assert expected_content_panel in content_panels_names + + def test_is_previewable_returns_false(self): + """ + Given a blank `LandingPage` model + When `is_previewable()` is called + Then False is returned + """ + # Given + blank_page = FakeLandingPageFactory.build_blank_page() + + # When + page_is_previewable: bool = blank_page.is_previewable() + + # Then + assert not page_is_previewable + + @mock.patch.object(UKHSAPage, "get_url_parts") + def test_get_url_paths(self, mocked_super_get_url_parts: mock.MagicMock): + """ + Given a `LandingPage` model + When `get_url_parts()` is called + Then the url parts are returned with + an empty string representing the page path + """ + # Given + expected_site_id = 123 + root_url = "https://my-prefix.dev.ukhsa-data-dashboard.gov.uk" + page_path = "topics" + mocked_super_get_url_parts.return_value = ( + expected_site_id, + root_url, + page_path, + ) + blank_page = FakeLandingPageFactory.build_blank_page() + + # When + url_parts: tuple[int, str, str] = blank_page.get_url_parts(request=mock.Mock()) + + # Then + assert url_parts[0] == expected_site_id + assert url_parts[1] == root_url + assert url_parts[2] == "" != page_path diff --git a/tests/unit/cms/metrics_interface/test_field_choices_callables.py b/tests/unit/cms/metrics_interface/test_field_choices_callables.py index 3a3ebf06a..d564be945 100644 --- a/tests/unit/cms/metrics_interface/test_field_choices_callables.py +++ b/tests/unit/cms/metrics_interface/test_field_choices_callables.py @@ -409,3 +409,24 @@ def test_delegates_call_correctly(self, mocked_get_all_age_names: mock.MagicMock # Then assert all_age_names == [(x, x) for x in retrieved_age_names] + + +class TestSimplifiedChartTypes: + @mock.patch.object(interface.MetricsAPIInterface, "get_simplified_chart_types") + def test_delegates_call_correctly( + self, mocked_get_simplified_chart_types: mock.MagicMock + ): + """ + Given an instance of the `MetricsAPIInterface` which returns simplified chart types + When `get_simplified_chart_types()` is called + Then the chart types are returned as a list of 2-item tuples + """ + # Given + retrieved_chart_types = ChartTypes.selectable_simplified_chart_choices() + mocked_get_simplified_chart_types.return_value = retrieved_chart_types + + # When + simplified_chart_types = field_choices_callables.get_simplified_chart_types() + + # Then + assert simplified_chart_types == retrieved_chart_types diff --git a/tests/unit/cms/metrics_interface/test_interface.py b/tests/unit/cms/metrics_interface/test_interface.py index 3cd74cc8f..549778b8e 100644 --- a/tests/unit/cms/metrics_interface/test_interface.py +++ b/tests/unit/cms/metrics_interface/test_interface.py @@ -41,6 +41,26 @@ def test_get_headline_chart_type_delegates_call_correctly(self): selectable_headline_chart_types == ChartTypes.selectable_headline_choices() ) + def test_get_simpliied_chart_type_delegates_calls_correctly(self): + """ + Given an instance of the `MetricsAPIInterface` + When `get_simplified_chart_types()` is called from that object + Then the call is delegated to the correct method on the `ChartTypes` enum + """ + # Given + metrics_api_interface = interface.MetricsAPIInterface() + + # When + selectable_simplified_chart_types = ( + metrics_api_interface.get_simplified_chart_types() + ) + + # Then + assert ( + selectable_simplified_chart_types + == ChartTypes.selectable_simplified_chart_choices() + ) + def test_get_chart_axis_choices_delegates_call_correctly(self): """ Given an instance of the `MetricsAPIInterface` diff --git a/tests/unit/metrics/domain/common/test_utils.py b/tests/unit/metrics/domain/common/test_utils.py index b48b11048..40d9182ab 100644 --- a/tests/unit/metrics/domain/common/test_utils.py +++ b/tests/unit/metrics/domain/common/test_utils.py @@ -230,6 +230,21 @@ def test_headline_selectable_choices(self): # Then assert (expected_choice, expected_choice) in choices + def test_simplified_chart_selectable_choices(self): + """ + Given an expected chioce + When the `selectable_simplified_chart_choices()` class method is called + Then choice is in the returned selectable choices + """ + # Given + expected_choice = "line_single_simplified" + + # When + choices = ChartTypes.selectable_simplified_chart_choices() + + # Then + assert (expected_choice, expected_choice) in choices + def test_selectable_choices_does_not_return_simple_line(self): """ Given the invalid choice of "simple_line"