Skip to content

Commit 2ab07d6

Browse files
committed
CDD-3342: Create DualCategoryDownloadsInterface
1 parent d9637c3 commit 2ab07d6

25 files changed

Lines changed: 5270 additions & 83 deletions

File tree

cms/dashboard/static/js/dual_category_chart_form.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ class DualCategoryChartCardBlockDefinition extends window.wagtailStreamField.blo
100100
static FIELD_SUFFIXES = {
101101
X_AXIS: 'x_axis',
102102
GEOGRAPHY: 'static_fields-geography_type',
103-
SECONDARY_CATEGORY: 'second_category',
103+
SECONDARY_CATEGORY: 'secondary_category',
104104
PRIMARY_VALUES: 'primary_field_values',
105105
SECONDARY_VALUES: 'secondary_field_values',
106106
DATA_SCRIPT: 'subcategory-data',

cms/dynamic_content/cards.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
MAXIMUM_TOPIC_TREND_CARD_CHARTS: int = 1
4646
MAXIMUM_TREND_NUMBER: int = 1
4747

48-
MINIMUM_SEGMENTS_COUNT: int = 1
48+
MINIMUM_SEGMENTS_COUNT: int = 2
4949

5050
POPULAR_TOPICS_SEGMENT_COUNT: int = 1
5151
POPULAR_TOPICS_RIGHT_COLUMN_BOTTOM_ROW_SEGMENT_COUNT: int = 2
@@ -521,7 +521,7 @@ class DualCategoryChartCard(blocks.StructBlock):
521521

522522
static_fields = DualCategoryChartStaticFieldComponent()
523523

524-
second_category = blocks.ChoiceBlock(
524+
secondary_category = blocks.ChoiceBlock(
525525
choices=get_dual_chart_secondary_category_choices,
526526
help_text=help_texts.SECONDARY_CATEGORY,
527527
)

cms/home/migrations/0037_alter_landingpage_body_rename_second_category.py

Lines changed: 1138 additions & 0 deletions
Large diffs are not rendered by default.

cms/topic/migrations/0034_alter_topicpage_body_rename_second_category.py

Lines changed: 1452 additions & 0 deletions
Large diffs are not rendered by default.

cms/topics_list/migrations/0008_alter_topicslistpage_body_rename_second_category.py

Lines changed: 1138 additions & 0 deletions
Large diffs are not rendered by default.

metrics/api/serializers/downloads/common.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,6 @@ class BaseDownloadsSerializer(serializers.Serializer):
2828
allow_null=True,
2929
help_text=help_texts.CHART_Y_AXIS,
3030
)
31-
confidence_intervals = serializers.BooleanField(
32-
required=False,
33-
default=False,
34-
allow_null=True,
35-
help_text=help_texts.CONFIDENCE_INTERVALS,
36-
)
3731

3832

3933
class BulkDownloadsSerializer(serializers.Serializer):
Lines changed: 159 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,162 @@
1-
from metrics.api.serializers.downloads.single_category import (
2-
DownloadListSerializer,
3-
SingleCategoryDownloadsSerializer,
1+
from rest_framework import serializers
2+
from rest_framework.request import Request
3+
4+
from metrics.api.serializers import help_texts
5+
from metrics.api.serializers.downloads.common import BaseDownloadsSerializer
6+
from metrics.api.serializers.plots import PlotSerializer
7+
from metrics.domain.charts.colour_scheme import RGBAChartLineColours
8+
from metrics.domain.common.utils import (
9+
DEFAULT_CHART_HEIGHT,
10+
DEFAULT_CHART_WIDTH,
11+
DEFAULT_X_AXIS,
12+
DEFAULT_Y_AXIS,
13+
ChartAxisFields,
14+
DataSourceFileType,
15+
extract_metric_group_from_metric,
16+
)
17+
from metrics.domain.models.downloads.dual_category import (
18+
DualCategoryDownloadRequestParams,
419
)
520

621

7-
class DualCategoryDownloadSerializer(SingleCategoryDownloadsSerializer):
8-
plots = DownloadListSerializer()
22+
class DualCategoryDownloadSegmentSerializer(serializers.Serializer):
23+
secondary_field_value = serializers.CharField(required=True)
24+
colour = serializers.ChoiceField(
25+
choices=RGBAChartLineColours.choices(),
26+
required=False,
27+
allow_blank=True,
28+
)
29+
label = serializers.CharField(required=False, allow_blank=True, default="")
30+
31+
32+
class DualCategoryDownloadSerializer(BaseDownloadsSerializer):
33+
chart_type = serializers.CharField(
34+
help_text=help_texts.CHART_TYPE_FIELD,
35+
required=False,
36+
default="stacked_bar",
37+
)
38+
primary_field_values = serializers.ListField(
39+
child=serializers.CharField(),
40+
required=False,
41+
allow_empty=True,
42+
)
43+
secondary_category = serializers.CharField(required=True)
44+
static_fields = PlotSerializer(required=True)
45+
segments = DualCategoryDownloadSegmentSerializer(many=True, required=True)
46+
47+
@classmethod
48+
def validate(cls, attrs: dict) -> dict:
49+
"""Validate primary field values based on the selected metric type.
50+
51+
Args:
52+
attrs: Serializer attributes to validate.
53+
54+
Returns:
55+
Validated attributes.
56+
"""
57+
x_axis = attrs.get("x_axis") or DEFAULT_X_AXIS
58+
primary_field_values = attrs.get("primary_field_values") or []
59+
metric = attrs["static_fields"]["metric"]
60+
metric_group = extract_metric_group_from_metric(metric=metric)
61+
is_timeseries_data = DataSourceFileType[metric_group].is_timeseries
62+
63+
if is_timeseries_data:
64+
if primary_field_values:
65+
raise serializers.ValidationError(
66+
{
67+
"primary_field_values": (
68+
"This field should not be provided for timeseries data."
69+
)
70+
}
71+
)
72+
if x_axis != ChartAxisFields.date.name:
73+
raise serializers.ValidationError(
74+
{
75+
"x_axis": (
76+
"This field should be set to 'date' for timeseries data."
77+
)
78+
}
79+
)
80+
81+
elif not is_timeseries_data and not primary_field_values:
82+
raise serializers.ValidationError(
83+
{"primary_field_values": "This field is required for headline data."}
84+
)
85+
86+
return attrs
87+
88+
def to_models(self, request: Request) -> DualCategoryDownloadRequestParams:
89+
"""Convert dual-category payload into plot queries for download.
90+
91+
Args:
92+
request: Incoming API request.
93+
94+
Returns:
95+
Dual-category download request parameters.
96+
"""
97+
98+
x_axis = self.data.get("x_axis") or DEFAULT_X_AXIS
99+
y_axis = self.data.get("y_axis") or DEFAULT_Y_AXIS
100+
primary_field_values = self.data.get("primary_field_values") or []
101+
secondary_category = self.data["secondary_category"]
102+
static_fields: dict = dict(self.validated_data["static_fields"])
103+
segments: list[dict] = self.data["segments"]
104+
chart_type = self.data.get("chart_type", "stacked_bar")
105+
segment_secondary_values = [
106+
segment["secondary_field_value"] for segment in segments
107+
]
108+
109+
if static_fields.get("date_from"):
110+
static_fields["date_from"] = static_fields["date_from"].isoformat()
111+
if static_fields.get("date_to"):
112+
static_fields["date_to"] = static_fields["date_to"].isoformat()
113+
114+
metric_group = extract_metric_group_from_metric(metric=static_fields["metric"])
115+
is_timeseries_data = DataSourceFileType[metric_group].is_timeseries
116+
plots: list[dict] = []
117+
118+
if is_timeseries_data:
119+
plots = [
120+
{
121+
"x_axis": x_axis,
122+
"y_axis": y_axis,
123+
"chart_type": chart_type,
124+
**static_fields,
125+
secondary_category: segment["secondary_field_value"],
126+
}
127+
for segment in segments
128+
]
129+
else:
130+
for primary_field_value in primary_field_values:
131+
for segment in segments:
132+
plots.append(
133+
{
134+
"x_axis": x_axis,
135+
"y_axis": y_axis,
136+
"chart_type": chart_type,
137+
**static_fields,
138+
x_axis: primary_field_value,
139+
secondary_category: segment["secondary_field_value"],
140+
}
141+
)
142+
143+
chart_request = DualCategoryDownloadRequestParams(
144+
metric_group=metric_group,
145+
plots=plots,
146+
file_format=self.data["file_format"],
147+
chart_height=DEFAULT_CHART_HEIGHT,
148+
chart_width=DEFAULT_CHART_WIDTH,
149+
x_axis=x_axis,
150+
y_axis=y_axis,
151+
confidence_intervals=False,
152+
request=request,
153+
secondary_category=secondary_category,
154+
segment_secondary_values=segment_secondary_values,
155+
primary_field_values=primary_field_values,
156+
)
157+
158+
if is_timeseries_data:
159+
for plot in chart_request.plots:
160+
plot.override_y_axis_choice_to_none = True
161+
162+
return chart_request

metrics/api/serializers/downloads/single_category.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,12 @@ class DownloadListSerializer(serializers.ListSerializer):
3434

3535
class SingleCategoryDownloadsSerializer(BaseDownloadsSerializer):
3636
plots = DownloadListSerializer()
37+
confidence_intervals = serializers.BooleanField(
38+
required=False,
39+
default=False,
40+
allow_null=True,
41+
help_text=help_texts.CONFIDENCE_INTERVALS,
42+
)
3743

3844
def to_models(self, request: Request) -> ChartRequestParams:
3945
"""Creates a `PlotsCollection` from the download

metrics/api/urls_construction.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,9 @@
3636
)
3737
from metrics.api.views.charts import DualCategoryChartsView
3838
from metrics.api.views.charts.subplot_charts import SubplotChartsView
39-
# from metrics.api.views.downloads.dual_category_downloads import (
40-
# DualCategoryDownloadsView,
41-
# )
39+
from metrics.api.views.downloads.dual_category_downloads import (
40+
DualCategoryDownloadsView,
41+
)
4242
from metrics.api.views.geographies import (
4343
GeographiesByGeographyTypeView,
4444
GeographiesView,
@@ -200,9 +200,9 @@ def construct_public_api_urlpatterns(
200200
re_path(f"^{API_PREFIX}downloads/v2", SingleCategoryDownloadsView.as_view()),
201201
re_path(f"^{API_PREFIX}bulkdownloads/v1", BulkDownloadsView.as_view()),
202202
re_path(f"^{API_PREFIX}downloads/subplot/v1", SubplotDownloadsView.as_view()),
203-
# re_path(
204-
# f"^{API_PREFIX}downloads/dual-category/v1", DualCategoryDownloadsView.as_view()
205-
# ),
203+
re_path(
204+
f"^{API_PREFIX}downloads/dual-category/v1", DualCategoryDownloadsView.as_view()
205+
),
206206
re_path(
207207
f"^{API_PREFIX}geographies/v2/(?P<topic>[^/]+)",
208208
GeographiesViewDeprecated.as_view(),

0 commit comments

Comments
 (0)