diff --git a/backend/api/v1/v1_data/serializers.py b/backend/api/v1/v1_data/serializers.py index ed22fb9e6..4e4e1489e 100644 --- a/backend/api/v1/v1_data/serializers.py +++ b/backend/api/v1/v1_data/serializers.py @@ -36,32 +36,40 @@ def __init__(self, **kwargs): 'question').queryset = Questions.objects.all() def validate_value(self, value): - if value == '': - raise ValidationError('Value is required') - if isinstance(value, list) and len(value) == 0: - raise ValidationError('Value is required') return value def validate(self, attrs): + if attrs.get('value') == '': + raise ValidationError('Value is required for Question:{0}'.format( + attrs.get('question').id)) + + if isinstance(attrs.get('value'), list) and len( + attrs.get('value')) == 0: + raise ValidationError('Value is required for Question:{0}'.format( + attrs.get('question').id)) + if not isinstance(attrs.get('value'), list) and attrs.get( 'question').type in [QuestionTypes.geo, QuestionTypes.option, QuestionTypes.multiple_option]: raise ValidationError( - {'value': 'Valid list value is required'}) + 'Valid list value is required for Question:{0}'.format( + attrs.get('question').id)) elif not isinstance(attrs.get('value'), str) and attrs.get( 'question').type in [QuestionTypes.text, QuestionTypes.photo, QuestionTypes.date]: - raise ValidationError( - {'value': 'Valid string value is required'}) + 'Valid string value is required for Question:{0}'.format( + attrs.get('question').id)) elif not isinstance(attrs.get('value'), int) and attrs.get( 'question').type in [QuestionTypes.number, QuestionTypes.administration]: + raise ValidationError( - {'value': 'Valid number value is required'}) + 'Valid number value is required for Question:{0}'.format( + attrs.get('question').id)) return attrs diff --git a/backend/api/v1/v1_forms/management/commands/form_seeder.py b/backend/api/v1/v1_forms/management/commands/form_seeder.py index 3fb92f04e..aa683128b 100644 --- a/backend/api/v1/v1_forms/management/commands/form_seeder.py +++ b/backend/api/v1/v1_forms/management/commands/form_seeder.py @@ -25,16 +25,18 @@ def handle(self, *args, **options): for json_file in os.listdir(source_folder) ] source_files = list( - filter(lambda x: "example" in x - if test else "example" not in x, source_files)) + filter(lambda x: "example" in x if test else "example" not in x, + source_files)) Forms.objects.all().delete() - for source in source_files: + for index, source in enumerate(source_files): json_form = open(source, 'r') json_form = json.load(json_form) - form = Forms(id=json_form["id"], - name=json_form["form"], - version=1, - type=FormTypes.national) + form = Forms( + id=json_form["id"], + name=json_form["form"], + version=1, + type=FormTypes.national if index > 2 else FormTypes.county + ) form.save() for qg in json_form["question_groups"]: question_group = QuestionGroup(name=qg["question_group"], diff --git a/backend/api/v1/v1_forms/models.py b/backend/api/v1/v1_forms/models.py index d561bdf69..da3f57016 100644 --- a/backend/api/v1/v1_forms/models.py +++ b/backend/api/v1/v1_forms/models.py @@ -42,6 +42,41 @@ class Meta: db_table = 'form_approval_rule' +""" +Approval Rule +Form - 1 +administration - Current user's administration 1 +Level - [2,4] +""" + +""" +path administration with level [2,4] + Removed Level - 3 +Assign Approval +Form - 1 +User - 2 +administration - User's administration - 2 + + +Assign Approval +Form - 1 +User - 3 +administration - User's administration - 3 + +Assign Approval +Form - 1 +User - 4 +administration - User's administration - 4 + +--------- GET /form/approvers/ ------------ + +form_id,administration_id +direct children - administration_id +# FormApprovalAssignment + +""" + + class FormApprovalAssignment(models.Model): form = models.ForeignKey(to=Forms, on_delete=models.CASCADE, diff --git a/backend/api/v1/v1_forms/serializers.py b/backend/api/v1/v1_forms/serializers.py index 17ed22a4e..e75d0a342 100644 --- a/backend/api/v1/v1_forms/serializers.py +++ b/backend/api/v1/v1_forms/serializers.py @@ -1,15 +1,19 @@ from collections import OrderedDict +from django.db.models import Q +from django.utils import timezone from drf_spectacular.types import OpenApiTypes from drf_spectacular.utils import extend_schema_field, inline_serializer from rest_framework import serializers from api.v1.v1_forms.constants import QuestionTypes, FormTypes from api.v1.v1_forms.models import Forms, QuestionGroup, Questions, \ - QuestionOptions -from api.v1.v1_profile.models import Administration + QuestionOptions, FormApprovalRule, FormApprovalAssignment +from api.v1.v1_profile.models import Administration, Levels +from api.v1.v1_users.models import SystemUser from rtmis.settings import FORM_GEO_VALUE -from utils.custom_serializer_fields import CustomChoiceField +from utils.custom_serializer_fields import CustomChoiceField, \ + CustomPrimaryKeyRelatedField, CustomListField class ListOptionSerializer(serializers.ModelSerializer): @@ -208,3 +212,93 @@ def get_question_group(self, instance: Forms): class Meta: model = Forms fields = ['id', 'name', 'question_group'] + + +class EditFormTypeSerializer(serializers.ModelSerializer): + form_id = CustomPrimaryKeyRelatedField(queryset=Forms.objects.none()) + type = CustomChoiceField(choices=list(FormTypes.FieldStr.keys())) + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.fields.get('form_id').queryset = Forms.objects.all() + + def create(self, validated_data): + form: Forms = validated_data.get('form_id') + form.type = validated_data.get('type') + form.save() + return form + + class Meta: + model = Forms + fields = ['form_id', 'type'] + + +class EditFormApprovalSerializer(serializers.ModelSerializer): + form_id = CustomPrimaryKeyRelatedField(queryset=Forms.objects.none(), + source='form') + level_id = CustomListField( + child=CustomPrimaryKeyRelatedField(queryset=Levels.objects.none()), + source='levels') + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.fields.get('form_id').queryset = Forms.objects.all() + self.fields.get('level_id').child.queryset = Levels.objects.all() + + def create(self, validated_data): + administration = self.context.get('user').user_access.administration + FormApprovalRule.objects.filter(form=validated_data.get('form'), + administration=administration).delete() + + validated_data['administration'] = administration + rule: FormApprovalRule = super(EditFormApprovalSerializer, + self).create(validated_data) + if administration.path: + path = f"{administration.path}{administration.id}." + else: + path = f"{administration.id}." + + # Get descendants of current admin with selected level + descendants = list(Administration.objects.filter( + path__startswith=path, + level_id__in=rule.levels.all().values_list('id', + flat=True)).values_list( + 'id', flat=True)) + # Delete assignment for the removed levels + FormApprovalAssignment.objects.filter( + ~Q(administration_id__in=descendants), form=rule.form).delete() + return rule + + class Meta: + model = FormApprovalRule + fields = ['form_id', 'level_id'] + + +class ApprovalFormUserSerializer(serializers.ModelSerializer): + user_id = CustomPrimaryKeyRelatedField(queryset=SystemUser.objects.none(), + source='user') + administration_id = CustomPrimaryKeyRelatedField( + queryset=Administration.objects.none(), source='administration') + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.fields.get('user_id').queryset = SystemUser.objects.all() + self.fields.get( + 'administration_id').queryset = Administration.objects.all() + + def create(self, validated_data): + print(validated_data) + + assignment, created = FormApprovalAssignment.objects.get_or_create( + form=self.context.get('form'), + administration=validated_data.get('administration'), + user=validated_data.get('user') + ) + if not created: + assignment.updated = timezone.now() + assignment.save() + return assignment + + class Meta: + model = FormApprovalAssignment + fields = ['user_id', 'administration_id'] diff --git a/backend/api/v1/v1_forms/tests/tests_form_submission.py b/backend/api/v1/v1_forms/tests/tests_form_submission.py index d62c6020c..03f0f4672 100644 --- a/backend/api/v1/v1_forms/tests/tests_form_submission.py +++ b/backend/api/v1/v1_forms/tests/tests_form_submission.py @@ -3,6 +3,7 @@ from api.v1.v1_forms.models import Forms from api.v1.v1_profile.models import Administration, Levels +from api.v1.v1_users.models import SystemUser def seed_administration_test(): @@ -24,29 +25,16 @@ class FormSubmissionTestCase(TestCase): def test_webform_endpoint(self): self.maxDiff = None call_command("form_seeder", "--test") - seed_administration_test() + call_command("administration_seeder", "--test") webform = self.client.get("/api/v1/web/form/1", follow=True) webform = webform.json() self.assertEqual(webform.get("name"), "Test Form") question_group = webform.get("question_group") self.assertEqual(len(question_group), 1) self.assertEqual(question_group[0].get("name"), "Question Group 01") - question = question_group[0].get("question") - self.assertEqual(question[3]["type"], "cascade") - self.assertEqual(question[3]["option"], "administration") self.assertEqual( - webform.get("cascade"), { - "administration": - [{ - "label": "Indonesia", - "value": 1, - "children": [{ - "label": "Jakarta", - "value": 2, - "children": [] - }] - }] - }) + list(webform.get("cascade").get('administration')[0]), + ['value', 'label', 'children']) def test_create_new_submission(self): self.maxDiff = None @@ -114,10 +102,97 @@ def test_form_data_endpoint(self): question_group = webform.get("question_group") self.assertEqual(len(question_group), 1) self.assertEqual(question_group[0].get("name"), "Question Group 01") - question = question_group[0].get("question") - self.assertEqual(1, question[0]['id']) - self.assertEqual(1, question[0]['form']) - self.assertEqual('Name', question[0]['name']) - self.assertEqual(True, question[0]['meta']) - self.assertEqual('text', question[0]['type']) - self.assertEqual(True, question[0]['required']) + + def test_edit_form_type(self): + call_command("administration_seeder", "--test") + call_command("form_seeder", "--test") + user_payload = {"email": "admin@rtmis.com", "password": "Test105*"} + user_response = self.client.post('/api/v1/login/', + user_payload, + content_type='application/json') + user = user_response.json() + token = user.get('token') + header = { + 'HTTP_AUTHORIZATION': f'Bearer {token}' + } + + form = Forms.objects.first() + payload = [{"form_id": form.id, "type": 3}] + response = self.client.put('/api/v1/edit/forms/', + payload, + content_type='application/json', + **header) + self.assertEqual(400, response.status_code) + + payload = [{"form_id": form.id, "type": 1}] + + response = self.client.put('/api/v1/edit/forms/', + payload, + content_type='application/json', + **header) + self.assertEqual(200, response.status_code) + self.assertEqual(response.json().get('message'), + 'Forms updated successfully') + + def test_edit_form_approval(self): + call_command("administration_seeder", "--test") + call_command("form_seeder", "--test") + user_payload = {"email": "admin@rtmis.com", "password": "Test105*"} + user_response = self.client.post('/api/v1/login/', + user_payload, + content_type='application/json') + user = user_response.json() + token = user.get('token') + header = { + 'HTTP_AUTHORIZATION': f'Bearer {token}' + } + + form = Forms.objects.first() + payload = [{"form_id": form.id, "level_id": 3}] + response = self.client.post('/api/v1/edit/form/approval/', + payload, + content_type='application/json', + **header) + + self.assertEqual(400, response.status_code) + level = Levels.objects.first() + payload = [{"form_id": form.id, "level_id": [level.id]}] + + response = self.client.post('/api/v1/edit/form/approval/', + payload, + content_type='application/json', + **header) + self.assertEqual(200, response.status_code) + self.assertEqual(response.json().get('message'), + 'Forms updated successfully') + + def test_approval_form_user(self): + call_command("administration_seeder", "--test") + call_command("form_seeder", "--test") + user_payload = {"email": "admin@rtmis.com", "password": "Test105*"} + user_response = self.client.post('/api/v1/login/', + user_payload, + content_type='application/json') + user = user_response.json() + token = user.get('token') + header = { + 'HTTP_AUTHORIZATION': f'Bearer {token}' + } + u = SystemUser.objects.first() + payload = [{"user_id": u.id, "administration_id": 0}] + response = self.client.post('/api/v1/approval/form/1/', + payload, + content_type='application/json', + **header) + + self.assertEqual(400, response.status_code) + + payload = [{"user_id": u.id, "administration_id": 1}] + + response = self.client.post('/api/v1/approval/form/1/', + payload, + content_type='application/json', + **header) + self.assertEqual(200, response.status_code) + self.assertEqual(response.json().get('message'), + 'Forms updated successfully') diff --git a/backend/api/v1/v1_forms/urls.py b/backend/api/v1/v1_forms/urls.py index c2d603c79..dbbbfe543 100644 --- a/backend/api/v1/v1_forms/urls.py +++ b/backend/api/v1/v1_forms/urls.py @@ -1,6 +1,7 @@ from django.urls import re_path -from api.v1.v1_forms.views import web_form_details, list_form, form_data +from api.v1.v1_forms.views import web_form_details, list_form, form_data, \ + edit_form_type, edit_form_approval, approval_form_users urlpatterns = [ re_path(r'^(?P(v1))/web/form/(?P[0-9]+)/', @@ -8,4 +9,8 @@ re_path(r'^(?P(v1))/forms/', list_form), re_path(r'^(?P(v1))/form/(?P[0-9]+)/', form_data), + re_path(r'^(?P(v1))/edit/forms/', edit_form_type), + re_path(r'^(?P(v1))/edit/form/approval/', edit_form_approval), + re_path(r'^(?P(v1))/approval/form/(?P[0-9]+)/', + approval_form_users), ] diff --git a/backend/api/v1/v1_forms/views.py b/backend/api/v1/v1_forms/views.py index 68ce1c281..9104cc720 100644 --- a/backend/api/v1/v1_forms/views.py +++ b/backend/api/v1/v1_forms/views.py @@ -1,14 +1,19 @@ # Create your views here. from drf_spectacular.types import OpenApiTypes -from drf_spectacular.utils import extend_schema, OpenApiParameter -from rest_framework import status -from rest_framework.decorators import api_view +from drf_spectacular.utils import extend_schema, OpenApiParameter, \ + inline_serializer +from rest_framework import status, serializers +from rest_framework.decorators import api_view, permission_classes from rest_framework.generics import get_object_or_404 +from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from api.v1.v1_forms.models import Forms from api.v1.v1_forms.serializers import ListFormSerializer, \ - WebFormDetailSerializer, FormDataSerializer, ListFormRequestSerializer + WebFormDetailSerializer, FormDataSerializer, ListFormRequestSerializer, \ + EditFormTypeSerializer, EditFormApprovalSerializer, \ + ApprovalFormUserSerializer +from utils.custom_permissions import IsSuperAdmin, IsAdmin from utils.custom_serializer_fields import validate_serializers_message @@ -52,3 +57,80 @@ def form_data(request, version, pk): instance = get_object_or_404(Forms, pk=pk) return Response(FormDataSerializer(instance=instance).data, status=status.HTTP_200_OK) + + +@extend_schema(request=EditFormTypeSerializer(many=True), + responses={ + (200, 'application/json'): + inline_serializer("EditForm", fields={ + "message": serializers.CharField() + }) + }, + tags=['Form']) +@api_view(['PUT']) +@permission_classes([IsAuthenticated, IsSuperAdmin]) +def edit_form_type(request, version): + serializer = EditFormTypeSerializer(data=request.data, many=True) + if not serializer.is_valid(): + return Response( + {'message': validate_serializers_message(serializer.errors)}, + status=status.HTTP_400_BAD_REQUEST + ) + serializer.save() + return Response({'message': 'Forms updated successfully'}, + status=status.HTTP_200_OK) + + +@extend_schema(request=EditFormApprovalSerializer(many=True), + responses={ + (200, 'application/json'): + inline_serializer("EditApproval", fields={ + "message": serializers.CharField() + }) + }, + tags=['Form']) +@api_view(['POST']) +@permission_classes([IsAuthenticated, IsSuperAdmin | IsAdmin]) +def edit_form_approval(request, version): + try: + serializer = EditFormApprovalSerializer(data=request.data, many=True, + context={'user': request.user}) + if not serializer.is_valid(): + return Response( + {'message': validate_serializers_message(serializer.errors)}, + status=status.HTTP_400_BAD_REQUEST + ) + serializer.save() + return Response({'message': 'Forms updated successfully'}, + status=status.HTTP_200_OK) + except Exception as ex: + return Response({'message': ex.args}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + +@extend_schema(request=ApprovalFormUserSerializer(many=True), + responses={ + (200, 'application/json'): + inline_serializer("EditApproval", fields={ + "message": serializers.CharField() + }) + }, + tags=['Form']) +@api_view(['POST']) +@permission_classes([IsAuthenticated, IsSuperAdmin | IsAdmin]) +def approval_form_users(request, version, pk): + form = get_object_or_404(Forms, pk=pk) + try: + serializer = ApprovalFormUserSerializer(data=request.data, many=True, + context={'form': form}) + if not serializer.is_valid(): + return Response( + {'message': validate_serializers_message(serializer.errors)}, + status=status.HTTP_400_BAD_REQUEST + ) + serializer.save() + return Response({'message': 'Forms updated successfully'}, + status=status.HTTP_200_OK) + except ArithmeticError as ex: + return Response({'message': ex.args}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR) diff --git a/backend/utils/custom_serializer_fields.py b/backend/utils/custom_serializer_fields.py index 77a621d58..09149f56f 100644 --- a/backend/utils/custom_serializer_fields.py +++ b/backend/utils/custom_serializer_fields.py @@ -270,16 +270,27 @@ def validate_serializers_message(errors): else: for k1, v1 in v.items(): - for val1 in v1: - if isinstance(val1, dict): - for xk1, xv1 in val1.items(): - for xvv1 in xv1: - msg.append(xvv1.replace("field_title", - key_map.get(xk1, - xk1))) - else: - msg.append(val1.replace("field_title", - key_map.get(str(k1), - str(k1)))) + if isinstance(v1, dict): + + for k1k1, v1v1 in v1.items(): + for v1v1v in v1v1: + msg.append(v1v1v.replace("field_title", + key_map.get( + k1k1, + str(k1k1)))) + else: + + for val1 in v1: + if isinstance(val1, dict): + for xk1, xv1 in val1.items(): + for xvv1 in xv1: + msg.append(xvv1.replace("field_title", + key_map.get( + xk1, + xk1))) + else: + msg.append(val1.replace("field_title", + key_map.get(str(k1), + str(k1)))) return "|".join(msg)