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

akvo / iwsims-demo / #111

06 May 2025 07:15AM UTC coverage: 86.248% (+0.02%) from 86.231%
#111

push

coveralls-python

web-flow
Merge pull request #32 from akvo/feature/31-eng-1328-new-question-type-attachment

Feature/31 eng 1328 new question type attachment

2615 of 3148 branches covered (83.07%)

Branch coverage included in aggregate %.

5952 of 6785 relevant lines covered (87.72%)

0.88 hits per line

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

84.06
backend/api/v1/v1_data/serializers.py
1
import requests
1✔
2
from django.db.models import Sum
1✔
3
from django.utils import timezone
1✔
4
from django_q.tasks import async_task
1✔
5
from drf_spectacular.types import OpenApiTypes
1✔
6
from drf_spectacular.utils import extend_schema_field, inline_serializer
1✔
7
from rest_framework import serializers
1✔
8
from rest_framework.exceptions import ValidationError
1✔
9

10
from api.v1.v1_data.constants import DataApprovalStatus
1✔
11
from api.v1.v1_data.models import (
1✔
12
    FormData,
13
    Answers,
14
    PendingFormData,
15
    PendingAnswers,
16
    PendingDataApproval,
17
    PendingDataBatch,
18
    PendingDataBatchComments,
19
    AnswerHistory,
20
    PendingAnswerHistory,
21
)
22
from api.v1.v1_forms.constants import QuestionTypes, SubmissionTypes
1✔
23
from api.v1.v1_forms.models import (
1✔
24
    Questions,
25
    Forms,
26
    FormApprovalAssignment,
27
)
28
from api.v1.v1_profile.constants import UserRoleTypes
1✔
29
from api.v1.v1_profile.models import Administration, EntityData, Levels
1✔
30
from api.v1.v1_users.models import SystemUser, Organisation
1✔
31
from utils.custom_serializer_fields import (
1✔
32
    CustomPrimaryKeyRelatedField,
33
    UnvalidatedField,
34
    CustomListField,
35
    CustomCharField,
36
    CustomChoiceField,
37
    CustomBooleanField,
38
    CustomIntegerField,
39
)
40
from utils.default_serializers import CommonDataSerializer
1✔
41
from utils.email_helper import send_email, EmailTypes
1✔
42
from utils.functions import update_date_time_format, get_answer_value
1✔
43
from utils.functions import get_answer_history
1✔
44

45

46
class SubmitFormDataSerializer(serializers.ModelSerializer):
1✔
47
    administration = CustomPrimaryKeyRelatedField(
1✔
48
        queryset=Administration.objects.none()
49
    )
50
    name = CustomCharField()
1✔
51

52
    def __init__(self, **kwargs):
1✔
53
        super().__init__(**kwargs)
1✔
54
        self.fields.get(
1✔
55
            "administration"
56
        ).queryset = Administration.objects.all()
57

58
    class Meta:
1✔
59
        model = FormData
1✔
60
        fields = ["name", "geo", "administration", "submission_type"]
1✔
61

62

63
class SubmitFormDataAnswerSerializer(serializers.ModelSerializer):
1✔
64
    value = UnvalidatedField(allow_null=False)
1✔
65
    question = CustomPrimaryKeyRelatedField(queryset=Questions.objects.none())
1✔
66

67
    def __init__(self, **kwargs):
1✔
68
        super().__init__(**kwargs)
1✔
69
        self.fields.get("question").queryset = Questions.objects.all()
1✔
70

71
    def validate_value(self, value):
1✔
72
        return value
1✔
73

74
    def validate(self, attrs):
1✔
75
        if attrs.get("value") == "":
1✔
76
            raise ValidationError(
1✔
77
                "Value is required for Question:{0}".format(
78
                    attrs.get("question").id
79
                )
80
            )
81

82
        if (
1!
83
            isinstance(attrs.get("value"), list)
84
            and len(attrs.get("value")) == 0
85
        ):
86
            raise ValidationError(
×
87
                "Value is required for Question:{0}".format(
88
                    attrs.get("question").id
89
                )
90
            )
91

92
        if not isinstance(attrs.get("value"), list) and attrs.get(
1!
93
            "question"
94
        ).type in [
95
            QuestionTypes.geo,
96
            QuestionTypes.option,
97
            QuestionTypes.multiple_option,
98
        ]:
99
            raise ValidationError(
×
100
                "Valid list value is required for Question:{0}".format(
101
                    attrs.get("question").id
102
                )
103
            )
104
        elif not isinstance(attrs.get("value"), str) and attrs.get(
1!
105
            "question"
106
        ).type in [
107
            QuestionTypes.text,
108
            QuestionTypes.photo,
109
            QuestionTypes.date,
110
            QuestionTypes.attachment,
111
        ]:
112
            raise ValidationError(
×
113
                "Valid string value is required for Question:{0}".format(
114
                    attrs.get("question").id
115
                )
116
            )
117
        elif not (
1!
118
            isinstance(attrs.get("value"), int)
119
            or isinstance(attrs.get("value"), float)
120
        ) and attrs.get("question").type in [
121
            QuestionTypes.number,
122
            QuestionTypes.administration,
123
            QuestionTypes.cascade,
124
        ]:
125
            raise ValidationError(
×
126
                "Valid number value is required for Question:{0}".format(
127
                    attrs.get("question").id
128
                )
129
            )
130

131
        if attrs.get("question").type == QuestionTypes.administration:
1✔
132
            attrs["value"] = int(float(attrs.get("value")))
1✔
133

134
        return attrs
1✔
135

136
    class Meta:
1✔
137
        model = Answers
1✔
138
        fields = ["question", "value"]
1✔
139

140

141
class SubmitFormSerializer(serializers.Serializer):
1✔
142
    data = SubmitFormDataSerializer()
1✔
143
    answer = SubmitFormDataAnswerSerializer(many=True)
1✔
144

145
    def __init__(self, **kwargs):
1✔
146
        super().__init__(**kwargs)
1✔
147

148
    def update(self, instance, validated_data):
1✔
149
        pass
×
150

151
    def create(self, validated_data):
1✔
152
        data = validated_data.get("data")
1✔
153
        data["form"] = self.context.get("form")
1✔
154
        data["created_by"] = self.context.get("user")
1✔
155
        data["updated_by"] = self.context.get("user")
1✔
156
        obj_data = self.fields.get("data").create(data)
1✔
157
        # Answer value based on Question type
158
        # - geo = 1 #option
159
        # - administration = 2 #value
160
        # - text = 3 #name
161
        # - number = 4 #value
162
        # - option = 5 #option
163
        # - multiple_option = 6 #option
164
        # - cascade = 7 #option
165
        # - photo = 8 #name
166
        # - date = 9 #name
167
        # - autofield = 10 #name
168
        # - attachment = 11 #name
169

170
        for answer in validated_data.get("answer"):
1✔
171
            name = None
1✔
172
            value = None
1✔
173
            option = None
1✔
174

175
            if answer.get("question").meta_uuid:
1✔
176
                obj_data.uuid = answer.get("value")
1✔
177
                obj_data.save()
1✔
178

179
            if answer.get("question").type in [
1✔
180
                QuestionTypes.geo,
181
                QuestionTypes.option,
182
                QuestionTypes.multiple_option,
183
            ]:
184
                option = answer.get("value")
1✔
185
            elif answer.get("question").type in [
1✔
186
                QuestionTypes.text,
187
                QuestionTypes.photo,
188
                QuestionTypes.date,
189
                QuestionTypes.autofield,
190
                QuestionTypes.attachment,
191
            ]:
192
                name = answer.get("value")
1✔
193
            elif answer.get("question").type == QuestionTypes.cascade:
1!
194
                id = answer.get("value")
×
195
                ep = answer.get("question").api.get("endpoint")
×
196
                val = None
×
197
                if "organisation" in ep:
×
198
                    val = Organisation.objects.filter(pk=id).first()
×
199
                    val = val.name
×
200
                if "entity-data" in ep:
×
201
                    val = EntityData.objects.filter(pk=id).first()
×
202
                    val = val.name
×
203
                if "entity-data" not in ep and "organisation" not in ep:
×
204
                    ep = ep.split("?")[0]
×
205
                    ep = f"{ep}?id={id}"
×
206
                    val = requests.get(ep).json()
×
207
                    val = val[0].get("name")
×
208
                name = val
×
209
            else:
210
                # for administration,number question type
211
                value = answer.get("value")
1✔
212

213
            Answers.objects.create(
1✔
214
                data=obj_data,
215
                question=answer.get("question"),
216
                name=name,
217
                value=value,
218
                options=option,
219
                created_by=self.context.get("user"),
220
            )
221
        obj_data.save_to_file
1✔
222

223
        return object
1✔
224

225

226
class AnswerHistorySerializer(serializers.Serializer):
1✔
227
    value = serializers.FloatField()
1✔
228
    created = CustomCharField()
1✔
229
    created_by = CustomCharField()
1✔
230

231

232
class ListDataAnswerSerializer(serializers.ModelSerializer):
1✔
233
    history = serializers.SerializerMethodField()
1✔
234
    value = serializers.SerializerMethodField()
1✔
235

236
    @extend_schema_field(AnswerHistorySerializer(many=True))
1✔
237
    def get_history(self, instance):
1✔
238
        answer_history = AnswerHistory.objects.filter(
1✔
239
            data=instance.data, question=instance.question
240
        ).all()
241
        history = []
1✔
242
        for h in answer_history:
1✔
243
            history.append(get_answer_history(h))
1✔
244
        return history if history else None
1✔
245

246
    @extend_schema_field(OpenApiTypes.ANY)
1✔
247
    def get_value(self, instance: Answers):
1✔
248
        return get_answer_value(instance)
1✔
249

250
    class Meta:
1✔
251
        model = Answers
1✔
252
        fields = ["history", "question", "value"]
1✔
253

254

255
class ListFormDataRequestSerializer(serializers.Serializer):
1✔
256
    administration = CustomPrimaryKeyRelatedField(
1✔
257
        queryset=Administration.objects.none(), required=False
258
    )
259
    questions = CustomListField(
1✔
260
        child=CustomPrimaryKeyRelatedField(queryset=Questions.objects.none()),
261
        required=False,
262
    )
263
    parent = CustomPrimaryKeyRelatedField(
1✔
264
        queryset=FormData.objects.none(), required=False
265
    )
266

267
    def __init__(self, **kwargs):
1✔
268
        super().__init__(**kwargs)
1✔
269
        self.fields.get(
1✔
270
            "administration"
271
        ).queryset = Administration.objects.all()
272
        self.fields.get("questions").child.queryset = Questions.objects.all()
1✔
273
        form_id = self.context.get("form_id")
1✔
274
        self.fields.get("parent").queryset = FormData.objects.filter(
1✔
275
            form_id=form_id
276
        ).all()
277

278

279
class ListFormDataSerializer(serializers.ModelSerializer):
1✔
280
    created_by = serializers.SerializerMethodField()
1✔
281
    updated_by = serializers.SerializerMethodField()
1✔
282
    created = serializers.SerializerMethodField()
1✔
283
    updated = serializers.SerializerMethodField()
1✔
284
    administration = serializers.SerializerMethodField()
1✔
285
    pending_data = serializers.SerializerMethodField()
1✔
286

287
    @extend_schema_field(OpenApiTypes.STR)
1✔
288
    def get_created_by(self, instance: FormData):
1✔
289
        return instance.created_by.get_full_name()
1✔
290

291
    @extend_schema_field(OpenApiTypes.STR)
1✔
292
    def get_updated_by(self, instance: FormData):
1✔
293
        if instance.updated_by:
1✔
294
            return instance.updated_by.get_full_name()
1✔
295
        return None
1✔
296

297
    @extend_schema_field(OpenApiTypes.STR)
1✔
298
    def get_created(self, instance: FormData):
1✔
299
        return update_date_time_format(instance.created)
1✔
300

301
    @extend_schema_field(OpenApiTypes.STR)
1✔
302
    def get_updated(self, instance: FormData):
1✔
303
        return update_date_time_format(instance.updated)
1✔
304

305
    @extend_schema_field(
1✔
306
        inline_serializer(
307
            "HasPendingData",
308
            fields={
309
                "id": serializers.IntegerField(),
310
                "created_by": serializers.CharField(),
311
            },
312
        )
313
    )
314
    def get_pending_data(self, instance: FormData):
1✔
315
        batch = None
1✔
316
        pending_data = PendingFormData.objects.select_related(
1✔
317
            "created_by"
318
        ).filter(data=instance.pk).first()
319
        if pending_data:
1✔
320
            batch = PendingDataBatch.objects.filter(
1✔
321
                pk=pending_data.batch_id
322
            ).first()
323
        if pending_data and (not batch or not batch.approved):
1!
324
            return {
×
325
                "id": pending_data.id,
326
                "created_by": pending_data.created_by.get_full_name(),
327
            }
328
        return None
1✔
329

330
    def get_administration(self, instance: FormData):
1✔
331
        return " - ".join(instance.administration.full_name.split("-")[1:])
1✔
332

333
    class Meta:
1✔
334
        model = FormData
1✔
335
        fields = [
1✔
336
            "id",
337
            "uuid",
338
            "name",
339
            "form",
340
            "administration",
341
            "geo",
342
            "created_by",
343
            "updated_by",
344
            "created",
345
            "updated",
346
            "pending_data",
347
            "submission_type",
348
        ]
349

350

351
class ListOptionsChartCriteriaSerializer(serializers.Serializer):
1✔
352
    question = CustomPrimaryKeyRelatedField(queryset=Questions.objects.none())
1✔
353
    option = CustomListField()
1✔
354

355
    def __init__(self, **kwargs):
1✔
356
        super().__init__(**kwargs)
×
357
        self.fields.get("question").queryset = Questions.objects.all()
×
358

359

360
class ListPendingFormDataRequestSerializer(serializers.Serializer):
1✔
361
    administration = CustomPrimaryKeyRelatedField(
1✔
362
        queryset=Administration.objects.none(), required=False
363
    )
364

365
    def __init__(self, **kwargs):
1✔
366
        super().__init__(**kwargs)
×
367
        self.fields.get(
×
368
            "administration"
369
        ).queryset = Administration.objects.all()
370

371

372
class ListPendingDataAnswerSerializer(serializers.ModelSerializer):
1✔
373
    history = serializers.SerializerMethodField()
1✔
374
    value = serializers.SerializerMethodField()
1✔
375
    last_value = serializers.SerializerMethodField()
1✔
376

377
    @extend_schema_field(AnswerHistorySerializer(many=True))
1✔
378
    def get_history(self, instance):
1✔
379
        pending_answer_history = PendingAnswerHistory.objects.filter(
1✔
380
            pending_data=instance.pending_data, question=instance.question
381
        ).all()
382
        history = []
1✔
383
        for h in pending_answer_history:
1✔
384
            history.append(get_answer_history(h))
1✔
385
        return history if history else None
1✔
386

387
    @extend_schema_field(OpenApiTypes.ANY)
1✔
388
    def get_value(self, instance: Answers):
1✔
389
        return get_answer_value(instance)
1✔
390

391
    @extend_schema_field(OpenApiTypes.ANY)
1✔
392
    def get_last_value(self, instance: Answers):
1✔
393
        if self.context["last_data"]:
1!
394
            answer = (
×
395
                self.context["last_data"]
396
                .data_answer.filter(question=instance.question)
397
                .first()
398
            )
399
            if answer:
×
400
                return get_answer_value(answer=answer)
×
401
        return None
1✔
402

403
    class Meta:
1✔
404
        model = PendingAnswers
1✔
405
        fields = ["history", "question", "value", "last_value"]
1✔
406

407

408
class PendingBatchDataFilterSerializer(serializers.Serializer):
1✔
409
    approved = CustomBooleanField(default=False)
1✔
410
    subordinate = CustomBooleanField(default=False)
1✔
411

412

413
class ListPendingDataBatchSerializer(serializers.ModelSerializer):
1✔
414
    created_by = serializers.SerializerMethodField()
1✔
415
    created = serializers.SerializerMethodField()
1✔
416
    approver = serializers.SerializerMethodField()
1✔
417
    form = serializers.SerializerMethodField()
1✔
418
    administration = serializers.SerializerMethodField()
1✔
419
    total_data = serializers.SerializerMethodField()
1✔
420

421
    @extend_schema_field(OpenApiTypes.STR)
1✔
422
    def get_created_by(self, instance: PendingDataBatch):
1✔
423
        return instance.user.get_full_name()
1✔
424

425
    @extend_schema_field(OpenApiTypes.INT)
1✔
426
    def get_total_data(self, instance: PendingDataBatch):
1✔
427
        return instance.batch_pending_data_batch.count()
1✔
428

429
    @extend_schema_field(CommonDataSerializer)
1✔
430
    def get_form(self, instance: PendingDataBatch):
1✔
431
        return {
1✔
432
            "id": instance.form.id,
433
            "name": instance.form.name,
434
            "approval_instructions": instance.form.approval_instructions,
435
        }
436

437
    @extend_schema_field(CommonDataSerializer)
1✔
438
    def get_administration(self, instance: PendingDataBatch):
1✔
439
        return {
1✔
440
            "id": instance.administration_id,
441
            "name": instance.administration.name,
442
        }
443

444
    @extend_schema_field(OpenApiTypes.STR)
1✔
445
    def get_created(self, instance: PendingDataBatch):
1✔
446
        return update_date_time_format(instance.created)
1✔
447

448
    @extend_schema_field(
1✔
449
        inline_serializer(
450
            "PendingBatchApprover",
451
            fields={
452
                "id": serializers.IntegerField(),
453
                "name": serializers.CharField(),
454
                "status": serializers.IntegerField(),
455
                "status_text": serializers.CharField(),
456
                "allow_approve": serializers.BooleanField(),
457
            },
458
        )
459
    )
460
    def get_approver(self, instance: PendingDataBatch):
1✔
461
        user: SystemUser = self.context.get("user")
1✔
462
        approved: bool = self.context.get("approved")
1✔
463
        next_level = (
1✔
464
            user.user_access.administration.level.level - 1
465
            if approved
466
            else user.user_access.administration.level.level + 1
467
        )
468
        approval = (
1✔
469
            instance.batch_approval.filter(
470
                level__level=next_level, status=DataApprovalStatus.pending
471
            )
472
            .order_by("-level__level")
473
            .first()
474
        )
475
        rejected: PendingDataApproval = instance.batch_approval.filter(
1✔
476
            status=DataApprovalStatus.rejected
477
        ).first()
478
        lowest_level = Levels.objects.order_by("-id")[1:2].first()
1✔
479
        data = {}
1✔
480
        if rejected:
1✔
481
            data["id"] = rejected.user.pk
1✔
482
            data["status"] = rejected.status
1✔
483
            data["status_text"] = DataApprovalStatus.FieldStr.get(
1✔
484
                rejected.status
485
            )
486
            if rejected.user.user_access.administration.level == lowest_level:
1!
487
                data["name"] = instance.user.get_full_name()
1✔
488
            else:
489
                prev_level = (
×
490
                    rejected.user.user_access.administration.level.level + 1
491
                )
492
                waiting_on = instance.batch_approval.filter(
×
493
                    level__level=prev_level
494
                ).first()
495
                data["name"] = waiting_on.user.get_full_name()
×
496
        if not rejected and approval:
1✔
497
            data["id"] = approval.user.pk
1✔
498
            data["name"] = approval.user.get_full_name()
1✔
499
            data["status"] = approval.status
1✔
500
            data["status_text"] = DataApprovalStatus.FieldStr.get(
1✔
501
                approval.status
502
            )
503
            if approval.status == DataApprovalStatus.approved:
1!
504
                data["allow_approve"] = True
×
505
            else:
506
                data["allow_approve"] = False
1✔
507
        if not approval and not rejected:
1✔
508
            approval = instance.batch_approval.get(user=user)
1✔
509
            data["id"] = approval.user.pk
1✔
510
            data["name"] = approval.user.get_full_name()
1✔
511
            data["status"] = approval.status
1✔
512
            data["status_text"] = DataApprovalStatus.FieldStr.get(
1✔
513
                approval.status
514
            )
515
            data["allow_approve"] = True
1✔
516
        final_approved = instance.batch_approval.filter(
1✔
517
            status=DataApprovalStatus.approved, level__level=1
518
        ).count()
519
        if final_approved:
1!
520
            data["name"] = "-"
×
521
        return data
1✔
522

523
    class Meta:
1✔
524
        model = PendingDataBatch
1✔
525
        fields = [
1✔
526
            "id",
527
            "name",
528
            "form",
529
            "administration",
530
            "created_by",
531
            "created",
532
            "approver",
533
            "approved",
534
            "total_data",
535
        ]
536

537

538
class ListPendingFormDataSerializer(serializers.ModelSerializer):
1✔
539
    created_by = serializers.SerializerMethodField()
1✔
540
    created = serializers.SerializerMethodField()
1✔
541
    administration = serializers.ReadOnlyField(source="administration.name")
1✔
542
    pending_answer_history = serializers.SerializerMethodField()
1✔
543
    submission_type = CustomChoiceField(choices=SubmissionTypes.FieldStr)
1✔
544

545
    @extend_schema_field(OpenApiTypes.STR)
1✔
546
    def get_created_by(self, instance: PendingFormData):
1✔
547
        return instance.created_by.get_full_name()
1✔
548

549
    @extend_schema_field(OpenApiTypes.STR)
1✔
550
    def get_created(self, instance: PendingFormData):
1✔
551
        return update_date_time_format(instance.created)
1✔
552

553
    @extend_schema_field(OpenApiTypes.BOOL)
1✔
554
    def get_pending_answer_history(self, instance: PendingFormData):
1✔
555
        history = PendingAnswerHistory.objects.filter(
1✔
556
            pending_data=instance
557
        ).count()
558
        return True if history > 0 else False
1✔
559

560
    class Meta:
1✔
561
        model = PendingFormData
1✔
562
        fields = [
1✔
563
            "id",
564
            "uuid",
565
            "data_id",
566
            "name",
567
            "form",
568
            "administration",
569
            "geo",
570
            "submitter",
571
            "duration",
572
            "created_by",
573
            "created",
574
            "submission_type",
575
            "pending_answer_history",
576
        ]
577

578

579
class ApprovePendingDataRequestSerializer(serializers.Serializer):
1✔
580
    batch = CustomPrimaryKeyRelatedField(
1✔
581
        queryset=PendingDataBatch.objects.none()
582
    )
583
    status = CustomChoiceField(
1✔
584
        choices=[DataApprovalStatus.approved, DataApprovalStatus.rejected]
585
    )
586
    comment = CustomCharField(required=False)
1✔
587

588
    def __init__(self, **kwargs):
1✔
589
        super().__init__(**kwargs)
1✔
590
        user: SystemUser = self.context.get("user")
1✔
591
        if user:
1✔
592
            self.fields.get(
1✔
593
                "batch"
594
            ).queryset = PendingDataBatch.objects.filter(
595
                batch_approval__user=user, approved=False
596
            )
597

598
    def create(self, validated_data):
1✔
599
        batch: PendingDataBatch = validated_data.get("batch")
1✔
600
        user = self.context.get("user")
1✔
601
        comment = validated_data.get("comment")
1✔
602
        user_level = user.user_access.administration.level
1✔
603
        approval = PendingDataApproval.objects.get(user=user, batch=batch)
1✔
604
        approval.status = validated_data.get("status")
1✔
605
        approval.save()
1✔
606
        first_data = PendingFormData.objects.filter(batch=batch).first()
1✔
607
        data_count = PendingFormData.objects.filter(batch=batch).count()
1✔
608
        data = {
1✔
609
            "send_to": [first_data.created_by.email],
610
            "batch": batch,
611
            "user": user,
612
        }
613
        listing = [
1✔
614
            {
615
                "name": "Batch Name",
616
                "value": batch.name,
617
            },
618
            {
619
                "name": "Number of Records",
620
                "value": data_count,
621
            },
622
            {
623
                "name": "Questionnaire",
624
                "value": batch.form.name,
625
            },
626
        ]
627
        if approval.status == DataApprovalStatus.approved:
1✔
628
            listing.append(
1✔
629
                {
630
                    "name": "Approver",
631
                    "value": f"{user.name}, {user.designation_name}",
632
                }
633
            )
634
            if comment:
1!
635
                listing.append({"name": "Comment", "value": comment})
1✔
636
            data.update(
1✔
637
                {
638
                    "listing": listing,
639
                    "extend_body": """
640
                Further approvals may be required before data is finalised.
641
                You can also track your data approval in the RUSH platform
642
                [My Profile > Data uploads > Pending Approval/Approved]
643
                """,
644
                }
645
            )
646
            send_email(context=data, type=EmailTypes.batch_approval)
1✔
647
        else:
648
            listing.append(
1✔
649
                {
650
                    "name": "Rejector",
651
                    "value": f"{user.name}, {user.designation_name}",
652
                }
653
            )
654
            if comment:
1!
655
                listing.append({"name": "Comment", "value": comment})
1✔
656
            # rejection request change to user
657
            data.update(
1✔
658
                {
659
                    "listing": listing,
660
                    "extend_body": """
661
                You can also access the rejected data in the RUSH platform
662
                [My Profile > Data uploads > Rejected]
663
                """,
664
                }
665
            )
666
            send_email(context=data, type=EmailTypes.batch_rejection)
1✔
667
            # send email to lower approval
668
            lower_approvals = PendingDataApproval.objects.filter(
1✔
669
                batch=batch, level__level__gt=user_level.level
670
            ).all()
671
            # filter --> send email only to lower approval
672
            lower_approval_user_ids = [u.user_id for u in lower_approvals]
1✔
673
            lower_approval_users = SystemUser.objects.filter(
1✔
674
                id__in=lower_approval_user_ids, deleted_at=None
675
            ).all()
676
            lower_approval_emails = [
1✔
677
                u.email for u in lower_approval_users if u.email != user.email
678
            ]
679
            if lower_approval_emails:
1!
680
                inform_data = {
×
681
                    "send_to": lower_approval_emails,
682
                    "listing": listing,
683
                    "extend_body": """
684
                    The data submitter has also been notified.
685
                    They can modify the data and submit again for approval
686
                    """,
687
                }
688
                send_email(
×
689
                    context=inform_data,
690
                    type=EmailTypes.inform_batch_rejection_approver,
691
                )
692
            # change approval status to pending
693
            # for la in lower_approvals:
694
            #     la.status = DataApprovalStatus.pending
695
            #     la.save()
696
        if validated_data.get("comment"):
1!
697
            PendingDataBatchComments.objects.create(
1✔
698
                user=user, batch=batch, comment=validated_data.get("comment")
699
            )
700
        if not PendingDataApproval.objects.filter(
1✔
701
            batch=batch,
702
            status__in=[
703
                DataApprovalStatus.pending,
704
                DataApprovalStatus.rejected,
705
            ],
706
        ).count():
707
            pending_data_list = PendingFormData.objects.filter(
1✔
708
                batch=batch
709
            ).all()
710
            # Seed data via Async Task
711
            for data in pending_data_list:
1✔
712
                async_task("api.v1.v1_data.tasks.seed_approved_data", data)
1✔
713
            batch.approved = True
1✔
714
            batch.updated = timezone.now()
1✔
715
            batch.save()
1✔
716
        return object
1✔
717

718
    def update(self, instance, validated_data):
1✔
719
        pass
×
720

721

722
class ListBatchSerializer(serializers.ModelSerializer):
1✔
723
    form = serializers.SerializerMethodField()
1✔
724
    administration = serializers.SerializerMethodField()
1✔
725
    file = serializers.SerializerMethodField()
1✔
726
    total_data = serializers.SerializerMethodField()
1✔
727
    status = serializers.ReadOnlyField(source="approved")
1✔
728
    approvers = serializers.SerializerMethodField()
1✔
729
    created = serializers.SerializerMethodField()
1✔
730
    updated = serializers.SerializerMethodField()
1✔
731

732
    @extend_schema_field(CommonDataSerializer)
1✔
733
    def get_form(self, instance: PendingDataBatch):
1✔
734
        return {
1✔
735
            "id": instance.form.id,
736
            "name": instance.form.name,
737
            "approval_instructions": instance.form.approval_instructions,
738
        }
739

740
    @extend_schema_field(CommonDataSerializer)
1✔
741
    def get_administration(self, instance: PendingDataBatch):
1✔
742
        return {
1✔
743
            "id": instance.administration_id,
744
            "name": instance.administration.name,
745
        }
746

747
    @extend_schema_field(
1✔
748
        inline_serializer(
749
            "BatchFile",
750
            fields={
751
                "name": serializers.CharField(),
752
                "file": serializers.URLField(),
753
            },
754
        )
755
    )
756
    def get_file(self, instance: PendingDataBatch):
1✔
757
        if instance.file:
1!
758
            path = instance.file
×
759
            first_pos = path.rfind("/")
×
760
            last_pos = len(path)
×
761
            return {
×
762
                "name": path[first_pos + 1: last_pos],
763
                "file": instance.file,
764
            }
765
        return None
1✔
766

767
    @extend_schema_field(OpenApiTypes.INT)
1✔
768
    def get_total_data(self, instance: PendingDataBatch):
1✔
769
        return instance.batch_pending_data_batch.all().count()
1✔
770

771
    @extend_schema_field(
1✔
772
        inline_serializer(
773
            "BatchApprover",
774
            fields={
775
                "name": serializers.CharField(),
776
                "administration": serializers.CharField(),
777
                "status": serializers.IntegerField(),
778
                "status_text": serializers.CharField(),
779
            },
780
            many=True,
781
        )
782
    )
783
    def get_approvers(self, instance: PendingDataBatch):
1✔
784
        data = []
1✔
785
        approvers = instance.batch_approval.order_by(
1✔
786
            "level"
787
        ).all()
788
        for approver in approvers:
1✔
789
            approver_administration = approver.user.user_access.administration
1✔
790
            data.append(
1✔
791
                {
792
                    "name": approver.user.get_full_name(),
793
                    "administration": approver_administration.name,
794
                    "status": approver.status,
795
                    "status_text": DataApprovalStatus.FieldStr.get(
796
                        approver.status
797
                    ),
798
                }
799
            )
800
        return data
1✔
801

802
    @extend_schema_field(OpenApiTypes.DATE)
1✔
803
    def get_created(self, instance):
1✔
804
        return update_date_time_format(instance.created)
1✔
805

806
    @extend_schema_field(OpenApiTypes.DATE)
1✔
807
    def get_updated(self, instance):
1✔
808
        return update_date_time_format(instance.updated)
1✔
809

810
    class Meta:
1✔
811
        model = PendingDataBatch
1✔
812
        fields = [
1✔
813
            "id",
814
            "name",
815
            "form",
816
            "administration",
817
            "file",
818
            "total_data",
819
            "created",
820
            "updated",
821
            "status",
822
            "approvers",
823
        ]
824

825

826
class ListBatchSummarySerializer(serializers.ModelSerializer):
1✔
827
    id = serializers.ReadOnlyField(source="question.id")
1✔
828
    question = serializers.ReadOnlyField(source="question.label")
1✔
829
    type = serializers.SerializerMethodField()
1✔
830
    value = serializers.SerializerMethodField()
1✔
831

832
    @extend_schema_field(
1✔
833
        CustomChoiceField(
834
            choices=[QuestionTypes.FieldStr[d] for d in QuestionTypes.FieldStr]
835
        )
836
    )
837
    def get_type(self, instance):
1✔
838
        return QuestionTypes.FieldStr.get(instance.question.type)
×
839

840
    @extend_schema_field(OpenApiTypes.ANY)
1✔
841
    def get_value(self, instance: PendingAnswers):
1✔
842
        batch: PendingDataBatch = self.context.get("batch")
×
843
        if instance.question.type == QuestionTypes.number:
×
844
            val = PendingAnswers.objects.filter(
×
845
                pending_data__batch=batch, question_id=instance.question.id
846
            ).aggregate(Sum("value"))
847
            return val.get("value__sum")
×
848
        elif instance.question.type == QuestionTypes.administration:
×
849
            return (
×
850
                PendingAnswers.objects.filter(
851
                    pending_data__batch=batch, question_id=instance.question.id
852
                )
853
                .distinct("value")
854
                .count()
855
            )
856
        else:
857
            data = []
×
858
            for option in instance.question.options.all():
×
859
                val = PendingAnswers.objects.filter(
×
860
                    pending_data__batch=batch,
861
                    question_id=instance.question.id,
862
                    options__contains=option.value,
863
                ).count()
864
                data.append({"type": option.label, "total": val})
×
865
            return data
×
866

867
    class Meta:
1✔
868
        model = PendingAnswers
1✔
869
        fields = ["id", "question", "type", "value"]
1✔
870

871

872
class ListBatchCommentSerializer(serializers.ModelSerializer):
1✔
873
    user = serializers.SerializerMethodField()
1✔
874
    created = serializers.SerializerMethodField()
1✔
875

876
    @extend_schema_field(
1✔
877
        inline_serializer(
878
            "BatchUserComment",
879
            fields={
880
                "name": serializers.CharField(),
881
                "email": serializers.CharField(),
882
            },
883
        )
884
    )
885
    def get_user(self, instance: PendingDataBatchComments):
1✔
886
        return {
1✔
887
            "name": instance.user.get_full_name(),
888
            "email": instance.user.email,
889
        }
890

891
    @extend_schema_field(OpenApiTypes.DATE)
1✔
892
    def get_created(self, instance: PendingDataBatchComments):
1✔
893
        return update_date_time_format(instance.created)
1✔
894

895
    class Meta:
1✔
896
        model = PendingDataBatchComments
1✔
897
        fields = ["user", "comment", "created"]
1✔
898

899

900
class BatchListRequestSerializer(serializers.Serializer):
1✔
901
    approved = CustomBooleanField(default=False)
1✔
902
    form = CustomPrimaryKeyRelatedField(
1✔
903
        queryset=Forms.objects.all(), required=False
904
    )
905

906

907
class CreateBatchSerializer(serializers.Serializer):
1✔
908
    name = CustomCharField()
1✔
909
    comment = CustomCharField(required=False)
1✔
910
    data = CustomListField(
1✔
911
        child=CustomPrimaryKeyRelatedField(
912
            queryset=PendingFormData.objects.none()
913
        ),
914
        required=False,
915
    )
916

917
    def __init__(self, **kwargs):
1✔
918
        super().__init__(**kwargs)
1✔
919
        self.fields.get("data").child.queryset = PendingFormData.objects.all()
1✔
920

921
    def validate_name(self, name):
1✔
922
        if PendingDataBatch.objects.filter(name__iexact=name).exists():
1✔
923
            raise ValidationError("name has already been taken")
1✔
924
        return name
1✔
925

926
    def validate_data(self, data):
1✔
927
        if len(data) == 0:
1✔
928
            raise ValidationError("No data found for this batch")
1✔
929
        return data
1✔
930

931
    def validate(self, attrs):
1✔
932
        if len(attrs.get("data")) == 0:
1!
933
            raise ValidationError(
×
934
                {"data": "No form found for this batch"}
935
            )
936
        form = attrs.get("data")[0].form
1✔
937
        # Get the list of administrations that the user has access to
938
        adms = [
1✔
939
            ac.pk
940
            for ac in self.context.get("user")
941
            .user_access.administration.ancestors
942
        ]
943
        adms += [
1✔
944
            self.context.get("user").user_access.administration.pk
945
        ]
946
        # Check if the form has any approvers in the user's administration
947
        if not form.form_data_approval.filter(
1!
948
            administration__pk__in=adms
949
        ).exists():
950
            raise ValidationError(
×
951
                {"data": "No approvers found for this batch"}
952
            )
953
        for pending in attrs.get("data"):
1✔
954
            if pending.form_id != form.id:
1!
955
                raise ValidationError({
×
956
                    "data": (
957
                        "Mismatched form ID for one or more"
958
                        " pending data items."
959
                    )
960
                })
961
        return attrs
1✔
962

963
    def create(self, validated_data):
1✔
964
        form_id = validated_data.get("data")[0].form_id
1✔
965
        user: SystemUser = validated_data.get("user")
1✔
966
        path = "{0}{1}".format(
1✔
967
            user.user_access.administration.path,
968
            user.user_access.administration_id,
969
        )
970
        obj = PendingDataBatch.objects.create(
1✔
971
            form_id=form_id,
972
            administration_id=user.user_access.administration_id,
973
            user=user,
974
            name=validated_data.get("name"),
975
        )
976
        for data in validated_data.get("data"):
1✔
977
            data.batch = obj
1✔
978
            data.save()
1✔
979
        for administration in Administration.objects.filter(
1✔
980
            id__in=path.split(".")
981
        ):
982
            assignment = FormApprovalAssignment.objects.filter(
1✔
983
                form_id=form_id, administration=administration
984
            ).first()
985
            if assignment:
1✔
986
                level = assignment.user.user_access.administration.level_id
1✔
987
                PendingDataApproval.objects.create(
1✔
988
                    batch=obj, user=assignment.user, level_id=level
989
                )
990
                number_of_records = PendingFormData.objects.filter(
1✔
991
                    batch=obj
992
                ).count()
993
                data = {
1✔
994
                    "send_to": [assignment.user.email],
995
                    "listing": [
996
                        {"name": "Batch Name", "value": obj.name},
997
                        {"name": "Questionnaire", "value": obj.form.name},
998
                        {
999
                            "name": "Number of Records",
1000
                            "value": number_of_records,
1001
                        },
1002
                        {
1003
                            "name": "Submitter",
1004
                            "value": f"""{obj.user.name},
1005
                        {obj.user.designation_name}""",
1006
                        },
1007
                    ],
1008
                }
1009
                send_email(context=data, type=EmailTypes.pending_approval)
1✔
1010
        if validated_data.get("comment"):
1!
1011
            PendingDataBatchComments.objects.create(
1✔
1012
                user=user, batch=obj, comment=validated_data.get("comment")
1013
            )
1014
        return obj
1✔
1015

1016
    def update(self, instance, validated_data):
1✔
1017
        pass
×
1018

1019

1020
class SubmitPendingFormDataSerializer(serializers.ModelSerializer):
1✔
1021
    administration = CustomPrimaryKeyRelatedField(
1✔
1022
        queryset=Administration.objects.none()
1023
    )
1024
    name = CustomCharField()
1✔
1025
    geo = CustomListField(required=False, allow_null=True)
1✔
1026
    submitter = CustomCharField(required=False)
1✔
1027
    duration = CustomIntegerField(required=False)
1✔
1028
    uuid = serializers.CharField(required=False)
1✔
1029
    submission_type = CustomChoiceField(
1✔
1030
        choices=SubmissionTypes.FieldStr, required=True
1031
    )
1032

1033
    def __init__(self, **kwargs):
1✔
1034
        super().__init__(**kwargs)
1✔
1035
        self.fields.get(
1✔
1036
            "administration"
1037
        ).queryset = Administration.objects.all()
1038

1039
    class Meta:
1✔
1040
        model = PendingFormData
1✔
1041
        fields = [
1✔
1042
            "name",
1043
            "geo",
1044
            "administration",
1045
            "submitter",
1046
            "duration",
1047
            "uuid",
1048
            "submission_type",
1049
        ]
1050

1051

1052
class SubmitPendingFormDataAnswerSerializer(serializers.ModelSerializer):
1✔
1053
    value = UnvalidatedField(allow_null=False)
1✔
1054
    question = CustomPrimaryKeyRelatedField(queryset=Questions.objects.none())
1✔
1055

1056
    def __init__(self, **kwargs):
1✔
1057
        super().__init__(**kwargs)
1✔
1058
        self.fields.get("question").queryset = Questions.objects.all()
1✔
1059

1060
    def validate_value(self, value):
1✔
1061
        return value
1✔
1062

1063
    def validate(self, attrs):
1✔
1064
        question = attrs.get("question")
1✔
1065
        value = attrs.get("value")
1✔
1066

1067
        if value == "":
1✔
1068
            raise ValidationError(
1✔
1069
                f"Value is required for Question: {question.id}"
1070
            )
1071

1072
        if isinstance(value, list) and len(value) == 0:
1✔
1073
            raise ValidationError(
1✔
1074
                f"Value is required for Question: {question.id}"
1075
            )
1076

1077
        if not isinstance(value, list) and question.type in [
1!
1078
            QuestionTypes.geo,
1079
            QuestionTypes.option,
1080
            QuestionTypes.multiple_option,
1081
        ]:
1082
            raise ValidationError(
×
1083
                f"Valid list value is required for Question: {question.id}"
1084
            )
1085

1086
        elif not isinstance(value, str) and question.type in [
1!
1087
            QuestionTypes.text,
1088
            QuestionTypes.photo,
1089
            QuestionTypes.date,
1090
            QuestionTypes.attachment,
1091
        ]:
1092
            raise ValidationError(
×
1093
                f"Valid string value is required for Question: {question.id}"
1094
            )
1095

1096
        elif not (
1!
1097
            isinstance(value, int) or isinstance(value, float)) \
1098
            and question.type in [
1099
            QuestionTypes.number,
1100
            QuestionTypes.administration,
1101
            QuestionTypes.cascade,
1102
        ]:
1103
            raise ValidationError(
×
1104
                f"Valid number value is required for Question: {question.id}"
1105
            )
1106

1107
        if question.type == QuestionTypes.administration:
1✔
1108
            attrs["value"] = int(float(value))
1✔
1109

1110
        return attrs
1✔
1111

1112
    class Meta:
1✔
1113
        model = PendingAnswers
1✔
1114
        fields = ["question", "value"]
1✔
1115

1116

1117
class SubmitPendingFormSerializer(serializers.Serializer):
1✔
1118
    data = SubmitPendingFormDataSerializer()
1✔
1119
    answer = SubmitPendingFormDataAnswerSerializer(many=True)
1✔
1120

1121
    def __init__(self, **kwargs):
1✔
1122
        super().__init__(**kwargs)
1✔
1123

1124
    def create(self, validated_data):
1✔
1125
        data = validated_data.get("data")
1✔
1126
        data["form"] = self.context.get("form")
1✔
1127
        data["created_by"] = self.context.get("user")
1✔
1128

1129
        # check user role and form type
1130
        user = self.context.get("user")
1✔
1131
        is_super_admin = user.user_access.role == UserRoleTypes.super_admin
1✔
1132

1133
        direct_to_data = is_super_admin
1✔
1134
        # check if the form has any approvers in the user's administration
1135
        # if no approvers found, save directly to form data
1136
        if not direct_to_data:
1✔
1137
            adms = (
1✔
1138
                [
1139
                    ac.pk
1140
                    for ac in user.user_access.administration.ancestors
1141
                ] + [
1142
                    user.user_access.administration.pk
1143
                ]
1144
            )
1145
            form_approval = FormApprovalAssignment.objects.filter(
1✔
1146
                form=data["form"],
1147
                administration__pk__in=adms
1148
            ).exists()
1149
            if not form_approval:
1✔
1150
                direct_to_data = True
1✔
1151

1152
        # save to pending data
1153
        if not direct_to_data:
1✔
1154
            obj_data = self.fields.get("data").create(data)
1✔
1155

1156
        # save to form data
1157
        if direct_to_data:
1✔
1158
            obj_data = FormData.objects.create(
1✔
1159
                name=data.get("name"),
1160
                form=data.get("form"),
1161
                administration=data.get("administration"),
1162
                geo=data.get("geo"),
1163
                created_by=data.get("created_by"),
1164
                created=data.get("submitedAt") or timezone.now(),
1165
                submission_type=data.get("submission_type"),
1166
            )
1167

1168
        pending_answers = []
1✔
1169
        answers = []
1✔
1170

1171
        for answer in validated_data.get("answer"):
1✔
1172
            question = answer.get("question")
1✔
1173
            name = None
1✔
1174
            value = None
1✔
1175
            option = None
1✔
1176

1177
            if question.meta_uuid:
1✔
1178
                obj_data.uuid = answer.get("value")
1✔
1179
                obj_data.save()
1✔
1180

1181
            if question.type in [
1✔
1182
                QuestionTypes.geo,
1183
                QuestionTypes.option,
1184
                QuestionTypes.multiple_option,
1185
            ]:
1186
                option = answer.get("value")
1✔
1187
            elif question.type in [
1✔
1188
                QuestionTypes.text,
1189
                QuestionTypes.photo,
1190
                QuestionTypes.date,
1191
                QuestionTypes.autofield,
1192
                QuestionTypes.attachment,
1193
            ]:
1194
                name = answer.get("value")
1✔
1195
            elif question.type == QuestionTypes.cascade:
1!
1196
                id = answer.get("value")
×
1197
                val = None
×
1198
                if question.api:
×
1199
                    ep = question.api.get("endpoint")
×
1200
                    if "organisation" in ep:
×
1201
                        name = Organisation.objects.filter(pk=id).values_list(
×
1202
                            'name', flat=True).first()
1203
                        val = name
×
1204
                    if "entity-data" in ep:
×
1205
                        name = EntityData.objects.filter(pk=id).values_list(
×
1206
                            'name', flat=True).first()
1207
                        val = name
×
1208
                    if "entity-data" not in ep and "organisation" not in ep:
×
1209
                        ep = ep.split("?")[0]
×
1210
                        ep = f"{ep}?id={id}"
×
1211
                        val = requests.get(ep).json()
×
1212
                        val = val[0].get("name")
×
1213

1214
                if question.extra:
×
1215
                    cs_type = question.extra.get("type")
×
1216
                    if cs_type == "entity":
×
1217
                        name = EntityData.objects.filter(pk=id).values_list(
×
1218
                            'name', flat=True).first()
1219
                        val = name
×
1220
                name = val
×
1221
            else:
1222
                # for administration,number question type
1223
                value = answer.get("value")
1✔
1224

1225
            # save to pending answer
1226
            if not direct_to_data:
1✔
1227
                pending_answers.append(PendingAnswers(
1✔
1228
                    pending_data=obj_data,
1229
                    question=question,
1230
                    name=name,
1231
                    value=value,
1232
                    options=option,
1233
                    created_by=self.context.get("user"),
1234
                    created=data.get("submitedAt") or timezone.now(),
1235
                ))
1236

1237
            # save to form data
1238
            if direct_to_data:
1✔
1239
                answers.append(Answers(
1✔
1240
                    data=obj_data,
1241
                    question=question,
1242
                    name=name,
1243
                    value=value,
1244
                    options=option,
1245
                    created_by=self.context.get("user"),
1246
                ))
1247

1248
        # bulk create pending answers / answers
1249
        if pending_answers:
1✔
1250
            PendingAnswers.objects.bulk_create(pending_answers)
1✔
1251
        if answers:
1✔
1252
            Answers.objects.bulk_create(answers)
1✔
1253

1254
        if direct_to_data:
1✔
1255
            if data.get("uuid"):
1!
1256
                obj_data.uuid = data["uuid"]
×
1257
            obj_data.save()
1✔
1258
            obj_data.save_to_file
1✔
1259

1260
        return obj_data
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

© 2025 Coveralls, Inc