Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
8782ea3
CDD-3295: Add dual category chart.
tushortz Jun 5, 2026
d9637c3
CDD-3342: Refactor downloads serializer
tushortz Jun 9, 2026
2ab07d6
CDD-3342: Create DualCategoryDownloadsInterface
tushortz Jun 11, 2026
45def2a
Add dual category support to tables
aidan Jun 10, 2026
993c598
CDD-3342 - Fixed the tabular endpoint warnings and errors
tushortz Jun 22, 2026
a51e149
pip dev: (deps-dev): bump cyclonedx-python-lib from 11.7.0 to 11.8.0
dependabot[bot] Jun 11, 2026
73e1c78
Update readme python version
Jun 10, 2026
0ae39b2
pip: (deps): bump cryptography from 48.0.0 to 48.0.1
dependabot[bot] Jun 16, 2026
74e9990
pip: (deps): bump virtualenv from 21.4.2 to 21.5.0
dependabot[bot] Jun 19, 2026
9db0854
Merge branch 'main' into feature/CDD-3342-dual-category-chart-downloa…
tushortz Jun 22, 2026
208e43c
Merge branch 'main' into feature/CDD-3342-dual-category-chart-downloa…
tushortz Jun 22, 2026
0c622e7
CDD-3342 - Fixed uhd quality architecture pipeline error
tushortz Jun 22, 2026
d0c59c9
CDD-3342 - Fixed sonar qube warnings
tushortz Jun 22, 2026
1e24491
CDD-3342: Add missing chart path to private api url patterns
tushortz Jun 25, 2026
5a1c32e
Cdd 3090 simplified BE chart watermark (#3199)
kathryn-dale Jun 26, 2026
2cd54f1
chore: commit migration from metric value update (#3262)
phill-stanley Jun 26, 2026
7823a3f
CDD-3295: Add dual category chart.
tushortz Jun 5, 2026
d7ab91d
CDD-3342 - Fixed the tabular endpoint warnings and errors
tushortz Jun 22, 2026
c575032
CDD-3342 - Fixed merge conflict
tushortz Jun 26, 2026
05f0362
CDD-3342 - Fixed merge conflict
tushortz Jun 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cms/dashboard/static/js/dual_category_chart_form.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ class DualCategoryChartCardBlockDefinition extends window.wagtailStreamField.blo
static FIELD_SUFFIXES = {
X_AXIS: 'x_axis',
GEOGRAPHY: 'static_fields-geography_type',
SECONDARY_CATEGORY: 'second_category',
SECONDARY_CATEGORY: 'secondary_category',
PRIMARY_VALUES: 'primary_field_values',
SECONDARY_VALUES: 'secondary_field_values',
DATA_SCRIPT: 'subcategory-data',
Expand Down
1 change: 0 additions & 1 deletion cms/dashboard/viewsets.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,6 @@ def get_queryset(self):
req = self.request

if req.auth is None:

# Get all page ids for pages with is_public
public_topic_page_ids = TopicPage.objects.filter(
is_public=True, page_ptr__in=queryset
Expand Down
4 changes: 2 additions & 2 deletions cms/dynamic_content/cards.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
MAXIMUM_TOPIC_TREND_CARD_CHARTS: int = 1
MAXIMUM_TREND_NUMBER: int = 1

MINIMUM_SEGMENTS_COUNT: int = 1
MINIMUM_SEGMENTS_COUNT: int = 2

POPULAR_TOPICS_SEGMENT_COUNT: int = 1
POPULAR_TOPICS_RIGHT_COLUMN_BOTTOM_ROW_SEGMENT_COUNT: int = 2
Expand Down Expand Up @@ -521,7 +521,7 @@ class DualCategoryChartCard(blocks.StructBlock):

static_fields = DualCategoryChartStaticFieldComponent()

second_category = blocks.ChoiceBlock(
secondary_category = blocks.ChoiceBlock(
choices=get_dual_chart_secondary_category_choices,
help_text=help_texts.SECONDARY_CATEGORY,
)
Expand Down
1,138 changes: 1,138 additions & 0 deletions cms/home/migrations/0037_alter_landingpage_body_rename_second_category.py

Large diffs are not rendered by default.

1,452 changes: 1,452 additions & 0 deletions cms/topic/migrations/0034_alter_topicpage_body_rename_second_category.py

Large diffs are not rendered by default.

Large diffs are not rendered by default.

20 changes: 16 additions & 4 deletions metrics/api/serializers/__init__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,21 @@
from .charts import ChartsSerializer
from .headlines import HeadlinesQuerySerializer, CoreHeadlineSerializer
from .trends import TrendsQuerySerializer, TrendsResponseSerializer
from .downloads import (
DownloadsSerializer,
BulkDownloadsSerializer,
)
from .downloads.single_category import SingleCategoryDownloadsSerializer
from .downloads.dual_category import DualCategoryDownloadSerializer
from .timeseries import CoreTimeSeriesSerializer
from .geographies_alerts import GeographiesForAlertsSerializer
from .downloads.common import BulkDownloadsSerializer

__all__ = [
"ChartsSerializer",
"HeadlinesQuerySerializer",
"CoreHeadlineSerializer",
"TrendsQuerySerializer",
"TrendsResponseSerializer",
"SingleCategoryDownloadsSerializer",
"DualCategoryDownloadSerializer",
"CoreTimeSeriesSerializer",
"GeographiesForAlertsSerializer",
"BulkDownloadsSerializer",
]
1 change: 0 additions & 1 deletion metrics/api/serializers/charts/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
ChartPlotSerializer,
ChartPlotsListSerializer,
ChartsResponseSerializer,
EncodedChartResponseSerializer,
EncodedChartsRequestSerializer,
ChartsSerializer,
)
30 changes: 30 additions & 0 deletions metrics/api/serializers/charts/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@


class BaseChartsSerializer(serializers.Serializer):
"""Base serializer for chart request payloads, containing common fields across different chart types."""

file_format = serializers.ChoiceField(
choices=FILE_FORMAT_CHOICES,
help_text=help_texts.CHART_FILE_FORMAT_FIELD,
Expand Down Expand Up @@ -94,3 +96,31 @@ class BaseChartsSerializer(serializers.Serializer):
allow_null=True,
default="",
)
is_public = serializers.BooleanField(
required=False, default=False, help_text=help_texts.IS_PUBLIC_FIELD
)
data_classification = serializers.CharField(
required=False,
allow_blank=True,
allow_null=True,
default=None,
help_text=help_texts.DATA_CLASSIFICATION_FIELD,
)


class ChartPreviewQueryParamsSerializer(serializers.Serializer):
"""Serializer for query parameters when requesting a chart preview."""

preview = serializers.BooleanField(required=False)


class EncodedChartResponseSerializer(serializers.Serializer):
"""Serializer for the response of an encoded chart generation, containing the encoded chart and related metadata."""

last_updated = serializers.CharField(
help_text=help_texts.ENCODED_CHARTS_LAST_UPDATED,
allow_blank=True,
)
chart = serializers.CharField(help_text=help_texts.ENCODED_CHARTS_RESPONSE)
alt_text = serializers.CharField(help_text=help_texts.CHARTS_ALT_TEXT)
figure = serializers.DictField(help_text=help_texts.CHARTS_FIGURE_OUTPUT)
89 changes: 66 additions & 23 deletions metrics/api/serializers/charts/dual_category_charts.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@

from metrics.api.serializers import help_texts
from metrics.api.serializers.charts.common import BaseChartsSerializer
from metrics.api.serializers.dual_category.common import (
validate_dual_category_fields,
)
from metrics.api.serializers.plots import PlotSerializer
from metrics.domain.charts.colour_scheme import RGBAChartLineColours
from metrics.domain.common.utils import (
Expand All @@ -11,9 +14,13 @@
DEFAULT_X_AXIS,
DEFAULT_Y_AXIS,
ChartTypes,
DataSourceFileType,
DEFAULT_Y_AXIS_MINIMUM_VAlUE,
extract_metric_group_from_metric,
)
from metrics.domain.models.charts.dual_category_charts import (
DualCategoryChartRequestParams,
)
from metrics.domain.models.charts import DualCategoryChartRequestParams


class DualCategoryChartSegmentSerializer(serializers.Serializer):
Expand All @@ -33,59 +40,94 @@ class DualCategoryChartSegmentSerializer(serializers.Serializer):
)


class StaticFieldsSerializer(PlotSerializer):
theme = serializers.CharField(
required=True,
allow_blank=True,
allow_null=True,
)
sub_theme = serializers.CharField(
required=True,
allow_blank=True,
allow_null=True,
)


class DualCategoryChartSerializer(BaseChartsSerializer):
chart_type = serializers.ChoiceField(
help_text=help_texts.CHART_TYPE_FIELD,
choices=ChartTypes.selectable_choices(),
choices=ChartTypes.dual_category_chart_options(),
required=True,
)
primary_field_values = serializers.ListField(
child=serializers.CharField(),
help_text="List of primary field values for this segment",
required=True,
allow_empty=False,
required=False,
allow_empty=True,
)

secondary_category = serializers.CharField(
help_text="Secondary category field for the chart",
required=True,
)

static_fields = StaticFieldsSerializer()
static_fields = PlotSerializer()

segments = serializers.ListField(
child=DualCategoryChartSegmentSerializer(),
help_text="Segments for the dual category chart",
required=True,
)

@classmethod
def validate(cls, attrs: dict) -> dict:
return validate_dual_category_fields(attrs)

def to_models(self, request: Request) -> DualCategoryChartRequestParams:
x_axis = self.data.get("x_axis") or DEFAULT_X_AXIS
y_axis = self.data.get("y_axis") or DEFAULT_Y_AXIS

for plot in self.data["segments"]:
plot["x_axis"] = x_axis
plot["y_axis"] = y_axis
primary_field_values = self.data.get("primary_field_values") or []
secondary_category = self.data["secondary_category"]
static_fields: dict[str, str | int] = self.validated_data.pop("static_fields")

if static_fields["date_to"]:
static_fields["date_to"] = static_fields["date_to"].isoformat()

if static_fields["date_from"]:
static_fields["date_from"] = static_fields["date_from"].isoformat()

groups_plots = []
segments: list[dict] = self.data["segments"]

metric_group = extract_metric_group_from_metric(metric=static_fields["metric"])
is_timeseries_data = DataSourceFileType[metric_group].is_timeseries

if is_timeseries_data:
plots = [
{
"x_axis": x_axis,
"y_axis": y_axis,
"line_colour": segment["colour"],
**static_fields,
secondary_category: segment["secondary_field_value"],
"chart_type": self.data["chart_type"],
"label": segment["label"],
}
for segment in segments
]
groups_plots.extend(plots)

else:
for primary_field_value in primary_field_values:
plots = [
{
"x_axis": x_axis,
"y_axis": y_axis,
"line_colour": segment["colour"],
**static_fields,
x_axis: primary_field_value,
secondary_category: segment["secondary_field_value"],
"chart_type": self.data["chart_type"],
"label": segment["label"],
}
for segment in segments
]
groups_plots.extend(plots)

return DualCategoryChartRequestParams(
chart_type=self.data["chart_type"],
primary_field_values=self.data["primary_field_values"],
primary_field_values=primary_field_values,
secondary_category=self.data["secondary_category"],
static_fields=self.data["static_fields"],
segments=self.data["segments"],
plots=groups_plots,
file_format=self.data["file_format"],
chart_height=self.data["chart_height"] or DEFAULT_CHART_HEIGHT,
chart_width=self.data["chart_width"] or DEFAULT_CHART_WIDTH,
Expand All @@ -97,4 +139,5 @@ def to_models(self, request: Request) -> DualCategoryChartRequestParams:
or DEFAULT_Y_AXIS_MINIMUM_VAlUE,
y_axis_maximum_value=self.data["y_axis_maximum_value"],
request=request,
legend_title=self.data.get("legend_title", ""),
)
16 changes: 6 additions & 10 deletions metrics/api/serializers/charts/single_category_charts.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,10 @@ def to_models(self, request: Request) -> ChartRequestParams:
plot["x_axis"] = x_axis
plot["y_axis"] = y_axis

# If not provided, default to public data
is_public: bool = self.data.get("is_public", True)
data_classification: str | None = self.data.get("data_classification")

return ChartRequestParams(
plots=self.data["plots"],
file_format=self.data["file_format"],
Expand All @@ -98,6 +102,8 @@ def to_models(self, request: Request) -> ChartRequestParams:
legend_title=self.data.get("legend_title", ""),
confidence_intervals=self.data.get("confidence_intervals", False),
confidence_colour=self.data.get("confidence_colour", ""),
is_public=is_public,
data_classification=data_classification,
request=request,
)

Expand All @@ -112,13 +118,3 @@ class EncodedChartsRequestSerializer(ChartsSerializer):
help_text=help_texts.ENCODED_CHARTS_FILE_FORMAT_FIELD,
default="svg",
)


class EncodedChartResponseSerializer(serializers.Serializer):
last_updated = serializers.CharField(
help_text=help_texts.ENCODED_CHARTS_LAST_UPDATED,
allow_blank=True,
)
chart = serializers.CharField(help_text=help_texts.ENCODED_CHARTS_RESPONSE)
alt_text = serializers.CharField(help_text=help_texts.CHARTS_ALT_TEXT)
figure = serializers.DictField(help_text=help_texts.CHARTS_FIGURE_OUTPUT)
19 changes: 15 additions & 4 deletions metrics/api/serializers/charts/subplot_charts.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,16 @@ class SubplotChartRequestSerializer(serializers.Serializer):
allow_blank=True,
allow_null=True,
)
is_public = serializers.BooleanField(
required=False, default=True, help_text=help_texts.IS_PUBLIC_FIELD
)
data_classification = serializers.CharField(
required=False,
allow_blank=True,
allow_null=True,
default=None,
help_text=help_texts.DATA_CLASSIFICATION_FIELD,
)

chart_parameters = ChartParametersSerializer()
subplots = SubplotsSerializer()
Expand Down Expand Up @@ -197,6 +207,9 @@ def to_models(self, request: Request) -> SubplotChartRequestParameters:
)
plot["metric_value_ranges"] = metric_value_ranges

is_public: bool = self.validated_data.get("is_public", True)
data_classification: str | None = self.validated_data.get("data_classification")

return SubplotChartRequestParameters(
file_format=self.validated_data["file_format"],
chart_height=self.validated_data["chart_height"] or DEFAULT_CHART_HEIGHT,
Expand All @@ -211,9 +224,7 @@ def to_models(self, request: Request) -> SubplotChartRequestParameters:
"target_threshold_label", None
),
subplots=self.validated_data["subplots"],
is_public=is_public,
data_classification=data_classification,
request=request,
)


class ChartPreviewQueryParamsSerializer(serializers.Serializer):
preview = serializers.BooleanField(required=False)
Empty file.
38 changes: 38 additions & 0 deletions metrics/api/serializers/downloads/common.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from rest_framework import serializers

from metrics.api.serializers import help_texts
from metrics.domain.common.utils import (
ChartAxisFields,
)

FILE_FORMAT_CHOICES: list[str] = ["json", "csv"]


class BaseDownloadsSerializer(serializers.Serializer):
file_format = serializers.ChoiceField(
choices=FILE_FORMAT_CHOICES,
required=True,
help_text=help_texts.FILE_DOWNLOAD_FORMAT,
)
x_axis = serializers.ChoiceField(
choices=ChartAxisFields.choices(),
required=False,
allow_blank=True,
allow_null=True,
help_text=help_texts.CHART_X_AXIS,
)
y_axis = serializers.ChoiceField(
choices=ChartAxisFields.choices(),
required=False,
allow_blank=True,
allow_null=True,
help_text=help_texts.CHART_Y_AXIS,
)


class BulkDownloadsSerializer(serializers.Serializer):
file_format = serializers.ChoiceField(
choices=FILE_FORMAT_CHOICES,
required=True,
help_text=help_texts.FILE_DOWNLOAD_FORMAT,
)
Loading