Skip to content

Commit 3dca217

Browse files
committed
Add dual category support to tables
1 parent c8300ed commit 3dca217

7 files changed

Lines changed: 411 additions & 1 deletion

File tree

metrics/api/serializers/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
from .charts import ChartsSerializer
2+
from .dual_category_tables import (
3+
DualCategoryTablesSerializer,
4+
DualCategoryTablesResponseSerializer,
5+
)
26
from .headlines import HeadlinesQuerySerializer, CoreHeadlineSerializer
37
from .trends import TrendsQuerySerializer, TrendsResponseSerializer
48
from .downloads import (
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import contextlib
2+
3+
from django.db.utils import OperationalError
4+
from rest_framework import serializers
5+
from rest_framework.request import Request
6+
7+
from metrics.api.serializers import help_texts
8+
from metrics.api.serializers.plots import PlotSerializer
9+
from metrics.domain.common.utils import (
10+
DEFAULT_CHART_HEIGHT,
11+
DEFAULT_CHART_WIDTH,
12+
DEFAULT_X_AXIS,
13+
DEFAULT_Y_AXIS,
14+
ChartAxisFields,
15+
)
16+
from metrics.domain.models import ChartRequestParams
17+
18+
19+
class DualCategoryTableSegmentSerializer(serializers.Serializer):
20+
colour = serializers.CharField(
21+
required=False,
22+
allow_blank=True,
23+
allow_null=True,
24+
default="",
25+
help_text=help_texts.LABEL_FIELD,
26+
)
27+
28+
secondary_field_value = serializers.CharField(
29+
required=False,
30+
allow_blank=True,
31+
allow_null=True,
32+
default="",
33+
help_text=help_texts.LABEL_FIELD,
34+
)
35+
36+
label = serializers.CharField(
37+
required=False,
38+
allow_blank=True,
39+
allow_null=True,
40+
default="",
41+
help_text=help_texts.LABEL_FIELD,
42+
)
43+
44+
45+
class DualCategoryTableSegmentListSerializer(serializers.ListSerializer):
46+
child = DualCategoryTableSegmentSerializer()
47+
48+
49+
class DualCategoryTablesSerializer(serializers.Serializer):
50+
51+
segments = DualCategoryTableSegmentListSerializer()
52+
53+
static_fields = PlotSerializer()
54+
55+
x_axis = serializers.ChoiceField(
56+
choices=ChartAxisFields.choices(),
57+
required=False,
58+
allow_blank=True,
59+
allow_null=True,
60+
help_text=help_texts.CHART_X_AXIS,
61+
default=DEFAULT_X_AXIS,
62+
)
63+
64+
y_axis = serializers.ChoiceField(
65+
choices=ChartAxisFields.choices(),
66+
required=False,
67+
allow_blank=True,
68+
allow_null=True,
69+
help_text=help_texts.CHART_Y_AXIS,
70+
default=DEFAULT_Y_AXIS,
71+
)
72+
73+
primary_field_values = serializers.ListField(
74+
child=serializers.CharField(),
75+
help_text="List of primary field values for this segment",
76+
required=True,
77+
allow_empty=False,
78+
)
79+
80+
secondary_category = serializers.CharField(
81+
help_text="Secondary category field for the chart",
82+
required=True,
83+
)
84+
85+
def __init__(self, *args, **kwargs):
86+
with contextlib.suppress(OperationalError):
87+
super().__init__(*args, **kwargs)
88+
89+
def to_models(self, request: Request) -> ChartRequestParams:
90+
91+
groups_plots = []
92+
primary_field_values = self.data.get("primary_field_values")
93+
x_axis = self.data.get("x_axis") or DEFAULT_X_AXIS
94+
y_axis = self.data.get("y_axis") or DEFAULT_Y_AXIS
95+
static_fields = self.data.get("static_fields")
96+
print(f"AIDAN static_fields {static_fields}")
97+
topic = static_fields.get("topic")
98+
99+
for primary_field_value in primary_field_values:
100+
for segment in self.data["segments"]:
101+
plot = {
102+
"y_axis": y_axis,
103+
"x_axis": primary_field_value,
104+
self.data.get("secondary_category"): segment[
105+
"secondary_field_value"
106+
],
107+
**static_fields,
108+
}
109+
groups_plots.append(plot)
110+
print(f"AIDAN: plots {groups_plots}")
111+
return ChartRequestParams(
112+
chart_height=DEFAULT_CHART_HEIGHT,
113+
chart_width=DEFAULT_CHART_WIDTH,
114+
file_format="svg",
115+
plots=groups_plots,
116+
request=request,
117+
x_axis=x_axis,
118+
y_axis=y_axis,
119+
)
120+
121+
122+
class DualCategoryTablesResponseValueSerializer(serializers.Serializer):
123+
label = serializers.CharField()
124+
value = serializers.CharField()
125+
in_reporting_delay_period = serializers.BooleanField()
126+
# Confidence intervals aren't implemented for dual category charts
127+
128+
129+
class DualCategoryTablesResponseValuesListSerializer(serializers.ListSerializer):
130+
child = DualCategoryTablesResponseValueSerializer()
131+
132+
133+
class DualCategoryTablesResponsePlotsListSerializer(serializers.Serializer):
134+
reference = serializers.CharField()
135+
values = DualCategoryTablesResponseValuesListSerializer()
136+
137+
138+
class DualCategoryTablesResponseSerializer(serializers.ListSerializer):
139+
child = DualCategoryTablesResponsePlotsListSerializer()

metrics/api/urls_construction.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
BulkDownloadsView,
2525
ChartsView,
2626
ColdAlertViewSet,
27+
DualCategoryTablesView,
2728
DownloadsView,
2829
EncodedChartsView,
2930
HeadlinesView,
@@ -206,6 +207,7 @@ def construct_public_api_urlpatterns(
206207
re_path(f"^{API_PREFIX}maps/v1", MapsView.as_view()),
207208
re_path(f"^{API_PREFIX}tables/v4", TablesView.as_view()),
208209
re_path(f"^{API_PREFIX}tables/subplot/v1", TablesSubplotView.as_view()),
210+
re_path(f"^{API_PREFIX}tables/dual-category/v1", DualCategoryTablesView.as_view()),
209211
re_path(f"^{API_PREFIX}trends/v3", TrendsView.as_view()),
210212
]
211213

metrics/api/views/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from .headlines import HeadlinesView
44
from .downloads import DownloadsView, BulkDownloadsView, SubplotDownloadsView
55
from .health import HealthView
6-
from .tables import TablesView, TablesSubplotView
6+
from .tables import DualCategoryTablesView, TablesView, TablesSubplotView
77
from .trends import TrendsView
88
from .audit import (
99
AuditAPITimeSeriesViewSet,
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
1+
from .dual_category_tables import DualCategoryTablesView
12
from .single_category_tables import TablesView
23
from .subplot_tables.api_view import TablesSubplotView
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
from http import HTTPStatus
2+
3+
from drf_spectacular.utils import extend_schema
4+
from rest_framework.response import Response
5+
from rest_framework.views import APIView
6+
7+
from caching.private_api.decorators import cache_response
8+
from metrics.api.decorators.auth import require_authorisation
9+
from metrics.api.serializers.dual_category_tables import (
10+
DualCategoryTablesResponseSerializer,
11+
DualCategoryTablesSerializer,
12+
)
13+
from metrics.api.views.tables.common import TABLES_API_TAG
14+
from metrics.interfaces.plots.access import (
15+
DataNotFoundForAnyPlotError,
16+
InvalidPlotParametersError,
17+
)
18+
from metrics.interfaces.tables import access
19+
20+
21+
class DualCategoryTablesView(APIView):
22+
permission_classes = []
23+
24+
@classmethod
25+
@extend_schema(
26+
request=DualCategoryTablesSerializer,
27+
responses={HTTPStatus.OK.value: DualCategoryTablesResponseSerializer},
28+
tags=[TABLES_API_TAG],
29+
)
30+
@require_authorisation
31+
def post(cls, request, *args, **kwargs):
32+
"""This endpoint can be used to generate chart data in tabular format.
33+
34+
Multiple plots can be added as an array of objects from the request body.
35+
36+
This payload takes the following set of parameters for each plot:
37+
38+
| Parameter name | Description | Example | Mandatory |
39+
|------------------|----------------------------------------------------------------------------|--------------------------|-----------|
40+
| `topic` | The name of the disease/threat | COVID-19 | Yes |
41+
| `metric` | The name of the metric being queried for | COVID-19_deaths_ONSByDay | Yes |
42+
| `stratum` | The smallest subgroup a metric can be broken down into | default | No |
43+
| `geography` | The geography constraints to apply any data filtering to | London | No |
44+
| `geography_type` | The type of geographical categorisation to apply any data filtering to | Nation | No |
45+
| `age` | The patient age band | 0_4 | No |
46+
| `date_from` | The date from which to start the data slice from. In the format YYYY-MM-DD | 2023-01-01 | No |
47+
| `date_to` | The date to end the data slice to. In the format YYYY-MM-DD | 2023-05-01 | No |
48+
| `label` | The label to assign on the legend for this individual plot | Daily Covid deaths | No |
49+
50+
---
51+
52+
# Main errors
53+
There are certain combination of `topic / metric` which do not make sense.
54+
This is primarily because a set of `metric` values are not available for every `topic`.
55+
As well as this, certain `metric` names reference data of a certain profile.
56+
57+
---
58+
59+
## Selected metric not available for topic
60+
61+
In these cases, this endpoint will return an HTTP 400 BAD REQUEST.
62+
For example, if a metric like `COVID-19_deaths_ONSByDay` (which is only used for `COVID-19`)
63+
is being asked for with a topic of `Influenza`.
64+
65+
Then an HTTP 400 BAD REQUEST is returned with the following error message:
66+
`Influenza` does not have a corresponding metric of `COVID-19`
67+
68+
---
69+
70+
## Ordering of data
71+
72+
Note that for tables which are `date` based i.e. where the `x_axis` field is set to `date`.
73+
74+
Then the data will be returned in descending order from newest -> oldest:
75+
76+
```
77+
| 2023-09-29 | 1 |
78+
| 2023-09-28 | 2 |
79+
| 2023-09-27 | 3 |
80+
```
81+
82+
For tables which **not** `date` based i.e. where the `x_axis` field is set to something like `age`.
83+
84+
Then the data will be returned in ascending order:
85+
86+
```
87+
| 00 - 04 | 1 |
88+
| 05 - 09 | 2 |
89+
| 10 - 14 | 3 |
90+
```
91+
92+
"""
93+
print(f"AIDAN: serialising {request.data}")
94+
request_serializer = DualCategoryTablesSerializer(data=request.data)
95+
print(f"AIDAN: validating serialiser")
96+
request_serializer.is_valid(raise_exception=True)
97+
98+
print(f"AIDAN: converting to models")
99+
request_params = request_serializer.to_models(request=request)
100+
101+
try:
102+
print(f"AIDAN: generating table")
103+
tabular_data: list[dict[str, str]] = access.generate_table_for_full_plots(
104+
request_params=request_params
105+
)
106+
except (InvalidPlotParametersError, DataNotFoundForAnyPlotError) as error:
107+
print(f"AIDAN: error due to {error}")
108+
return Response(
109+
status=HTTPStatus.BAD_REQUEST, data={"error_message": str(error)}
110+
)
111+
112+
return Response(tabular_data)

0 commit comments

Comments
 (0)