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

akvo / akvo-mis / #293

21 Aug 2025 12:14PM UTC coverage: 88.112% (-0.05%) from 88.158%
#293

push

coveralls-python

ifirmawan
FE: Fix optional chaining for children in ApproversTree component

3414 of 3989 branches covered (85.59%)

Branch coverage included in aggregate %.

7133 of 7981 relevant lines covered (89.37%)

0.89 hits per line

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

97.02
backend/api/v1/v1_forms/serializers.py
1
import numpy as np
1✔
2
from collections import OrderedDict
1✔
3

4
from drf_spectacular.types import OpenApiTypes
1✔
5
from drf_spectacular.utils import extend_schema_field, inline_serializer
1✔
6
from rest_framework import serializers
1✔
7

8
from api.v1.v1_forms.constants import QuestionTypes, AttributeTypes
1✔
9
from api.v1.v1_forms.models import (
1✔
10
    Forms,
11
    QuestionGroup,
12
    Questions,
13
    QuestionOptions,
14
    QuestionAttribute,
15
)
16
from api.v1.v1_profile.models import (
1✔
17
    Administration,
18
    Entity,
19
    DataAccessTypes,
20
)
21
from api.v1.v1_users.models import SystemUser
1✔
22
from mis.settings import FORM_GEO_VALUE
1✔
23
from utils.custom_serializer_fields import (
1✔
24
    CustomChoiceField,
25
    CustomPrimaryKeyRelatedField,
26
    CustomMultipleChoiceField,
27
)
28
from utils.default_serializers import CommonDataSerializer, GeoFormatSerializer
1✔
29

30

31
class ListOptionSerializer(serializers.ModelSerializer):
1✔
32
    def to_representation(self, instance):
1✔
33
        result = super(ListOptionSerializer, self).to_representation(instance)
1✔
34
        return OrderedDict(
1✔
35
            [(key, result[key]) for key in result if result[key] is not None]
36
        )
37

38
    class Meta:
1✔
39
        model = QuestionOptions
1✔
40
        fields = ["id", "value", "label", "order", "color"]
1✔
41

42

43
class ListQuestionSerializer(serializers.ModelSerializer):
1✔
44
    option = serializers.SerializerMethodField()
1✔
45
    type = serializers.SerializerMethodField()
1✔
46
    center = serializers.SerializerMethodField()
1✔
47
    api = serializers.SerializerMethodField()
1✔
48
    rule = serializers.SerializerMethodField()
1✔
49
    extra = serializers.SerializerMethodField()
1✔
50
    source = serializers.SerializerMethodField()
1✔
51
    tooltip = serializers.SerializerMethodField()
1✔
52
    fn = serializers.SerializerMethodField()
1✔
53
    pre = serializers.SerializerMethodField()
1✔
54
    displayOnly = serializers.BooleanField(source="display_only")
1✔
55

56
    @extend_schema_field(ListOptionSerializer(many=True))
1✔
57
    def get_option(self, instance: Questions):
1✔
58
        if instance.type in [
1✔
59
            QuestionTypes.option,
60
            QuestionTypes.multiple_option,
61
        ]:
62
            return ListOptionSerializer(
1✔
63
                instance=instance.options.all(), many=True
64
            ).data
65
        return None
1✔
66

67
    @extend_schema_field(OpenApiTypes.STR)
1✔
68
    def get_type(self, instance: Questions):
1✔
69
        if instance.type == QuestionTypes.administration:
1✔
70
            return QuestionTypes.FieldStr.get(QuestionTypes.cascade).lower()
1✔
71
        return QuestionTypes.FieldStr.get(instance.type).lower()
1✔
72

73
    @extend_schema_field(
1✔
74
        inline_serializer(
75
            "CascadeApiFormat",
76
            fields={
77
                "endpoint": serializers.CharField(),
78
                "list": serializers.CharField(),
79
                "initial": serializers.CharField(),
80
                "query_params": serializers.CharField(),
81
            },
82
        )
83
    )
84
    def get_api(self, instance: Questions):
1✔
85
        if instance.type == QuestionTypes.administration:
1✔
86
            user = self.context.get("user")
1✔
87
            administration = Administration.objects.filter(
1✔
88
                parent__isnull=True
89
            ).first()
90
            user_role = user.user_user_role.filter(
1✔
91
                administration__parent__isnull=False
92
            ).order_by("administration__level__level").first()
93
            if user_role:
1✔
94
                administration = user_role.administration
1✔
95
            # max depth for cascade question in national form
96
            max_level = False
1✔
97
            extra_objects = {}
1✔
98
            if max_level:
1!
99
                extra_objects = {
×
100
                    "query_params": "?max_level=1",
101
                }
102
            if not user.is_superuser:
1✔
103
                if max_level:
1!
104
                    extra_objects = {
×
105
                        "query_params": "&max_level=1",
106
                    }
107
                initial = administration.id
1✔
108
                if administration.parent:
1✔
109
                    filter_children = "&".join([
1✔
110
                        f"filter_children={ur.administration.id}"
111
                        for ur in user.user_user_role.filter(
112
                            administration__parent=administration.parent
113
                        ).all()
114
                    ])
115
                    initial = f"{administration.parent.id}?{filter_children}"
1✔
116
                return {
1✔
117
                    "endpoint": "/api/v1/administration",
118
                    "list": "children",
119
                    "initial":  initial,
120
                    **extra_objects,
121
                }
122
            return {
1✔
123
                "endpoint": "/api/v1/administration",
124
                "list": "children",
125
                "initial": administration.id,
126
                **extra_objects,
127
            }
128
        if instance.type == QuestionTypes.cascade:
1✔
129
            return instance.api
1✔
130
        if instance.type == QuestionTypes.attachment:
1✔
131
            return instance.api
1✔
132
        return None
1✔
133

134
    @extend_schema_field(GeoFormatSerializer)
1✔
135
    def get_center(self, instance: Questions):
1✔
136
        if instance.type == QuestionTypes.geo:
1✔
137
            return FORM_GEO_VALUE
1✔
138
        return None
1✔
139

140
    @extend_schema_field(
1✔
141
        inline_serializer(
142
            "QuestionRuleFormat",
143
            fields={
144
                "min": serializers.FloatField(),
145
                "max": serializers.FloatField(),
146
                "allowDecimal": serializers.BooleanField(),
147
            },
148
        )
149
    )
150
    def get_rule(self, instance: Questions):
1✔
151
        return instance.rule
1✔
152

153
    @extend_schema_field(
1✔
154
        inline_serializer(
155
            "QuestionExtraFormat",
156
            fields={"allowOther": serializers.BooleanField()},
157
        )
158
    )
159
    def get_extra(self, instance: Questions):
1✔
160
        return instance.extra
1✔
161

162
    @extend_schema_field(
1✔
163
        inline_serializer(
164
            "QuestionTooltipFormat", fields={"text": serializers.CharField()}
165
        )
166
    )
167
    def get_tooltip(self, instance: Questions):
1✔
168
        return instance.tooltip
1✔
169

170
    @extend_schema_field(
1✔
171
        inline_serializer(
172
            "QuestionFnFormat",
173
            fields={
174
                "fnColor": serializers.JSONField(),
175
                "fnString": serializers.CharField(),
176
                "multiline": serializers.BooleanField(),
177
            },
178
        )
179
    )
180
    def get_fn(self, instance: Questions):
1✔
181
        return instance.fn
1✔
182

183
    @extend_schema_field(
1✔
184
        inline_serializer(
185
            "QuestionPreFormat",
186
            fields={
187
                "answer": serializers.CharField(),
188
                "fill": serializers.JSONField(),
189
            },
190
        )
191
    )
192
    def get_pre(self, instance: Questions):
1✔
193
        return instance.pre
1✔
194

195
    def to_representation(self, instance):
1✔
196
        result = super(ListQuestionSerializer, self).to_representation(
1✔
197
            instance
198
        )
199
        return OrderedDict(
1✔
200
            [(key, result[key]) for key in result if result[key] is not None]
201
        )
202

203
    @extend_schema_field(
1✔
204
        inline_serializer(
205
            "QuestionSourceFormat",
206
            fields={
207
                "file": serializers.CharField(),
208
                "parent": serializers.ListField(
209
                    child=serializers.IntegerField()
210
                ),
211
                "max_level": serializers.IntegerField(),
212
            },
213
        )
214
    )
215
    def get_source(self, instance: Questions):
1✔
216
        user = self.context.get("user")
1✔
217
        assignment = self.context.get("mobile_assignment")
1✔
218
        max_level = False
1✔
219
        extra_objects = {}
1✔
220
        if instance.type == QuestionTypes.cascade:
1✔
221
            if instance.extra:
1!
222
                cascade_type = instance.extra.get("type")
1✔
223
                cascade_name = instance.extra.get("name")
1✔
224
                if cascade_type == "entity":
1!
225
                    entity_type = Entity.objects.filter(
1✔
226
                        name=cascade_name
227
                    ).first()
228
                    entity_id = entity_type.id if entity_type else None
1✔
229
                    return {
1✔
230
                        "file": "entity_data.sqlite",
231
                        "cascade_type": entity_id,
232
                        "cascade_parent": "administrator.sqlite",
233
                    }
234
            return {"file": "organisation.sqlite", "parent_id": [0]}
×
235
        if instance.type == QuestionTypes.administration:
1✔
236
            if max_level:
1!
237
                extra_objects = {
×
238
                    "max_level": 1,
239
                }
240
            return {
1✔
241
                "file": "administrator.sqlite",
242
                "parent_id": [a.id for a in assignment.administrations.all()]
243
                if assignment
244
                else [
245
                    ur.administration.id
246
                    for ur in
247
                    user.user_user_role.all()
248
                ],
249
                **extra_objects,
250
            }
251
        return None
1✔
252

253
    class Meta:
1✔
254
        model = Questions
1✔
255
        fields = [
1✔
256
            "id",
257
            "order",
258
            "name",
259
            "label",
260
            "short_label",
261
            "type",
262
            "required",
263
            "dependency",
264
            "option",
265
            "center",
266
            "api",
267
            "meta",
268
            "rule",
269
            "extra",
270
            "source",
271
            "tooltip",
272
            "fn",
273
            "pre",
274
            "displayOnly",
275
        ]
276

277

278
# TODO: confirm Order in QuestionGroup model
279
class ListQuestionGroupSerializer(serializers.ModelSerializer):
1✔
280
    question = serializers.SerializerMethodField()
1✔
281

282
    @extend_schema_field(ListQuestionSerializer(many=True))
1✔
283
    def get_question(self, instance: QuestionGroup):
1✔
284
        return ListQuestionSerializer(
1✔
285
            instance=instance.question_group_question.all().order_by("order"),
286
            context=self.context,
287
            many=True,
288
        ).data
289

290
    class Meta:
1✔
291
        model = QuestionGroup
1✔
292
        fields = ["name", "label", "question", "repeatable", "repeat_text"]
1✔
293

294

295
class ListAdministrationCascadeSerializer(serializers.ModelSerializer):
1✔
296
    value = serializers.ReadOnlyField(source="id")
1✔
297
    label = serializers.ReadOnlyField(source="name")
1✔
298
    children = serializers.SerializerMethodField()
1✔
299

300
    @extend_schema_field(
1✔
301
        inline_serializer(
302
            "children",
303
            fields={
304
                "value": serializers.IntegerField(),
305
                "label": serializers.CharField(),
306
            },
307
            many=True,
308
        )
309
    )
310
    def get_children(self, instance: Administration):
1✔
311
        return ListAdministrationCascadeSerializer(
×
312
            instance=instance.parent_administration.all(), many=True
313
        ).data
314

315
    class Meta:
1✔
316
        model = Administration
1✔
317
        fields = ["value", "label", "children"]
1✔
318

319

320
class WebFormDetailSerializer(serializers.ModelSerializer):
1✔
321
    question_group = serializers.SerializerMethodField()
1✔
322
    cascades = serializers.SerializerMethodField()
1✔
323

324
    @extend_schema_field(ListQuestionGroupSerializer(many=True))
1✔
325
    def get_question_group(self, instance: Forms):
1✔
326
        return ListQuestionGroupSerializer(
1✔
327
            instance=instance.form_question_group.all().order_by("order"),
328
            many=True,
329
            context=self.context,
330
        ).data
331

332
    @extend_schema_field(serializers.ListField())
1✔
333
    def get_cascades(self, instance: Forms):
1✔
334
        cascade_questions = Questions.objects.filter(
1✔
335
            type__in=[QuestionTypes.cascade, QuestionTypes.administration],
336
            form=instance,
337
        ).all()
338
        source = []
1✔
339
        for cascade_question in cascade_questions:
1✔
340
            if cascade_question.type == QuestionTypes.administration:
1✔
341
                source.append("/sqlite/administrator.sqlite")
1✔
342
            if (
1✔
343
                cascade_question.extra
344
                and cascade_question.extra.get("type") == "entity"
345
            ):
346
                source.append("/sqlite/entity_data.sqlite")
1✔
347
            else:
348
                source.append("/sqlite/organisation.sqlite")
1✔
349
        return np.unique(source)
1✔
350

351
    class Meta:
1✔
352
        model = Forms
1✔
353
        fields = [
1✔
354
            "id",
355
            "name",
356
            "version",
357
            "cascades",
358
            "approval_instructions",
359
            "parent",
360
            "question_group",
361
        ]
362

363

364
class ListFormSerializer(serializers.ModelSerializer):
1✔
365
    class Meta:
1✔
366
        model = Forms
1✔
367
        fields = [
1✔
368
            "id",
369
            "name",
370
            "version",
371
            "parent",
372
        ]
373

374

375
class FormDataListQuestionSerializer(serializers.ModelSerializer):
1✔
376
    option = serializers.SerializerMethodField()
1✔
377
    type = serializers.SerializerMethodField()
1✔
378
    attributes = serializers.SerializerMethodField()
1✔
379

380
    @extend_schema_field(ListOptionSerializer(many=True))
1✔
381
    def get_option(self, instance: Questions):
1✔
382
        if instance.type in [
1✔
383
            QuestionTypes.geo,
384
            QuestionTypes.option,
385
            QuestionTypes.multiple_option,
386
        ]:
387
            return ListOptionSerializer(
1✔
388
                instance=instance.options.all(), many=True
389
            ).data
390
        return None
1✔
391

392
    @extend_schema_field(
1✔
393
        CustomChoiceField(
394
            choices=[
395
                QuestionTypes.FieldStr[d].lower()
396
                for d in QuestionTypes.FieldStr
397
            ]
398
        )
399
    )
400
    def get_type(self, instance: Questions):
1✔
401
        if instance.type == QuestionTypes.administration:
1✔
402
            return QuestionTypes.FieldStr.get(QuestionTypes.cascade).lower()
1✔
403
        return QuestionTypes.FieldStr.get(instance.type).lower()
1✔
404

405
    @extend_schema_field(
1✔
406
        CustomMultipleChoiceField(
407
            choices=[
408
                AttributeTypes.FieldStr[a] for a in AttributeTypes.FieldStr
409
            ]
410
        )
411
    )
412
    def get_attributes(self, instance: Questions):
1✔
413
        attribute_ids = (
1✔
414
            QuestionAttribute.objects.filter(question=instance)
415
            .values_list("attribute", flat=True)
416
            .distinct()
417
        )
418
        if attribute_ids:
1✔
419
            return [AttributeTypes.FieldStr.get(a) for a in attribute_ids]
1✔
420
        return []
1✔
421

422
    @extend_schema_field(GeoFormatSerializer)
1✔
423
    def to_representation(self, instance):
1✔
424
        result = super(FormDataListQuestionSerializer, self).to_representation(
1✔
425
            instance
426
        )
427
        return OrderedDict(
1✔
428
            [(key, result[key]) for key in result if result[key] is not None]
429
        )
430

431
    class Meta:
1✔
432
        model = Questions
1✔
433
        fields = [
1✔
434
            "id",
435
            "form",
436
            "question_group",
437
            "name",
438
            "label",
439
            "short_label",
440
            "order",
441
            "meta",
442
            "api",
443
            "type",
444
            "required",
445
            "rule",
446
            "option",
447
            "dependency",
448
            "display_only",
449
            "attributes",
450
        ]
451

452

453
class FormDataQuestionGroupSerializer(serializers.ModelSerializer):
1✔
454
    question = serializers.SerializerMethodField()
1✔
455
    repeatable = serializers.BooleanField(default=False)
1✔
456
    repeat_text = serializers.CharField(required=False, allow_null=True)
1✔
457

458
    @extend_schema_field(FormDataListQuestionSerializer(many=True))
1✔
459
    def get_question(self, instance: QuestionGroup):
1✔
460
        return FormDataListQuestionSerializer(
1✔
461
            instance=instance.question_group_question.all().order_by("order"),
462
            many=True,
463
        ).data
464

465
    class Meta:
1✔
466
        model = QuestionGroup
1✔
467
        fields = [
1✔
468
            "id", "label", "name", "question", "repeatable", "repeat_text"
469
        ]
470

471

472
class FormDataSerializer(serializers.ModelSerializer):
1✔
473
    question_group = serializers.SerializerMethodField()
1✔
474

475
    @extend_schema_field(FormDataQuestionGroupSerializer(many=True))
1✔
476
    def get_question_group(self, instance: Forms):
1✔
477
        return FormDataQuestionGroupSerializer(
1✔
478
            instance=instance.form_question_group.all().order_by("order"),
479
            many=True,
480
        ).data
481

482
    class Meta:
1✔
483
        model = Forms
1✔
484
        fields = [
1✔
485
            "id",
486
            "name",
487
            "question_group",
488
            "approval_instructions",
489
            "parent",
490
        ]
491

492

493
class FormApproverRequestSerializer(serializers.Serializer):
1✔
494
    administration_id = CustomPrimaryKeyRelatedField(
1✔
495
        queryset=Administration.objects.none()
496
    )
497
    form_id = CustomPrimaryKeyRelatedField(queryset=Forms.objects.none())
1✔
498

499
    def __init__(self, **kwargs):
1✔
500
        super().__init__(**kwargs)
1✔
501
        self.fields.get("form_id").queryset = Forms.objects.all()
1✔
502
        self.fields.get(
1✔
503
            "administration_id"
504
        ).queryset = Administration.objects.all()
505

506

507
class FormApproverUserSerializer(serializers.ModelSerializer):
1✔
508
    class Meta:
1✔
509
        model = SystemUser
1✔
510
        fields = ["id", "first_name", "last_name", "email"]
1✔
511

512

513
class FormApproverResponseSerializer(serializers.ModelSerializer):
1✔
514
    users = serializers.SerializerMethodField()
1✔
515
    administration = serializers.SerializerMethodField()
1✔
516

517
    @extend_schema_field(FormApproverUserSerializer(many=True))
1✔
518
    def get_users(self, instance: Administration):
1✔
519
        approvers = instance.user_role_administration.filter(
1✔
520
            role__role_role_access__data_access=DataAccessTypes.approve,
521
            user__user_form__form=self.context.get("form"),
522
        ).select_related("user")
523
        approvers = [
1✔
524
            approver.user for approver in approvers
525
        ]
526
        return FormApproverUserSerializer(
1✔
527
            instance=approvers, many=True
528
        ).data
529

530
    @extend_schema_field(CommonDataSerializer)
1✔
531
    def get_administration(self, instance: Administration):
1✔
532
        return {"id": instance.id, "name": instance.name}
1✔
533

534
    class Meta:
1✔
535
        model = Administration
1✔
536
        fields = ["users", "administration"]
1✔
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