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

akvo / iwsims-demo / #72

28 Apr 2025 03:28AM UTC coverage: 86.134% (+1.1%) from 85.024%
#72

push

coveralls-python

web-flow
Merge pull request #20 from akvo/feature/19-eng-1231-dynamic-level-approval

Feature/19 eng 1231 dynamic level approval

2646 of 3188 branches covered (83.0%)

Branch coverage included in aggregate %.

5995 of 6844 relevant lines covered (87.59%)

0.88 hits per line

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

85.89
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
        ]:
111
            raise ValidationError(
×
112
                "Valid string value is required for Question:{0}".format(
113
                    attrs.get("question").id
114
                )
115
            )
116
        elif not (
1!
117
            isinstance(attrs.get("value"), int)
118
            or isinstance(attrs.get("value"), float)
119
        ) and attrs.get("question").type in [
120
            QuestionTypes.number,
121
            QuestionTypes.administration,
122
            QuestionTypes.cascade,
123
        ]:
124
            raise ValidationError(
×
125
                "Valid number value is required for Question:{0}".format(
126
                    attrs.get("question").id
127
                )
128
            )
129

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

133
        return attrs
1✔
134

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

139

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

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

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

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

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

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

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

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

220
        return object
1✔
221

222

223
class AnswerHistorySerializer(serializers.Serializer):
1✔
224
    value = serializers.FloatField()
1✔
225
    created = CustomCharField()
1✔
226
    created_by = CustomCharField()
1✔
227

228

229
class ListDataAnswerSerializer(serializers.ModelSerializer):
1✔
230
    history = serializers.SerializerMethodField()
1✔
231
    value = serializers.SerializerMethodField()
1✔
232

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

243
    @extend_schema_field(OpenApiTypes.ANY)
1✔
244
    def get_value(self, instance: Answers):
1✔
245
        return get_answer_value(instance)
1✔
246

247
    class Meta:
1✔
248
        model = Answers
1✔
249
        fields = ["history", "question", "value"]
1✔
250

251

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

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

275

276
class ListFormDataSerializer(serializers.ModelSerializer):
1✔
277
    created_by = serializers.SerializerMethodField()
1✔
278
    updated_by = serializers.SerializerMethodField()
1✔
279
    created = serializers.SerializerMethodField()
1✔
280
    updated = serializers.SerializerMethodField()
1✔
281
    administration = serializers.SerializerMethodField()
1✔
282
    pending_data = serializers.SerializerMethodField()
1✔
283

284
    @extend_schema_field(OpenApiTypes.STR)
1✔
285
    def get_created_by(self, instance: FormData):
1✔
286
        return instance.created_by.get_full_name()
1✔
287

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

294
    @extend_schema_field(OpenApiTypes.STR)
1✔
295
    def get_created(self, instance: FormData):
1✔
296
        return update_date_time_format(instance.created)
1✔
297

298
    @extend_schema_field(OpenApiTypes.STR)
1✔
299
    def get_updated(self, instance: FormData):
1✔
300
        return update_date_time_format(instance.updated)
1✔
301

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

327
    def get_administration(self, instance: FormData):
1✔
328
        return " - ".join(instance.administration.full_name.split("-")[1:])
1✔
329

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

347

348
class ListOptionsChartCriteriaSerializer(serializers.Serializer):
1✔
349
    question = CustomPrimaryKeyRelatedField(queryset=Questions.objects.none())
1✔
350
    option = CustomListField()
1✔
351

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

356

357
class ListPendingFormDataRequestSerializer(serializers.Serializer):
1✔
358
    administration = CustomPrimaryKeyRelatedField(
1✔
359
        queryset=Administration.objects.none(), required=False
360
    )
361

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

368

369
class ListPendingDataAnswerSerializer(serializers.ModelSerializer):
1✔
370
    history = serializers.SerializerMethodField()
1✔
371
    value = serializers.SerializerMethodField()
1✔
372
    last_value = serializers.SerializerMethodField()
1✔
373

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

384
    @extend_schema_field(OpenApiTypes.ANY)
1✔
385
    def get_value(self, instance: Answers):
1✔
386
        return get_answer_value(instance)
1✔
387

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

400
    class Meta:
1✔
401
        model = PendingAnswers
1✔
402
        fields = ["history", "question", "value", "last_value"]
1✔
403

404

405
class PendingBatchDataFilterSerializer(serializers.Serializer):
1✔
406
    approved = CustomBooleanField(default=False)
1✔
407
    subordinate = CustomBooleanField(default=False)
1✔
408

409

410
class ListPendingDataBatchSerializer(serializers.ModelSerializer):
1✔
411
    created_by = serializers.SerializerMethodField()
1✔
412
    created = serializers.SerializerMethodField()
1✔
413
    approver = serializers.SerializerMethodField()
1✔
414
    form = serializers.SerializerMethodField()
1✔
415
    administration = serializers.SerializerMethodField()
1✔
416
    total_data = serializers.SerializerMethodField()
1✔
417

418
    @extend_schema_field(OpenApiTypes.STR)
1✔
419
    def get_created_by(self, instance: PendingDataBatch):
1✔
420
        return instance.user.get_full_name()
1✔
421

422
    @extend_schema_field(OpenApiTypes.INT)
1✔
423
    def get_total_data(self, instance: PendingDataBatch):
1✔
424
        return instance.batch_pending_data_batch.count()
1✔
425

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

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

441
    @extend_schema_field(OpenApiTypes.STR)
1✔
442
    def get_created(self, instance: PendingDataBatch):
1✔
443
        return update_date_time_format(instance.created)
1✔
444

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

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

534

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

542
    @extend_schema_field(OpenApiTypes.STR)
1✔
543
    def get_created_by(self, instance: PendingFormData):
1✔
544
        return instance.created_by.get_full_name()
1✔
545

546
    @extend_schema_field(OpenApiTypes.STR)
1✔
547
    def get_created(self, instance: PendingFormData):
1✔
548
        return update_date_time_format(instance.created)
1✔
549

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

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

575

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

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

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

715
    def update(self, instance, validated_data):
1✔
716
        pass
×
717

718

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

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

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

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

764
    @extend_schema_field(OpenApiTypes.INT)
1✔
765
    def get_total_data(self, instance: PendingDataBatch):
1✔
766
        return instance.batch_pending_data_batch.all().count()
1✔
767

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

799
    @extend_schema_field(OpenApiTypes.DATE)
1✔
800
    def get_created(self, instance):
1✔
801
        return update_date_time_format(instance.created)
1✔
802

803
    @extend_schema_field(OpenApiTypes.DATE)
1✔
804
    def get_updated(self, instance):
1✔
805
        return update_date_time_format(instance.updated)
1✔
806

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

822

823
class ListBatchSummarySerializer(serializers.ModelSerializer):
1✔
824
    id = serializers.ReadOnlyField(source="question.id")
1✔
825
    question = serializers.ReadOnlyField(source="question.label")
1✔
826
    type = serializers.SerializerMethodField()
1✔
827
    value = serializers.SerializerMethodField()
1✔
828

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

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

864
    class Meta:
1✔
865
        model = PendingAnswers
1✔
866
        fields = ["id", "question", "type", "value"]
1✔
867

868

869
class ListBatchCommentSerializer(serializers.ModelSerializer):
1✔
870
    user = serializers.SerializerMethodField()
1✔
871
    created = serializers.SerializerMethodField()
1✔
872

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

888
    @extend_schema_field(OpenApiTypes.DATE)
1✔
889
    def get_created(self, instance: PendingDataBatchComments):
1✔
890
        return update_date_time_format(instance.created)
1✔
891

892
    class Meta:
1✔
893
        model = PendingDataBatchComments
1✔
894
        fields = ["user", "comment", "created"]
1✔
895

896

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

903

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

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

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

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

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

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

1013
    def update(self, instance, validated_data):
1✔
1014
        pass
×
1015

1016

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

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

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

1048

1049
class SubmitPendingFormDataAnswerSerializer(serializers.ModelSerializer):
1✔
1050
    value = UnvalidatedField(allow_null=False)
1✔
1051
    question = CustomPrimaryKeyRelatedField(queryset=Questions.objects.none())
1✔
1052

1053
    def __init__(self, **kwargs):
1✔
1054
        super().__init__(**kwargs)
1✔
1055
        self.fields.get("question").queryset = Questions.objects.all()
1✔
1056

1057
    def validate_value(self, value):
1✔
1058
        return value
1✔
1059

1060
    def validate(self, attrs):
1✔
1061
        question = attrs.get("question")
1✔
1062
        value = attrs.get("value")
1✔
1063

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

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

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

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

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

1103
        if question.type == QuestionTypes.administration:
1✔
1104
            attrs["value"] = int(float(value))
1✔
1105

1106
        return attrs
1✔
1107

1108
    class Meta:
1✔
1109
        model = PendingAnswers
1✔
1110
        fields = ["question", "value"]
1✔
1111

1112

1113
class SubmitPendingFormSerializer(serializers.Serializer):
1✔
1114
    data = SubmitPendingFormDataSerializer()
1✔
1115
    answer = SubmitPendingFormDataAnswerSerializer(many=True)
1✔
1116

1117
    def __init__(self, **kwargs):
1✔
1118
        super().__init__(**kwargs)
1✔
1119

1120
    def create(self, validated_data):
1✔
1121
        data = validated_data.get("data")
1✔
1122
        data["form"] = self.context.get("form")
1✔
1123
        data["created_by"] = self.context.get("user")
1✔
1124

1125
        # check user role and form type
1126
        user = self.context.get("user")
1✔
1127
        is_super_admin = user.user_access.role == UserRoleTypes.super_admin
1✔
1128

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

1148
        # save to pending data
1149
        if not direct_to_data:
1✔
1150
            obj_data = self.fields.get("data").create(data)
1✔
1151

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

1164
        pending_answers = []
1✔
1165
        answers = []
1✔
1166

1167
        for answer in validated_data.get("answer"):
1✔
1168
            question = answer.get("question")
1✔
1169
            name = None
1✔
1170
            value = None
1✔
1171
            option = None
1✔
1172

1173
            if question.meta_uuid:
1✔
1174
                obj_data.uuid = answer.get("value")
1✔
1175
                obj_data.save()
1✔
1176

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

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

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

1232
            # save to form data
1233
            if direct_to_data:
1✔
1234
                answers.append(Answers(
1✔
1235
                    data=obj_data,
1236
                    question=question,
1237
                    name=name,
1238
                    value=value,
1239
                    options=option,
1240
                    created_by=self.context.get("user"),
1241
                ))
1242

1243
        # bulk create pending answers / answers
1244
        if pending_answers:
1✔
1245
            PendingAnswers.objects.bulk_create(pending_answers)
1✔
1246
        if answers:
1✔
1247
            Answers.objects.bulk_create(answers)
1✔
1248

1249
        if direct_to_data:
1✔
1250
            if data.get("uuid"):
1!
1251
                obj_data.uuid = data["uuid"]
×
1252
            obj_data.save()
1✔
1253
            obj_data.save_to_file
1✔
1254

1255
        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