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 index ca310a7066..4f3d29099d 100644 --- a/cms/dashboard/management/commands/build_cms_site_helpers/landing_page.py +++ b/cms/dashboard/management/commands/build_cms_site_helpers/landing_page.py @@ -35,6 +35,8 @@ def create_landing_page_body_wih_page_links() -> list[dict]: { "type": "plot", "value": { + "theme": "infectious_disease", + "sub_theme": "respiratory", "topic": "COVID-19", "metric": "COVID-19_cases_countRollingMean", "geography": "England", @@ -65,6 +67,8 @@ def create_landing_page_body_wih_page_links() -> list[dict]: { "type": "plot", "value": { + "theme": "infectious_disease", + "sub_theme": "respiratory", "topic": "Influenza", "metric": "influenza_healthcare_ICUHDUadmissionRateByWeek", "geography": "England", @@ -95,6 +99,8 @@ def create_landing_page_body_wih_page_links() -> list[dict]: { "type": "plot", "value": { + "theme": "infectious_disease", + "sub_theme": "respiratory", "topic": "RSV", "metric": "RSV_testing_positivityByWeek", "geography": "England", diff --git a/cms/dashboard/templates/cms_starting_pages/covid_19.json b/cms/dashboard/templates/cms_starting_pages/covid_19.json index 74c465df6c..8c5d035ed8 100644 --- a/cms/dashboard/templates/cms_starting_pages/covid_19.json +++ b/cms/dashboard/templates/cms_starting_pages/covid_19.json @@ -43,6 +43,8 @@ { "type": "headline_number", "value": { + "theme": "infectious_disease", + "sub_theme": "respiratory", "topic": "COVID-19", "metric": "COVID-19_headline_cases_7DayTotals", "geography": "England", @@ -57,6 +59,8 @@ { "type": "trend_number", "value": { + "theme": "infectious_disease", + "sub_theme": "respiratory", "topic": "COVID-19", "metric": "COVID-19_headline_cases_7DayChange", "geography": "England", @@ -81,6 +85,8 @@ { "type": "headline_number", "value": { + "theme": "infectious_disease", + "sub_theme": "respiratory", "topic": "COVID-19", "metric": "COVID-19_headline_ONSdeaths_7DayTotals", "geography": "England", @@ -95,6 +101,8 @@ { "type": "trend_number", "value": { + "theme": "infectious_disease", + "sub_theme": "respiratory", "topic": "COVID-19", "metric": "COVID-19_headline_ONSdeaths_7DayChange", "geography": "England", @@ -119,6 +127,8 @@ { "type": "headline_number", "value": { + "theme": "infectious_disease", + "sub_theme": "respiratory", "topic": "COVID-19", "metric": "COVID-19_headline_7DayAdmissions", "geography": "England", @@ -133,6 +143,8 @@ { "type": "trend_number", "value": { + "theme": "infectious_disease", + "sub_theme": "respiratory", "topic": "COVID-19", "metric": "COVID-19_headline_7DayAdmissionsChange", "geography": "England", @@ -157,6 +169,8 @@ { "type": "percentage_number", "value": { + "theme": "infectious_disease", + "sub_theme": "respiratory", "topic": "COVID-19", "metric": "COVID-19_headline_vaccines_autumn23Uptake", "geography": "England", @@ -180,6 +194,8 @@ { "type": "percentage_number", "value": { + "theme": "infectious_disease", + "sub_theme": "respiratory", "topic": "COVID-19", "metric": "COVID-19_headline_positivity_latest", "geography": "England", @@ -225,6 +241,8 @@ { "type": "plot", "value": { + "theme": "infectious_disease", + "sub_theme": "respiratory", "topic": "COVID-19", "metric": "COVID-19_cases_casesByDay", "geography": "England", @@ -268,6 +286,8 @@ { "type": "plot", "value": { + "theme": "infectious_disease", + "sub_theme": "respiratory", "topic": "COVID-19", "metric": "COVID-19_cases_countRollingMean", "geography": "England", @@ -303,6 +323,8 @@ { "type": "plot", "value": { + "theme": "infectious_disease", + "sub_theme": "respiratory", "topic": "COVID-19", "metric": "COVID-19_cases_rateRollingMean", "geography": "England", @@ -324,6 +346,8 @@ { "type": "plot", "value": { + "theme": "infectious_disease", + "sub_theme": "respiratory", "topic": "COVID-19", "metric": "COVID-19_cases_rateRollingMean", "geography": "England", @@ -376,6 +400,8 @@ { "type": "plot", "value": { + "theme": "infectious_disease", + "sub_theme": "respiratory", "topic": "COVID-19", "metric": "COVID-19_deaths_ONSByDay", "geography": "England", @@ -411,6 +437,8 @@ { "type": "plot", "value": { + "theme": "infectious_disease", + "sub_theme": "respiratory", "topic": "COVID-19", "metric": "COVID-19_deaths_ONSRollingMean", "geography": "England", @@ -463,6 +491,8 @@ { "type": "plot", "value": { + "theme": "infectious_disease", + "sub_theme": "respiratory", "topic": "COVID-19", "metric": "COVID-19_healthcare_admissionByDay", "geography": "England", @@ -498,6 +528,8 @@ { "type": "plot", "value": { + "theme": "infectious_disease", + "sub_theme": "respiratory", "topic": "COVID-19", "metric": "COVID-19_healthcare_occupiedBedsByDay", "geography": "England", @@ -550,6 +582,8 @@ { "type": "plot", "value": { + "theme": "infectious_disease", + "sub_theme": "respiratory", "topic": "COVID-19", "metric": "COVID-19_testing_PCRcountByDay", "geography": "England", @@ -585,6 +619,8 @@ { "type": "plot", "value": { + "theme": "infectious_disease", + "sub_theme": "respiratory", "topic": "COVID-19", "metric": "COVID-19_testing_positivity7DayRolling", "geography": "England", @@ -637,6 +673,8 @@ { "type": "plot", "value": { + "theme": "infectious_disease", + "sub_theme": "respiratory", "topic": "COVID-19", "metric": "COVID-19_vaccinations_spring23_dosesByDay", "geography": "England", @@ -672,6 +710,8 @@ { "type": "plot", "value": { + "theme": "infectious_disease", + "sub_theme": "respiratory", "topic": "COVID-19", "metric": "COVID-19_vaccinations_spring23_uptakeByDay", "geography": "England", @@ -693,6 +733,8 @@ { "type": "plot", "value": { + "theme": "infectious_disease", + "sub_theme": "respiratory", "topic": "COVID-19", "metric": "COVID-19_vaccinations_spring23_uptakeByDay", "geography": "England", @@ -736,6 +778,8 @@ { "type": "plot", "value": { + "theme": "infectious_disease", + "sub_theme": "respiratory", "topic": "COVID-19", "metric": "COVID-19_vaccinations_autumn22_dosesByDay", "geography": "England", @@ -771,6 +815,8 @@ { "type": "plot", "value": { + "theme": "infectious_disease", + "sub_theme": "respiratory", "topic": "COVID-19", "metric": "COVID-19_vaccinations_autumn22_uptakeByDay", "geography": "England", @@ -792,6 +838,8 @@ { "type": "plot", "value": { + "theme": "infectious_disease", + "sub_theme": "respiratory", "topic": "COVID-19", "metric": "COVID-19_vaccinations_autumn22_uptakeByDay", "geography": "England", @@ -813,6 +861,8 @@ { "type": "plot", "value": { + "theme": "infectious_disease", + "sub_theme": "respiratory", "topic": "COVID-19", "metric": "COVID-19_vaccinations_autumn22_uptakeByDay", "geography": "England", @@ -834,6 +884,8 @@ { "type": "plot", "value": { + "theme": "infectious_disease", + "sub_theme": "respiratory", "topic": "COVID-19", "metric": "COVID-19_vaccinations_autumn22_uptakeByDay", "geography": "England", @@ -855,6 +907,8 @@ { "type": "plot", "value": { + "theme": "infectious_disease", + "sub_theme": "respiratory", "topic": "COVID-19", "metric": "COVID-19_vaccinations_autumn22_uptakeByDay", "geography": "England", @@ -876,6 +930,8 @@ { "type": "plot", "value": { + "theme": "infectious_disease", + "sub_theme": "respiratory", "topic": "COVID-19", "metric": "COVID-19_vaccinations_autumn22_uptakeByDay", "geography": "England", @@ -897,6 +953,8 @@ { "type": "plot", "value": { + "theme": "infectious_disease", + "sub_theme": "respiratory", "topic": "COVID-19", "metric": "COVID-19_vaccinations_autumn22_uptakeByDay", "geography": "England", @@ -918,6 +976,8 @@ { "type": "plot", "value": { + "theme": "infectious_disease", + "sub_theme": "respiratory", "topic": "COVID-19", "metric": "COVID-19_vaccinations_autumn22_uptakeByDay", "geography": "England", @@ -939,6 +999,8 @@ { "type": "plot", "value": { + "theme": "infectious_disease", + "sub_theme": "respiratory", "topic": "COVID-19", "metric": "COVID-19_vaccinations_autumn22_uptakeByDay", "geography": "England", diff --git a/cms/dashboard/templates/cms_starting_pages/influenza.json b/cms/dashboard/templates/cms_starting_pages/influenza.json index efb9174601..8848371525 100644 --- a/cms/dashboard/templates/cms_starting_pages/influenza.json +++ b/cms/dashboard/templates/cms_starting_pages/influenza.json @@ -43,6 +43,8 @@ { "type": "percentage_number", "value": { + "theme": "infectious_disease", + "sub_theme": "respiratory", "topic": "Influenza", "metric": "influenza_headline_positivityLatest", "geography": "England", @@ -66,6 +68,8 @@ { "type": "headline_number", "value": { + "theme": "infectious_disease", + "sub_theme": "respiratory", "topic": "Influenza", "metric": "influenza_headline_ICUHDUadmissionRateLatest", "geography": "England", @@ -80,6 +84,8 @@ { "type": "trend_number", "value": { + "theme": "infectious_disease", + "sub_theme": "respiratory", "topic": "Influenza", "metric": "influenza_headline_ICUHDUadmissionRateChange", "geography": "England", @@ -126,6 +132,8 @@ { "type": "plot", "value": { + "theme": "infectious_disease", + "sub_theme": "respiratory", "topic": "Influenza", "metric": "influenza_healthcare_ICUHDUadmissionRateByWeek", "geography": "England", @@ -161,6 +169,8 @@ { "type": "plot", "value": { + "theme": "infectious_disease", + "sub_theme": "respiratory", "topic": "Influenza", "metric": "influenza_healthcare_ICUHDUadmissionRateByWeek", "geography": "England", @@ -182,6 +192,8 @@ { "type": "plot", "value": { + "theme": "infectious_disease", + "sub_theme": "respiratory", "topic": "Influenza", "metric": "influenza_healthcare_ICUHDUadmissionRateByWeek", "geography": "England", @@ -203,6 +215,8 @@ { "type": "plot", "value": { + "theme": "infectious_disease", + "sub_theme": "respiratory", "topic": "Influenza", "metric": "influenza_healthcare_ICUHDUadmissionRateByWeek", "geography": "England", @@ -224,6 +238,8 @@ { "type": "plot", "value": { + "theme": "infectious_disease", + "sub_theme": "respiratory", "topic": "Influenza", "metric": "influenza_healthcare_ICUHDUadmissionRateByWeek", "geography": "England", @@ -245,6 +261,8 @@ { "type": "plot", "value": { + "theme": "infectious_disease", + "sub_theme": "respiratory", "topic": "Influenza", "metric": "influenza_healthcare_ICUHDUadmissionRateByWeek", "geography": "England", @@ -266,6 +284,8 @@ { "type": "plot", "value": { + "theme": "infectious_disease", + "sub_theme": "respiratory", "topic": "Influenza", "metric": "influenza_healthcare_ICUHDUadmissionRateByWeek", "geography": "England", @@ -287,6 +307,8 @@ { "type": "plot", "value": { + "theme": "infectious_disease", + "sub_theme": "respiratory", "topic": "Influenza", "metric": "influenza_healthcare_ICUHDUadmissionRateByWeek", "geography": "England", @@ -308,6 +330,8 @@ { "type": "plot", "value": { + "theme": "infectious_disease", + "sub_theme": "respiratory", "topic": "Influenza", "metric": "influenza_healthcare_ICUHDUadmissionRateByWeek", "geography": "England", @@ -360,6 +384,8 @@ { "type": "plot", "value": { + "theme": "infectious_disease", + "sub_theme": "respiratory", "topic": "Influenza", "metric": "influenza_testing_positivityByWeek", "geography": "England", @@ -395,6 +421,8 @@ { "type": "plot", "value": { + "theme": "infectious_disease", + "sub_theme": "respiratory", "topic": "Influenza", "metric": "influenza_testing_positivityByWeek", "geography": "England", @@ -416,6 +444,8 @@ { "type": "plot", "value": { + "theme": "infectious_disease", + "sub_theme": "respiratory", "topic": "Influenza", "metric": "influenza_testing_positivityByWeek", "geography": "England", @@ -437,6 +467,8 @@ { "type": "plot", "value": { + "theme": "infectious_disease", + "sub_theme": "respiratory", "topic": "Influenza", "metric": "influenza_testing_positivityByWeek", "geography": "England", @@ -458,6 +490,8 @@ { "type": "plot", "value": { + "theme": "infectious_disease", + "sub_theme": "respiratory", "topic": "Influenza", "metric": "influenza_testing_positivityByWeek", "geography": "England", @@ -479,6 +513,8 @@ { "type": "plot", "value": { + "theme": "infectious_disease", + "sub_theme": "respiratory", "topic": "Influenza", "metric": "influenza_testing_positivityByWeek", "geography": "England", diff --git a/cms/dashboard/templates/cms_starting_pages/other_respiratory_viruses.json b/cms/dashboard/templates/cms_starting_pages/other_respiratory_viruses.json index eb558b2b2f..b91ee52d90 100644 --- a/cms/dashboard/templates/cms_starting_pages/other_respiratory_viruses.json +++ b/cms/dashboard/templates/cms_starting_pages/other_respiratory_viruses.json @@ -43,6 +43,8 @@ { "type": "percentage_number", "value": { + "theme": "infectious_disease", + "sub_theme": "respiratory", "topic": "Adenovirus", "metric": "adenovirus_headline_positivityLatest", "geography": "England", @@ -66,6 +68,8 @@ { "type": "percentage_number", "value": { + "theme": "infectious_disease", + "sub_theme": "respiratory", "topic": "hMPV", "metric": "hMPV_headline_positivityLatest", "geography": "England", @@ -89,6 +93,8 @@ { "type": "percentage_number", "value": { + "theme": "infectious_disease", + "sub_theme": "respiratory", "topic": "Parainfluenza", "metric": "parainfluenza_headline_positivityLatest", "geography": "England", @@ -112,6 +118,8 @@ { "type": "percentage_number", "value": { + "theme": "infectious_disease", + "sub_theme": "respiratory", "topic": "Rhinovirus", "metric": "rhinovirus_headline_positivityLatest", "geography": "England", @@ -157,6 +165,8 @@ { "type": "plot", "value": { + "theme": "infectious_disease", + "sub_theme": "respiratory", "topic": "RSV", "metric": "RSV_healthcare_admissionRateByWeek", "geography": "England", @@ -178,6 +188,8 @@ { "type": "plot", "value": { + "theme": "infectious_disease", + "sub_theme": "respiratory", "topic": "RSV", "metric": "RSV_healthcare_admissionRateByWeek", "geography": "England", @@ -199,6 +211,8 @@ { "type": "plot", "value": { + "theme": "infectious_disease", + "sub_theme": "respiratory", "topic": "RSV", "metric": "RSV_healthcare_admissionRateByWeek", "geography": "England", @@ -220,6 +234,8 @@ { "type": "plot", "value": { + "theme": "infectious_disease", + "sub_theme": "respiratory", "topic": "RSV", "metric": "RSV_healthcare_admissionRateByWeek", "geography": "England", @@ -241,6 +257,8 @@ { "type": "plot", "value": { + "theme": "infectious_disease", + "sub_theme": "respiratory", "topic": "RSV", "metric": "RSV_healthcare_admissionRateByWeek", "geography": "England", @@ -262,6 +280,8 @@ { "type": "plot", "value": { + "theme": "infectious_disease", + "sub_theme": "respiratory", "topic": "RSV", "metric": "RSV_healthcare_admissionRateByWeek", "geography": "England", @@ -283,6 +303,8 @@ { "type": "plot", "value": { + "theme": "infectious_disease", + "sub_theme": "respiratory", "topic": "RSV", "metric": "RSV_healthcare_admissionRateByWeek", "geography": "England", @@ -304,6 +326,8 @@ { "type": "plot", "value": { + "theme": "infectious_disease", + "sub_theme": "respiratory", "topic": "RSV", "metric": "RSV_healthcare_admissionRateByWeek", "geography": "England", @@ -356,6 +380,8 @@ { "type": "plot", "value": { + "theme": "infectious_disease", + "sub_theme": "respiratory", "topic": "Adenovirus", "metric": "adenovirus_testing_positivityByWeek", "geography": "England", @@ -399,6 +425,8 @@ { "type": "plot", "value": { + "theme": "infectious_disease", + "sub_theme": "respiratory", "topic": "hMPV", "metric": "hMPV_testing_positivityByWeek", "geography": "England", @@ -442,6 +470,8 @@ { "type": "plot", "value": { + "theme": "infectious_disease", + "sub_theme": "respiratory", "topic": "Parainfluenza", "metric": "parainfluenza_testing_positivityByWeek", "geography": "England", @@ -485,6 +515,8 @@ { "type": "plot", "value": { + "theme": "infectious_disease", + "sub_theme": "respiratory", "topic": "Rhinovirus", "metric": "rhinovirus_testing_positivityByWeek", "geography": "England", @@ -528,6 +560,8 @@ { "type": "plot", "value": { + "theme": "infectious_disease", + "sub_theme": "respiratory", "topic": "RSV", "metric": "RSV_testing_positivityByWeek", "geography": "England", diff --git a/cms/dynamic_content/elements.py b/cms/dynamic_content/elements.py index 06155a2c2a..799e97ca5c 100644 --- a/cms/dynamic_content/elements.py +++ b/cms/dynamic_content/elements.py @@ -11,9 +11,11 @@ get_all_headline_metric_names, get_all_sex_names, get_all_stratum_names, + get_all_theme_names, get_all_timeseries_metric_names, get_all_topic_names, get_all_unique_metric_names, + get_all_unique_sub_theme_names, get_chart_line_types, get_chart_types, get_colours, @@ -32,6 +34,24 @@ class BaseMetricsElement(blocks.StructBlock): + """ + Base block for the chart plots that are embedded in each CMS page body. + + Each field below is a dropdown with options that are supplied by its + "choices=get_all_XXX_names", which queries valid values. The values a CMS + editor selects are stored on the page's body and serialized by the pages API. + """ + + theme = blocks.ChoiceBlock( + required=True, + choices=get_all_theme_names, + help_text=help_texts.THEME_FIELD, + ) + sub_theme = blocks.ChoiceBlock( + required=True, + choices=get_all_unique_sub_theme_names, + help_text=help_texts.SUB_THEME_FIELD, + ) topic = blocks.ChoiceBlock( required=True, choices=get_all_topic_names, diff --git a/cms/home/migrations/0041_alter_landingpage_body.py b/cms/home/migrations/0041_alter_landingpage_body.py new file mode 100644 index 0000000000..59dc3bdbc0 --- /dev/null +++ b/cms/home/migrations/0041_alter_landingpage_body.py @@ -0,0 +1,1168 @@ +# Generated by Django 5.2.14 on 2026-06-29 19:15 + +import cms.dynamic_content.cards +import cms.metrics_interface.field_choices_callables +import validation.url +import wagtail.fields +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("home", "0040_add_dual_cat_chart_date_preix"), + ] + + operations = [ + migrations.AlterField( + model_name="landingpage", + name="body", + field=wagtail.fields.StreamField( + [("section", 110)], + block_lookup={ + 0: ( + "wagtail.blocks.TextBlock", + (), + { + "help_text": "\nThe text you add here will be used as the heading for this section. \n", + "required": True, + }, + ), + 1: ( + "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, + }, + ), + 2: ( + "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", + }, + ), + 3: ("wagtail.blocks.StructBlock", [[("body", 2)]], {}), + 4: ( + "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, + }, + ), + 5: ( + "wagtail.blocks.TextBlock", + (), + { + "help_text": "\nAn optional body of text to accompany this block. This text will be displayed below the chart title.\n", + "label": "Subtitle", + "required": False, + }, + ), + 6: ( + "wagtail.blocks.TextBlock", + (), + { + "default": "", + "help_text": "\nAn optional body of text to accompany this block. This text will be displayed in the about content of the chart.\n", + "required": False, + }, + ), + 7: ( + "wagtail.blocks.CharBlock", + (), + { + "help_text": "\nThe Text that will be displayed for the URL.\n", + "required": True, + }, + ), + 8: ( + "wagtail.blocks.CharBlock", + (), + { + "help_text": "\nThe URL that the user will be navigated to when clicked.\nAn optional body of text to accompany this block. This text will be displayed below the chart title.\n", + "required": True, + }, + ), + 9: ( + "wagtail.blocks.StructBlock", + [[("link_display_text", 7), ("link", 8)]], + {}, + ), + 10: ( + "wagtail.blocks.StreamBlock", + [[("related_link", 9)]], + { + "help_text": "\nProvide optional URLs that can provide further contextual information for the data displayed in the chart.\n", + "required": False, + }, + ), + 11: ( + "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, + }, + ), + 12: ( + "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, + }, + ), + 13: ( + "wagtail.blocks.CharBlock", + (), + { + "default": "", + "help_text": "\nAn optional title to display along the x-axis of the chart.\nIf nothing is provided, then no title will be displayed.\n", + "required": False, + }, + ), + 14: ( + "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, + }, + ), + 15: ( + "wagtail.blocks.CharBlock", + (), + { + "default": "", + "help_text": "\nAn optional title to display along the y-axis of the chart.\nIf nothing is provided, then no title will be displayed.\n", + "required": False, + }, + ), + 16: ( + "wagtail.blocks.DecimalBlock", + (), + { + "default": 0, + "help_text": "\nThis field allows you to set the first value in the chart's y-axis range. Please\nnote that a value provided here, which is higher than the lowest value in the data will\nbe overridden and the value from the dataset will be used.\n", + "required": False, + }, + ), + 17: ( + "wagtail.blocks.DecimalBlock", + (), + { + "help_text": "\nThis field allows you to set the last value in the chart's y-axis range. Please\nnote that a value provided here, which is lower than the highest value in the data will\nbe overridden and the value from the dataset will be used. \n", + "required": False, + }, + ), + 18: ( + "wagtail.blocks.BooleanBlock", + (), + { + "default": False, + "help_text": "\nThis is a switch to show tooltips on hover within the chart.\nDefaults to False.\n", + "required": False, + }, + ), + 19: ( + "wagtail.blocks.CharBlock", + (), + { + "default": "Up to and including", + "help_texts": "\nThis is the accompanying text for chart dates Eg: `Up to and including` 21 Oct 2024\n", + "requried": True, + }, + ), + 20: ( + "wagtail.blocks.BooleanBlock", + (), + { + "default": False, + "help_text": "\nThis option enables timeseries filter for this chart.\nThe timeseries filter allows a user to change the timeseries range for example between\n1m, 3m, 6m, 1y etc.\n", + "required": False, + }, + ), + 21: ( + "wagtail.blocks.ChoiceBlock", + [], + { + "choices": cms.metrics_interface.field_choices_callables.get_all_theme_names, + "help_text": "\nThe related theme for the metric. Eg: infectious disease\n", + }, + ), + 22: ( + "wagtail.blocks.ChoiceBlock", + [], + { + "choices": cms.metrics_interface.field_choices_callables.get_all_unique_sub_theme_names, + "help_text": "\nThe related sub theme for the metric. Eg: respiratory\n", + }, + ), + 23: ( + "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.", + }, + ), + 24: ( + "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', + }, + ), + 25: ( + "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", + }, + ), + 26: ( + "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", + }, + ), + 27: ( + "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", + }, + ), + 28: ( + "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", + }, + ), + 29: ( + "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", + }, + ), + 30: ( + "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. bar\n", + }, + ), + 31: ( + "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, + }, + ), + 32: ( + "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, + }, + ), + 33: ( + "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, + }, + ), + 34: ( + "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', + }, + ), + 35: ( + "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, + }, + ), + 36: ( + "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, + }, + ), + 37: ( + "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, + }, + ), + 38: ( + "wagtail.blocks.StructBlock", + [ + [ + ("theme", 21), + ("sub_theme", 22), + ("topic", 23), + ("metric", 24), + ("geography", 25), + ("geography_type", 26), + ("sex", 27), + ("age", 28), + ("stratum", 29), + ("chart_type", 30), + ("date_from", 31), + ("date_to", 32), + ("label", 33), + ("line_colour", 34), + ("line_type", 35), + ("use_markers", 36), + ("use_smooth_lines", 37), + ] + ], + {}, + ), + 39: ( + "wagtail.blocks.StreamBlock", + [[("plot", 38)]], + { + "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" + }, + ), + 40: ( + "wagtail.blocks.StructBlock", + [ + [ + ("title", 4), + ("body", 5), + ("about", 6), + ("related_links", 10), + ("tag_manager_event_id", 11), + ("x_axis", 12), + ("x_axis_title", 13), + ("y_axis", 14), + ("y_axis_title", 15), + ("y_axis_minimum_value", 16), + ("y_axis_maximum_value", 17), + ("show_tooltips", 18), + ("date_prefix", 19), + ("show_timeseries_filter", 20), + ("chart", 39), + ] + ], + {}, + ), + 41: ( + "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, + }, + ), + 42: ( + "wagtail.blocks.TextBlock", + (), + { + "help_text": "\nRequired description for the chart.\n", + "required": True, + }, + ), + 43: ( + "wagtail.blocks.CharBlock", + (), + { + "help_text": "\nDisplay text for the link.\n", + "required": False, + }, + ), + 44: ( + "cms.dynamic_content.blocks.PageLinkChooserBlock", + (), + { + "help_text": "\nFor linking to internal pages. (If chosen, external_url must be blank).\n", + "page_type": ["topic.TopicPage"], + "required": False, + }, + ), + 45: ( + "wagtail.blocks.URLBlock", + (), + { + "help_text": "\nFor linking to external url. (Only one of page or external_url must be filled not both).\n", + "required": False, + "validators": [validation.url.validate_https_scheme], + }, + ), + 46: ( + "wagtail.blocks.StructBlock", + [ + [ + ("link_display_text", 43), + ("page", 44), + ("external_url", 45), + ] + ], + { + "help_text": "\nSource link (internal or external).\n", + "required": False, + }, + ), + 47: ( + "wagtail.blocks.StructBlock", + [ + [ + ("title", 4), + ("sub_title", 5), + ("topic_page", 41), + ("description", 42), + ("source", 46), + ("tag_manager_event_id", 11), + ("x_axis", 12), + ("x_axis_title", 13), + ("y_axis", 14), + ("y_axis_title", 15), + ("y_axis_minimum_value", 16), + ("y_axis_maximum_value", 17), + ("show_tooltips", 18), + ("chart", 39), + ] + ], + {}, + ), + 48: ( + "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", + }, + ), + 49: ( + "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', + }, + ), + 50: ( + "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. bar\n", + }, + ), + 51: ( + "wagtail.blocks.StructBlock", + [ + [ + ("theme", 21), + ("sub_theme", 22), + ("topic", 23), + ("metric", 49), + ("geography", 25), + ("geography_type", 26), + ("sex", 27), + ("age", 28), + ("stratum", 29), + ("chart_type", 50), + ("line_colour", 34), + ("label", 33), + ] + ], + {}, + ), + 52: ( + "wagtail.blocks.StreamBlock", + [[("plot", 51)]], + { + "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" + }, + ), + 53: ( + "wagtail.blocks.BooleanBlock", + (), + { + "default": False, + "help_text": "\nToggle to enable confidence intervals if they are present in the data set\n", + "required": False, + }, + ), + 54: ( + "wagtail.blocks.TextBlock", + (), + { + "default": "Metric column includes 95% lower and upper confidence intervals, in brackets.", + "help_text": "\nAn optional body of text to accompany this block.\nThis text will be displayed above the metrics table if confidence intervals is enabled.\n", + "required": False, + }, + ), + 55: ( + "wagtail.blocks.ChoiceBlock", + [], + { + "choices": cms.metrics_interface.field_choices_callables.get_colours, + "help_text": '\nThe colour to display the confidence interval in. The colours conform to the GDS specification.\nNote that if nothing is provided, a default of "BLACK" will be applied.\n', + "required": False, + }, + ), + 56: ( + "wagtail.blocks.StructBlock", + [ + [ + ("title", 4), + ("body", 5), + ("about", 6), + ("related_links", 10), + ("tag_manager_event_id", 11), + ("x_axis", 48), + ("x_axis_title", 13), + ("y_axis", 14), + ("y_axis_title", 15), + ("y_axis_minimum_value", 16), + ("y_axis_maximum_value", 17), + ("show_tooltips", 18), + ("date_prefix", 19), + ("show_timeseries_filter", 20), + ("chart", 52), + ("confidence_intervals", 53), + ("confidence_intervals_description", 54), + ("confidence_colour", 55), + ] + ], + {}, + ), + 57: ( + "wagtail.blocks.TextBlock", + (), + { + "help_text": "\nAn optional body of text to accompany this block. This text will be displayed in the about content of the chart.\n", + "required": False, + }, + ), + 58: ( + "wagtail.blocks.CharBlock", + (), + { + "default": "Up to and including", + "help_text": "\nThis is the accompanying text for chart dates Eg: `Up to and including` 21 Oct 2024\n", + "required": True, + }, + ), + 59: ( + "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', + }, + ), + 60: ( + "wagtail.blocks.TextBlock", + (), + { + "help_text": "\nAn optional body of text to accompany this block. This text will be displayed below the chart title.\n", + "required": False, + }, + ), + 61: ( + "wagtail.blocks.StructBlock", + [ + [ + ("theme", 21), + ("sub_theme", 22), + ("topic", 23), + ("metric", 59), + ("geography", 25), + ("geography_type", 26), + ("sex", 27), + ("age", 28), + ("stratum", 29), + ("body", 60), + ] + ], + { + "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' + }, + ), + 62: ( + "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", + }, + ), + 63: ( + "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", + }, + ), + 64: ( + "wagtail.blocks.StructBlock", + [ + [ + ("theme", 21), + ("sub_theme", 22), + ("topic", 23), + ("metric", 62), + ("geography", 25), + ("geography_type", 26), + ("sex", 27), + ("age", 28), + ("stratum", 29), + ("body", 60), + ("percentage_metric", 63), + ] + ], + { + "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' + }, + ), + 65: ( + "wagtail.blocks.StructBlock", + [ + [ + ("theme", 21), + ("sub_theme", 22), + ("topic", 23), + ("metric", 59), + ("geography", 25), + ("geography_type", 26), + ("sex", 27), + ("age", 28), + ("stratum", 29), + ("body", 60), + ] + ], + { + "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' + }, + ), + 66: ( + "wagtail.blocks.StreamBlock", + [ + [ + ("headline_number", 61), + ("trend_number", 64), + ("percentage_number", 65), + ] + ], + { + "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, + }, + ), + 67: ( + "wagtail.blocks.StructBlock", + [ + [ + ("title", 4), + ("body", 5), + ("about", 57), + ("related_links", 10), + ("tag_manager_event_id", 11), + ("x_axis", 12), + ("x_axis_title", 13), + ("y_axis", 14), + ("y_axis_title", 15), + ("y_axis_minimum_value", 16), + ("y_axis_maximum_value", 17), + ("show_tooltips", 18), + ("date_prefix", 58), + ("show_timeseries_filter", 20), + ("chart", 39), + ("headline_number_columns", 66), + ] + ], + {}, + ), + 68: ( + "wagtail.blocks.CharBlock", + (), + { + "help_text": "\nThe sub title to display for this component.\n", + "required": False, + }, + ), + 69: ( + "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, + }, + ), + 70: ( + "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", + }, + ), + 71: ( + "wagtail.blocks.ChoiceBlock", + [], + { + "choices": cms.metrics_interface.field_choices_callables.get_simplified_chart_types + }, + ), + 72: ( + "wagtail.blocks.BooleanBlock", + (), + { + "default": False, + "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, + }, + ), + 73: ( + "wagtail.blocks.StructBlock", + [ + [ + ("theme", 21), + ("sub_theme", 22), + ("topic", 23), + ("metric", 24), + ("geography", 25), + ("geography_type", 26), + ("sex", 27), + ("age", 28), + ("stratum", 29), + ("chart_type", 71), + ("date_from", 31), + ("date_to", 32), + ("use_smooth_lines", 72), + ] + ], + {}, + ), + 74: ( + "wagtail.blocks.StreamBlock", + [[("plot", 73)]], + { + "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, + }, + ), + 75: ( + "wagtail.blocks.StructBlock", + [ + [ + ("title", 4), + ("sub_title", 68), + ("tag_manager_event_id", 11), + ("topic_page", 41), + ("x_axis", 69), + ("x_axis_title", 13), + ("y_axis", 70), + ("y_axis_title", 15), + ("y_axis_minimum_value", 16), + ("y_axis_maximum_value", 17), + ("chart", 74), + ] + ], + {}, + ), + 76: ( + "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", + }, + ), + 77: ( + "wagtail.blocks.MultipleChoiceBlock", + [], + { + "choices": cms.metrics_interface.field_choices_callables.get_all_subcategory_choices, + "help_text": "\nSelect a list of primary field values for the chart, these will be you're x-axis.\nFor example if we're creating a stacked bar chart that has a metric value in y and geographies along\nthe x-axis. The `primary field values` should be the list of geographies to include in the chart.\n", + "required": False, + }, + ), + 78: ( + "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", + }, + ), + 79: ( + "wagtail.blocks.ChoiceBlock", + [], + { + "choices": cms.metrics_interface.field_choices_callables.get_dual_category_chart_types, + "help_text": "\nThe name of the type of chart which you want to create e.g. bar\n", + "required": False, + }, + ), + 80: ( + "wagtail.blocks.StructBlock", + [ + [ + ("theme", 21), + ("sub_theme", 22), + ("topic", 23), + ("metric", 59), + ("geography", 25), + ("geography_type", 26), + ("sex", 27), + ("age", 28), + ("stratum", 29), + ("date_from", 31), + ("date_to", 32), + ] + ], + {}, + ), + 81: ( + "wagtail.blocks.ChoiceBlock", + [], + { + "choices": cms.metrics_interface.field_choices_callables.get_dual_chart_secondary_category_choices, + "help_text": "\nThis is for selecting the Second categorical variable type for Dual category charts.\nFor example when building a `Stacked bar chart` where the x-axis may be of type `Sex` and\ndisplay `Male` and `Female` along the x-axis. If our stacked bar chart then breaks each bar up into\nage groups, then our `Secondary Category` type is `Age`.\n", + }, + ), + 82: ( + "wagtail.blocks.ChoiceBlock", + [], + { + "choices": cms.metrics_interface.field_choices_callables.get_all_subcategory_choices, + "help_text": "\nSelect the secondary field for a `Segments` this is the second categorical variable used to create segments\nof a `stacked bar` chart. For example if we're creating a stacked bar chart that has a metric in the y-axis\nand geographies along the x-axis. If each bar is broken into segments by `age group` this field\nshould be the age group for this segment.\n", + }, + ), + 83: ( + "wagtail.blocks.StructBlock", + [ + [ + ("secondary_field_value", 82), + ("colour", 34), + ("label", 33), + ] + ], + {}, + ), + 84: ( + "wagtail.blocks.StreamBlock", + [[("segment", 83)]], + {"min_num": 1}, + ), + 85: ( + "wagtail.blocks.StructBlock", + [ + [ + ("title", 4), + ("body", 5), + ("about", 6), + ("related_links", 10), + ("tag_manager_event_id", 11), + ("x_axis", 76), + ("x_axis_title", 13), + ("primary_field_values", 77), + ("y_axis", 78), + ("y_axis_title", 15), + ("y_axis_minimum_value", 16), + ("y_axis_maximum_value", 17), + ("chart_type", 79), + ("static_fields", 80), + ("second_category", 81), + ("segments", 84), + ] + ], + {}, + ), + 86: ( + "wagtail.blocks.StreamBlock", + [ + [ + ("chart_card", 40), + ("chart_with_description_card", 47), + ("headline_chart_card", 56), + ("chart_with_headline_and_trend_card", 67), + ("simplified_chart_with_link", 75), + ("dual_category_chart_card", 85), + ] + ], + { + "help_text": "\nHere you can add chart cards to a section and the layout will change based on the number of cards added.\nA single card will expand to take up half the row. When 2 or 3 cards are added they will share the width\nof a row equally, creating either a 2 or 3 column layout.\n", + "min_num": 1, + }, + ), + 87: ("wagtail.blocks.StructBlock", [[("cards", 86)]], {}), + 88: ( + "wagtail.blocks.TextBlock", + (), + { + "default": "Up to", + "help_text": "\nThis is the accompanying text for headline column dates Eg: `Up to` 27 Oct 2024 \n", + "required": True, + }, + ), + 89: ( + "wagtail.blocks.StreamBlock", + [ + [ + ("headline_number", 61), + ("trend_number", 64), + ("percentage_number", 65), + ] + ], + { + "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, + }, + ), + 90: ( + "wagtail.blocks.StructBlock", + [[("title", 4), ("date_prefix", 88), ("rows", 89)]], + {}, + ), + 91: ( + "wagtail.blocks.StreamBlock", + [[("column", 90)]], + { + "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, + }, + ), + 92: ("wagtail.blocks.StructBlock", [[("columns", 91)]], {}), + 93: ( + "wagtail.blocks.TextBlock", + (), + { + "help_text": "\nThe sub title to display for this component.\n", + "required": True, + }, + ), + 94: ( + "wagtail.blocks.TextBlock", + (), + { + "help_text": "\nOptional description for the weather health alerts card.\n", + "required": False, + }, + ), + 95: ( + "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", + }, + ), + 96: ( + "wagtail.blocks.StructBlock", + [ + [ + ("link_display_text", 43), + ("page", 44), + ("external_url", 45), + ] + ], + {"help_text": "\nOptional source link.\n", "required": False}, + ), + 97: ( + "wagtail.blocks.StructBlock", + [ + [ + ("title", 4), + ("sub_title", 93), + ("description", 94), + ("alert_type", 95), + ("source", 96), + ] + ], + {}, + ), + 98: ( + "wagtail.blocks.StreamBlock", + [ + [ + ("weather_health_alert_card", 97), + ("chart_card_with_description", 47), + ] + ], + { + "help_text": "\nThis will be used to display a full height card on the left column.\nChoose either a weather health alerts card or a chart card with description.\n", + "max_num": 1, + "min_num": 1, + }, + ), + 99: ( + "wagtail.blocks.StreamBlock", + [[("chart_card", 75)]], + { + "help_text": "\nThis will be used to display a chart card in the top row of the second (right) column.\n", + "max_num": 1, + "min_num": 1, + }, + ), + 100: ( + "wagtail.blocks.StreamBlock", + [[("headline_number", 61), ("trend_number", 64)]], + { + "help_text": "\nThis block only allows 2 headline number blocks to be added.\nIt can be used to add headline number and trend number.\n", + "max_num": 2, + "min_num": 2, + "required": True, + }, + ), + 101: ( + "wagtail.blocks.StructBlock", + [ + [ + ("title", 4), + ("date_prefix", 88), + ("topic_page", 41), + ("headline_metrics", 100), + ] + ], + { + "help_text": "\nEach card will be displayed from left to right and will share and occupy half \nof the bottom row right column of the popular topics component.\n", + "max_num": 2, + "min_num": 2, + "required": True, + }, + ), + 102: ( + "wagtail.blocks.StreamBlock", + [[("headline_metric_card", 101)]], + { + "help_text": "\nThis will require 2 headline metrics cards which will be displayed from left to right \nwith each card occupying and sharing half of the bottom row right column of the \npopular topics component.\n", + "max_num": 2, + "min_num": 2, + }, + ), + 103: ( + "wagtail.blocks.StructBlock", + [ + [ + ("left_column", 98), + ("right_column_top_row", 99), + ("right_column_bottom_row", 102), + ] + ], + {}, + ), + 104: ( + "wagtail.blocks.StreamBlock", + [ + [ + ("text_card", 3), + ("chart_card_section", 87), + ("headline_numbers_row_card", 92), + ("weather_health_alert_card", 97), + ("popular_topics_card", 103), + ] + ], + { + "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" + }, + ), + 105: ( + "wagtail.blocks.CharBlock", + (), + { + "help_text": '\nThis is the label used for the a section footer link badge"\n', + "required": True, + }, + ), + 106: ( + "wagtail.blocks.CharBlock", + (), + { + "help_text": "\nThis is the text to displayed along side a link in a the sections footer link.\n", + "required": True, + }, + ), + 107: ( + "wagtail.blocks.StructBlock", + [ + [ + ("link_display_text", 43), + ("page", 44), + ("external_url", 45), + ] + ], + { + "help_text": "\nThis is a link component that allows the user to setup an internal or external link along with a short description of the link's content.\n", + "required": True, + }, + ), + 108: ( + "wagtail.blocks.StructBlock", + [[("badge_label", 105), ("text", 106), ("link", 107)]], + {"max_num": 1}, + ), + 109: ( + "wagtail.blocks.StreamBlock", + [[("section_link", 108)]], + { + "help_text": "\nThis is an optional footer for a section to provide a link to further information.\n", + "required": False, + }, + ), + 110: ( + "wagtail.blocks.StructBlock", + [ + [ + ("heading", 0), + ("page_link", 1), + ("content", 104), + ("footer", 109), + ] + ], + {}, + ), + }, + ), + ), + ] diff --git a/cms/home/migrations/0042_backfill_theme_subtheme.py b/cms/home/migrations/0042_backfill_theme_subtheme.py new file mode 100644 index 0000000000..f254582744 --- /dev/null +++ b/cms/home/migrations/0042_backfill_theme_subtheme.py @@ -0,0 +1,20 @@ +from django.db import migrations +from cms.utils.migrations.backfill_helpers import backfill_pages_with_theme_and_subtheme + + +def backfill_landing_pages(apps, schema_editor): + backfill_pages_with_theme_and_subtheme(apps, "home", "LandingPage") + + +def noop(apps, schema_editor): + """Backfill is one-directional; nothing sensible to reverse.""" + pass + + +class Migration(migrations.Migration): + dependencies = [ + ("home", "0041_alter_landingpage_body"), + ] + operations = [ + migrations.RunPython(backfill_landing_pages, noop), + ] diff --git a/cms/topic/migrations/0038_alter_topicpage_body.py b/cms/topic/migrations/0038_alter_topicpage_body.py new file mode 100644 index 0000000000..0a362f30a3 --- /dev/null +++ b/cms/topic/migrations/0038_alter_topicpage_body.py @@ -0,0 +1,1482 @@ +# Generated by Django 5.2.14 on 2026-06-29 19:15 + +import cms.metrics_interface.field_choices_callables +import validation.url +import wagtail.fields +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("topic", "0037_add_dual_cat_chart_date_prefix"), + ] + + operations = [ + migrations.AlterField( + model_name="topicpage", + name="body", + field=wagtail.fields.StreamField( + [("section", 154)], + block_lookup={ + 0: ( + "wagtail.blocks.TextBlock", + (), + { + "help_text": "\nThe text you add here will be used as the heading for this section. \n", + "required": True, + }, + ), + 1: ( + "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", + }, + ), + 2: ("wagtail.blocks.StructBlock", [[("body", 1)]], {}), + 3: ( + "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, + }, + ), + 4: ( + "wagtail.blocks.TextBlock", + (), + { + "help_text": "\nAn optional body of text to accompany this block. This text will be displayed below the chart title.\n", + "label": "Subtitle", + "required": False, + }, + ), + 5: ( + "wagtail.blocks.TextBlock", + (), + { + "default": "", + "help_text": "\nAn optional body of text to accompany this block. This text will be displayed in the about content of the chart.\n", + "required": False, + }, + ), + 6: ( + "wagtail.blocks.CharBlock", + (), + { + "help_text": "\nThe Text that will be displayed for the URL.\n", + "required": True, + }, + ), + 7: ( + "wagtail.blocks.CharBlock", + (), + { + "help_text": "\nThe URL that the user will be navigated to when clicked.\nAn optional body of text to accompany this block. This text will be displayed below the chart title.\n", + "required": True, + }, + ), + 8: ( + "wagtail.blocks.StructBlock", + [[("link_display_text", 6), ("link", 7)]], + {}, + ), + 9: ( + "wagtail.blocks.StreamBlock", + [[("related_link", 8)]], + { + "help_text": "\nProvide optional URLs that can provide further contextual information for the data displayed in the chart.\n", + "required": False, + }, + ), + 10: ( + "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, + }, + ), + 11: ( + "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, + }, + ), + 12: ( + "wagtail.blocks.CharBlock", + (), + { + "default": "", + "help_text": "\nAn optional title to display along the x-axis of the chart.\nIf nothing is provided, then no title will be displayed.\n", + "required": False, + }, + ), + 13: ( + "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, + }, + ), + 14: ( + "wagtail.blocks.CharBlock", + (), + { + "default": "", + "help_text": "\nAn optional title to display along the y-axis of the chart.\nIf nothing is provided, then no title will be displayed.\n", + "required": False, + }, + ), + 15: ( + "wagtail.blocks.DecimalBlock", + (), + { + "default": 0, + "help_text": "\nThis field allows you to set the first value in the chart's y-axis range. Please\nnote that a value provided here, which is higher than the lowest value in the data will\nbe overridden and the value from the dataset will be used.\n", + "required": False, + }, + ), + 16: ( + "wagtail.blocks.DecimalBlock", + (), + { + "help_text": "\nThis field allows you to set the last value in the chart's y-axis range. Please\nnote that a value provided here, which is lower than the highest value in the data will\nbe overridden and the value from the dataset will be used. \n", + "required": False, + }, + ), + 17: ( + "wagtail.blocks.BooleanBlock", + (), + { + "default": False, + "help_text": "\nThis is a switch to show tooltips on hover within the chart.\nDefaults to False.\n", + "required": False, + }, + ), + 18: ( + "wagtail.blocks.CharBlock", + (), + { + "default": "Up to and including", + "help_texts": "\nThis is the accompanying text for chart dates Eg: `Up to and including` 21 Oct 2024\n", + "requried": True, + }, + ), + 19: ( + "wagtail.blocks.BooleanBlock", + (), + { + "default": False, + "help_text": "\nThis option enables timeseries filter for this chart.\nThe timeseries filter allows a user to change the timeseries range for example between\n1m, 3m, 6m, 1y etc.\n", + "required": False, + }, + ), + 20: ( + "wagtail.blocks.ChoiceBlock", + [], + { + "choices": cms.metrics_interface.field_choices_callables.get_all_theme_names, + "help_text": "\nThe related theme for the metric. Eg: infectious disease\n", + }, + ), + 21: ( + "wagtail.blocks.ChoiceBlock", + [], + { + "choices": cms.metrics_interface.field_choices_callables.get_all_unique_sub_theme_names, + "help_text": "\nThe related sub theme for the metric. Eg: respiratory\n", + }, + ), + 22: ( + "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.", + }, + ), + 23: ( + "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', + }, + ), + 24: ( + "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", + }, + ), + 25: ( + "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", + }, + ), + 26: ( + "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", + }, + ), + 27: ( + "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", + }, + ), + 28: ( + "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", + }, + ), + 29: ( + "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. bar\n", + }, + ), + 30: ( + "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, + }, + ), + 31: ( + "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, + }, + ), + 32: ( + "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, + }, + ), + 33: ( + "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', + }, + ), + 34: ( + "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, + }, + ), + 35: ( + "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, + }, + ), + 36: ( + "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, + }, + ), + 37: ( + "wagtail.blocks.StructBlock", + [ + [ + ("theme", 20), + ("sub_theme", 21), + ("topic", 22), + ("metric", 23), + ("geography", 24), + ("geography_type", 25), + ("sex", 26), + ("age", 27), + ("stratum", 28), + ("chart_type", 29), + ("date_from", 30), + ("date_to", 31), + ("label", 32), + ("line_colour", 33), + ("line_type", 34), + ("use_markers", 35), + ("use_smooth_lines", 36), + ] + ], + {}, + ), + 38: ( + "wagtail.blocks.StreamBlock", + [[("plot", 37)]], + { + "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" + }, + ), + 39: ( + "wagtail.blocks.StructBlock", + [ + [ + ("title", 3), + ("body", 4), + ("about", 5), + ("related_links", 9), + ("tag_manager_event_id", 10), + ("x_axis", 11), + ("x_axis_title", 12), + ("y_axis", 13), + ("y_axis_title", 14), + ("y_axis_minimum_value", 15), + ("y_axis_maximum_value", 16), + ("show_tooltips", 17), + ("date_prefix", 18), + ("show_timeseries_filter", 19), + ("chart", 38), + ] + ], + {}, + ), + 40: ( + "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, + }, + ), + 41: ( + "wagtail.blocks.TextBlock", + (), + { + "help_text": "\nRequired description for the chart.\n", + "required": True, + }, + ), + 42: ( + "wagtail.blocks.CharBlock", + (), + { + "help_text": "\nDisplay text for the link.\n", + "required": False, + }, + ), + 43: ( + "cms.dynamic_content.blocks.PageLinkChooserBlock", + (), + { + "help_text": "\nFor linking to internal pages. (If chosen, external_url must be blank).\n", + "page_type": ["topic.TopicPage"], + "required": False, + }, + ), + 44: ( + "wagtail.blocks.URLBlock", + (), + { + "help_text": "\nFor linking to external url. (Only one of page or external_url must be filled not both).\n", + "required": False, + "validators": [validation.url.validate_https_scheme], + }, + ), + 45: ( + "wagtail.blocks.StructBlock", + [ + [ + ("link_display_text", 42), + ("page", 43), + ("external_url", 44), + ] + ], + { + "help_text": "\nSource link (internal or external).\n", + "required": False, + }, + ), + 46: ( + "wagtail.blocks.StructBlock", + [ + [ + ("title", 3), + ("sub_title", 4), + ("topic_page", 40), + ("description", 41), + ("source", 45), + ("tag_manager_event_id", 10), + ("x_axis", 11), + ("x_axis_title", 12), + ("y_axis", 13), + ("y_axis_title", 14), + ("y_axis_minimum_value", 15), + ("y_axis_maximum_value", 16), + ("show_tooltips", 17), + ("chart", 38), + ] + ], + {}, + ), + 47: ( + "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", + }, + ), + 48: ( + "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', + }, + ), + 49: ( + "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. bar\n", + }, + ), + 50: ( + "wagtail.blocks.StructBlock", + [ + [ + ("theme", 20), + ("sub_theme", 21), + ("topic", 22), + ("metric", 48), + ("geography", 24), + ("geography_type", 25), + ("sex", 26), + ("age", 27), + ("stratum", 28), + ("chart_type", 49), + ("line_colour", 33), + ("label", 32), + ] + ], + {}, + ), + 51: ( + "wagtail.blocks.StreamBlock", + [[("plot", 50)]], + { + "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" + }, + ), + 52: ( + "wagtail.blocks.BooleanBlock", + (), + { + "default": False, + "help_text": "\nToggle to enable confidence intervals if they are present in the data set\n", + "required": False, + }, + ), + 53: ( + "wagtail.blocks.TextBlock", + (), + { + "default": "Metric column includes 95% lower and upper confidence intervals, in brackets.", + "help_text": "\nAn optional body of text to accompany this block.\nThis text will be displayed above the metrics table if confidence intervals is enabled.\n", + "required": False, + }, + ), + 54: ( + "wagtail.blocks.ChoiceBlock", + [], + { + "choices": cms.metrics_interface.field_choices_callables.get_colours, + "help_text": '\nThe colour to display the confidence interval in. The colours conform to the GDS specification.\nNote that if nothing is provided, a default of "BLACK" will be applied.\n', + "required": False, + }, + ), + 55: ( + "wagtail.blocks.StructBlock", + [ + [ + ("title", 3), + ("body", 4), + ("about", 5), + ("related_links", 9), + ("tag_manager_event_id", 10), + ("x_axis", 47), + ("x_axis_title", 12), + ("y_axis", 13), + ("y_axis_title", 14), + ("y_axis_minimum_value", 15), + ("y_axis_maximum_value", 16), + ("show_tooltips", 17), + ("date_prefix", 18), + ("show_timeseries_filter", 19), + ("chart", 51), + ("confidence_intervals", 52), + ("confidence_intervals_description", 53), + ("confidence_colour", 54), + ] + ], + {}, + ), + 56: ( + "wagtail.blocks.TextBlock", + (), + { + "help_text": "\nAn optional body of text to accompany this block. This text will be displayed in the about content of the chart.\n", + "required": False, + }, + ), + 57: ( + "wagtail.blocks.CharBlock", + (), + { + "default": "Up to and including", + "help_text": "\nThis is the accompanying text for chart dates Eg: `Up to and including` 21 Oct 2024\n", + "required": True, + }, + ), + 58: ( + "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', + }, + ), + 59: ( + "wagtail.blocks.TextBlock", + (), + { + "help_text": "\nAn optional body of text to accompany this block. This text will be displayed below the chart title.\n", + "required": False, + }, + ), + 60: ( + "wagtail.blocks.StructBlock", + [ + [ + ("theme", 20), + ("sub_theme", 21), + ("topic", 22), + ("metric", 58), + ("geography", 24), + ("geography_type", 25), + ("sex", 26), + ("age", 27), + ("stratum", 28), + ("body", 59), + ] + ], + { + "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' + }, + ), + 61: ( + "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", + }, + ), + 62: ( + "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", + }, + ), + 63: ( + "wagtail.blocks.StructBlock", + [ + [ + ("theme", 20), + ("sub_theme", 21), + ("topic", 22), + ("metric", 61), + ("geography", 24), + ("geography_type", 25), + ("sex", 26), + ("age", 27), + ("stratum", 28), + ("body", 59), + ("percentage_metric", 62), + ] + ], + { + "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' + }, + ), + 64: ( + "wagtail.blocks.StructBlock", + [ + [ + ("theme", 20), + ("sub_theme", 21), + ("topic", 22), + ("metric", 58), + ("geography", 24), + ("geography_type", 25), + ("sex", 26), + ("age", 27), + ("stratum", 28), + ("body", 59), + ] + ], + { + "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' + }, + ), + 65: ( + "wagtail.blocks.StreamBlock", + [ + [ + ("headline_number", 60), + ("trend_number", 63), + ("percentage_number", 64), + ] + ], + { + "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, + }, + ), + 66: ( + "wagtail.blocks.StructBlock", + [ + [ + ("title", 3), + ("body", 4), + ("about", 56), + ("related_links", 9), + ("tag_manager_event_id", 10), + ("x_axis", 11), + ("x_axis_title", 12), + ("y_axis", 13), + ("y_axis_title", 14), + ("y_axis_minimum_value", 15), + ("y_axis_maximum_value", 16), + ("show_tooltips", 17), + ("date_prefix", 57), + ("show_timeseries_filter", 19), + ("chart", 38), + ("headline_number_columns", 65), + ] + ], + {}, + ), + 67: ( + "wagtail.blocks.CharBlock", + (), + { + "help_text": "\nThe sub title to display for this component.\n", + "required": False, + }, + ), + 68: ( + "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, + }, + ), + 69: ( + "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", + }, + ), + 70: ( + "wagtail.blocks.ChoiceBlock", + [], + { + "choices": cms.metrics_interface.field_choices_callables.get_simplified_chart_types + }, + ), + 71: ( + "wagtail.blocks.BooleanBlock", + (), + { + "default": False, + "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, + }, + ), + 72: ( + "wagtail.blocks.StructBlock", + [ + [ + ("theme", 20), + ("sub_theme", 21), + ("topic", 22), + ("metric", 23), + ("geography", 24), + ("geography_type", 25), + ("sex", 26), + ("age", 27), + ("stratum", 28), + ("chart_type", 70), + ("date_from", 30), + ("date_to", 31), + ("use_smooth_lines", 71), + ] + ], + {}, + ), + 73: ( + "wagtail.blocks.StreamBlock", + [[("plot", 72)]], + { + "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, + }, + ), + 74: ( + "wagtail.blocks.StructBlock", + [ + [ + ("title", 3), + ("sub_title", 67), + ("tag_manager_event_id", 10), + ("topic_page", 40), + ("x_axis", 68), + ("x_axis_title", 12), + ("y_axis", 69), + ("y_axis_title", 14), + ("y_axis_minimum_value", 15), + ("y_axis_maximum_value", 16), + ("chart", 73), + ] + ], + {}, + ), + 75: ( + "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", + }, + ), + 76: ( + "wagtail.blocks.MultipleChoiceBlock", + [], + { + "choices": cms.metrics_interface.field_choices_callables.get_all_subcategory_choices, + "help_text": "\nSelect a list of primary field values for the chart, these will be you're x-axis.\nFor example if we're creating a stacked bar chart that has a metric value in y and geographies along\nthe x-axis. The `primary field values` should be the list of geographies to include in the chart.\n", + "required": False, + }, + ), + 77: ( + "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", + }, + ), + 78: ( + "wagtail.blocks.ChoiceBlock", + [], + { + "choices": cms.metrics_interface.field_choices_callables.get_dual_category_chart_types, + "help_text": "\nThe name of the type of chart which you want to create e.g. bar\n", + "required": False, + }, + ), + 79: ( + "wagtail.blocks.StructBlock", + [ + [ + ("theme", 20), + ("sub_theme", 21), + ("topic", 22), + ("metric", 58), + ("geography", 24), + ("geography_type", 25), + ("sex", 26), + ("age", 27), + ("stratum", 28), + ("date_from", 30), + ("date_to", 31), + ] + ], + {}, + ), + 80: ( + "wagtail.blocks.ChoiceBlock", + [], + { + "choices": cms.metrics_interface.field_choices_callables.get_dual_chart_secondary_category_choices, + "help_text": "\nThis is for selecting the Second categorical variable type for Dual category charts.\nFor example when building a `Stacked bar chart` where the x-axis may be of type `Sex` and\ndisplay `Male` and `Female` along the x-axis. If our stacked bar chart then breaks each bar up into\nage groups, then our `Secondary Category` type is `Age`.\n", + }, + ), + 81: ( + "wagtail.blocks.ChoiceBlock", + [], + { + "choices": cms.metrics_interface.field_choices_callables.get_all_subcategory_choices, + "help_text": "\nSelect the secondary field for a `Segments` this is the second categorical variable used to create segments\nof a `stacked bar` chart. For example if we're creating a stacked bar chart that has a metric in the y-axis\nand geographies along the x-axis. If each bar is broken into segments by `age group` this field\nshould be the age group for this segment.\n", + }, + ), + 82: ( + "wagtail.blocks.StructBlock", + [ + [ + ("secondary_field_value", 81), + ("colour", 33), + ("label", 32), + ] + ], + {}, + ), + 83: ( + "wagtail.blocks.StreamBlock", + [[("segment", 82)]], + {"min_num": 1}, + ), + 84: ( + "wagtail.blocks.StructBlock", + [ + [ + ("title", 3), + ("body", 4), + ("about", 5), + ("related_links", 9), + ("tag_manager_event_id", 10), + ("x_axis", 75), + ("x_axis_title", 12), + ("primary_field_values", 76), + ("y_axis", 77), + ("y_axis_title", 14), + ("y_axis_minimum_value", 15), + ("y_axis_maximum_value", 16), + ("chart_type", 78), + ("static_fields", 79), + ("second_category", 80), + ("segments", 83), + ] + ], + {}, + ), + 85: ( + "wagtail.blocks.StreamBlock", + [ + [ + ("chart_card", 39), + ("chart_with_description_card", 46), + ("headline_chart_card", 55), + ("chart_with_headline_and_trend_card", 66), + ("simplified_chart_with_link", 74), + ("dual_category_chart_card", 84), + ] + ], + { + "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": 2, + "min_num": 1, + }, + ), + 86: ("wagtail.blocks.StructBlock", [[("columns", 85)]], {}), + 87: ( + "wagtail.blocks.TextBlock", + (), + { + "default": "Up to", + "help_text": "\nThis is the accompanying text for headline column dates Eg: `Up to` 27 Oct 2024 \n", + "required": True, + }, + ), + 88: ( + "wagtail.blocks.StreamBlock", + [ + [ + ("headline_number", 60), + ("trend_number", 63), + ("percentage_number", 64), + ] + ], + { + "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, + }, + ), + 89: ( + "wagtail.blocks.StructBlock", + [[("title", 3), ("date_prefix", 87), ("rows", 88)]], + {}, + ), + 90: ( + "wagtail.blocks.StreamBlock", + [[("column", 89)]], + { + "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, + }, + ), + 91: ("wagtail.blocks.StructBlock", [[("columns", 90)]], {}), + 92: ("wagtail.blocks.CharBlock", (), {"required": True}), + 93: ( + "wagtail.blocks.DateBlock", + (), + { + "help_text": "\nThis field is used to provide a start date for the current time range option. Its date\nmust be earlier than the date provided for the corresponding `date_to`field. \n", + "required": True, + }, + ), + 94: ( + "wagtail.blocks.DateBlock", + (), + { + "help_text": "\nThis field is used to provide an end date for the current time range option. Its date\nmust be later than the date provided in the corresponding `date_from` field.\n", + "required": True, + }, + ), + 95: ( + "wagtail.blocks.StructBlock", + [[("label", 92), ("date_from", 93), ("date_to", 94)]], + {}, + ), + 96: ( + "wagtail.blocks.StreamBlock", + [[("time_period", 95)]], + { + "help_text": "\nThis mandatory time range is used to set the time range that a user can navigate through when\ninteracting with a filter linked chart or map card. Eg. Year selection can include a range of\nyears as dates that can be displayed and navigated between.\nWhen entering multiple periods in the time filter each proceeding `date_from` must be 1 day after the `date_to` \nthat came before it to maintain sequence order.\n", + "min_num": 1, + }, + ), + 97: ( + "wagtail.blocks.StructBlock", + [[("title", 92), ("time_periods", 96)]], + { + "help_text": "\nThis mandatory time range is used to set the time range that a user can navigate through when\ninteracting with a filter linked chart or map card. Eg. Year selection can include a range of\nyears as dates that can be displayed and navigated between.\nWhen entering multiple periods in the time filter each proceeding `date_from` must be 1 day after the `date_to` \nthat came before it to maintain sequence order.\n", + "required": True, + }, + ), + 98: ("wagtail.blocks.CharBlock", (), {"required": False}), + 99: ( + "wagtail.blocks.ChoiceBlock", + [], + { + "choices": cms.metrics_interface.field_choices_callables.get_colours, + "help_text": "\nFilter colour selections are used to represent the colours of a filter across filter linked\ncomponents Eg. a colour chosen for a given geography type such as `UKHSA Region` will be used\nto highlight that region on filter linked charts\n", + }, + ), + 100: ( + "wagtail.blocks.ChoiceBlock", + [], + { + "choices": cms.metrics_interface.field_choices_callables.get_all_geography_type_names, + "help_text": "\nThis generates a list of selectable geographies on the dashboard based on the geography type selected\nhere. Eg. A selection of `Lower Tier Local Authority` here will result in all LTLA's being displayed\nin the filter drop down on the dashboard for a user to select from.\n", + }, + ), + 101: ( + "wagtail.blocks.StructBlock", + [[("label", 98), ("colour", 99), ("geography_type", 100)]], + {}, + ), + 102: ( + "wagtail.blocks.StreamBlock", + [[("geography_filter", 101)]], + { + "help_text": "\nThis geography filter can be used to setup selectable geography lists based on geography types\nsuch as `UKHSA Region` or `Lower Tier Local Authority`.\n", + "min_num": 1, + }, + ), + 103: ( + "wagtail.blocks.StructBlock", + [[("geography_types", 102)]], + {}, + ), + 104: ( + "wagtail.blocks.ChoiceBlock", + [], + { + "choices": cms.metrics_interface.field_choices_callables.get_colours, + "help_text": "\nThis colour option is used to represent the threshold across other filter linked components.\nEg: The colour used for the legend on filter linked map component.\n", + }, + ), + 105: ( + "wagtail.blocks.DecimalBlock", + (), + { + "help_text": "\nThe minimum and maximum boundary values are used to set the range for the threshold as a float between\n0.00 and 100 Eg. a threshold of over 95% will have a minimum value of 95 and a maximum value of 100\n", + "required": True, + }, + ), + 106: ( + "wagtail.blocks.StructBlock", + [ + [ + ("label", 92), + ("colour", 104), + ("boundary_minimum_value", 105), + ("boundary_maximum_value", 105), + ] + ], + {}, + ), + 107: ( + "wagtail.blocks.StreamBlock", + [[("threshold", 106)]], + { + "help_text": "\nThe threshold filter is used to configure threshold boundaries that can be used to filter data on the dashboard.\nEg. over 95% boundary 95 -> 100\n", + "min_num": 1, + }, + ), + 108: ( + "wagtail.blocks.StructBlock", + [[("label", 92), ("thresholds", 107)]], + {}, + ), + 109: ( + "wagtail.blocks.CharBlock", + (), + {"help_text": "", "required": True}, + ), + 110: ( + "wagtail.blocks.ChoiceBlock", + [], + { + "choices": cms.metrics_interface.field_choices_callables.get_colours, + "help_text": "", + }, + ), + 111: ( + "wagtail.blocks.ChoiceBlock", + [], + { + "choices": cms.metrics_interface.field_choices_callables.get_all_theme_names + }, + ), + 112: ( + "wagtail.blocks.StructBlock", + [[("label", 98), ("value", 111)]], + { + "help_text": "\nThe related theme for the metric. Eg: infectious disease\n" + }, + ), + 113: ( + "wagtail.blocks.ChoiceBlock", + [], + { + "choices": cms.metrics_interface.field_choices_callables.get_all_unique_sub_theme_names + }, + ), + 114: ( + "wagtail.blocks.StructBlock", + [[("label", 98), ("value", 113)]], + { + "help_text": "\nThe related sub theme for the metric. Eg: respiratory\n" + }, + ), + 115: ( + "wagtail.blocks.ChoiceBlock", + [], + { + "choices": cms.metrics_interface.field_choices_callables.get_all_topic_names + }, + ), + 116: ( + "wagtail.blocks.StructBlock", + [[("label", 98), ("value", 115)]], + { + "help_text": "The name of the topic to pull data e.g. COVID-19." + }, + ), + 117: ( + "wagtail.blocks.ChoiceBlock", + [], + { + "choices": cms.metrics_interface.field_choices_callables.get_all_stratum_names + }, + ), + 118: ( + "wagtail.blocks.StructBlock", + [[("label", 98), ("value", 117)]], + { + "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.\n" + }, + ), + 119: ( + "wagtail.blocks.ChoiceBlock", + [], + { + "choices": cms.metrics_interface.field_choices_callables.get_all_unique_metric_names + }, + ), + 120: ( + "wagtail.blocks.StructBlock", + [[("label", 98), ("value", 119)]], + { + "help_text": '\nThe name of the metric to pull data for e.g. "COVID-19_deaths_ONSByDay".\n' + }, + ), + 121: ( + "wagtail.blocks.ChoiceBlock", + [], + { + "choices": cms.metrics_interface.field_choices_callables.get_all_age_names + }, + ), + 122: ( + "wagtail.blocks.StructBlock", + [[("label", 98), ("value", 121)]], + { + "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" + }, + ), + 123: ( + "wagtail.blocks.ChoiceBlock", + [], + { + "choices": cms.metrics_interface.field_choices_callables.get_all_sex_names + }, + ), + 124: ( + "wagtail.blocks.StructBlock", + [[("label", 98), ("value", 123)]], + { + "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" + }, + ), + 125: ( + "wagtail.blocks.StructBlock", + [ + [ + ("theme", 112), + ("sub_theme", 114), + ("topic", 116), + ("stratum", 118), + ("metric", 120), + ("age", 122), + ("sex", 124), + ] + ], + {}, + ), + 126: ( + "wagtail.blocks.ChoiceBlock", + [], + { + "choices": cms.metrics_interface.field_choices_callables.get_all_geography_names + }, + ), + 127: ( + "wagtail.blocks.StructBlock", + [[("label", 98), ("value", 126)]], + {"help_text": ""}, + ), + 128: ( + "wagtail.blocks.ChoiceBlock", + [], + { + "choices": cms.metrics_interface.field_choices_callables.get_all_geography_type_names + }, + ), + 129: ( + "wagtail.blocks.StructBlock", + [[("label", 98), ("value", 128)]], + {"help_text": ""}, + ), + 130: ( + "wagtail.blocks.StreamBlock", + [ + [ + ("theme", 112), + ("sub_theme", 114), + ("topic", 116), + ("stratum", 118), + ("metric", 120), + ("age", 122), + ("sex", 124), + ("geography", 127), + ("geography_type", 129), + ] + ], + {}, + ), + 131: ( + "wagtail.blocks.StructBlock", + [ + [ + ("label_prefix", 92), + ("label_suffix", 98), + ("parameters", 130), + ] + ], + { + "help_text": '\nThese additional points configure alternative requests to compare against or supplement the current data filter. For example,\nselecting a `metric` field here could provide a percentage change metric that complements the main data filter selection.\nThe label prefix appears before the metric value in map tooltips, while the suffix appears after.\nFor a percentage change metric, using prefix `change` and suffix `%` would display as "change %" in tooltips.\n', + "required": False, + }, + ), + 132: ( + "wagtail.blocks.StreamBlock", + [[("accompanying_point", 131)]], + { + "help_text": '\nThese additional points configure alternative requests to compare against or supplement the current data filter. For example,\nselecting a `metric` field here could provide a percentage change metric that complements the main data filter selection.\nThe label prefix appears before the metric value in map tooltips, while the suffix appears after.\nFor a percentage change metric, using prefix `change` and suffix `%` would display as "change %" in tooltips.\n', + "required": False, + }, + ), + 133: ( + "wagtail.blocks.StructBlock", + [ + [ + ("label", 109), + ("colour", 110), + ("parameters", 125), + ("accompanying_points", 132), + ] + ], + {}, + ), + 134: ( + "wagtail.blocks.StreamBlock", + [[("data_filter", 133)]], + { + "help_text": "\nThe data filter is used to create filter options based on metrics and combined attributes. Eg:\nmetric and stratum.\n", + "min_num": 1, + }, + ), + 135: ( + "wagtail.blocks.ChoiceBlock", + [], + { + "choices": [ + ("theme", "theme"), + ("sub_theme", "sub_theme"), + ("topic", "topic"), + ("metric", "metric"), + ("sex", "sex"), + ("age", "age"), + ("stratum", "stratum"), + ] + }, + ), + 136: ("wagtail.blocks.StructBlock", [[("data_category", 135)]], {}), + 137: ( + "wagtail.blocks.StreamBlock", + [[("category", 136)]], + { + "help_text": "\nSelect the categories for the dropdown options to be grouped by.\nE.g. To build the following:\n
\n|- stratum one\n    |- topic one\n    |- topic two\n| stratum two\n    |- topic one\n    |- topic two\n
\nThe selections for this should be `stratum` and then `topic`.\n", + "max_num": 2, + "min_num": 1, + }, + ), + 138: ( + "wagtail.blocks.StructBlock", + [ + [ + ("label", 92), + ("data_filters", 134), + ("categories_to_group_by", 137), + ] + ], + {}, + ), + 139: ( + "wagtail.blocks.StreamBlock", + [ + [ + ("geography_filters", 103), + ("threshold_filters", 108), + ("data_filters", 138), + ] + ], + {}, + ), + 140: ( + "wagtail.blocks.StructBlock", + [[("title", 92), ("filters", 139)]], + {}, + ), + 141: ( + "wagtail.blocks.StreamBlock", + [[("row", 140)]], + { + "help_text": "\nA filter row can contain one or more filters for the global filter component.\nThey can be used to group filters around a context Eg: data filter with threshold filter.\n", + "min_num": 1, + "required": True, + }, + ), + 142: ( + "wagtail.blocks.StructBlock", + [[("time_range", 97), ("rows", 141)]], + { + "help_text": "\nThe global filter component enables a user of the dashboard to create a variable number of filters\nwhich can be applied across multi components on a dashboard page. This CMS component can be used to configure \nthose options.\n" + }, + ), + 143: ( + "wagtail.blocks.CharBlock", + (), + { + "help_text": "\nThis will form the prefix of the title which will be associated with the filter linked component.\nThe full title will be structured as follows:\n\n
\n[this prefix] between [the selected time period label] ([the selected region], [the selected lower geography type value]).\n
\n", + "required": True, + }, + ), + 144: ( + "wagtail.blocks.RichTextBlock", + (), + { + "default": "", + "features": ["h2", "h3", "h4", "bold", "ul", "link"], + "help_text": "\nAn optional body of text to accompany this block. This text will be displayed in the about content for this map.\n", + "required": False, + }, + ), + 145: ( + "wagtail.blocks.StructBlock", + [[("title_prefix", 143), ("about", 144)]], + { + "help_text": "\nThis filter linked map can be used to display metric data configured through a data filter\nin the global filter component to represent this data geographically.\n", + "required": False, + }, + ), + 146: ( + "wagtail.blocks.CharBlock", + (), + { + "help_text": "\nThis legend title will be used for the title of the sub plot chart legends. \nEg. the colours associated with the geographies from global filter.\n", + "required": True, + }, + ), + 147: ( + "wagtail.blocks.FloatBlock", + (), + { + "help_text": "\nAn optional value to draw a target threshold for.\n", + "required": False, + }, + ), + 148: ( + "wagtail.blocks.CharBlock", + (), + { + "help_text": "\nAn optional label to add as for the target threshold indicator on the charts.\n", + "required": False, + }, + ), + 149: ( + "wagtail.blocks.RichTextBlock", + (), + { + "default": "", + "features": ["h2", "h3", "h4", "bold", "ul", "link"], + "help_text": "\nAn optional body of text to accompany this block. This text will be displayed in the about content of the chart.\n", + "required": False, + }, + ), + 150: ( + "wagtail.blocks.StructBlock", + [ + [ + ("title_prefix", 143), + ("legend_title", 146), + ("target_threshold", 147), + ("target_threshold_label", 148), + ("about", 149), + ] + ], + { + "help_text": "\nThis filter linked sub plot chart can be used to display metric data configured through a data filter\nin the global filter component. \nThis template will render a new chart for each geography selected by the user \nfor the selected entities according to the data filter.\n", + "required": False, + }, + ), + 151: ( + "wagtail.blocks.CharBlock", + (), + { + "help_text": "\nThis legend title will be used for the title of the time series chart legends. \nEg. the colours associated with the selected data entities from global filter.\n", + "required": True, + }, + ), + 152: ( + "wagtail.blocks.StructBlock", + [ + [ + ("title_prefix", 143), + ("legend_title", 151), + ("about", 149), + ] + ], + { + "help_text": "\nThis filter linked time series chart can be used to display metric data configured through a data filter\nin the global filter component. \nThis template will render a new chart for each geography selected by the user \nfor the selected entities according to the data filter.\n", + "required": False, + }, + ), + 153: ( + "wagtail.blocks.StreamBlock", + [ + [ + ("text_card", 2), + ("chart_row_card", 86), + ("headline_numbers_row_card", 91), + ("global_filter_card", 142), + ("filter_linked_map", 145), + ("filter_linked_sub_plot_chart_template", 150), + ("filter_linked_time_series_chart_template", 152), + ] + ], + { + "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" + }, + ), + 154: ( + "wagtail.blocks.StructBlock", + [[("heading", 0), ("content", 153)]], + {}, + ), + }, + ), + ), + ] diff --git a/cms/topic/migrations/0039_backfill_theme_subtheme.py b/cms/topic/migrations/0039_backfill_theme_subtheme.py new file mode 100644 index 0000000000..8256a47853 --- /dev/null +++ b/cms/topic/migrations/0039_backfill_theme_subtheme.py @@ -0,0 +1,20 @@ +from django.db import migrations +from cms.utils.migrations.backfill_helpers import backfill_pages_with_theme_and_subtheme + + +def backfill_topic_pages(apps, schema_editor): + backfill_pages_with_theme_and_subtheme(apps, "topic", "TopicPage") + + +def noop(apps, schema_editor): + """Backfill is one-directional; nothing sensible to reverse.""" + pass + + +class Migration(migrations.Migration): + dependencies = [ + ("topic", "0038_alter_topicpage_body"), + ] + operations = [ + migrations.RunPython(backfill_topic_pages, noop), + ] diff --git a/cms/topics_list/migrations/0012_alter_topicslistpage_body.py b/cms/topics_list/migrations/0012_alter_topicslistpage_body.py new file mode 100644 index 0000000000..ba0dc06e60 --- /dev/null +++ b/cms/topics_list/migrations/0012_alter_topicslistpage_body.py @@ -0,0 +1,1171 @@ +# Generated by Django 5.2.14 on 2026-06-29 19:15 + +import cms.dynamic_content.cards +import cms.metrics_interface.field_choices_callables +import validation.url +import wagtail.fields +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ( + "topics_list", + "0011_add_dual_cat_chart_date_preix", + ), + ] + + operations = [ + migrations.AlterField( + model_name="topicslistpage", + name="body", + field=wagtail.fields.StreamField( + [("section", 110)], + block_lookup={ + 0: ( + "wagtail.blocks.TextBlock", + (), + { + "help_text": "\nThe text you add here will be used as the heading for this section. \n", + "required": True, + }, + ), + 1: ( + "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, + }, + ), + 2: ( + "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", + }, + ), + 3: ("wagtail.blocks.StructBlock", [[("body", 2)]], {}), + 4: ( + "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, + }, + ), + 5: ( + "wagtail.blocks.TextBlock", + (), + { + "help_text": "\nAn optional body of text to accompany this block. This text will be displayed below the chart title.\n", + "label": "Subtitle", + "required": False, + }, + ), + 6: ( + "wagtail.blocks.TextBlock", + (), + { + "default": "", + "help_text": "\nAn optional body of text to accompany this block. This text will be displayed in the about content of the chart.\n", + "required": False, + }, + ), + 7: ( + "wagtail.blocks.CharBlock", + (), + { + "help_text": "\nThe Text that will be displayed for the URL.\n", + "required": True, + }, + ), + 8: ( + "wagtail.blocks.CharBlock", + (), + { + "help_text": "\nThe URL that the user will be navigated to when clicked.\nAn optional body of text to accompany this block. This text will be displayed below the chart title.\n", + "required": True, + }, + ), + 9: ( + "wagtail.blocks.StructBlock", + [[("link_display_text", 7), ("link", 8)]], + {}, + ), + 10: ( + "wagtail.blocks.StreamBlock", + [[("related_link", 9)]], + { + "help_text": "\nProvide optional URLs that can provide further contextual information for the data displayed in the chart.\n", + "required": False, + }, + ), + 11: ( + "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, + }, + ), + 12: ( + "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, + }, + ), + 13: ( + "wagtail.blocks.CharBlock", + (), + { + "default": "", + "help_text": "\nAn optional title to display along the x-axis of the chart.\nIf nothing is provided, then no title will be displayed.\n", + "required": False, + }, + ), + 14: ( + "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, + }, + ), + 15: ( + "wagtail.blocks.CharBlock", + (), + { + "default": "", + "help_text": "\nAn optional title to display along the y-axis of the chart.\nIf nothing is provided, then no title will be displayed.\n", + "required": False, + }, + ), + 16: ( + "wagtail.blocks.DecimalBlock", + (), + { + "default": 0, + "help_text": "\nThis field allows you to set the first value in the chart's y-axis range. Please\nnote that a value provided here, which is higher than the lowest value in the data will\nbe overridden and the value from the dataset will be used.\n", + "required": False, + }, + ), + 17: ( + "wagtail.blocks.DecimalBlock", + (), + { + "help_text": "\nThis field allows you to set the last value in the chart's y-axis range. Please\nnote that a value provided here, which is lower than the highest value in the data will\nbe overridden and the value from the dataset will be used. \n", + "required": False, + }, + ), + 18: ( + "wagtail.blocks.BooleanBlock", + (), + { + "default": False, + "help_text": "\nThis is a switch to show tooltips on hover within the chart.\nDefaults to False.\n", + "required": False, + }, + ), + 19: ( + "wagtail.blocks.CharBlock", + (), + { + "default": "Up to and including", + "help_texts": "\nThis is the accompanying text for chart dates Eg: `Up to and including` 21 Oct 2024\n", + "requried": True, + }, + ), + 20: ( + "wagtail.blocks.BooleanBlock", + (), + { + "default": False, + "help_text": "\nThis option enables timeseries filter for this chart.\nThe timeseries filter allows a user to change the timeseries range for example between\n1m, 3m, 6m, 1y etc.\n", + "required": False, + }, + ), + 21: ( + "wagtail.blocks.ChoiceBlock", + [], + { + "choices": cms.metrics_interface.field_choices_callables.get_all_theme_names, + "help_text": "\nThe related theme for the metric. Eg: infectious disease\n", + }, + ), + 22: ( + "wagtail.blocks.ChoiceBlock", + [], + { + "choices": cms.metrics_interface.field_choices_callables.get_all_unique_sub_theme_names, + "help_text": "\nThe related sub theme for the metric. Eg: respiratory\n", + }, + ), + 23: ( + "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.", + }, + ), + 24: ( + "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', + }, + ), + 25: ( + "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", + }, + ), + 26: ( + "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", + }, + ), + 27: ( + "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", + }, + ), + 28: ( + "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", + }, + ), + 29: ( + "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", + }, + ), + 30: ( + "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. bar\n", + }, + ), + 31: ( + "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, + }, + ), + 32: ( + "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, + }, + ), + 33: ( + "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, + }, + ), + 34: ( + "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', + }, + ), + 35: ( + "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, + }, + ), + 36: ( + "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, + }, + ), + 37: ( + "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, + }, + ), + 38: ( + "wagtail.blocks.StructBlock", + [ + [ + ("theme", 21), + ("sub_theme", 22), + ("topic", 23), + ("metric", 24), + ("geography", 25), + ("geography_type", 26), + ("sex", 27), + ("age", 28), + ("stratum", 29), + ("chart_type", 30), + ("date_from", 31), + ("date_to", 32), + ("label", 33), + ("line_colour", 34), + ("line_type", 35), + ("use_markers", 36), + ("use_smooth_lines", 37), + ] + ], + {}, + ), + 39: ( + "wagtail.blocks.StreamBlock", + [[("plot", 38)]], + { + "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" + }, + ), + 40: ( + "wagtail.blocks.StructBlock", + [ + [ + ("title", 4), + ("body", 5), + ("about", 6), + ("related_links", 10), + ("tag_manager_event_id", 11), + ("x_axis", 12), + ("x_axis_title", 13), + ("y_axis", 14), + ("y_axis_title", 15), + ("y_axis_minimum_value", 16), + ("y_axis_maximum_value", 17), + ("show_tooltips", 18), + ("date_prefix", 19), + ("show_timeseries_filter", 20), + ("chart", 39), + ] + ], + {}, + ), + 41: ( + "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, + }, + ), + 42: ( + "wagtail.blocks.TextBlock", + (), + { + "help_text": "\nRequired description for the chart.\n", + "required": True, + }, + ), + 43: ( + "wagtail.blocks.CharBlock", + (), + { + "help_text": "\nDisplay text for the link.\n", + "required": False, + }, + ), + 44: ( + "cms.dynamic_content.blocks.PageLinkChooserBlock", + (), + { + "help_text": "\nFor linking to internal pages. (If chosen, external_url must be blank).\n", + "page_type": ["topic.TopicPage"], + "required": False, + }, + ), + 45: ( + "wagtail.blocks.URLBlock", + (), + { + "help_text": "\nFor linking to external url. (Only one of page or external_url must be filled not both).\n", + "required": False, + "validators": [validation.url.validate_https_scheme], + }, + ), + 46: ( + "wagtail.blocks.StructBlock", + [ + [ + ("link_display_text", 43), + ("page", 44), + ("external_url", 45), + ] + ], + { + "help_text": "\nSource link (internal or external).\n", + "required": False, + }, + ), + 47: ( + "wagtail.blocks.StructBlock", + [ + [ + ("title", 4), + ("sub_title", 5), + ("topic_page", 41), + ("description", 42), + ("source", 46), + ("tag_manager_event_id", 11), + ("x_axis", 12), + ("x_axis_title", 13), + ("y_axis", 14), + ("y_axis_title", 15), + ("y_axis_minimum_value", 16), + ("y_axis_maximum_value", 17), + ("show_tooltips", 18), + ("chart", 39), + ] + ], + {}, + ), + 48: ( + "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", + }, + ), + 49: ( + "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', + }, + ), + 50: ( + "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. bar\n", + }, + ), + 51: ( + "wagtail.blocks.StructBlock", + [ + [ + ("theme", 21), + ("sub_theme", 22), + ("topic", 23), + ("metric", 49), + ("geography", 25), + ("geography_type", 26), + ("sex", 27), + ("age", 28), + ("stratum", 29), + ("chart_type", 50), + ("line_colour", 34), + ("label", 33), + ] + ], + {}, + ), + 52: ( + "wagtail.blocks.StreamBlock", + [[("plot", 51)]], + { + "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" + }, + ), + 53: ( + "wagtail.blocks.BooleanBlock", + (), + { + "default": False, + "help_text": "\nToggle to enable confidence intervals if they are present in the data set\n", + "required": False, + }, + ), + 54: ( + "wagtail.blocks.TextBlock", + (), + { + "default": "Metric column includes 95% lower and upper confidence intervals, in brackets.", + "help_text": "\nAn optional body of text to accompany this block.\nThis text will be displayed above the metrics table if confidence intervals is enabled.\n", + "required": False, + }, + ), + 55: ( + "wagtail.blocks.ChoiceBlock", + [], + { + "choices": cms.metrics_interface.field_choices_callables.get_colours, + "help_text": '\nThe colour to display the confidence interval in. The colours conform to the GDS specification.\nNote that if nothing is provided, a default of "BLACK" will be applied.\n', + "required": False, + }, + ), + 56: ( + "wagtail.blocks.StructBlock", + [ + [ + ("title", 4), + ("body", 5), + ("about", 6), + ("related_links", 10), + ("tag_manager_event_id", 11), + ("x_axis", 48), + ("x_axis_title", 13), + ("y_axis", 14), + ("y_axis_title", 15), + ("y_axis_minimum_value", 16), + ("y_axis_maximum_value", 17), + ("show_tooltips", 18), + ("date_prefix", 19), + ("show_timeseries_filter", 20), + ("chart", 52), + ("confidence_intervals", 53), + ("confidence_intervals_description", 54), + ("confidence_colour", 55), + ] + ], + {}, + ), + 57: ( + "wagtail.blocks.TextBlock", + (), + { + "help_text": "\nAn optional body of text to accompany this block. This text will be displayed in the about content of the chart.\n", + "required": False, + }, + ), + 58: ( + "wagtail.blocks.CharBlock", + (), + { + "default": "Up to and including", + "help_text": "\nThis is the accompanying text for chart dates Eg: `Up to and including` 21 Oct 2024\n", + "required": True, + }, + ), + 59: ( + "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', + }, + ), + 60: ( + "wagtail.blocks.TextBlock", + (), + { + "help_text": "\nAn optional body of text to accompany this block. This text will be displayed below the chart title.\n", + "required": False, + }, + ), + 61: ( + "wagtail.blocks.StructBlock", + [ + [ + ("theme", 21), + ("sub_theme", 22), + ("topic", 23), + ("metric", 59), + ("geography", 25), + ("geography_type", 26), + ("sex", 27), + ("age", 28), + ("stratum", 29), + ("body", 60), + ] + ], + { + "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' + }, + ), + 62: ( + "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", + }, + ), + 63: ( + "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", + }, + ), + 64: ( + "wagtail.blocks.StructBlock", + [ + [ + ("theme", 21), + ("sub_theme", 22), + ("topic", 23), + ("metric", 62), + ("geography", 25), + ("geography_type", 26), + ("sex", 27), + ("age", 28), + ("stratum", 29), + ("body", 60), + ("percentage_metric", 63), + ] + ], + { + "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' + }, + ), + 65: ( + "wagtail.blocks.StructBlock", + [ + [ + ("theme", 21), + ("sub_theme", 22), + ("topic", 23), + ("metric", 59), + ("geography", 25), + ("geography_type", 26), + ("sex", 27), + ("age", 28), + ("stratum", 29), + ("body", 60), + ] + ], + { + "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' + }, + ), + 66: ( + "wagtail.blocks.StreamBlock", + [ + [ + ("headline_number", 61), + ("trend_number", 64), + ("percentage_number", 65), + ] + ], + { + "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, + }, + ), + 67: ( + "wagtail.blocks.StructBlock", + [ + [ + ("title", 4), + ("body", 5), + ("about", 57), + ("related_links", 10), + ("tag_manager_event_id", 11), + ("x_axis", 12), + ("x_axis_title", 13), + ("y_axis", 14), + ("y_axis_title", 15), + ("y_axis_minimum_value", 16), + ("y_axis_maximum_value", 17), + ("show_tooltips", 18), + ("date_prefix", 58), + ("show_timeseries_filter", 20), + ("chart", 39), + ("headline_number_columns", 66), + ] + ], + {}, + ), + 68: ( + "wagtail.blocks.CharBlock", + (), + { + "help_text": "\nThe sub title to display for this component.\n", + "required": False, + }, + ), + 69: ( + "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, + }, + ), + 70: ( + "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", + }, + ), + 71: ( + "wagtail.blocks.ChoiceBlock", + [], + { + "choices": cms.metrics_interface.field_choices_callables.get_simplified_chart_types + }, + ), + 72: ( + "wagtail.blocks.BooleanBlock", + (), + { + "default": False, + "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, + }, + ), + 73: ( + "wagtail.blocks.StructBlock", + [ + [ + ("theme", 21), + ("sub_theme", 22), + ("topic", 23), + ("metric", 24), + ("geography", 25), + ("geography_type", 26), + ("sex", 27), + ("age", 28), + ("stratum", 29), + ("chart_type", 71), + ("date_from", 31), + ("date_to", 32), + ("use_smooth_lines", 72), + ] + ], + {}, + ), + 74: ( + "wagtail.blocks.StreamBlock", + [[("plot", 73)]], + { + "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, + }, + ), + 75: ( + "wagtail.blocks.StructBlock", + [ + [ + ("title", 4), + ("sub_title", 68), + ("tag_manager_event_id", 11), + ("topic_page", 41), + ("x_axis", 69), + ("x_axis_title", 13), + ("y_axis", 70), + ("y_axis_title", 15), + ("y_axis_minimum_value", 16), + ("y_axis_maximum_value", 17), + ("chart", 74), + ] + ], + {}, + ), + 76: ( + "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", + }, + ), + 77: ( + "wagtail.blocks.MultipleChoiceBlock", + [], + { + "choices": cms.metrics_interface.field_choices_callables.get_all_subcategory_choices, + "help_text": "\nSelect a list of primary field values for the chart, these will be you're x-axis.\nFor example if we're creating a stacked bar chart that has a metric value in y and geographies along\nthe x-axis. The `primary field values` should be the list of geographies to include in the chart.\n", + "required": False, + }, + ), + 78: ( + "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", + }, + ), + 79: ( + "wagtail.blocks.ChoiceBlock", + [], + { + "choices": cms.metrics_interface.field_choices_callables.get_dual_category_chart_types, + "help_text": "\nThe name of the type of chart which you want to create e.g. bar\n", + "required": False, + }, + ), + 80: ( + "wagtail.blocks.StructBlock", + [ + [ + ("theme", 21), + ("sub_theme", 22), + ("topic", 23), + ("metric", 59), + ("geography", 25), + ("geography_type", 26), + ("sex", 27), + ("age", 28), + ("stratum", 29), + ("date_from", 31), + ("date_to", 32), + ] + ], + {}, + ), + 81: ( + "wagtail.blocks.ChoiceBlock", + [], + { + "choices": cms.metrics_interface.field_choices_callables.get_dual_chart_secondary_category_choices, + "help_text": "\nThis is for selecting the Second categorical variable type for Dual category charts.\nFor example when building a `Stacked bar chart` where the x-axis may be of type `Sex` and\ndisplay `Male` and `Female` along the x-axis. If our stacked bar chart then breaks each bar up into\nage groups, then our `Secondary Category` type is `Age`.\n", + }, + ), + 82: ( + "wagtail.blocks.ChoiceBlock", + [], + { + "choices": cms.metrics_interface.field_choices_callables.get_all_subcategory_choices, + "help_text": "\nSelect the secondary field for a `Segments` this is the second categorical variable used to create segments\nof a `stacked bar` chart. For example if we're creating a stacked bar chart that has a metric in the y-axis\nand geographies along the x-axis. If each bar is broken into segments by `age group` this field\nshould be the age group for this segment.\n", + }, + ), + 83: ( + "wagtail.blocks.StructBlock", + [ + [ + ("secondary_field_value", 82), + ("colour", 34), + ("label", 33), + ] + ], + {}, + ), + 84: ( + "wagtail.blocks.StreamBlock", + [[("segment", 83)]], + {"min_num": 1}, + ), + 85: ( + "wagtail.blocks.StructBlock", + [ + [ + ("title", 4), + ("body", 5), + ("about", 6), + ("related_links", 10), + ("tag_manager_event_id", 11), + ("x_axis", 76), + ("x_axis_title", 13), + ("primary_field_values", 77), + ("y_axis", 78), + ("y_axis_title", 15), + ("y_axis_minimum_value", 16), + ("y_axis_maximum_value", 17), + ("chart_type", 79), + ("static_fields", 80), + ("second_category", 81), + ("segments", 84), + ] + ], + {}, + ), + 86: ( + "wagtail.blocks.StreamBlock", + [ + [ + ("chart_card", 40), + ("chart_with_description_card", 47), + ("headline_chart_card", 56), + ("chart_with_headline_and_trend_card", 67), + ("simplified_chart_with_link", 75), + ("dual_category_chart_card", 85), + ] + ], + { + "help_text": "\nHere you can add chart cards to a section and the layout will change based on the number of cards added.\nA single card will expand to take up half the row. When 2 or 3 cards are added they will share the width\nof a row equally, creating either a 2 or 3 column layout.\n", + "min_num": 1, + }, + ), + 87: ("wagtail.blocks.StructBlock", [[("cards", 86)]], {}), + 88: ( + "wagtail.blocks.TextBlock", + (), + { + "default": "Up to", + "help_text": "\nThis is the accompanying text for headline column dates Eg: `Up to` 27 Oct 2024 \n", + "required": True, + }, + ), + 89: ( + "wagtail.blocks.StreamBlock", + [ + [ + ("headline_number", 61), + ("trend_number", 64), + ("percentage_number", 65), + ] + ], + { + "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, + }, + ), + 90: ( + "wagtail.blocks.StructBlock", + [[("title", 4), ("date_prefix", 88), ("rows", 89)]], + {}, + ), + 91: ( + "wagtail.blocks.StreamBlock", + [[("column", 90)]], + { + "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, + }, + ), + 92: ("wagtail.blocks.StructBlock", [[("columns", 91)]], {}), + 93: ( + "wagtail.blocks.TextBlock", + (), + { + "help_text": "\nThe sub title to display for this component.\n", + "required": True, + }, + ), + 94: ( + "wagtail.blocks.TextBlock", + (), + { + "help_text": "\nOptional description for the weather health alerts card.\n", + "required": False, + }, + ), + 95: ( + "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", + }, + ), + 96: ( + "wagtail.blocks.StructBlock", + [ + [ + ("link_display_text", 43), + ("page", 44), + ("external_url", 45), + ] + ], + {"help_text": "\nOptional source link.\n", "required": False}, + ), + 97: ( + "wagtail.blocks.StructBlock", + [ + [ + ("title", 4), + ("sub_title", 93), + ("description", 94), + ("alert_type", 95), + ("source", 96), + ] + ], + {}, + ), + 98: ( + "wagtail.blocks.StreamBlock", + [ + [ + ("weather_health_alert_card", 97), + ("chart_card_with_description", 47), + ] + ], + { + "help_text": "\nThis will be used to display a full height card on the left column.\nChoose either a weather health alerts card or a chart card with description.\n", + "max_num": 1, + "min_num": 1, + }, + ), + 99: ( + "wagtail.blocks.StreamBlock", + [[("chart_card", 75)]], + { + "help_text": "\nThis will be used to display a chart card in the top row of the second (right) column.\n", + "max_num": 1, + "min_num": 1, + }, + ), + 100: ( + "wagtail.blocks.StreamBlock", + [[("headline_number", 61), ("trend_number", 64)]], + { + "help_text": "\nThis block only allows 2 headline number blocks to be added.\nIt can be used to add headline number and trend number.\n", + "max_num": 2, + "min_num": 2, + "required": True, + }, + ), + 101: ( + "wagtail.blocks.StructBlock", + [ + [ + ("title", 4), + ("date_prefix", 88), + ("topic_page", 41), + ("headline_metrics", 100), + ] + ], + { + "help_text": "\nEach card will be displayed from left to right and will share and occupy half \nof the bottom row right column of the popular topics component.\n", + "max_num": 2, + "min_num": 2, + "required": True, + }, + ), + 102: ( + "wagtail.blocks.StreamBlock", + [[("headline_metric_card", 101)]], + { + "help_text": "\nThis will require 2 headline metrics cards which will be displayed from left to right \nwith each card occupying and sharing half of the bottom row right column of the \npopular topics component.\n", + "max_num": 2, + "min_num": 2, + }, + ), + 103: ( + "wagtail.blocks.StructBlock", + [ + [ + ("left_column", 98), + ("right_column_top_row", 99), + ("right_column_bottom_row", 102), + ] + ], + {}, + ), + 104: ( + "wagtail.blocks.StreamBlock", + [ + [ + ("text_card", 3), + ("chart_card_section", 87), + ("headline_numbers_row_card", 92), + ("weather_health_alert_card", 97), + ("popular_topics_card", 103), + ] + ], + { + "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" + }, + ), + 105: ( + "wagtail.blocks.CharBlock", + (), + { + "help_text": '\nThis is the label used for the a section footer link badge"\n', + "required": True, + }, + ), + 106: ( + "wagtail.blocks.CharBlock", + (), + { + "help_text": "\nThis is the text to displayed along side a link in a the sections footer link.\n", + "required": True, + }, + ), + 107: ( + "wagtail.blocks.StructBlock", + [ + [ + ("link_display_text", 43), + ("page", 44), + ("external_url", 45), + ] + ], + { + "help_text": "\nThis is a link component that allows the user to setup an internal or external link along with a short description of the link's content.\n", + "required": True, + }, + ), + 108: ( + "wagtail.blocks.StructBlock", + [[("badge_label", 105), ("text", 106), ("link", 107)]], + {"max_num": 1}, + ), + 109: ( + "wagtail.blocks.StreamBlock", + [[("section_link", 108)]], + { + "help_text": "\nThis is an optional footer for a section to provide a link to further information.\n", + "required": False, + }, + ), + 110: ( + "wagtail.blocks.StructBlock", + [ + [ + ("heading", 0), + ("page_link", 1), + ("content", 104), + ("footer", 109), + ] + ], + {}, + ), + }, + ), + ), + ] diff --git a/cms/topics_list/migrations/0013_backfill_theme_subtheme.py b/cms/topics_list/migrations/0013_backfill_theme_subtheme.py new file mode 100644 index 0000000000..50ad81f46a --- /dev/null +++ b/cms/topics_list/migrations/0013_backfill_theme_subtheme.py @@ -0,0 +1,20 @@ +from django.db import migrations +from cms.utils.migrations.backfill_helpers import backfill_pages_with_theme_and_subtheme + + +def backfill_topics_list_pages(apps, schema_editor): + backfill_pages_with_theme_and_subtheme(apps, "topics_list", "TopicsListPage") + + +def noop(apps, schema_editor): + """Backfill is one-directional; nothing sensible to reverse.""" + pass + + +class Migration(migrations.Migration): + dependencies = [ + ("topics_list", "0012_alter_topicslistpage_body"), + ] + operations = [ + migrations.RunPython(backfill_topics_list_pages, noop), + ] diff --git a/cms/utils/migrations/backfill_helpers.py b/cms/utils/migrations/backfill_helpers.py new file mode 100644 index 0000000000..b043a9ac09 --- /dev/null +++ b/cms/utils/migrations/backfill_helpers.py @@ -0,0 +1,106 @@ +""" +This file contains a set of utility functions for handling the backfill of data via Django migrations. + +backfill_pages_with_theme_and_subtheme was introduced when changes were added to the codebase +that made theme and subtheme mandatory across all pages. Introducing this would be a breaking +change that would require extensive changes throughout the CMS following deployment if not +automated. These functions apply the required changes throughout the database to prevent +any potential downtime, or error pages encountered by users due to a schema mismatch. +""" + +import json +from wagtail.fields import StreamValue + + +def backfill_pages_with_theme_and_subtheme(apps, model_section, model_name): + topic_map = _build_topic_map(apps) + + app_model = apps.get_model(model_section, model_name) + + for page in app_model.objects.all().iterator(): + if not page.body: + continue + + raw_data = page.body.get_prep_value() + + if not _fill_node(raw_data, topic_map): + continue + + page.body = StreamValue(page.body.stream_block, raw_data, is_lazy=True) + page.save(update_fields=["body"]) + + _backfill_revisions_for_content_type(apps, model_section, model_name, topic_map) + + +def _build_topic_map(apps): + Topic = apps.get_model("data", "Topic") + + topic_map = {} + for topic in Topic.objects.select_related("sub_theme__theme"): + sub_theme = topic.sub_theme + if sub_theme and sub_theme.theme: + topic_map[topic.name] = (sub_theme.theme.name, sub_theme.name) + return topic_map + + +def _fill_node(node, topic_map) -> bool: + """Recursively fill theme/sub_theme on any plot, operating on raw dicts/lists.""" + changed = False + + if isinstance(node, dict): + topic = node.get("topic") + + if isinstance(topic, str) and topic in topic_map: + theme_name, sub_theme_name = topic_map[topic] + + if not node.get("theme"): + node["theme"] = theme_name + changed = True + + if not node.get("sub_theme"): + node["sub_theme"] = sub_theme_name + changed = True + + for value in node.values(): + changed |= _fill_node(value, topic_map) + elif isinstance(node, list): + for item in node: + changed |= _fill_node(item, topic_map) + + return changed + + +def _backfill_revisions_for_content_type(apps, model_section, model_name, topic_map): + revision_model = apps.get_model("wagtailcore", "Revision") + content_type_model = apps.get_model("contenttypes", "ContentType") + + try: + content_type = content_type_model.objects.get( + app_label=model_section, model=model_name.lower() + ) + except content_type_model.DoesNotExist: + return + + revisions = revision_model.objects.filter(content_type=content_type).iterator() + + updated = [] + for revision in revisions: + content = revision.content + raw_body = content.get("body") + + if not raw_body: + continue + + body_data = json.loads(raw_body) if isinstance(raw_body, str) else raw_body + + if not _fill_node(body_data, topic_map): + continue + + content["body"] = ( + json.dumps(body_data) if isinstance(raw_body, str) else body_data + ) + revision.content = content + updated.append(revision) + + if updated: + revision_model.objects.bulk_update(updated, ["content"], batch_size=500) diff --git a/common/auth/permissions.py b/common/auth/permissions.py index 55d63599f6..90b045d8ae 100644 --- a/common/auth/permissions.py +++ b/common/auth/permissions.py @@ -54,16 +54,19 @@ def check_chart_permissions_by_name( return True topic_manager = MetricsAPIInterface.get_topic_manager() - metric_manager = MetricsAPIInterface.get_metric_manager() - geography_type_manager = MetricsAPIInterface.get_geography_type_manager() geography_manager = MetricsAPIInterface.get_geography_manager() - theme_id, sub_theme_id, topic_id = topic_manager.get_id_by_name( - theme_name, sub_theme_name, topic_name + # Any inconsistent combination below resolves to None ids and is denied + theme_id, sub_theme_id, topic_id, metric_id = ( + topic_manager.get_theme_sub_theme_topic_and_metric_id_by_name( + theme_name, sub_theme_name, topic_name, metric_name + ) + ) + geography_type_id, geography_id = ( + geography_manager.get_geography_type_id_and_code_by_name( + geography_name, geography_type + ) ) - metric_id = metric_manager.get_id_by_name(metric_name) - geography_type_id = geography_type_manager.get_id_by_name(geography_type) - geography_id = geography_manager.get_code_by_name(geography_name, geography_type) # Sanity check, because front-end must always # send content for any of these 6 requests diff --git a/metrics/api/serializers/headlines.py b/metrics/api/serializers/headlines.py index 0ccfcea1a7..8b366e79ba 100644 --- a/metrics/api/serializers/headlines.py +++ b/metrics/api/serializers/headlines.py @@ -20,6 +20,14 @@ class HeadlinesQuerySerializer(serializers.Serializer): + theme = serializers.CharField( + required=True, + help_text=help_texts.THEME_FIELD, + ) + sub_theme = serializers.CharField( + required=True, + help_text=help_texts.SUB_THEME_FIELD, + ) topic = serializers.ChoiceField( choices=[], required=True, @@ -34,12 +42,12 @@ class HeadlinesQuerySerializer(serializers.Serializer): ) geography = serializers.ChoiceField( choices=[], - required=False, + required=True, help_text=help_texts.GEOGRAPHY_FIELD, ) geography_type = serializers.ChoiceField( choices=[], - required=False, + required=True, help_text=help_texts.GEOGRAPHY_TYPE_FIELD, ) stratum = serializers.ChoiceField( diff --git a/metrics/api/serializers/help_texts.py b/metrics/api/serializers/help_texts.py index dc0366489a..847484e526 100644 --- a/metrics/api/serializers/help_texts.py +++ b/metrics/api/serializers/help_texts.py @@ -1,3 +1,5 @@ +THEME_FIELD: str = "The name of the theme being queried for. E.g. `infectious_disease`" +SUB_THEME_FIELD: str = "The name of the sub theme being queried for. E.g. `respiratory`" TOPIC_FIELD: str = "The name of the topic being queried for. E.g. `COVID-19`" METRIC_FIELD: str = "The name of the metric being queried for. E.g. `{}`" STRATUM_FIELD: str = """ diff --git a/metrics/api/serializers/plots.py b/metrics/api/serializers/plots.py index 795c114777..7244aad9c2 100644 --- a/metrics/api/serializers/plots.py +++ b/metrics/api/serializers/plots.py @@ -10,6 +10,14 @@ class PlotSerializer(serializers.Serializer): # Required fields + theme = serializers.CharField( + help_text=help_texts.THEME_FIELD, + required=True, + ) + sub_theme = serializers.CharField( + help_text=help_texts.SUB_THEME_FIELD, + required=True, + ) topic = serializers.ChoiceField( help_text=help_texts.TOPIC_FIELD, choices=[], @@ -20,6 +28,14 @@ class PlotSerializer(serializers.Serializer): choices=[], required=True, ) + geography = serializers.CharField( + help_text=help_texts.GEOGRAPHY_FIELD, + required=True, + ) + geography_type = serializers.CharField( + help_text=help_texts.GEOGRAPHY_TYPE_FIELD, + required=True, + ) # Optional fields stratum = serializers.CharField( help_text=help_texts.STRATUM_FIELD, @@ -35,20 +51,6 @@ class PlotSerializer(serializers.Serializer): allow_null=True, default="", ) - geography = serializers.CharField( - help_text=help_texts.GEOGRAPHY_FIELD, - required=False, - allow_blank=True, - allow_null=True, - default="", - ) - geography_type = serializers.CharField( - help_text=help_texts.GEOGRAPHY_TYPE_FIELD, - required=False, - allow_blank=True, - allow_null=True, - default="", - ) sex = serializers.CharField( help_text=help_texts.SEX_FIELD, required=False, diff --git a/metrics/api/views/charts/dual_category_charts.py b/metrics/api/views/charts/dual_category_charts.py index cd7eba2230..4cb4d6f507 100644 --- a/metrics/api/views/charts/dual_category_charts.py +++ b/metrics/api/views/charts/dual_category_charts.py @@ -56,6 +56,8 @@ "chart_type": "stacked_bar", "secondary_category": "age", "static_fields": { + "theme": "infectious_disease", + "sub_theme": "respiratory", "topic": "COVID-19", "metric": "COVID-19_cases_rateRollingMean", "stratum": "default", diff --git a/metrics/data/managers/core_models/geography.py b/metrics/data/managers/core_models/geography.py index f280df4cbd..15941d6dc6 100644 --- a/metrics/data/managers/core_models/geography.py +++ b/metrics/data/managers/core_models/geography.py @@ -83,6 +83,31 @@ def get_code_by_name( .first() ) + def get_geography_type_id_and_code_by_name( + self, geography_name: str, geography_type_name: str + ) -> tuple[int | None, str | None]: + """ + Gets the geography_type id and geography code for the given names. + + Both are resolved in a single query, which also enforces that the geography + actually belongs to the given geography_type. An inconsistent pair matches + no row, so this returns (None, None) and access is denied. + + Returns: + A tuple of (geography_type_id, geography_code) if the pair exists, + or (None, None) if it does not. + """ + record = ( + self.select_related("geography_type") + .filter(name=geography_name, geography_type__name=geography_type_name) + .first() + ) + + if record: + return int(record.geography_type_id), record.geography_code + + return None, None + def get_all_geography_codes_by_geography_type( self, geography_type_name: str ) -> Self: @@ -233,6 +258,21 @@ def get_code_by_name( geography_name, geography_type_name=geography_type_name ) + def get_geography_type_id_and_code_by_name( + self, geography_name: str, geography_type_name: str + ) -> tuple[int | None, str | None]: + """ + Gets the geography_type id and geography code for the given names in a + single query that also validates the pair is consistent. + + Returns: + A tuple of (geography_type_id, geography_code) if the pair exists, + or (None, None) if it does not. + """ + return self.get_queryset().get_geography_type_id_and_code_by_name( + geography_name, geography_type_name + ) + def get_all_names(self) -> GeographyQuerySet: """Gets all available deduplicated geography names as a flat list queryset. diff --git a/metrics/data/managers/core_models/topic.py b/metrics/data/managers/core_models/topic.py index 7a1342efa4..e1f63c5acc 100644 --- a/metrics/data/managers/core_models/topic.py +++ b/metrics/data/managers/core_models/topic.py @@ -40,33 +40,51 @@ def get_name_by_id(self, topic_id: int) -> str | None: """ return self.filter(id=topic_id).values_list("name", flat=True).first() - def get_id_by_name( - self, theme_name: str, sub_theme_name: str, topic_name: str - ) -> tuple[int | None, int | None, int | None]: - """ - Gets the theme, sub-theme and topic IDs matching the given names. + def get_theme_sub_theme_topic_and_metric_id_by_name( + self, + theme_name: str, + sub_theme_name: str, + topic_name: str, + metric_name: str, + ) -> tuple[int | None, int | None, int | None, int | None]: + """ + Gets the theme, sub-theme, topic and metric IDs matching the given names. + + This resolves all 4 ids in a single query which also enforces that the + combination is internally consistent. The metric must belong to the topic, + which must belong to the given sub-theme and theme. An invalid combination + (e.g. "respiratory" paired with "MMR1") matches no row, so this returns + (None, None, None, None) and access is denied. Returns: - A tuple of (theme_id, sub_theme_id, topic_id) if found, - or (None, None, None) if not found. - """ - record = self.filter( - sub_theme__theme__name=theme_name, - sub_theme__name=sub_theme_name, - name=topic_name, - ).first() + A tuple of (theme_id, sub_theme_id, topic_id, metric_id) if the whole + combination exists, or (None, None, None, None) if it does not. + """ + record = ( + self.filter( + sub_theme__theme__name=theme_name, + sub_theme__name=sub_theme_name, + name=topic_name, + metric__name=metric_name, + ) + .select_related("sub_theme__theme") + .annotate(matched_metric_id=models.F("metric__id")) + .first() + ) if record: return ( int(record.sub_theme.theme_id), int(record.sub_theme_id), int(record.id), + int(record.matched_metric_id), ) return ( None, None, None, + None, ) def get_all_unique_names(self) -> models.QuerySet: @@ -120,7 +138,7 @@ def get_all_names_and_ids(self) -> models.QuerySet: class TopicManager(models.Manager): - """Custom model manager class for the `Metric` model.""" + """Custom model manager class for the `Topic` model.""" def get_queryset(self) -> TopicQuerySet: return TopicQuerySet(model=self.model, using=self.db) @@ -142,18 +160,25 @@ def get_name_by_id(self, topic_id: int) -> str | None: """ return self.get_queryset().get_name_by_id(topic_id) - def get_id_by_name( - self, theme_name: str, sub_theme_name: str, topic_name: str - ) -> tuple[int | None, int | None, int | None]: + def get_theme_sub_theme_topic_and_metric_id_by_name( + self, + theme_name: str, + sub_theme_name: str, + topic_name: str, + metric_name: str, + ) -> tuple[int | None, int | None, int | None, int | None]: """ - Gets the theme, sub-theme and topic IDs matching the given names. + Gets the theme, sub-theme, topic and metric IDs matching the given names. + + Resolves all 4 ids in a single query that also validates the + combination is consistent. Returns: - A tuple of (theme_id, sub_theme_id, topic_id) if found, - or (None, None, None) if not found. + A tuple of (theme_id, sub_theme_id, topic_id, metric_id) if the whole + combination exists, or (None, None, None, None) if it does not. """ - return self.get_queryset().get_id_by_name( - theme_name, sub_theme_name, topic_name + return self.get_queryset().get_theme_sub_theme_topic_and_metric_id_by_name( + theme_name, sub_theme_name, topic_name, metric_name ) def get_all_names(self) -> TopicQuerySet: diff --git a/metrics/domain/models/headline.py b/metrics/domain/models/headline.py index b969912e4e..a961b6c772 100644 --- a/metrics/domain/models/headline.py +++ b/metrics/domain/models/headline.py @@ -2,6 +2,8 @@ class HeadlineParameters(BaseRequestParams): + theme: str + sub_theme: str topic: str metric: str stratum: str @@ -12,6 +14,14 @@ class HeadlineParameters(BaseRequestParams): is_public: bool | None = True data_classification: str | None = None + @property + def theme_name(self) -> str: + return self.theme + + @property + def sub_theme_name(self) -> str: + return self.sub_theme + @property def topic_name(self) -> str: return self.topic @@ -52,6 +62,8 @@ def to_dict_for_query(self) -> dict[str, str]: """ return { + "theme": self.theme_name, + "sub_theme": self.sub_theme_name, "topic": self.topic_name, "metric": self.metric_name, "geography": self.geography_name, diff --git a/metrics/domain/models/plots.py b/metrics/domain/models/plots.py index a184d84837..71e0fa068f 100644 --- a/metrics/domain/models/plots.py +++ b/metrics/domain/models/plots.py @@ -20,11 +20,13 @@ class PlotParameters(BaseModel): """Holds all the request information / parameters for an individual plot on a chart.""" chart_type: str = "" + theme: str = "" + sub_theme: str = "" topic: str metric: str + geography: str + geography_type: str stratum: str | None = "" - geography: str | None = "" - geography_type: str | None = "" sex: str | None = "" age: str | None = "" date_from: str | None = "" @@ -40,8 +42,6 @@ class PlotParameters(BaseModel): use_smooth_lines: bool = True use_markers: bool = False metric_value_ranges: Iterable[tuple[Decimal, Decimal]] | None = None - theme: str = "" - sub_theme: str = "" @property def metric_group(self) -> str: @@ -103,6 +103,8 @@ def to_dict_for_query(self) -> dict[str, str]: ], "metric": self.metric or "", "topic": self.topic or "", + "theme": self.theme or "", + "sub_theme": self.sub_theme or "", "stratum": self.stratum or "", "geography": self.geography or "", "geography_type": self.geography_type or "", diff --git a/metrics/interfaces/headlines/access.py b/metrics/interfaces/headlines/access.py index 74ec223364..64fe58c384 100644 --- a/metrics/interfaces/headlines/access.py +++ b/metrics/interfaces/headlines/access.py @@ -1,6 +1,5 @@ from django.db.models import Manager -from metrics.api.settings import auth from metrics.data.models.core_models import CoreHeadline, Topic from metrics.domain.headlines.state import Headline from metrics.domain.models.headline import HeadlineParameters @@ -35,15 +34,8 @@ def get_latest_metric_value(self) -> Headline: `HeadlineNumberDataNotFoundError`: If the query returned no records. """ - params = self.headline_parameters.to_dict_for_query() - if auth.AUTH_ENABLED: - # Needed for the downstream permissions check - topic = self.topic_manager.get_by_name( - name=self.headline_parameters.topic_name - ) - params["theme"] = topic.sub_theme.theme.name - params["sub_theme"] = topic.sub_theme.name + params = self.headline_parameters.to_dict_for_query() core_headline: CoreHeadline | None = ( self.core_headline_manager.get_latest_headline(**params) diff --git a/metrics/interfaces/plots/access.py b/metrics/interfaces/plots/access.py index ed1832e91d..4bbe8349b8 100644 --- a/metrics/interfaces/plots/access.py +++ b/metrics/interfaces/plots/access.py @@ -7,7 +7,6 @@ from django.db.models import Manager, QuerySet from pydantic import BaseModel -from metrics.api.settings import auth from metrics.data.models.core_models import CoreTimeSeries, Topic from metrics.domain.common.utils import ChartAxisFields from metrics.domain.models import ( @@ -147,11 +146,6 @@ def get_queryset_from_core_model_manager( ('05-10', Decimal('9.0')) ]>` """ - if auth.AUTH_ENABLED: - # Needed for the downstream permissions check - topic = self.topic_model_manager.get_by_name(name=plot_params["topic"]) - plot_params["theme"] = topic.sub_theme.theme.name - plot_params["sub_theme"] = topic.sub_theme.name # Sometimes this is a Subplots request which doesn't support confidence intervals confidence_intervals = getattr( diff --git a/metrics/interfaces/trends/access.py b/metrics/interfaces/trends/access.py index e0ee399b5f..107237ea27 100644 --- a/metrics/interfaces/trends/access.py +++ b/metrics/interfaces/trends/access.py @@ -1,6 +1,5 @@ from django.db.models import Manager -from metrics.api.settings import auth from metrics.data.models.core_models import CoreHeadline, Topic from metrics.domain.models.trends import TrendsParameters from metrics.domain.trends.state import TREND_AS_DICT, Trend @@ -66,15 +65,12 @@ def get_trend(self) -> Trend: `topic` / `metric` / `percentage_metric`. """ + main_metric_params = self.trend_parameters.to_dict_for_main_metric_query() percentage_metric_params = ( self.trend_parameters.to_dict_for_percentage_metric_query() ) - if auth.AUTH_ENABLED: - self._add_theme_info_to_params(params=main_metric_params) - self._add_theme_info_to_params(params=percentage_metric_params) - core_headline_percentage_metric: CoreHeadline = self.get_latest_metric_value( params=percentage_metric_params ) @@ -91,11 +87,6 @@ def get_trend(self) -> Trend: percentage_metric_period_end=core_headline_percentage_metric.period_end, ) - def _add_theme_info_to_params(self, *, params: dict) -> None: - topic = self.topic_manager.get_by_name(name=self.trend_parameters.topic_name) - params["theme"] = topic.sub_theme.theme.name - params["sub_theme"] = topic.sub_theme.name - def generate_trend_numbers( *, diff --git a/tests/conftest.py b/tests/conftest.py index 5948521217..2b9f4f49a6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -52,6 +52,7 @@ def example_subplot_chart_generation_payload() -> list[dict[str, str | Decimal]] "metric": "fake-metric", "topic": "fake-topic", "geography": "Darlington", + "geography_type": "Upper Tier Local Authority", }, "x_axis_values": ["Darlington"], "y_axis_values": [Decimal("95.4")], @@ -61,6 +62,7 @@ def example_subplot_chart_generation_payload() -> list[dict[str, str | Decimal]] "metric": "fake-metric", "topic": "fake-topic", "geography": "Hartlepool", + "geography_type": "Upper Tier Local Authority", }, "x_axis_values": ["Hartlepool"], "y_axis_values": [Decimal("93.4")], @@ -70,6 +72,7 @@ def example_subplot_chart_generation_payload() -> list[dict[str, str | Decimal]] "metric": "fake-metric", "topic": "fake-topic", "geography": "Stockton-on-Tees", + "geography_type": "Upper Tier Local Authority", }, "x_axis_values": ["Stockton-on-Tees"], "y_axis_values": [Decimal("95.4")], @@ -84,6 +87,7 @@ def example_subplot_chart_generation_payload() -> list[dict[str, str | Decimal]] "metric": "fake-metric-two", "topic": "fake-topic-two", "geography": "Darlington", + "geography_type": "Upper Tier Local Authority", }, "x_axis_values": ["Darlington"], "y_axis_values": [Decimal("92.4")], @@ -93,6 +97,7 @@ def example_subplot_chart_generation_payload() -> list[dict[str, str | Decimal]] "metric": "fake-metric-two", "topic": "fake-topic-two", "geography": "Hartlepool", + "geography_type": "Upper Tier Local Authority", }, "x_axis_values": ["Hartlepool"], "y_axis_values": [Decimal("91.4")], @@ -102,6 +107,7 @@ def example_subplot_chart_generation_payload() -> list[dict[str, str | Decimal]] "metric": "fake-metric", "topic": "fake-topic", "geography": "Stockton-on-Tees", + "geography_type": "Upper Tier Local Authority", }, "x_axis_values": ["Stockton-on-Tees"], "y_axis_values": [Decimal("95.4")], @@ -121,11 +127,11 @@ def create_subplots_data() -> list[Subplots]: chart_type="bar", topic="COVID-19", metric="COVID-19_testing_positivity7DayRolling", + geography="England", + geography_type="Nation", stratum="default", date_from="2023-01-01", date_to="2023-12-31", - geography="England", - geography_type="Nation", x_axis="date", y_axis="metric", ) @@ -190,6 +196,8 @@ def fake_chart_plot_parameters() -> PlotParameters: chart_type="line_multi_coloured", topic="COVID-19", metric="COVID-19_testing_positivity7DayRolling", + geography="England", + geography_type="Nation", stratum="default", date_from="2023-01-01", date_to="2023-12-31", @@ -215,9 +223,11 @@ def fake_chart_request_params( @pytest.fixture def fake_chart_plot_parameters_headline_data() -> PlotParameters: return PlotParameters( - metric="COVID-19_headline_vaccines_spring24Uptake", - topic="COVID-19", chart_type=ChartTypes.bar, + topic="COVID-19", + metric="COVID-19_headline_vaccines_spring24Uptake", + geography="England", + geography_type="Nation", x_axis="age", y_axis="metric", date_from=None, @@ -231,6 +241,8 @@ def fake_chart_plot_parameters_covid_cases() -> PlotParameters: chart_type="line_multi_coloured", topic="COVID-19", metric="COVID-19_deaths_ONSByDay", + geography="England", + geography_type="Nation", date_from="2023-01-01", date_to="2023-12-31", ) @@ -239,9 +251,11 @@ def fake_chart_plot_parameters_covid_cases() -> PlotParameters: @pytest.fixture def valid_plot_parameters() -> PlotParameters: return PlotParameters( - metric="COVID-19_deaths_ONSByDay", - topic="COVID-19", chart_type=ChartTypes.line_multi_coloured.value, + topic="COVID-19", + metric="COVID-19_deaths_ONSByDay", + geography="England", + geography_type="Nation", date_from="2023-01-01", date_to="2023-12-31", x_axis="date", @@ -252,9 +266,11 @@ def valid_plot_parameters() -> PlotParameters: @pytest.fixture def valid_plot_parameters_for_headline_data() -> PlotParameters: return PlotParameters( - metric="COVID-19_headline_vaccines_spring24Uptake", - topic="COVID-19", chart_type=ChartTypes.bar.value, + topic="COVID-19", + metric="COVID-19_headline_vaccines_spring24Uptake", + geography="England", + geography_type="Nation", date_from="", date_to="", x_axis="age", @@ -268,9 +284,10 @@ def fake_plot_data() -> PlotGenerationData: chart_type="line_multi_coloured", topic="COVID-19", metric="COVID-19_deaths_ONSByDay", + geography="London", + geography_type="Nation", x_axis="metric", y_axis="date", - geography="London", age="all", sex="all", line_type="SOLID", @@ -305,8 +322,12 @@ def plot_serializer_payload_and_model_managers() -> ( fake_topic = fake_metric.metric_group.topic data: DATA_PAYLOAD_HINT = { + "theme": fake_topic.sub_theme.theme.name, + "sub_theme": fake_topic.sub_theme.name, "topic": fake_topic.name, "metric": fake_metric.name, + "geography": "England", + "geography_type": "Nation", "chart_type": ChartTypes.bar.value, } diff --git a/tests/fakes/managers/headline_manager.py b/tests/fakes/managers/headline_manager.py index 1698ed3dcd..eba0f3da55 100644 --- a/tests/fakes/managers/headline_manager.py +++ b/tests/fakes/managers/headline_manager.py @@ -19,6 +19,8 @@ def get_latest_headline( *, topic: str, metric: str, + theme: str = "", + sub_theme: str = "", geography: str | None = None, geography_type: str | None = None, geography_code: str | None = None, diff --git a/tests/fakes/managers/time_series_manager.py b/tests/fakes/managers/time_series_manager.py index 5fb591571c..f0479b4b81 100644 --- a/tests/fakes/managers/time_series_manager.py +++ b/tests/fakes/managers/time_series_manager.py @@ -78,7 +78,7 @@ def query_for_data( filtered_time_series = [ x for x in filtered_time_series - if x.geography.geography_type.name == geography + if x.geography.geography_type.name == geography_type ] if stratum: diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 522536d1d6..195e0dd731 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -10,6 +10,10 @@ Metric, MetricGroup, Topic, + Geography, + GeographyType, + Theme, + SubTheme, ) from tests.factories.common.auth.permissions import UserPermissionsFactory @@ -94,13 +98,19 @@ def core_trend_example(request) -> tuple[CoreHeadline, CoreHeadline]: @pytest.fixture def core_timeseries_example() -> list[CoreTimeSeries]: - topic = Topic.objects.create(name="COVID-19") + theme = Theme.objects.create(name="infectious_disease") + sub_theme = SubTheme.objects.create(name="respiratory", theme=theme) + topic = Topic.objects.create(name="COVID-19", sub_theme=sub_theme) metric_group = MetricGroup.objects.create(name="deaths", topic=topic) metric = Metric.objects.create( name="COVID-19_deaths_ONSByDay", metric_group=metric_group, topic=topic, ) + geography_type = GeographyType.objects.create(name="Nation") + geography = Geography.objects.create( + name="England", geography_type=geography_type, geography_code="E92000001" + ) age = Age.objects.create(name="all") year = 2023 month = 1 @@ -108,6 +118,7 @@ def core_timeseries_example() -> list[CoreTimeSeries]: CoreTimeSeries.objects.create( metric_value=123, metric=metric, + geography=geography, age=age, year=year, epiweek=1, diff --git a/tests/integration/metrics/api/views/charts/test_single_category_charts.py b/tests/integration/metrics/api/views/charts/test_single_category_charts.py index 000a9df052..c8081d6a20 100644 --- a/tests/integration/metrics/api/views/charts/test_single_category_charts.py +++ b/tests/integration/metrics/api/views/charts/test_single_category_charts.py @@ -17,8 +17,12 @@ def _build_valid_payload_for_existing_timeseries(core_timeseries: CoreTimeSeries "file_format": "png", "plots": [ { + "theme": "infectious_disease", + "sub_theme": "respiratory", "topic": core_timeseries.metric.metric_group.topic.name, "metric": core_timeseries.metric.name, + "geography": core_timeseries.geography.name, + "geography_type": core_timeseries.geography.geography_type.name, "chart_type": "bar", "date_from": "2000-01-01", "date_to": datetime.date.today(), @@ -152,8 +156,12 @@ def test_returns_correct_response_for_v3_age_based_chart(self): "x_axis": "age", "plots": [ { + "theme": "infectious_disease", + "sub_theme": "respiratory", "topic": "COVID-19", "metric": "COVID-19_deaths_ONSByDay", + "geography": "England", + "geography_type": "Nation", "chart_type": "bar", "date_from": "2000-01-01", "date_to": datetime.date.today(), @@ -203,8 +211,12 @@ def test_returns_bad_request_response_when_queried_data_does_not_exist( "file_format": "svg", "plots": [ { + "theme": "infectious_disease", + "sub_theme": "respiratory", "topic": core_timeseries_example[0].metric.topic.name, "metric": core_timeseries_example[0].metric.name, + "geography": "England", + "geography_type": "Nation", "chart_type": "bar", "age": "non-existent-age", } diff --git a/tests/integration/metrics/api/views/downloads/test_single_category_downloads.py b/tests/integration/metrics/api/views/downloads/test_single_category_downloads.py index d50c8a0b33..ca56d36f0b 100644 --- a/tests/integration/metrics/api/views/downloads/test_single_category_downloads.py +++ b/tests/integration/metrics/api/views/downloads/test_single_category_downloads.py @@ -36,9 +36,9 @@ class TestDownloadsView: "theme": "infectious_disease", "sub_theme": "respiratory", "topic": "COVID-19", - "geography_type": "", - "geography": "", "metric": "COVID-19_headline_vaccines_spring24Uptake", + "geography_type": "Nation", + "geography": "England", "stratum": "default", "sex": "", "age": "01-04", @@ -55,6 +55,8 @@ def _build_valid_payload( "file_format": "csv", "plots": [ { + "theme": self.core_timeseries_data["theme"], + "sub_theme": self.core_timeseries_data["sub_theme"], "metric": metric_prefix + self.core_timeseries_data["metric"], "topic": self.core_timeseries_data["topic"], "stratum": self.core_timeseries_data["stratum"], @@ -78,6 +80,8 @@ def _build_valid_headline_payload( "y_axis": "metric", "plots": [ { + "theme": self.core_headline_data["theme"], + "sub_theme": self.core_headline_data["sub_theme"], "metric": metric_prefix + self.core_headline_data["metric"], "topic": self.core_headline_data["topic"], "stratum": self.core_headline_data["stratum"], diff --git a/tests/integration/metrics/api/views/tables/test_dual_category.py b/tests/integration/metrics/api/views/tables/test_dual_category.py index 83bfa8e63b..bee8d72371 100644 --- a/tests/integration/metrics/api/views/tables/test_dual_category.py +++ b/tests/integration/metrics/api/views/tables/test_dual_category.py @@ -29,8 +29,12 @@ def test_timeseries_plot( client = APIClient() core_timeseries: CoreTimeSeries = core_timeseries_example[0] latest_timeseries: CoreTimeSeries = core_timeseries_example[-1] + theme: str = core_timeseries.metric.metric_group.topic.sub_theme.theme.name + sub_theme: str = core_timeseries.metric.metric_group.topic.sub_theme.name topic: str = core_timeseries.metric.metric_group.topic.name metric: str = core_timeseries.metric.name + geography: str = core_timeseries.geography.name + geography_type: str = core_timeseries.geography.geography_type.name valid_payload = { "x_axis": "date", @@ -40,6 +44,10 @@ def test_timeseries_plot( "date_to": datetime.date.today(), "metric": metric, "topic": topic, + "sub_theme": sub_theme, + "theme": theme, + "geography": geography, + "geography_type": geography_type, }, "secondary_category": "age", "segments": [ @@ -78,15 +86,23 @@ def test_headline_plot( """ # Given client = APIClient() + theme: str = ( + core_headline_example.metric.metric_group.topic.sub_theme.theme.name + ) + sub_theme: str = core_headline_example.metric.metric_group.topic.sub_theme.name if not core_headline_example.is_public: client.force_authenticate(user=user_global_access, token="token") topic: str = core_headline_example.metric.metric_group.topic.name metric: str = core_headline_example.metric.name + geography: str = core_headline_example.geography.name + geography_type: str = core_headline_example.geography.geography_type.name CoreHeadlineFactory.create_record( metric_value=123.0000, metric=metric, + theme=theme, + sub_theme=sub_theme, topic=topic, geography="England", geography_type="Nation", @@ -110,6 +126,10 @@ def test_headline_plot( "date_to": datetime.date.today(), "metric": metric, "topic": topic, + "sub_theme": sub_theme, + "theme": theme, + "geography": geography, + "geography_type": geography_type, }, "secondary_category": "sex", "segments": [ diff --git a/tests/integration/metrics/api/views/tables/test_single_category_tables.py b/tests/integration/metrics/api/views/tables/test_single_category_tables.py index 296afe4d6d..2af3574da5 100644 --- a/tests/integration/metrics/api/views/tables/test_single_category_tables.py +++ b/tests/integration/metrics/api/views/tables/test_single_category_tables.py @@ -33,8 +33,12 @@ def test_hitting_endpoint_without_appended_forward_slash_redirects_correctly( "file_format": "svg", "plots": [ { + "theme": "infectious_disease", + "sub_theme": "respiratory", "topic": topic_name, "metric": metric_name, + "geography": "England", + "geography_type": "Nation", "chart_type": "bar", "chart_height": 220, "chart_width": 435, @@ -73,8 +77,12 @@ def test_returns_correct_response_type( "file_format": "svg", "plots": [ { + "theme": "infectious_disease", + "sub_theme": "respiratory", "topic": topic_name, "metric": metric_name, + "geography": "England", + "geography_type": "Nation", "chart_type": "bar", "chart_height": 220, "chart_width": 435, @@ -115,8 +123,12 @@ def test_single_plot_output_is_as_expected( "file_format": "svg", "plots": [ { + "theme": "infectious_disease", + "sub_theme": "respiratory", "topic": topic_name, "metric": metric_name, + "geography": "England", + "geography_type": "Nation", "chart_type": "bar", "chart_height": 220, "chart_width": 435, @@ -178,15 +190,23 @@ def test_multiple_plot_output_is_as_expected( "file_format": "svg", "plots": [ { + "theme": "infectious_disease", + "sub_theme": "respiratory", "topic": topic_name, "metric": metric_name, + "geography": "England", + "geography_type": "Nation", "chart_type": "bar", "date_from": "2000-01-01", "date_to": datetime.date.today(), }, { + "theme": "infectious_disease", + "sub_theme": "respiratory", "topic": topic_name, "metric": metric_name, + "geography": "England", + "geography_type": "Nation", "chart_type": "bar", "label": "plot_label", "date_from": "2000-01-01", @@ -262,8 +282,12 @@ def test_headline_charts_with_plot_label_uses_plot_label_for_reference( "y_axis": "metric", "plots": [ { + "theme": "infectious_disease", + "sub_theme": "respiratory", "topic": topic_name, "metric": metric_name, + "geography": "England", + "geography_type": "Nation", "chart_type": "bar", "sex": "f", "label": "female", @@ -306,8 +330,12 @@ def test_returns_bad_request_response_when_queried_data_does_not_exist( valid_payload = { "plots": [ { + "theme": "infectious_disease", + "sub_theme": "respiratory", "topic": core_timeseries_example[0].metric.topic.name, "metric": core_timeseries_example[0].metric.name, + "geography": "England", + "geography_type": "Nation", "chart_type": "bar", "age": "non-existent-age", } diff --git a/tests/integration/metrics/api/views/test_headlines.py b/tests/integration/metrics/api/views/test_headlines.py index a89a0cfb67..27bbdc5414 100644 --- a/tests/integration/metrics/api/views/test_headlines.py +++ b/tests/integration/metrics/api/views/test_headlines.py @@ -31,6 +31,8 @@ def test_get_returns_correct_response( if not core_headline_example.is_public: client.force_authenticate(user=user_global_access, token="token") payload = { + "theme": "infectious_disease", + "sub_theme": "respiratory", "topic": core_headline_example.metric.topic.name, "metric": core_headline_example.metric.name, "geography": core_headline_example.geography.name, @@ -79,6 +81,8 @@ def test_get_returns_error_for_invalid_request( response: Response = client.get( path=self.path, data={ + "theme": "infectious_disease", + "sub_theme": "respiratory", "topic": incorrect_topic_name, "metric": core_headline_example.metric.name, "geography": core_headline_example.geography.name, diff --git a/tests/integration/metrics/api/views/test_permission_sets.py b/tests/integration/metrics/api/views/test_permission_sets.py index 2c9b22f64d..b3a0bb478f 100644 --- a/tests/integration/metrics/api/views/test_permission_sets.py +++ b/tests/integration/metrics/api/views/test_permission_sets.py @@ -25,7 +25,7 @@ def test_get_sub_themes_by_theme_id_should_return_tuple_of_id_and_name(self): name="respiratory", theme="infectious_disease" ) childhoodVaccinesSubtheme = SubThemeFactory.create_with_theme( - name="immunisation", theme="childhood_vaccines" + name="immunisation", theme="childhood-vaccines" ) # Retrieve the subthemes @@ -51,7 +51,7 @@ def test_get_sub_themes_by_theme_id_should_return_wildcard(self): name="respiratory", theme="infectious_disease" ) childhoodVaccinesSubtheme = SubThemeFactory.create_with_theme( - name="immunisation", theme="childhood_vaccines" + name="immunisation", theme="childhood-vaccines" ) # Retrieve the subthemes @@ -77,7 +77,7 @@ def test_get_sub_themes_by_theme_id_should_return_an_error(self): name="respiratory", theme="infectious_disease" ) childhoodVaccinesSubtheme = SubThemeFactory.create_with_theme( - name="immunisation", theme="childhood_vaccines" + name="immunisation", theme="childhood-vaccines" ) # Retrieve the subthemes diff --git a/tests/integration/metrics/api/views/test_trends.py b/tests/integration/metrics/api/views/test_trends.py index 236f4bdad4..b7d0c3f335 100644 --- a/tests/integration/metrics/api/views/test_trends.py +++ b/tests/integration/metrics/api/views/test_trends.py @@ -37,11 +37,13 @@ def test_get_returns_correct_response( response: Response = client.get( path=self.path, data={ + "theme": "infectious_disease", + "sub_theme": "respiratory", "topic": topic_name, "metric": metric_name, - "percentage_metric": percentage_metric_name, "geography": main_record.geography.name, "geography_type": main_record.geography.geography_type.name, + "percentage_metric": percentage_metric_name, "sex": main_record.sex, "age": main_record.age, "stratum": main_record.stratum.name, @@ -90,6 +92,8 @@ def test_get_returns_error_message_for_timeseries_type_metric( response: Response = client.get( path=self.path, data={ + "theme": "infectious_disease", + "sub_theme": "respiratory", "topic": incorrect_topic_name, "metric": metric_name, "percentage_metric": percentage_metric_name, diff --git a/tests/integration/metrics/data/managers/core_models/test_geography.py b/tests/integration/metrics/data/managers/core_models/test_geography.py index 77fa001e30..d4c47dab22 100644 --- a/tests/integration/metrics/data/managers/core_models/test_geography.py +++ b/tests/integration/metrics/data/managers/core_models/test_geography.py @@ -186,3 +186,59 @@ def test_get_code_by_name(self): # Then assert geography_code == liverpool_combined_authority.geography_code + + @pytest.mark.django_db + def test_get_geography_type_id_and_code_by_name(self): + """ + Given some Geography records that share a name across geography types + When get_geography_type_id_and_code_by_name() is called with a specific geography_type + Then the matching (geography_type_id, geography_code) pair is returned + """ + + # Given + GeographyFactory.create_with_geography_type( + name="Liverpool", + geography_code="E08000012", + geography_type="Lower Tier Local Authority", + ) + liverpool_combined_authority = GeographyFactory.create_with_geography_type( + name="Liverpool", + geography_code="E47000004", + geography_type="Combined Authority", + ) + + # When + result = Geography.objects.get_geography_type_id_and_code_by_name( + "Liverpool", "Combined Authority" + ) + + # Then + assert result == ( + liverpool_combined_authority.geography_type_id, + liverpool_combined_authority.geography_code, + ) + + @pytest.mark.django_db + def test_get_geography_type_id_and_code_by_name_returns_none_for_inconsistent_pair( + self, + ): + """ + Given a Geography that belongs to one geography_type + When get_geography_type_id_and_code_by_name() is called with a different geography_type + Then (None, None) is returned + """ + + # Given + GeographyFactory.create_with_geography_type( + name="Liverpool", + geography_code="E47000004", + geography_type="Combined Authority", + ) + + # When + result = Geography.objects.get_geography_type_id_and_code_by_name( + "Liverpool", "Nation" + ) + + # Then + assert result == (None, None) diff --git a/tests/integration/metrics/data/managers/core_models/test_topic.py b/tests/integration/metrics/data/managers/core_models/test_topic.py index da01286b5a..205779a857 100644 --- a/tests/integration/metrics/data/managers/core_models/test_topic.py +++ b/tests/integration/metrics/data/managers/core_models/test_topic.py @@ -1,6 +1,6 @@ import pytest -from metrics.data.models.core_models.supporting import SubTheme, Theme, Topic +from metrics.data.models.core_models.supporting import Metric, SubTheme, Theme, Topic from tests.factories.metrics.topic import TopicFactory @@ -73,14 +73,45 @@ def test_query_get_name_by_id(self): # Then assert get_name_by_id == fake_topic_name_three + @pytest.mark.django_db + def test_get_by_name(self): + """ + Given a number of existing Topic records + When get_by_name is called with a matching name + Then the corresponding Topic record is returned + """ + + # Given + TopicFactory(name="Cold-alert") + expected_topic = TopicFactory(name="COVID-19") + + # When + retrieved_topic = Topic.objects.get_by_name(name="COVID-19") + + # Then + assert retrieved_topic == expected_topic + @pytest.mark.django_db @pytest.mark.parametrize( - "theme_name, sub_theme_name, topic_name, expected_index", + "theme_name, sub_theme_name, topic_name, metric_name, is_match", [ - ("Infectious Diseases", "Respiratory", "COVID-19", 0), - ("NON-EXISTENT", "Respiratory", "COVID-19", None), - ("Infectious Diseases", "NON-EXISTENT", "COVID-19", None), - ("Infectious Diseases", "Respiratory", "NON-EXISTENT", None), + ("Infectious Diseases", "Respiratory", "COVID-19", "COVID-19_metric", True), + ("NON-EXISTENT", "Respiratory", "COVID-19", "COVID-19_metric", False), + ( + "Infectious Diseases", + "NON-EXISTENT", + "COVID-19", + "COVID-19_metric", + False, + ), + ( + "Infectious Diseases", + "Respiratory", + "NON-EXISTENT", + "COVID-19_metric", + False, + ), + ("Infectious Diseases", "Respiratory", "COVID-19", "NON-EXISTENT", False), ], ) def test_get_id_by_name( @@ -88,32 +119,35 @@ def test_get_id_by_name( theme_name: str, sub_theme_name: str, topic_name: str, - expected_index: int | None, + metric_name: str, + is_match: bool, ): """ - Given some theme, sub-theme and topic records - When get_id_by_name() is called - Then the matching 3 ids are returned, or 3 None values if no match + Given some theme, sub-theme, topic and metric records + When get_theme_sub_theme_topic_and_metric_id_by_name() is called + Then the matching 4 ids are returned, or 4 None values if the combination is not consistent """ # Given given_theme = Theme.objects.create(name="Infectious Diseases") given_sub_theme = SubTheme.objects.create(name="Respiratory", theme=given_theme) - given_topics = [ - Topic.objects.create(name="COVID-19", sub_theme=given_sub_theme) - ] + given_topic = Topic.objects.create(name="COVID-19", sub_theme=given_sub_theme) + given_metric = Metric.objects.create(name="COVID-19_metric", topic=given_topic) # When - ids = Topic.objects.get_id_by_name(theme_name, sub_theme_name, topic_name) + ids = Topic.objects.get_theme_sub_theme_topic_and_metric_id_by_name( + theme_name, sub_theme_name, topic_name, metric_name + ) # Then expected_ids = ( ( given_theme.id, given_sub_theme.id, - given_topics[expected_index].id, + given_topic.id, + given_metric.id, ) - if expected_index is not None - else (None, None, None) + if is_match + else (None, None, None, None) ) assert ids == expected_ids diff --git a/tests/integration/metrics/domain/charts/line_multi_coloured/test_generation.py b/tests/integration/metrics/domain/charts/line_multi_coloured/test_generation.py index 534ac466ac..81d733c936 100644 --- a/tests/integration/metrics/domain/charts/line_multi_coloured/test_generation.py +++ b/tests/integration/metrics/domain/charts/line_multi_coloured/test_generation.py @@ -45,6 +45,8 @@ def _setup_chart_plot_data( chart_type="line_multi_coloured", topic="COVID-19", metric="COVID-19_deaths_ONSByDay", + geography="England", + geography_type="Nation", stratum="default", label=label, line_type=line_type, diff --git a/tests/integration/metrics/interfaces/charts/single_category_charts/test_access.py b/tests/integration/metrics/interfaces/charts/single_category_charts/test_access.py index 2e913215e2..b3d87248eb 100644 --- a/tests/integration/metrics/interfaces/charts/single_category_charts/test_access.py +++ b/tests/integration/metrics/interfaces/charts/single_category_charts/test_access.py @@ -91,6 +91,8 @@ def _setup_chart_plot_data( chart_type="line_multi_coloured", topic="COVID-19", metric="COVID-19_cases_casesByDay", + geography="England", + geography_type="Nation", stratum="default", label=label, line_type=line_type, diff --git a/tests/integration/metrics/interfaces/headlines/test_access.py b/tests/integration/metrics/interfaces/headlines/test_access.py index 6ff1698a18..c48c3e10db 100644 --- a/tests/integration/metrics/interfaces/headlines/test_access.py +++ b/tests/integration/metrics/interfaces/headlines/test_access.py @@ -12,25 +12,20 @@ from tests.factories.common.auth.permissions import UserPermissionsFactory from tests.factories.metrics.headline import CoreHeadlineFactory -MODULE_PATH = "metrics.interfaces.headlines.access" - class TestHeadlinesInterface: @pytest.mark.django_db - @mock.patch(f"{MODULE_PATH}.auth.AUTH_ENABLED") def test_get_latest_metric_value_returns_non_public_record_for_matching_permission( - self, mocked_auth_enabled: mock.MagicMock + self, ): """ Given public and non-public `CoreHeadline` records And an `RBACPermission` which gives access to the non-public portion of the data - And `AUTH_ENABLED` is set to True And `ENFORCE_PUBLIC_DATA_ONLY` is disabled When `get_latest_metric_value()` is called from the `HeadlinesInterface` Then the non-public record is returned """ # Given - mocked_auth_enabled.return_value = True public_record = CoreHeadlineFactory.create_record( period_end="2025-01-01", metric_value=1, is_public=True ) @@ -50,6 +45,8 @@ def test_get_latest_metric_value_returns_non_public_record_for_matching_permissi fake_request.user.permission_sets = permission_sets headline_parameters = HeadlineParameters( + theme=public_record.metric.topic.sub_theme.theme.name, + sub_theme=public_record.metric.topic.sub_theme.name, topic=public_record.metric.topic.name, metric=public_record.metric.name, stratum=public_record.stratum.name, @@ -74,19 +71,16 @@ def test_get_latest_metric_value_returns_non_public_record_for_matching_permissi ) @pytest.mark.django_db - @mock.patch(f"{MODULE_PATH}.auth.AUTH_ENABLED") def test_get_latest_metric_value_excludes_non_public_record_for_no_matching_permission( - self, mocked_auth_enabled: mock.MagicMock + self, ): """ Given public and non-public `CoreHeadline` records And no `RBACPermission` which allows access to the non-public portion of this dataset - And `AUTH_ENABLED` is set to True When `get_latest_metric_value()` is called from the `HeadlinesInterface` Then the non-public record is excluded """ # Given - mocked_auth_enabled.return_value = True public_record = CoreHeadlineFactory.create_record( period_end="2025-01-01", metric_value=1, is_public=True ) @@ -99,6 +93,8 @@ def test_get_latest_metric_value_excludes_non_public_record_for_no_matching_perm fake_request.rbac_permissions = [] headline_parameters = HeadlineParameters( + theme=public_record.metric.topic.sub_theme.theme.name, + sub_theme=public_record.metric.topic.sub_theme.name, topic=public_record.metric.topic.name, metric=public_record.metric.name, stratum=public_record.stratum.name, diff --git a/tests/system/non_public_data/test_charts.py b/tests/system/non_public_data/test_charts.py index 6e720a6711..8d53a8ca0c 100644 --- a/tests/system/non_public_data/test_charts.py +++ b/tests/system/non_public_data/test_charts.py @@ -25,13 +25,15 @@ def get_valid_payload(cls, core_time_series) -> dict: "file_format": "svg", "plots": [ { - "metric": core_time_series.metric.name, + "theme": core_time_series.metric.topic.sub_theme.theme.name, + "sub_theme": core_time_series.metric.topic.sub_theme.name, "topic": core_time_series.metric.topic.name, + "metric": core_time_series.metric.name, + "geography": core_time_series.geography.name, + "geography_type": core_time_series.geography.geography_type.name, "stratum": core_time_series.stratum.name, "age": core_time_series.age.name, "sex": core_time_series.sex, - "geography": core_time_series.geography.name, - "geography_type": core_time_series.geography.geography_type.name, "date_from": "2000-01-01", "date_to": "2025-12-31", "chart_type": "bar", diff --git a/tests/system/non_public_data/test_downloads.py b/tests/system/non_public_data/test_downloads.py index b5d9986acd..57ed68bdef 100644 --- a/tests/system/non_public_data/test_downloads.py +++ b/tests/system/non_public_data/test_downloads.py @@ -25,13 +25,15 @@ def get_valid_payload(cls, core_time_series) -> dict: "file_format": "json", "plots": [ { + "theme": core_time_series.metric.topic.sub_theme.theme.name, + "sub_theme": core_time_series.metric.topic.sub_theme.name, "metric": core_time_series.metric.name, "topic": core_time_series.metric.topic.name, + "geography": core_time_series.geography.name, + "geography_type": core_time_series.geography.geography_type.name, "stratum": core_time_series.stratum.name, "age": core_time_series.age.name, "sex": core_time_series.sex, - "geography": core_time_series.geography.name, - "geography_type": core_time_series.geography.geography_type.name, "date_from": "2000-01-01", "date_to": "2025-12-31", } diff --git a/tests/system/non_public_data/test_headlines.py b/tests/system/non_public_data/test_headlines.py index be6d19f6e5..d836df8fdf 100644 --- a/tests/system/non_public_data/test_headlines.py +++ b/tests/system/non_public_data/test_headlines.py @@ -24,6 +24,8 @@ def path(self) -> str: @classmethod def get_valid_payload(cls, core_headline) -> dict: return { + "theme": core_headline.metric.topic.sub_theme.theme.name, + "sub_theme": core_headline.metric.topic.sub_theme.name, "topic": core_headline.metric.topic.name, "metric": core_headline.metric.name, "geography": core_headline.geography.name, diff --git a/tests/system/non_public_data/test_tables.py b/tests/system/non_public_data/test_tables.py index f225cda902..55fde97e71 100644 --- a/tests/system/non_public_data/test_tables.py +++ b/tests/system/non_public_data/test_tables.py @@ -25,8 +25,12 @@ def get_valid_payload(cls, core_time_series) -> dict: "file_format": "svg", "plots": [ { + "theme": core_time_series.metric.topic.sub_theme.theme.name, + "sub_theme": core_time_series.metric.topic.sub_theme.name, "topic": core_time_series.metric.topic.name, "metric": core_time_series.metric.name, + "geography": core_time_series.geography.name, + "geography_type": core_time_series.geography.geography_type.name, "chart_type": "bar", "chart_height": 220, "chart_width": 435, diff --git a/tests/system/non_public_data/test_trends.py b/tests/system/non_public_data/test_trends.py index 6bab58aaab..2a46c36378 100644 --- a/tests/system/non_public_data/test_trends.py +++ b/tests/system/non_public_data/test_trends.py @@ -24,11 +24,13 @@ def path(self) -> str: @classmethod def get_valid_payload(cls, core_main_headline, core_percentage_headline) -> dict: return { + "theme": core_main_headline.metric.topic.sub_theme.theme.name, + "sub_theme": core_main_headline.metric.topic.sub_theme.name, "topic": core_main_headline.metric.topic.name, "metric": core_main_headline.metric.name, - "percentage_metric": core_percentage_headline.metric.name, "geography": core_main_headline.geography.name, "geography_type": core_main_headline.geography.geography_type.name, + "percentage_metric": core_percentage_headline.metric.name, "age": core_main_headline.age.name, "sex": core_main_headline.sex, "stratum": core_main_headline.stratum.name, diff --git a/tests/system/test_ingestion.py b/tests/system/test_ingestion.py index 46bb15bf16..daf11fc437 100644 --- a/tests/system/test_ingestion.py +++ b/tests/system/test_ingestion.py @@ -80,8 +80,12 @@ def test_data_can_be_ingested_and_queried_from_tables_endpoint( "file_format": "svg", "plots": [ { + "theme": example_time_series_data["parent_theme"], + "sub_theme": example_time_series_data["child_theme"], "topic": example_time_series_data["topic"], "metric": example_time_series_data["metric"], + "geography": example_time_series_data["geography"], + "geography_type": example_time_series_data["geography_type"], "date_from": "2020-01-01", "chart_type": "bar", } @@ -255,9 +259,21 @@ def test_data_is_deduplicated_on_write_to_db_and_return_latest_data_from_apis( # Check that the `tables/` endpoint returns the correct data # which matches the first ingested file + theme = first_sample_data["parent_theme"] + sub_theme = first_sample_data["child_theme"] topic = first_sample_data["topic"] metric = first_sample_data["metric"] - tables_response = self._hit_tables_endpoint(topic=topic, metric=metric) + geography = first_sample_data["geography"] + geography_type = first_sample_data["geography_type"] + + tables_response = self._hit_tables_endpoint( + theme=theme, + sub_theme=sub_theme, + topic=topic, + metric=metric, + geography=geography, + geography_type=geography_type, + ) expected_first_metric_value = ( f"{first_sample_data['time_series'][1]['metric_value']:.4f}" ) @@ -302,7 +318,14 @@ def test_data_is_deduplicated_on_write_to_db_and_return_latest_data_from_apis( ) # After the 2nd file was ingested and subsequently de-duplicated # the `tables/` endpoint should still return the same values as before - tables_response = self._hit_tables_endpoint(topic=topic, metric=metric) + tables_response = self._hit_tables_endpoint( + theme=theme, + sub_theme=sub_theme, + topic=topic, + metric=metric, + geography=geography, + geography_type=geography_type, + ) returned_metric_values = [row["values"][0]["value"] for row in tables_response] assert returned_metric_values[0] == expected_first_metric_value assert returned_metric_values[1] == expected_second_metric_value @@ -329,7 +352,14 @@ def test_data_is_deduplicated_on_write_to_db_and_return_latest_data_from_apis( assert final_refresh_date in CoreTimeSeries.objects.all().values_list( "refresh_date", flat=True ) - tables_response = self._hit_tables_endpoint(topic=topic, metric=metric) + tables_response = self._hit_tables_endpoint( + theme=theme, + sub_theme=sub_theme, + topic=topic, + metric=metric, + geography=geography, + geography_type=geography_type, + ) returned_metric_values = [row["values"][0]["value"] for row in tables_response] assert returned_metric_values[0] == expected_first_metric_value # We expect the 1st row to be as per the original data @@ -366,6 +396,8 @@ def test_multiple_data_points_with_sequential_refresh_dates_can_be_ingested( example_headline_data["data"].pop(1) headlines_endpoint_payload = { + "theme": example_headline_data["parent_theme"], + "sub_theme": example_headline_data["child_theme"], "topic": example_headline_data["topic"], "metric": example_headline_data["metric"], "geography": example_headline_data["geography"], @@ -556,7 +588,14 @@ def _rebuild_data_with_single_retrospective_update( return sample_data_with_one_retrospective_update @staticmethod - def _hit_tables_endpoint(topic: str, metric: str) -> list[dict]: + def _hit_tables_endpoint( + theme: str, + sub_theme: str, + topic: str, + metric: str, + geography: str, + geography_type: str, + ) -> list[dict]: client = APIClient() path = "/api/tables/v4/" @@ -564,8 +603,12 @@ def _hit_tables_endpoint(topic: str, metric: str) -> list[dict]: "file_format": "svg", "plots": [ { + "theme": theme, + "sub_theme": sub_theme, "topic": topic, "metric": metric, + "geography": geography, + "geography_type": geography_type, "date_from": "2020-01-01", "chart_type": "bar", } diff --git a/tests/unit/cms/dynamic_content/test_elements.py b/tests/unit/cms/dynamic_content/test_elements.py index 238a86849b..a254028ff9 100644 --- a/tests/unit/cms/dynamic_content/test_elements.py +++ b/tests/unit/cms/dynamic_content/test_elements.py @@ -122,6 +122,8 @@ def valid_payload(self) -> dict[str, str]: A valid basic block payload for all tests to use. """ return { + "theme": "infectious_disease", + "sub_theme": "respiratory", "topic": "Adenovirus", "metric": "adenovirus_testing_positivityByWeek", "geography": "England", diff --git a/tests/unit/common/auth/test_permissions.py b/tests/unit/common/auth/test_permissions.py index 4caa970939..3bb50e9c49 100644 --- a/tests/unit/common/auth/test_permissions.py +++ b/tests/unit/common/auth/test_permissions.py @@ -59,39 +59,32 @@ def _check_permissions_by_name(self, permission_sets: PermissionSetsType) -> boo def _patch_lookups( self, topic_result=None, - metric_result=None, - geography_type_result=None, geography_result=None, ): - """Return patches for all four DB manager methods.""" + """Return patches for the two combined DB manager lookups.""" - topic_result = topic_result or (self.THEME_ID, self.SUB_THEME_ID, self.TOPIC_ID) - metric_result = metric_result or self.METRIC_ID - geography_type_result = geography_type_result or self.GEOGRAPHY_TYPE_ID - geography_result = geography_result or self.GEOGRAPHY_ID + topic_result = topic_result or ( + self.THEME_ID, + self.SUB_THEME_ID, + self.TOPIC_ID, + self.METRIC_ID, + ) + geography_result = geography_result or ( + self.GEOGRAPHY_TYPE_ID, + self.GEOGRAPHY_ID, + ) stack = ExitStack() stack.enter_context( patch( - "metrics.data.managers.core_models.topic.TopicQuerySet.get_id_by_name", + "metrics.data.managers.core_models.topic.TopicQuerySet.get_theme_sub_theme_topic_and_metric_id_by_name", return_value=topic_result, ) ) stack.enter_context( patch( - "metrics.data.managers.core_models.metric.MetricQuerySet.get_id_by_name", - return_value=metric_result, - ) - ) - stack.enter_context( - patch( - "metrics.data.managers.core_models.geography_type.GeographyTypeQuerySet.get_id_by_name", - return_value=geography_type_result, - ) - ) - stack.enter_context( - patch( - "metrics.data.managers.core_models.geography.GeographyQuerySet.get_code_by_name", + "metrics.data.managers.core_models.geography.GeographyQuerySet" + ".get_geography_type_id_and_code_by_name", return_value=geography_result, ) ) @@ -99,93 +92,27 @@ def _patch_lookups( return stack def test_returns_false_when_topic_lookup_fails(self): - with ( - patch( - "metrics.data.managers.core_models.topic.TopicQuerySet.get_id_by_name", - return_value=(None, None, None), - ), - patch( - "metrics.data.managers.core_models.metric.MetricQuerySet.get_id_by_name", - return_value=self.METRIC_ID, - ), - patch( - "metrics.data.managers.core_models.geography_type.GeographyTypeQuerySet.get_id_by_name", - return_value=self.GEOGRAPHY_TYPE_ID, - ), - patch( - "metrics.data.managers.core_models.geography.GeographyQuerySet.get_code_by_name", - return_value=self.GEOGRAPHY_ID, - ), - ): + with self._patch_lookups(topic_result=(None, None, None, None)): assert not self._check_permissions_by_name( self._build_permission_sets([self._permissions_by_id()]) ) def test_returns_false_when_metric_lookup_fails(self): - with ( - patch( - "metrics.data.managers.core_models.topic.TopicQuerySet.get_id_by_name", - return_value=(self.THEME_ID, self.SUB_THEME_ID, self.TOPIC_ID), - ), - patch( - "metrics.data.managers.core_models.metric.MetricQuerySet.get_id_by_name", - return_value=None, - ), - patch( - "metrics.data.managers.core_models.geography_type.GeographyTypeQuerySet.get_id_by_name", - return_value=self.GEOGRAPHY_TYPE_ID, - ), - patch( - "metrics.data.managers.core_models.geography.GeographyQuerySet.get_code_by_name", - return_value=self.GEOGRAPHY_ID, - ), + with self._patch_lookups( + topic_result=(self.THEME_ID, self.SUB_THEME_ID, self.TOPIC_ID, None) ): assert not self._check_permissions_by_name( self._build_permission_sets([self._permissions_by_id()]) ) def test_returns_false_when_geography_type_lookup_fails(self): - with ( - patch( - "metrics.data.managers.core_models.topic.TopicQuerySet.get_id_by_name", - return_value=(self.THEME_ID, self.SUB_THEME_ID, self.TOPIC_ID), - ), - patch( - "metrics.data.managers.core_models.metric.MetricQuerySet.get_id_by_name", - return_value=self.METRIC_ID, - ), - patch( - "metrics.data.managers.core_models.geography_type.GeographyTypeQuerySet.get_id_by_name", - return_value=None, - ), - patch( - "metrics.data.managers.core_models.geography.GeographyQuerySet.get_code_by_name", - return_value=self.GEOGRAPHY_ID, - ), - ): + with self._patch_lookups(geography_result=(None, self.GEOGRAPHY_ID)): assert not self._check_permissions_by_name( self._build_permission_sets([self._permissions_by_id()]) ) def test_returns_false_when_geography_lookup_fails(self): - with ( - patch( - "metrics.data.managers.core_models.topic.TopicQuerySet.get_id_by_name", - return_value=(self.THEME_ID, self.SUB_THEME_ID, self.TOPIC_ID), - ), - patch( - "metrics.data.managers.core_models.metric.MetricQuerySet.get_id_by_name", - return_value=self.METRIC_ID, - ), - patch( - "metrics.data.managers.core_models.geography_type.GeographyTypeQuerySet.get_id_by_name", - return_value=self.GEOGRAPHY_TYPE_ID, - ), - patch( - "metrics.data.managers.core_models.geography.GeographyQuerySet.get_code_by_name", - return_value=None, - ), - ): + with self._patch_lookups(geography_result=(self.GEOGRAPHY_TYPE_ID, None)): assert not self._check_permissions_by_name( self._build_permission_sets([self._permissions_by_id()]) ) diff --git a/tests/unit/metrics/api/serializers/charts/test_single_category_charts.py b/tests/unit/metrics/api/serializers/charts/test_single_category_charts.py index 02cadf0027..6b52b7f4dd 100644 --- a/tests/unit/metrics/api/serializers/charts/test_single_category_charts.py +++ b/tests/unit/metrics/api/serializers/charts/test_single_category_charts.py @@ -23,8 +23,6 @@ class TestChartPlotSerializer: optional_field_names = [ "stratum", - "geography", - "geography_type", "sex", "label", "line_colour", diff --git a/tests/unit/metrics/api/serializers/downloads/test_dual_category.py b/tests/unit/metrics/api/serializers/downloads/test_dual_category.py index 37299449f1..08c87fe890 100644 --- a/tests/unit/metrics/api/serializers/downloads/test_dual_category.py +++ b/tests/unit/metrics/api/serializers/downloads/test_dual_category.py @@ -37,6 +37,8 @@ def test_to_models_expands_headline_plots(self): payload = EXAMPLE_DUAL_CATEGORY_DOWNLOAD_REQUEST_PAYLOAD.copy() payload["static_fields"] = payload["static_fields"].copy() + payload["static_fields"]["theme"] = fake_topic.sub_theme.theme.name + payload["static_fields"]["sub_theme"] = fake_topic.sub_theme.name payload["static_fields"]["topic"] = fake_topic.name payload["static_fields"]["metric"] = HEADLINE_METRIC @@ -80,6 +82,8 @@ def test_to_models_expands_timeseries_plots( "y_axis": "metric", "chart_type": "stacked_bar", "static_fields": { + "theme": plot_payload["theme"], + "sub_theme": plot_payload["sub_theme"], "topic": plot_payload["topic"], "metric": plot_payload["metric"], "stratum": plot_payload.get("stratum", "default"), diff --git a/tests/unit/metrics/api/serializers/test_downloads.py b/tests/unit/metrics/api/serializers/test_downloads.py index 5d16f3625d..ffa5572e37 100644 --- a/tests/unit/metrics/api/serializers/test_downloads.py +++ b/tests/unit/metrics/api/serializers/test_downloads.py @@ -9,6 +9,8 @@ class TestDownloadsSerializer: def _build_valid_payload() -> dict[str, str | list[dict[str, str]]]: requested_file_format = "csv" requested_plot = { + "theme": "infectious_disease", + "sub_theme": "respiratory", "topic": "COVID-19", "metric": "COVID-19_cases_rateRollingMean", "age": "all", diff --git a/tests/unit/metrics/api/serializers/test_headlines.py b/tests/unit/metrics/api/serializers/test_headlines.py index a358dfb6d4..26de9e07d2 100644 --- a/tests/unit/metrics/api/serializers/test_headlines.py +++ b/tests/unit/metrics/api/serializers/test_headlines.py @@ -52,8 +52,12 @@ def _setup_valid_data_payload_and_model_managers( fake_topic = fake_metric.metric_group.topic data: cls.DATA_PAYLOAD_HINT = { + "theme": fake_topic.sub_theme.theme.name, + "sub_theme": fake_topic.sub_theme.name, "topic": fake_topic.name, "metric": fake_metric.name, + "geography": "England", + "geography_type": "Nation", } return data, FakeMetricManager([fake_metric]), FakeTopicManager([fake_topic]) @@ -71,11 +75,20 @@ def test_can_validate_successfully(self): topic_manager, ) = self._setup_valid_data_payload_and_model_managers() + fake_geography = mock.Mock() + fake_geography.name = "England" + fake_geography_type = mock.Mock() + fake_geography_type.name = "Nation" + serializer = HeadlinesQuerySerializer( data=data_payload, context={ "topic_manager": topic_manager, "metric_manager": metric_manager, + "geography_manager": FakeGeographyManager([fake_geography]), + "geography_type_manager": FakeGeographyTypeManager( + [fake_geography_type] + ), }, ) diff --git a/tests/unit/metrics/api/serializers/test_plots.py b/tests/unit/metrics/api/serializers/test_plots.py index 7616a6b6e5..c1332fda2c 100644 --- a/tests/unit/metrics/api/serializers/test_plots.py +++ b/tests/unit/metrics/api/serializers/test_plots.py @@ -7,8 +7,6 @@ class TestPlotSerializer: optional_field_names = [ "stratum", - "geography", - "geography_type", "sex", "age", ] diff --git a/tests/unit/metrics/api/serializers/test_tables.py b/tests/unit/metrics/api/serializers/test_tables.py index e411dec02f..43fd32ef60 100644 --- a/tests/unit/metrics/api/serializers/test_tables.py +++ b/tests/unit/metrics/api/serializers/test_tables.py @@ -15,8 +15,6 @@ class TestTablePlotSerializer: optional_field_names = [ "stratum", - "geography", - "geography_type", "sex", "label", "x_axis", diff --git a/tests/unit/metrics/api/serializers/test_trends.py b/tests/unit/metrics/api/serializers/test_trends.py index 0ce17d37f3..efbb06b000 100644 --- a/tests/unit/metrics/api/serializers/test_trends.py +++ b/tests/unit/metrics/api/serializers/test_trends.py @@ -54,14 +54,16 @@ def _setup_valid_data_payload_and_model_managers( fake_stratum = FakeStratumFactory.build_example(stratum_name="default") data: cls.DATA_PAYLOAD_HINT = { + "theme": fake_topic.sub_theme.theme.name, + "sub_theme": fake_topic.sub_theme.name, "topic": fake_topic.name, "metric": fake_metric.name, + "geography": fake_geography.name, + "geography_type": fake_geography.geography_type.name, "percentage_metric": fake_percentage_metric.name, "stratum": fake_stratum.name, "age": fake_age.name, "sex": "all", - "geography": fake_geography.name, - "geography_type": fake_geography.geography_type.name, } return ( diff --git a/tests/unit/metrics/data/managers/core_models/test_geography.py b/tests/unit/metrics/data/managers/core_models/test_geography.py index a5eb953838..baa7bd5b82 100644 --- a/tests/unit/metrics/data/managers/core_models/test_geography.py +++ b/tests/unit/metrics/data/managers/core_models/test_geography.py @@ -1,9 +1,13 @@ from unittest import mock +import pytest + from metrics.data.managers.core_models.geography import ( GeographyManager, GeographyQuerySet, ) +from metrics.data.models.core_models import Geography +from tests.factories.metrics.geography import GeographyFactory class TestGeographyManager: @@ -76,3 +80,185 @@ def test_get_name_by_code(self, spy_get_name_by_code: mock.MagicMock): # Then spy_get_name_by_code.assert_called_with(fake_geography_code) + + +@pytest.mark.django_db +class TestGeographyManagerDatabaseQueries: + @pytest.fixture + def geography_records(self): + nation = GeographyFactory.create_with_geography_type( + name="England", + geography_code="E92000001", + geography_type="Nation", + ) + region = GeographyFactory.create_with_geography_type( + name="London", + geography_code="E12000007", + geography_type="Region", + ) + utla = GeographyFactory.create_with_geography_type( + name="London", + geography_code="E09000001", + geography_type="Upper tier local authority", + ) + + return { + "nation": nation, + "region": region, + "utla": utla, + } + + def test_get_all_names_returns_distinct_ordered_names(self, geography_records): + assert list(Geography.objects.get_all_names()) == ["England", "London"] + + def test_get_name_by_code_returns_matching_name(self, geography_records): + assert Geography.objects.get_name_by_code("E92000001") == "England" + + def test_get_name_by_code_returns_none_when_missing(self, geography_records): + assert Geography.objects.get_name_by_code("missing") is None + + def test_get_id_by_name_uses_geography_type_to_disambiguate_shared_names( + self, geography_records + ): + assert ( + Geography.objects.get_id_by_name( + geography_name="London", + geography_type_name="Region", + ) + == geography_records["region"].id + ) + + def test_get_id_by_name_returns_none_when_pair_does_not_exist( + self, geography_records + ): + assert ( + Geography.objects.get_id_by_name( + geography_name="England", + geography_type_name="Region", + ) + is None + ) + + def test_get_code_by_name_uses_geography_type_to_disambiguate_shared_names( + self, geography_records + ): + assert ( + Geography.objects.get_code_by_name( + geography_name="London", + geography_type_name="Upper tier local authority", + ) + == "E09000001" + ) + + def test_get_code_by_name_returns_none_when_pair_does_not_exist( + self, geography_records + ): + assert ( + Geography.objects.get_code_by_name( + geography_name="England", + geography_type_name="Region", + ) + is None + ) + + def test_get_geography_type_id_and_code_by_name_returns_matching_pair( + self, geography_records + ): + geography_type_id, geography_code = ( + Geography.objects.get_geography_type_id_and_code_by_name( + geography_name="London", + geography_type_name="Region", + ) + ) + + assert geography_type_id == geography_records["region"].geography_type_id + assert geography_code == "E12000007" + + def test_get_geography_type_id_and_code_by_name_returns_none_pair_when_missing( + self, geography_records + ): + assert Geography.objects.get_geography_type_id_and_code_by_name( + geography_name="England", + geography_type_name="Region", + ) == (None, None) + + def test_get_all_geography_codes_by_geography_type_returns_ordered_codes( + self, geography_records + ): + assert list( + Geography.objects.get_all_geography_codes_by_geography_type( + geography_type_name="Region", + ) + ) == ["E12000007"] + + def test_get_all_geography_names_by_geography_type_returns_ordered_names( + self, geography_records + ): + assert list( + Geography.objects.get_all_geography_names_by_geography_type( + geography_type_name="Region", + ) + ) == ["London"] + + def test_get_geography_codes_and_names_by_geography_type_returns_ordered_tuples( + self, geography_records + ): + assert list( + Geography.objects.get_geography_codes_and_names_by_geography_type( + geography_type_name="Region", + ) + ) == [("E12000007", "London")] + + def test_get_geography_codes_and_names_by_geography_type_id_returns_ordered_dicts( + self, geography_records + ): + assert list( + Geography.objects.get_geography_codes_and_names_by_geography_type_id( + geography_type_id=geography_records["region"].geography_type_id, + ) + ) == [{"geography_code": "E12000007", "name": "London"}] + + def test_get_geographies_by_geography_type_returns_ordered_dicts( + self, geography_records + ): + assert list( + Geography.objects.get_geographies_by_geography_type( + geography_type_name="Nation", + ) + ) == [{"name": "England", "geography_code": "E92000001"}] + + def test_does_geography_code_exist_returns_true_for_matching_type( + self, geography_records + ): + assert Geography.objects.does_geography_code_exist( + geography_code="E12000007", + geography_type_name="Region", + ) + + def test_does_geography_code_exist_returns_false_for_different_type( + self, geography_records + ): + assert not Geography.objects.does_geography_code_exist( + geography_code="E12000007", + geography_type_name="Nation", + ) + + def test_get_geography_code_for_geography_returns_matching_code( + self, geography_records + ): + assert ( + Geography.objects.get_geography_code_for_geography( + geography="England", + geography_type="Nation", + ) + == "E92000001" + ) + + def test_get_all_names_and_codes_returns_records_ordered_by_code( + self, geography_records + ): + assert list(Geography.objects.get_all_names_and_codes()) == [ + {"name": "London", "geography_code": "E09000001"}, + {"name": "London", "geography_code": "E12000007"}, + {"name": "England", "geography_code": "E92000001"}, + ] diff --git a/tests/unit/metrics/domain/models/test_common.py b/tests/unit/metrics/domain/models/test_common.py index 8a7f810e3c..93f5eb939c 100644 --- a/tests/unit/metrics/domain/models/test_common.py +++ b/tests/unit/metrics/domain/models/test_common.py @@ -33,6 +33,8 @@ def _build_request(*, permission_sets=None) -> Request: def _build_models(request: Request) -> tuple: return ( HeadlineParameters( + theme="infectious_disease", + sub_theme="respiratory", topic="COVID-19", metric="COVID-19_metric", geography="England", @@ -77,6 +79,8 @@ def _build_models(request: Request) -> tuple: chart_type="bar", topic="COVID-19", metric="COVID-19_metric", + geography="England", + geography_type="Nation", ) ], request=request, diff --git a/tests/unit/metrics/domain/models/test_headline.py b/tests/unit/metrics/domain/models/test_headline.py index e2f2746568..4b0cf84b22 100644 --- a/tests/unit/metrics/domain/models/test_headline.py +++ b/tests/unit/metrics/domain/models/test_headline.py @@ -6,6 +6,8 @@ class TestHeadlineParameters: valid_payload = { + "theme": "infectious_disease", + "sub_theme": "respiratory", "topic": "COVID-19", "metric": "COVID-19_headline_ONSdeaths_7DayTotals", "geography": "England", @@ -19,13 +21,15 @@ class TestHeadlineParameters: @pytest.mark.parametrize( "field", [ - "stratum", + "theme", + "sub_theme", + "topic", + "metric", "geography", "geography_type", + "stratum", "sex", "age", - "topic", - "metric", ], ) def test_mandatory_fields_are_enforced(self, field: str): @@ -58,6 +62,8 @@ def test_to_dict_for_query(self): # Then expected_params = { + "theme": self.valid_payload["theme"], + "sub_theme": self.valid_payload["sub_theme"], "topic": self.valid_payload["topic"], "metric": self.valid_payload["metric"], "geography": self.valid_payload["geography"], diff --git a/tests/unit/metrics/domain/models/test_plots.py b/tests/unit/metrics/domain/models/test_plots.py index fb2073db46..ccfb9b4b65 100644 --- a/tests/unit/metrics/domain/models/test_plots.py +++ b/tests/unit/metrics/domain/models/test_plots.py @@ -21,11 +21,11 @@ class TestPlotParameters: "chart_type": "bar", "topic": "COVID-19", "metric": "COVID-19_deaths_ONSByDay", + "geography": "England", + "geography_type": "Nation", } optional_field_names = [ "stratum", - "geography", - "geography_type", "sex", "age", "date_from", @@ -125,6 +125,8 @@ def test_to_dict_for_query_for_timeseries_data( ].value, "topic": fake_chart_plot_parameters.topic, "metric": fake_chart_plot_parameters.metric, + "theme": fake_chart_plot_parameters.theme, + "sub_theme": fake_chart_plot_parameters.sub_theme, "stratum": fake_chart_plot_parameters.stratum, "geography": fake_chart_plot_parameters.geography, "geography_type": fake_chart_plot_parameters.geography_type, @@ -180,6 +182,8 @@ def test_to_dict_for_query_for_headline_data( ], "topic": fake_chart_plot_parameters.topic, "metric": fake_chart_plot_parameters.metric, + "theme": fake_chart_plot_parameters.theme, + "sub_theme": fake_chart_plot_parameters.sub_theme, "stratum": fake_chart_plot_parameters.stratum, "geography": fake_chart_plot_parameters.geography, "geography_type": fake_chart_plot_parameters.geography_type, @@ -258,6 +262,8 @@ def test_override_y_axis_choice_to_none_defaults_as_false(self): topic=topic_name, metric=metric_name, chart_type="bar", + geography="England", + geography_type="Nation", ) # Then @@ -279,6 +285,8 @@ def test_y_axis_returns_none_when_override_y_axis_choice_to_none_is_true(self): topic=topic_name, metric=metric_name, chart_type="bar", + geography="England", + geography_type="Nation", override_y_axis_choice_to_none=True, ) diff --git a/tests/unit/metrics/domain/models/test_trends.py b/tests/unit/metrics/domain/models/test_trends.py index 81c0366564..73d5a4d94e 100644 --- a/tests/unit/metrics/domain/models/test_trends.py +++ b/tests/unit/metrics/domain/models/test_trends.py @@ -6,11 +6,13 @@ class TestTrendParameters: valid_payload = { + "theme": "infectious_disease", + "sub_theme": "respiratory", "topic": "COVID-19", "metric": "COVID-19_headline_ONSdeaths_7DayChange", - "percentage_metric": "COVID-19_headline_ONSdeaths_7DayPercentChange", "geography": "England", "geography_type": "Nation", + "percentage_metric": "COVID-19_headline_ONSdeaths_7DayPercentChange", "stratum": "default", "sex": "all", "age": "all", @@ -19,13 +21,15 @@ class TestTrendParameters: @pytest.mark.parametrize( "field", [ - "stratum", + "theme", + "sub_theme", + "topic", + "metric", "geography", "geography_type", + "stratum", "sex", "age", - "topic", - "metric", "percentage_metric", ], ) @@ -59,6 +63,8 @@ def test_to_dict_for_main_metric_query(self): # Then expected = { + "theme": input_data["theme"], + "sub_theme": input_data["sub_theme"], "topic": input_data["topic"], "metric": input_data["metric"], "geography": input_data["geography"], @@ -85,6 +91,8 @@ def test_to_dict_for_percentage_metric_query(self): # Then expected = { + "theme": input_data["theme"], + "sub_theme": input_data["sub_theme"], "topic": input_data["topic"], "metric": input_data["percentage_metric"], "geography": input_data["geography"], diff --git a/tests/unit/metrics/domain/tables/test_dual_category.py b/tests/unit/metrics/domain/tables/test_dual_category.py index db9b19378e..5837957723 100644 --- a/tests/unit/metrics/domain/tables/test_dual_category.py +++ b/tests/unit/metrics/domain/tables/test_dual_category.py @@ -31,6 +31,8 @@ def _plot( x_axis=x_axis, y_axis=ChartAxisFields.metric.name, sex="f" if label == "Females" else "m", + geography="England", + geography_type="Nation", ), x_axis_values=x_axis_values, y_axis_values=y_axis_values, diff --git a/tests/unit/metrics/domain/tables/test_tables_generation.py b/tests/unit/metrics/domain/tables/test_tables_generation.py index c79e7701d8..5bf8218a9e 100644 --- a/tests/unit/metrics/domain/tables/test_tables_generation.py +++ b/tests/unit/metrics/domain/tables/test_tables_generation.py @@ -35,6 +35,8 @@ def _create_plot_data( chart_type="", topic="COVID-19", metric="COVID-19_deaths_ONSByDay", + geography="England", + geography_type="Nation", stratum="default", label=label, x_axis=x_axis, diff --git a/tests/unit/metrics/interfaces/headlines/test_access.py b/tests/unit/metrics/interfaces/headlines/test_access.py index dbb1d48cbb..686a95cf30 100644 --- a/tests/unit/metrics/interfaces/headlines/test_access.py +++ b/tests/unit/metrics/interfaces/headlines/test_access.py @@ -15,6 +15,8 @@ @pytest.fixture def example_headline_args() -> dict[str, str]: return { + "theme": "infectious_disease", + "sub_theme": "respiratory", "topic": "COVID-19", "metric": "COVID-19_headline_ONSdeaths_7DayChange", "geography": "England", @@ -82,6 +84,8 @@ def test_get_metric_value_calls_core_headline_manager_with_correct_args( ) spy_core_headline_manager.get_latest_headline.assert_called_once_with( + theme=headline_parameters.theme_name, + sub_theme=headline_parameters.sub_theme_name, topic=headline_parameters.topic, metric=headline_parameters.metric, geography=headline_parameters.geography, diff --git a/tests/unit/metrics/interfaces/plots/test_access.py b/tests/unit/metrics/interfaces/plots/test_access.py index b794cb3810..1fcb1586e3 100644 --- a/tests/unit/metrics/interfaces/plots/test_access.py +++ b/tests/unit/metrics/interfaces/plots/test_access.py @@ -27,7 +27,6 @@ ) from tests.fakes.factories.metrics.metric_factory import FakeMetricFactory from tests.fakes.managers.time_series_manager import FakeCoreTimeSeriesManager -from tests.fakes.managers.topic_manager import FakeTopicManager from tests.fakes.models.metrics.core_time_series import FakeCoreTimeSeries from tests.fakes.models.queryset import FakeQuerySet @@ -106,9 +105,11 @@ def test_build_plots_data_passes_for_plot_parameters_with_no_supporting_data( """ # Given plot_parameters_with_no_supporting_data = PlotParameters( - metric="non_cases_topic_cases_abc", - topic="non_cases_topic", chart_type="line", + topic="non_cases_topic", + metric="non_cases_topic_cases_abc", + geography="England", + geography_type="Nation", date_from="2023-01-01", date_to="2023-12-31", ) @@ -241,9 +242,11 @@ def test_build_plots_data_for_full_queryset_passes_for_plot_parameters_with_no_s """ # Given plot_parameters_with_no_supporting_data = PlotParameters( - metric="non_cases_topic_cases_abc", - topic="non_cases_topic", chart_type="line", + topic="non_cases_topic", + metric="non_cases_topic_cases_abc", + geography="England", + geography_type="Nation", date_from="2023-01-01", date_to="2023-12-31", ) @@ -617,19 +620,20 @@ def test_get_headline_data_calls_core_headline_manager_with_confidence_intervals permission_sets=mocked_chart_request_params.permission_sets, ) - @mock.patch(f"{MODULE_PATH}.auth.AUTH_ENABLED", True) - def test_get_queryset_from_core_model_manager_passes_theme_and_topic_into_query_when_auth_enabled( + def test_get_queryset_from_core_model_manager_passes_theme_and_sub_theme_through_from_plot_params( self, ): """ - Given a `CoreHeadlineManager` - When `get_headline_data` is called from an instance of `PlotsInterface` - Then the correct method is called from `CoreHeadlineManager` to retrieve headline data. + Given `plot_params` which already contain `theme` and `sub_theme` + (posted by the front-end) + When `get_queryset_from_core_model_manager` is called + from an instance of `PlotsInterface` + Then those `theme` and `sub_theme` values are passed straight through to + the core model manager (i.e. they are no longer derived from the topic) """ # Given spy_core_headline_manager = mock.Mock() fake_metric = FakeMetricFactory.build_example_metric() - fake_topic_manager = FakeTopicManager(topics=[fake_metric.topic]) mocked_x_axis = mock.Mock() mocked_y_axis = mock.Mock() @@ -644,12 +648,13 @@ def test_get_queryset_from_core_model_manager_passes_theme_and_topic_into_query_ plots_interface = PlotsInterface( chart_request_params=mocked_chart_request_params, core_model_manager=spy_core_headline_manager, - topic_model_manager=fake_topic_manager, ) # When plots_params = { "fields_to_export": [mocked_x_axis, mocked_y_axis], + "theme": fake_metric.topic.sub_theme.theme.name, + "sub_theme": fake_metric.topic.sub_theme.name, "topic": fake_metric.topic.name, "metric": fake_metric.name, "geography": mocked_geography, @@ -667,6 +672,8 @@ def test_get_queryset_from_core_model_manager_passes_theme_and_topic_into_query_ assert headline_data == spy_core_headline_manager.query_for_data.return_value spy_core_headline_manager.query_for_data.assert_called_once_with( fields_to_export=[mocked_x_axis, mocked_y_axis], + theme=fake_metric.topic.sub_theme.theme.name, + sub_theme=fake_metric.topic.sub_theme.name, topic=fake_metric.topic.name, metric=fake_metric.name, geography=mocked_geography, @@ -676,8 +683,6 @@ def test_get_queryset_from_core_model_manager_passes_theme_and_topic_into_query_ sex=mocked_sex, age=mocked_age, rbac_permissions=mocked_chart_request_params.rbac_permissions, - theme=fake_metric.topic.sub_theme.theme.name, - sub_theme=fake_metric.topic.sub_theme.name, permission_sets=mocked_chart_request_params.permission_sets, ) diff --git a/tests/unit/metrics/interfaces/plots/test_validation.py b/tests/unit/metrics/interfaces/plots/test_validation.py index 7f04ce48a9..0ee933ce79 100644 --- a/tests/unit/metrics/interfaces/plots/test_validation.py +++ b/tests/unit/metrics/interfaces/plots/test_validation.py @@ -157,9 +157,11 @@ def test_raises_error_if_not_in_chronological_order(self): date_from = datetime.datetime(year=2022, month=10, day=2) date_to = datetime.datetime(year=2021, month=7, day=1) plot_parameters = PlotParameters( - metric="COVID-19_deaths_ONSByDay", - topic="COVID-19", chart_type=ChartTypes.line_multi_coloured.value, + topic="COVID-19", + metric="COVID-19_deaths_ONSByDay", + geography="England", + geography_type="Nation", date_from=date_from.strftime(EXPECTED_DATE_FORMAT), date_to=date_to.strftime(EXPECTED_DATE_FORMAT), ) diff --git a/tests/unit/metrics/interfaces/trends/test_access.py b/tests/unit/metrics/interfaces/trends/test_access.py index 93ba2e69f9..3a41ad0c72 100644 --- a/tests/unit/metrics/interfaces/trends/test_access.py +++ b/tests/unit/metrics/interfaces/trends/test_access.py @@ -15,6 +15,8 @@ class TestTrendsInterface: @property def example_trend_parameters(self) -> TrendsParameters: return TrendsParameters( + theme="infectious_disease", + sub_theme="respiratory", topic="COVID-19", metric="COVID-19_headline_ONSdeaths_7DayChange", percentage_metric="COVID-19_headline_ONSdeaths_7DayPercentChange",