• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

akvo / iwsims / #59

18 Jun 2026 07:20AM UTC coverage: 88.033% (-0.1%) from 88.13%
#59

push

coveralls-python

web-flow
Merge 5dfcb298b into a6f6761c9

5183 of 6053 branches covered (85.63%)

Branch coverage included in aggregate %.

9979 of 11170 relevant lines covered (89.34%)

0.89 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

94.33
backend/api/v1/v1_visualization/serializers.py
1
import json
1✔
2
from rest_framework import serializers
1✔
3
from api.v1.v1_data.models import (
1✔
4
    FormData,
5
    Administration,
6
)
7
from api.v1.v1_forms.models import (
1✔
8
    Questions,
9
    QuestionOptions,
10
    QuestionTypes,
11
)
12
from utils.custom_serializer_fields import (
1✔
13
    CustomPrimaryKeyRelatedField,
14
    CustomIntegerField,
15
)
16
from api.v1.v1_visualization.formula import validate_shape
1✔
17
from api.v1.v1_visualization.constants import (
1✔
18
    VALID_VALUES_CRITERIA_TYPES,
19
)
20
from api.v1.v1_visualization.functions import (
1✔
21
    parse_criteria_string,
22
)
23

24

25
class OptionSerializer(serializers.ModelSerializer):
1✔
26
    class Meta:
1✔
27
        model = QuestionOptions
1✔
28
        fields = ["id", "label", "color"]
1✔
29

30

31
class FormDataAnswerSerializer(serializers.Serializer):
1✔
32
    id = CustomIntegerField()
1✔
33
    value = CustomIntegerField()
1✔
34

35
    class Meta:
1✔
36
        fields = ["id", "value"]
1✔
37

38

39
class FormDataStatSerializer(serializers.Serializer):
1✔
40
    options = serializers.ListField(
1✔
41
        child=OptionSerializer()
42
    )
43
    data = serializers.ListField(
1✔
44
        child=FormDataAnswerSerializer()
45
    )
46

47
    class Meta:
1✔
48
        fields = ["options", "data"]
1✔
49

50

51
class FormDataStatsFilterSerializer(serializers.Serializer):
1✔
52
    question_id = CustomPrimaryKeyRelatedField(
1✔
53
        queryset=Questions.objects.none(),
54
        required=True,
55
    )
56

57
    def validate_question_id(self, value):
1✔
58
        valid_types = [
1✔
59
            QuestionTypes.number,
60
            QuestionTypes.option,
61
            QuestionTypes.multiple_option,
62
        ]
63
        if value.type not in valid_types:
1!
64
            raise serializers.ValidationError(
×
65
                "Question type must be one of: "
66
                "number, option, multiple_option."
67
            )
68
        return value
1✔
69

70
    def __init__(self, *args, **kwargs):
1✔
71
        super().__init__(*args, **kwargs)
1✔
72
        form = self.context.get('form')
1✔
73
        self.fields['question_id'].queryset = Questions.objects.filter(
1✔
74
            form=form,
75
            type__in=[
76
                QuestionTypes.number,
77
                QuestionTypes.option,
78
                QuestionTypes.multiple_option,
79
            ]
80
        ).all()
81

82
    class Meta:
1✔
83
        fields = [
1✔
84
            "question_id",
85
        ]
86

87

88
class MonitoringStatSerializer(serializers.Serializer):
1✔
89
    date = serializers.DateField()
1✔
90
    value = serializers.FloatField()
1✔
91

92

93
class GeoLocationListSerializer(serializers.ModelSerializer):
1✔
94
    class Meta:
1✔
95
        model = FormData
1✔
96
        fields = ["id", "name", "geo", "administration_id"]
1✔
97

98

99
class DatapointDetailSerializer(serializers.ModelSerializer):
1✔
100
    administration_full_name = serializers.SerializerMethodField()
1✔
101

102
    def get_administration_full_name(self, obj):
1✔
103
        if obj.administration:
1!
104
            return obj.administration.full_name
1✔
105
        return ""
×
106

107
    class Meta:
1✔
108
        model = FormData
1✔
109
        fields = ["id", "name", "administration_full_name", "updated"]
1✔
110

111

112
class GeoLocationFilterSerializer(serializers.Serializer):
1✔
113
    administration = CustomPrimaryKeyRelatedField(
1✔
114
        queryset=Administration.objects.none(), required=False
115
    )
116
    criteria = serializers.CharField(required=False)
1✔
117
    from_date = serializers.DateField(required=False)
1✔
118
    to_date = serializers.DateField(required=False)
1✔
119
    include_monitoring = serializers.BooleanField(
1✔
120
        required=False, default=False
121
    )
122
    monitoring_form_id = serializers.IntegerField(required=False)
1✔
123

124
    def __init__(self, **kwargs):
1✔
125
        super().__init__(**kwargs)
1✔
126
        self.fields.get(
1✔
127
            "administration"
128
        ).queryset = Administration.objects.all()
129

130
    def validate_criteria(self, value):
1✔
131
        try:
1✔
132
            return parse_criteria_string(
1✔
133
                value, VALID_VALUES_CRITERIA_TYPES,
134
            )
135
        except ValueError as e:
1✔
136
            raise serializers.ValidationError(str(e))
1✔
137

138
    class Meta:
1✔
139
        fields = [
1✔
140
            "administration", "criteria",
141
            "from_date", "to_date",
142
            "include_monitoring", "monitoring_form_id",
143
        ]
144

145

146
class FormulaValuesSerializer(serializers.Serializer):
1✔
147
    """Validates query params for the formula evaluator endpoint.
148

149
    The ``formula`` param arrives as a URL-encoded JSON string; the
150
    ``validate_formula`` hook parses and structurally validates it.
151
    """
152

153
    form_id = serializers.IntegerField(required=True)
1✔
154
    group_by = serializers.ChoiceField(
1✔
155
        choices=["parent_id"], required=True,
156
    )
157
    monitoring = serializers.ChoiceField(
1✔
158
        choices=["latest"], required=False, default="latest",
159
    )
160
    formula = serializers.CharField(required=True)
1✔
161
    criteria = serializers.CharField(required=False)
1✔
162
    from_date = serializers.DateField(required=False)
1✔
163
    to_date = serializers.DateField(required=False)
1✔
164

165
    def validate_formula(self, value):
1✔
166
        from api.v1.v1_visualization.functions import validate_qname
1✔
167
        try:
1✔
168
            parsed = json.loads(value)
1✔
169
        except (TypeError, ValueError) as exc:
1✔
170
            raise serializers.ValidationError(
1✔
171
                f"formula is not valid JSON: {exc}"
172
            )
173
        try:
1✔
174
            validated = validate_shape(parsed)
1✔
175
        except ValueError as exc:
×
176
            raise serializers.ValidationError(str(exc))
×
177
        # Conditions are question_name-only: reject a numeric question_name
178
        # (a leaked question_id) with a 400 (Part A guard).
179
        for bucket in validated.get("buckets", []):
1✔
180
            for cond in bucket.get("all_of", []):
1✔
181
                validate_qname(cond.get("question_name"))
1✔
182
        return validated
1✔
183

184
    def validate_criteria(self, value):
1✔
185
        from api.v1.v1_visualization.constants import (
1✔
186
            VALID_VALUES_CRITERIA_TYPES,
187
        )
188
        from api.v1.v1_visualization.functions import (
1✔
189
            parse_criteria_string,
190
        )
191
        try:
1✔
192
            return parse_criteria_string(
1✔
193
                value, VALID_VALUES_CRITERIA_TYPES,
194
            )
195
        except ValueError as e:
×
196
            raise serializers.ValidationError(str(e))
×
197

198
    class Meta:
1✔
199
        fields = [
1✔
200
            "form_id", "group_by", "monitoring", "formula",
201
            "criteria", "from_date", "to_date",
202
        ]
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc