From 7c3ff3df3ba99d070529e41f61abdf82fcb3de9a Mon Sep 17 00:00:00 2001 From: wayangalihpratama Date: Tue, 5 Sep 2023 12:50:36 +0800 Subject: [PATCH 01/18] [#12] Initial add form & question group serializer --- backend/akvo/core_forms/serializers/form.py | 38 +- .../core_forms/serializers/question_group.py | 29 + .../core_forms/tests/test_form_serializers.py | 16 +- .../source/static/example_form_payload.json | 907 ++++++++++++++++++ 4 files changed, 988 insertions(+), 2 deletions(-) create mode 100644 backend/source/static/example_form_payload.json diff --git a/backend/akvo/core_forms/serializers/form.py b/backend/akvo/core_forms/serializers/form.py index 56af49d..d6cbdf3 100644 --- a/backend/akvo/core_forms/serializers/form.py +++ b/backend/akvo/core_forms/serializers/form.py @@ -4,7 +4,13 @@ from akvo.core_forms.models import Forms, QuestionGroups from akvo.core_forms.serializers.question_group import ( - ListQuestionGroupSerializer + ListQuestionGroupSerializer, + AddQuestionGroupSerializer +) +from akvo.utils.custom_serializer_fields import ( + CustomIntegerField, + CustomCharField, + CustomListField ) @@ -55,3 +61,33 @@ class Meta: "translations", "question_group" ] + + +class AddFormSerializer(serializers.ModelSerializer): + id = CustomIntegerField() + name = CustomCharField() + description = CustomCharField(required=False, allow_null=True) + defaultLanguage = CustomCharField( + required=False, allow_null=True, default="en") + languages = CustomListField( + required=False, allow_null=True, default=["en"]) + version = CustomIntegerField( + required=False, allow_null=True, default=1) + translations = CustomListField(required=False, allow_null=True) + question_group = AddQuestionGroupSerializer(many=True) + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + class Meta: + model = Forms + fields = [ + "id", + "name", + "description", + "defaultLanguage", + "languages", + "version", + "translations", + "question_group" + ] diff --git a/backend/akvo/core_forms/serializers/question_group.py b/backend/akvo/core_forms/serializers/question_group.py index 2378099..d3612cc 100644 --- a/backend/akvo/core_forms/serializers/question_group.py +++ b/backend/akvo/core_forms/serializers/question_group.py @@ -3,6 +3,12 @@ from akvo.core_forms.models import QuestionGroups from akvo.core_forms.serializers.question import ListQuestionSerializer +from akvo.utils.custom_serializer_fields import ( + CustomIntegerField, + CustomListField, + CustomCharField, + CustomBooleanField +) class ListQuestionGroupSerializer(serializers.ModelSerializer): @@ -26,3 +32,26 @@ class Meta: "translations", "question", ] + + +class AddQuestionGroupSerializer(serializers.ModelSerializer): + id = CustomIntegerField() + name = CustomCharField() + description = CustomCharField(required=False, allow_null=True) + order = CustomIntegerField() + repeatable = CustomBooleanField( + required=False, allow_null=True, default=False) + translations = CustomListField(required=False, allow_null=True) + question = CustomListField() + + class Meta: + model = QuestionGroups + fields = [ + "id", + "name", + "description", + "order", + "repeatable", + "translations", + "question" + ] diff --git a/backend/akvo/core_forms/tests/test_form_serializers.py b/backend/akvo/core_forms/tests/test_form_serializers.py index 8828e09..717b1ac 100644 --- a/backend/akvo/core_forms/tests/test_form_serializers.py +++ b/backend/akvo/core_forms/tests/test_form_serializers.py @@ -1,7 +1,12 @@ +import json + from django.test import TestCase from rest_framework.test import APIClient from akvo.core_forms.models import Forms -from akvo.core_forms.serializers.form import ListFormSerializer +from akvo.core_forms.serializers.form import ( + ListFormSerializer, + AddFormSerializer +) class TestFormSerializers(TestCase): @@ -40,3 +45,12 @@ def test_list_form_serializer_return_expected_data(self): "translations": None, } self.assertEqual(data, expected_data) + + def test_add_form_serializer_valid(self): + expected_payload = {} + # Load expected form payload + with open('./source/static/example_form_payload.json', 'r') as f: + expected_payload = json.load(f) + serializer = AddFormSerializer(data=expected_payload) + self.assertEqual(expected_payload, expected_payload) + self.assertTrue(serializer.is_valid()) diff --git a/backend/source/static/example_form_payload.json b/backend/source/static/example_form_payload.json new file mode 100644 index 0000000..a03130c --- /dev/null +++ b/backend/source/static/example_form_payload.json @@ -0,0 +1,907 @@ +{ + "id": 1693886026376, + "name": "Community Culinary Survey 2021", + "description": null, + "languages": [ + "en", + "id" + ], + "defaultLanguage": "en", + "translations": [ + { + "name": "Komunitas Kuliner Survey 2021", + "language": "id" + } + ], + "question_group": [ + { + "id": 1693886026370, + "name": "Registration", + "order": 1, + "question": [ + { + "id": 1, + "name": "Geolocation", + "order": 1, + "type": "geo", + "center": { + "lat": 9.145, + "lng": 40.4897 + }, + "required": true, + "meta": true, + "tooltip": { + "text": "Please allow browser to access your test", + "translations": [ + { + "text": "Mohon izinkan peramban untuk mengakses lokasi saat ini", + "language": "id" + } + ] + }, + "extra": [ + { + "placement": "after", + "content": "Please click on the maps or type it manually", + "translations": [ + { + "content": "Silakan Klik peta atau ketik secara manual", + "language": "id" + } + ] + } + ], + "translations": [ + { + "name": "Geolokasi", + "language": "id" + } + ], + "questionGroupId": 1693886026370 + }, + { + "id": 2, + "name": "Name", + "order": 2, + "type": "input", + "required": true, + "meta": true, + "disableDelete": true, + "tooltip": { + "text": "Fullname or Nickname", + "translations": [ + { + "text": "Nama", + "language": "id" + } + ] + }, + "questionGroupId": 1693886026370 + }, + { + "id": 3, + "name": "Phone Number", + "order": 3, + "type": "number", + "required": true, + "meta": true, + "disableDelete": true, + "translations": [ + { + "name": "Nomor Telepon", + "language": "id" + } + ], + "addonBefore": "+62", + "extra": [ + { + "placement": "before", + "content": "We will not share your phone number to public", + "translations": [ + { + "content": "Kita tidak akan mempublikasikan nomor anda", + "language": "id" + } + ] + } + ], + "questionGroupId": 1693886026370 + }, + { + "id": 4, + "name": "Location (Using API)", + "order": 4, + "type": "cascade", + "api": { + "endpoint": "https://rtmis.akvotest.org/api/v1/administration", + "initial": 1, + "list": "children" + }, + "required": true, + "meta": true, + "translations": [ + { + "name": "Lokasi (Menggunakan API)", + "language": "id" + } + ], + "extra": [ + { + "placement": "before", + "content": "Please select your current origin administration", + "translations": [ + { + "content": "Silakan pilih Kecamatan anda sekarang", + "language": "id" + } + ] + } + ], + "questionGroupId": 1693886026370 + }, + { + "id": 5, + "name": "Birthdate", + "order": 5, + "type": "date", + "required": true, + "meta": true, + "translations": [ + { + "name": "Tanggal Lahir", + "language": "id" + } + ], + "questionGroupId": 1693886026370 + }, + { + "id": 6, + "name": "Gender", + "order": 6, + "type": "option", + "required": true, + "meta": true, + "translations": [ + { + "name": "Jenis Kelamin", + "language": "id" + } + ], + "questionGroupId": 1693886026370, + "option": [ + { + "id": 7, + "name": "Male", + "order": 1, + "translations": [ + { + "name": "Laki-Laki", + "language": "id" + } + ] + }, + { + "id": 8, + "name": "Female", + "order": 2, + "translations": [ + { + "name": "Perempuan", + "language": "id" + } + ] + }, + { + "id": 9, + "name": "Other", + "order": 3, + "translations": [ + { + "name": "Lainnya", + "language": "id" + } + ] + } + ] + }, + { + "id": 7, + "name": "Marital Status", + "dependency": [ + { + "id": 6, + "options": [ + "Female", + "Male" + ] + } + ], + "order": 7, + "type": "option", + "required": true, + "translations": [ + { + "name": "Status Keluarga", + "language": "id" + } + ], + "questionGroupId": 1693886026370, + "option": [ + { + "id": 8, + "name": "Single", + "order": 1, + "translations": [ + { + "name": "Jomblo", + "language": "id" + } + ] + }, + { + "id": 9, + "name": "Maried", + "order": 2, + "translations": [ + { + "name": "Menikah", + "language": "id" + } + ] + }, + { + "id": 10, + "name": "Widowed", + "order": 3, + "translations": [ + { + "name": "Janda / Duda", + "language": "id" + } + ] + } + ] + } + ], + "translations": [ + { + "name": "Registrasi", + "language": "id" + } + ] + }, + { + "id": 1693886026375, + "name": "Culinary Group", + "order": 2, + "question": [ + { + "id": 8, + "name": "How much do you spent for meals a day?", + "order": 1, + "type": "number", + "required": false, + "translations": [ + { + "name": "Berapa biasanya uang yang anda habiskan per hari untuk makanan", + "language": "id" + } + ], + "questionGroupId": 1693886026375 + }, + { + "id": 9, + "name": "How many times do you usually eat in a day?", + "order": 2, + "type": "number", + "required": false, + "translations": [ + { + "name": "Berapa kali anda biasanya makan dalam sehari", + "language": "id" + } + ], + "questionGroupId": 1693886026375 + }, + { + "id": 11, + "name": "Favorite Food", + "order": 4, + "type": "multiple_option", + "allowOther": true, + "allowOtherText": "Please input other food if any", + "required": false, + "meta": true, + "tooltip": { + "text": "Please mention the available options", + "translations": [ + { + "text": "Tolong pilih contoh yang ada", + "language": "id" + } + ] + }, + "translations": [ + { + "name": "Makanan Favorit", + "allowOtherText": "Silahkan menambahkan menu lain jika ada", + "language": "id" + } + ], + "questionGroupId": 1693886026375, + "option": [ + { + "id": 4, + "name": "Asian", + "order": 1, + "translations": [ + { + "name": "Asia", + "language": "id" + } + ] + }, + { + "id": 5, + "name": "Western", + "order": 2, + "translations": [ + { + "name": "Barat", + "language": "id" + } + ] + }, + { + "id": 6, + "name": "Vegetarian", + "order": 3 + } + ] + }, + { + "id": 12, + "name": "Please specify", + "order": 5, + "type": "input", + "required": true, + "dependency": [ + { + "id": 11, + "options": [ + "Asian", + "Western" + ] + } + ], + "tooltip": { + "text": "Desert or snacks are allowed too", + "translations": [ + { + "text": "Makanan Penutup dan Makanan Ringan juga diperbolehkan", + "language": "id" + } + ] + }, + "translations": [ + { + "name": "Tolong sebutkan", + "language": "id" + } + ], + "questionGroupId": 1693886026375 + }, + { + "id": 13, + "name": "Do you know beef rendang?", + "order": 6, + "type": "option", + "allowOtherText": "Other Option", + "allowOther": true, + "required": false, + "translations": [ + { + "name": "Apakah anda tahu Rendang Daging?", + "allowOtherText": "Jawaban Lain", + "language": "id" + } + ], + "questionGroupId": 1693886026375, + "option": [ + { + "id": 6, + "name": "Yes", + "order": 1, + "translations": [ + { + "name": "Ya", + "language": "id" + } + ] + }, + { + "id": 7, + "name": "No", + "order": 2, + "translations": [ + { + "name": "Tidak", + "language": "id" + } + ] + } + ] + }, + { + "id": 14, + "name": "Weight", + "order": 7, + "type": "number", + "required": true, + "rule": { + "min": 5, + "max": 10 + }, + "translations": [ + { + "name": "Berat Badan", + "language": "id" + } + ], + "addonAfter": "Kilograms", + "questionGroupId": 1693886026375 + }, + { + "id": 15, + "name": "Where do you usually order Rendang from ?", + "dependency": [ + { + "id": 13, + "options": [ + "Yes" + ] + }, + { + "id": 14, + "min": 8 + } + ], + "order": 8, + "type": "option", + "required": true, + "translations": [ + { + "name": "Dimana anda biasanya membeli Rendang?", + "language": "id" + } + ], + "questionGroupId": 1693886026375, + "option": [ + { + "id": 8, + "name": "Pagi Sore", + "order": 1 + }, + { + "id": 9, + "name": "Any Rendang Restaurant", + "order": 2, + "translations": [ + { + "name": "Restoran Rendang Manapun", + "language": "id" + } + ] + } + ] + }, + { + "id": 16, + "name": "Do you want to order Rendang from Pagi Sore now?", + "dependency": [ + { + "id": 15, + "options": [ + "Pagi Sore" + ] + } + ], + "order": 9, + "type": "option", + "required": true, + "translations": [ + { + "name": "Apakah anda ingin memesan Rendang dari Pagi Sore?", + "language": "id" + } + ], + "questionGroupId": 1693886026375, + "option": [ + { + "id": 9, + "name": "Yes", + "order": 1, + "translations": [ + { + "name": "Ya", + "language": "id" + } + ] + }, + { + "id": 10, + "name": "No", + "order": 2, + "translations": [ + { + "name": "Tidak", + "language": "id" + } + ] + } + ] + }, + { + "id": 17, + "name": "Order List", + "dependency": [ + { + "id": 16, + "options": [ + "Yes" + ] + } + ], + "order": 10, + "type": "table", + "columns": [ + { + "name": "items", + "label": "Items", + "type": "option", + "options": [ + { + "name": "Rendang", + "order": 1 + }, + { + "name": "Ayam Pop", + "order": 2 + }, + { + "name": "Paru Goreng", + "order": 3 + }, + { + "name": "Baluik Goreng", + "order": 4 + } + ] + }, + { + "name": "amount", + "label": "Amount", + "type": "number" + }, + { + "name": "note", + "label": "Note", + "type": "input" + } + ], + "required": true, + "translations": [ + { + "name": "Daftar Pesanan", + "language": "id" + } + ], + "extra": [ + { + "placement": "before", + "content": "Price: Rendang (IDR 5000), Ayam Pop (IDR 8000)", + "translations": [ + { + "content": "Harga: Rendang (Rp 5000), Ayam Pop (Rp 8000)", + "language": "id" + } + ] + } + ], + "questionGroupId": 1693886026375 + } + ], + "description": "Example placeholder description text for Culinary Question Group. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus imperdiet orci in feugiat tincidunt. Donec tempor suscipit accumsan. Vestibulum orci risus, mattis vitae ex et, consectetur consequat augue. Nunc et ante vel massa sollicitudin posuere vel ornare ligula. Ut a mattis massa. Mauris pulvinar congue sem, et venenatis orci vulputate id. Praesent odio purus, ultricies non eros at, iaculis imperdiet turpis. Donec non massa ligula.", + "translations": [ + { + "name": "Pertanyaan Tentang Kuliner", + "description": "Contoh teks pengganti deskripsi untuk Grup Pertanyaan Kuliner. Demikian pula, tidak adakah orang yang mencintai atau mengejar atau ingin mengalami penderitaan, bukan semata-mata karena penderitaan itu sendiri, tetapi karena sesekali terjadi keadaan di mana susah-payah dan penderitaan dapat memberikan kepadanya kesenangan yang besar.", + "language": "id" + } + ] + }, + { + "id": 1693886026377, + "name": "Other Questions", + "order": 3, + "repeatable": true, + "question": [ + { + "id": 18, + "name": "Comment", + "order": 1, + "type": "text", + "required": true, + "translations": [ + { + "name": "Komentar", + "language": "id" + } + ], + "extra": [ + { + "placement": "after", + "content": "Please write at least 10 words", + "translations": [ + { + "content": "Tolong tulis minimal 10 kata", + "language": "id" + } + ] + } + ], + "questionGroupId": 1693886026377 + }, + { + "id": 19, + "name": "Job title", + "order": 2, + "type": "option", + "required": true, + "translations": [ + { + "name": "Titel Pekerjaan", + "language": "id" + } + ], + "questionGroupId": 1693886026377, + "option": [ + { + "id": 3, + "name": "Director", + "order": 1, + "translations": [ + { + "name": "Direktur", + "language": "id" + } + ] + }, + { + "id": 4, + "name": "Manager", + "order": 2, + "translations": [ + { + "name": "Manajer", + "language": "id" + } + ] + }, + { + "id": 5, + "name": "Staff", + "order": 3, + "translations": [ + { + "name": "Staf", + "language": "id" + } + ] + } + ] + }, + { + "id": 20, + "name": "Dependency on Gender Male/Female", + "dependency": [ + { + "id": 6, + "options": [ + "Female", + "Male" + ] + } + ], + "order": 3, + "type": "input", + "required": true, + "questionGroupId": 1693886026377 + }, + { + "id": 21, + "name": "Dependency on Job Title Staff", + "dependency": [ + { + "id": 19, + "options": [ + "Staff" + ] + } + ], + "order": 4, + "type": "option", + "required": true, + "questionGroupId": 1693886026377, + "option": [ + { + "id": 5, + "name": "Contract", + "order": 1 + }, + { + "id": 6, + "name": "Internship", + "order": 2 + } + ] + }, + { + "id": 22, + "name": "Tree Select", + "order": 5, + "type": "tree", + "option": "administration", + "checkStrategy": "children", + "expandAll": true, + "required": true, + "questionGroupId": 1693886026377 + }, + { + "id": 23, + "name": "Please input any number start from 0 (no decimal)", + "order": 6, + "type": "number", + "required": true, + "translations": [ + { + "name": "Masukkan angka berapapun, dimulai dari 0 (selain decimal)", + "language": "id" + } + ], + "questionGroupId": 1693886026377 + }, + { + "id": 24, + "name": "Please input any number start from 0 to 10 (allow decimal)", + "order": 7, + "type": "number", + "required": true, + "rule": { + "min": 0, + "max": 10, + "allowDecimal": true + }, + "translations": [ + { + "name": "Masukkan angka berapapun, dimulai dari 1 sampai 10 (boleh decimal)", + "language": "id" + } + ], + "questionGroupId": 1693886026377 + }, + { + "id": 25, + "name": "Please input any number start from 0 to 5 (allow decimal)", + "order": 8, + "type": "number", + "required": false, + "rule": { + "min": 0, + "max": 5, + "allowDecimal": true + }, + "translations": [ + { + "name": "Masukkan angka berapapun, dimulai dari 0 sampai 5 (boleh decimal)", + "language": "id" + } + ], + "questionGroupId": 1693886026377 + } + ], + "repeatText": "Repeat text", + "description": "This is an example of repeat group question", + "translations": [ + { + "name": "Pertanyaan Lain", + "repeat_text": "Isi lagi", + "description": "Ini contoh dari pertanyaan berulang", + "language": "id" + } + ] + }, + { + "id": 1693886026378, + "name": "Repeat Question", + "order": 4, + "repeatable": true, + "question": [ + { + "id": 26, + "name": "Comment for Pagi Sore", + "order": 1, + "type": "text", + "required": false, + "translations": [ + { + "name": "Komentar", + "language": "id" + } + ], + "dependency": [ + { + "id": 15, + "options": [ + "Pagi Sore" + ] + } + ], + "questionGroupId": 1693886026378 + }, + { + "id": 27, + "name": "Date with Rule", + "order": 2, + "type": "date", + "required": false, + "translations": [ + { + "name": "Tanggal dengan ketentuan", + "language": "id" + } + ], + "rule": { + "minDate": "2022-01-01", + "maxDate": "2022-12-31" + }, + "questionGroupId": 1693886026378 + }, + { + "id": 28, + "name": "Question with Custom Params", + "order": 3, + "type": "text", + "required": false, + "translations": [ + { + "name": "Pertanyaan dengan Parameter Khusus", + "language": "id" + } + ], + "params_name_a": [ + "SO1" + ], + "params_name_b": [ + "MO1", + "MO2" + ], + "params_name_c": [ + "member" + ], + "questionGroupId": 1693886026378 + } + ], + "repeatText": "Add another Repeat Question", + "description": "This is an example of repeat group question", + "translations": [ + { + "name": "Pertanyaan Berulang", + "repeatText": "Tambahkan Pertanyaan Berulang", + "description": "Ini contoh dari pertanyaan berulang", + "language": "id" + } + ] + } + ] + } \ No newline at end of file From c29f715f0ba948f6505f71f6b5950e53362831ae Mon Sep 17 00:00:00 2001 From: wayangalihpratama Date: Tue, 5 Sep 2023 14:26:55 +0800 Subject: [PATCH 02/18] [#12] Init AddQuestionSerializer --- backend/akvo/core_forms/serializers/form.py | 7 +- .../akvo/core_forms/serializers/question.py | 70 ++++++++++++++++ .../core_forms/serializers/question_group.py | 10 ++- .../core_forms/tests/test_form_serializers.py | 9 ++- .../source/static/example_form_payload.json | 80 ------------------- 5 files changed, 90 insertions(+), 86 deletions(-) diff --git a/backend/akvo/core_forms/serializers/form.py b/backend/akvo/core_forms/serializers/form.py index d6cbdf3..931789b 100644 --- a/backend/akvo/core_forms/serializers/form.py +++ b/backend/akvo/core_forms/serializers/form.py @@ -67,8 +67,9 @@ class AddFormSerializer(serializers.ModelSerializer): id = CustomIntegerField() name = CustomCharField() description = CustomCharField(required=False, allow_null=True) - defaultLanguage = CustomCharField( - required=False, allow_null=True, default="en") + default_language = CustomCharField( + required=False, allow_null=True, + default="en", source="defaultLanguage") languages = CustomListField( required=False, allow_null=True, default=["en"]) version = CustomIntegerField( @@ -85,7 +86,7 @@ class Meta: "id", "name", "description", - "defaultLanguage", + "default_language", "languages", "version", "translations", diff --git a/backend/akvo/core_forms/serializers/question.py b/backend/akvo/core_forms/serializers/question.py index 97bc5ca..643857f 100644 --- a/backend/akvo/core_forms/serializers/question.py +++ b/backend/akvo/core_forms/serializers/question.py @@ -7,6 +7,13 @@ from akvo.core_forms.constants import QuestionTypes from akvo.core_forms.models import Questions from akvo.core_forms.serializers.option import ListOptionSerializer +from akvo.utils.custom_serializer_fields import ( + CustomIntegerField, + CustomCharField, + CustomJSONField, + CustomListField, + CustomBooleanField, +) class ListQuestionSerializer(serializers.ModelSerializer): @@ -113,3 +120,66 @@ class Meta: "dataApiUrl", "option", ] + + +class AddQuestionSerializer(serializers.ModelSerializer): + id = CustomIntegerField() + name = CustomCharField() + order = CustomIntegerField() + type = serializers.ChoiceField( + choices=[ + (value, key) for key, value + in QuestionTypes.FieldStr.items() + ], + required=True, + ) + tooltip = CustomJSONField(required=False, allow_null=True) + required = CustomBooleanField() + meta = CustomBooleanField( + required=False, allow_null=True, default=False) + rule = CustomJSONField(required=False, allow_null=True) + dependency = CustomListField(required=False, allow_null=True) + api = CustomJSONField(required=False, allow_null=True) + extra = CustomJSONField(required=False, allow_null=True) + autofield = CustomJSONField(required=False, allow_null=True) + data_api_url = CustomCharField(required=False, allow_null=True) + translations = CustomListField(required=False, allow_null=True) + option = CustomListField(required=False, allow_null=True) + + def __init__(self, *args, **kwargs): + # Get the value + data_api_url = kwargs.pop('dataApiUrl', None) + autofield = kwargs.pop('fn', None) + super(AddQuestionSerializer, self).__init__(*args, **kwargs) + # Set the value + if data_api_url: + self.fields['data_api_url'].initial = data_api_url + if autofield: + self.fields['autofield'].initial = data_api_url + + def validate_type(self, value): + qtype = getattr(QuestionTypes, value) + print(getattr(QuestionTypes, value), value, '++++++++++++++++++=') + if not qtype: + raise serializers.ValidationError("Invalid question type") + return qtype + + class Meta: + model = Questions + fields = [ + "id", + "name", + "order", + "type", + "tooltip", + "required", + "dependency", + "meta", + "rule", + "api", + "extra", + "translations", + "data_api_url", + "autofield", + "option", + ] diff --git a/backend/akvo/core_forms/serializers/question_group.py b/backend/akvo/core_forms/serializers/question_group.py index d3612cc..4834660 100644 --- a/backend/akvo/core_forms/serializers/question_group.py +++ b/backend/akvo/core_forms/serializers/question_group.py @@ -2,7 +2,10 @@ from rest_framework import serializers from akvo.core_forms.models import QuestionGroups -from akvo.core_forms.serializers.question import ListQuestionSerializer +from akvo.core_forms.serializers.question import ( + ListQuestionSerializer, + AddQuestionSerializer +) from akvo.utils.custom_serializer_fields import ( CustomIntegerField, CustomListField, @@ -42,7 +45,10 @@ class AddQuestionGroupSerializer(serializers.ModelSerializer): repeatable = CustomBooleanField( required=False, allow_null=True, default=False) translations = CustomListField(required=False, allow_null=True) - question = CustomListField() + question = AddQuestionSerializer(many=True) + + def __init__(self, **kwargs): + super().__init__(**kwargs) class Meta: model = QuestionGroups diff --git a/backend/akvo/core_forms/tests/test_form_serializers.py b/backend/akvo/core_forms/tests/test_form_serializers.py index 717b1ac..93a8f1e 100644 --- a/backend/akvo/core_forms/tests/test_form_serializers.py +++ b/backend/akvo/core_forms/tests/test_form_serializers.py @@ -7,6 +7,7 @@ ListFormSerializer, AddFormSerializer ) +from akvo.core_forms.constants import QuestionTypes class TestFormSerializers(TestCase): @@ -49,8 +50,14 @@ def test_list_form_serializer_return_expected_data(self): def test_add_form_serializer_valid(self): expected_payload = {} # Load expected form payload + # TODO:: Support question type: table, tree with open('./source/static/example_form_payload.json', 'r') as f: expected_payload = json.load(f) serializer = AddFormSerializer(data=expected_payload) - self.assertEqual(expected_payload, expected_payload) + if not serializer.is_valid(): + print('[ ERROR ]', serializer.errors) + print([ + (key, value) for key, value + in QuestionTypes.FieldStr.items() + ]) self.assertTrue(serializer.is_valid()) diff --git a/backend/source/static/example_form_payload.json b/backend/source/static/example_form_payload.json index a03130c..8722d46 100644 --- a/backend/source/static/example_form_payload.json +++ b/backend/source/static/example_form_payload.json @@ -540,75 +540,6 @@ ] } ] - }, - { - "id": 17, - "name": "Order List", - "dependency": [ - { - "id": 16, - "options": [ - "Yes" - ] - } - ], - "order": 10, - "type": "table", - "columns": [ - { - "name": "items", - "label": "Items", - "type": "option", - "options": [ - { - "name": "Rendang", - "order": 1 - }, - { - "name": "Ayam Pop", - "order": 2 - }, - { - "name": "Paru Goreng", - "order": 3 - }, - { - "name": "Baluik Goreng", - "order": 4 - } - ] - }, - { - "name": "amount", - "label": "Amount", - "type": "number" - }, - { - "name": "note", - "label": "Note", - "type": "input" - } - ], - "required": true, - "translations": [ - { - "name": "Daftar Pesanan", - "language": "id" - } - ], - "extra": [ - { - "placement": "before", - "content": "Price: Rendang (IDR 5000), Ayam Pop (IDR 8000)", - "translations": [ - { - "content": "Harga: Rendang (Rp 5000), Ayam Pop (Rp 8000)", - "language": "id" - } - ] - } - ], - "questionGroupId": 1693886026375 } ], "description": "Example placeholder description text for Culinary Question Group. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus imperdiet orci in feugiat tincidunt. Donec tempor suscipit accumsan. Vestibulum orci risus, mattis vitae ex et, consectetur consequat augue. Nunc et ante vel massa sollicitudin posuere vel ornare ligula. Ut a mattis massa. Mauris pulvinar congue sem, et venenatis orci vulputate id. Praesent odio purus, ultricies non eros at, iaculis imperdiet turpis. Donec non massa ligula.", @@ -746,17 +677,6 @@ } ] }, - { - "id": 22, - "name": "Tree Select", - "order": 5, - "type": "tree", - "option": "administration", - "checkStrategy": "children", - "expandAll": true, - "required": true, - "questionGroupId": 1693886026377 - }, { "id": 23, "name": "Please input any number start from 0 (no decimal)", From c3c4f58ccb577934547a81ec8872c89e433f5c23 Mon Sep 17 00:00:00 2001 From: wayangalihpratama Date: Tue, 5 Sep 2023 15:47:41 +0800 Subject: [PATCH 03/18] [#12] Fix question type serializer --- backend/akvo/core_forms/constants.py | 2 +- backend/akvo/core_forms/serializers/question.py | 6 +----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/backend/akvo/core_forms/constants.py b/backend/akvo/core_forms/constants.py index c287c1a..9f9f36d 100644 --- a/backend/akvo/core_forms/constants.py +++ b/backend/akvo/core_forms/constants.py @@ -16,7 +16,7 @@ class QuestionTypes: number: "number", geo: "geo", option: "option", - multiple_option: "multiple_pption", + multiple_option: "multiple_option", cascade: "cascade", photo: "photo", date: "date", diff --git a/backend/akvo/core_forms/serializers/question.py b/backend/akvo/core_forms/serializers/question.py index 643857f..2b9990d 100644 --- a/backend/akvo/core_forms/serializers/question.py +++ b/backend/akvo/core_forms/serializers/question.py @@ -127,10 +127,7 @@ class AddQuestionSerializer(serializers.ModelSerializer): name = CustomCharField() order = CustomIntegerField() type = serializers.ChoiceField( - choices=[ - (value, key) for key, value - in QuestionTypes.FieldStr.items() - ], + choices=list(QuestionTypes.FieldStr.values()), required=True, ) tooltip = CustomJSONField(required=False, allow_null=True) @@ -159,7 +156,6 @@ def __init__(self, *args, **kwargs): def validate_type(self, value): qtype = getattr(QuestionTypes, value) - print(getattr(QuestionTypes, value), value, '++++++++++++++++++=') if not qtype: raise serializers.ValidationError("Invalid question type") return qtype From 0808190e81a7a66277a0e5b69caf32a71b981399 Mon Sep 17 00:00:00 2001 From: wayangalihpratama Date: Tue, 5 Sep 2023 19:46:27 +0800 Subject: [PATCH 04/18] [#12] Update add form, question_group, question serializer --- backend/akvo/core_forms/serializers/form.py | 30 +++++++++++++++---- .../akvo/core_forms/serializers/question.py | 25 ++++++++++++++-- .../core_forms/serializers/question_group.py | 20 +++++++++++-- 3 files changed, 64 insertions(+), 11 deletions(-) diff --git a/backend/akvo/core_forms/serializers/form.py b/backend/akvo/core_forms/serializers/form.py index 931789b..9dfcc1e 100644 --- a/backend/akvo/core_forms/serializers/form.py +++ b/backend/akvo/core_forms/serializers/form.py @@ -12,6 +12,7 @@ CustomCharField, CustomListField ) +from akvo.utils.custom_serializer_fields import validate_serializers_message class ListFormSerializer(serializers.ModelSerializer): @@ -68,17 +69,35 @@ class AddFormSerializer(serializers.ModelSerializer): name = CustomCharField() description = CustomCharField(required=False, allow_null=True) default_language = CustomCharField( - required=False, allow_null=True, - default="en", source="defaultLanguage") + required=False, allow_null=True, default="en") languages = CustomListField( required=False, allow_null=True, default=["en"]) version = CustomIntegerField( required=False, allow_null=True, default=1) translations = CustomListField(required=False, allow_null=True) - question_group = AddQuestionGroupSerializer(many=True) - def __init__(self, **kwargs): - super().__init__(**kwargs) + def __init__(self, *args, **kwargs): + # Get the value + default_language = kwargs.pop('defaultLanguage', None) + super(AddFormSerializer, self).__init__(*args, **kwargs) + # Set the value + if default_language: + self.fields['default_language'].initial = default_language + + def create(self, validated_data): + question_groups_data = validated_data.pop("question_group", []) + form = Forms.objects.create(**validated_data) + for qg in question_groups_data: + qg["form"] = form + serializer = AddQuestionGroupSerializer(data=qg) + if not serializer.is_valid(): + raise serializers.ValidationError({ + "message": validate_serializers_message(serializer.errors), + "details": serializer.errors, + }) + serializer.save() + return object + return form class Meta: model = Forms @@ -90,5 +109,4 @@ class Meta: "languages", "version", "translations", - "question_group" ] diff --git a/backend/akvo/core_forms/serializers/question.py b/backend/akvo/core_forms/serializers/question.py index 2b9990d..742f2dc 100644 --- a/backend/akvo/core_forms/serializers/question.py +++ b/backend/akvo/core_forms/serializers/question.py @@ -6,7 +6,10 @@ from akvo.core_forms.constants import QuestionTypes from akvo.core_forms.models import Questions -from akvo.core_forms.serializers.option import ListOptionSerializer +from akvo.core_forms.serializers.option import ( + ListOptionSerializer, + AddOptionSerializer, +) from akvo.utils.custom_serializer_fields import ( CustomIntegerField, CustomCharField, @@ -14,6 +17,7 @@ CustomListField, CustomBooleanField, ) +from akvo.utils.custom_serializer_fields import validate_serializers_message class ListQuestionSerializer(serializers.ModelSerializer): @@ -123,6 +127,8 @@ class Meta: class AddQuestionSerializer(serializers.ModelSerializer): + form = CustomIntegerField(read_only=True) + question_group = CustomIntegerField(read_only=True) id = CustomIntegerField() name = CustomCharField() order = CustomIntegerField() @@ -141,7 +147,6 @@ class AddQuestionSerializer(serializers.ModelSerializer): autofield = CustomJSONField(required=False, allow_null=True) data_api_url = CustomCharField(required=False, allow_null=True) translations = CustomListField(required=False, allow_null=True) - option = CustomListField(required=False, allow_null=True) def __init__(self, *args, **kwargs): # Get the value @@ -160,6 +165,21 @@ def validate_type(self, value): raise serializers.ValidationError("Invalid question type") return qtype + def create(self, validated_data): + options_data = validated_data.pop("option", []) + q = Questions.objects.create(**validated_data) + for opt in options_data: + opt["question"] = q + serializer = AddOptionSerializer(data=opt) + if not serializer.is_valid(): + raise serializers.ValidationError({ + "message": validate_serializers_message(serializer.errors), + "details": serializer.errors, + }) + serializer.save() + return object + return q + class Meta: model = Questions fields = [ @@ -177,5 +197,4 @@ class Meta: "translations", "data_api_url", "autofield", - "option", ] diff --git a/backend/akvo/core_forms/serializers/question_group.py b/backend/akvo/core_forms/serializers/question_group.py index 4834660..6aa6e21 100644 --- a/backend/akvo/core_forms/serializers/question_group.py +++ b/backend/akvo/core_forms/serializers/question_group.py @@ -12,6 +12,7 @@ CustomCharField, CustomBooleanField ) +from akvo.utils.custom_serializer_fields import validate_serializers_message class ListQuestionGroupSerializer(serializers.ModelSerializer): @@ -38,6 +39,7 @@ class Meta: class AddQuestionGroupSerializer(serializers.ModelSerializer): + form = CustomIntegerField(read_only=True) id = CustomIntegerField() name = CustomCharField() description = CustomCharField(required=False, allow_null=True) @@ -45,11 +47,26 @@ class AddQuestionGroupSerializer(serializers.ModelSerializer): repeatable = CustomBooleanField( required=False, allow_null=True, default=False) translations = CustomListField(required=False, allow_null=True) - question = AddQuestionSerializer(many=True) def __init__(self, **kwargs): super().__init__(**kwargs) + def create(self, validated_data): + questions_data = validated_data.pop("question", []) + qg = QuestionGroups.objects.create(**validated_data) + for q in questions_data: + q["form"] = validated_data.get("form") + q["question_group"] = qg + serializer = AddQuestionSerializer(data=q) + if not serializer.is_valid(): + raise serializers.ValidationError({ + "message": validate_serializers_message(serializer.errors), + "details": serializer.errors, + }) + serializer.save() + return object + return qg + class Meta: model = QuestionGroups fields = [ @@ -59,5 +76,4 @@ class Meta: "order", "repeatable", "translations", - "question" ] From 5fcae37db2500aece64bb32edbe862baf65b7116 Mon Sep 17 00:00:00 2001 From: wayangalihpratama Date: Tue, 5 Sep 2023 19:47:03 +0800 Subject: [PATCH 05/18] [#12] Create add option serializer --- backend/akvo/core_forms/serializers/option.py | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/backend/akvo/core_forms/serializers/option.py b/backend/akvo/core_forms/serializers/option.py index ca91761..b292f11 100644 --- a/backend/akvo/core_forms/serializers/option.py +++ b/backend/akvo/core_forms/serializers/option.py @@ -3,6 +3,11 @@ from rest_framework import serializers from akvo.core_forms.models import Options +from akvo.utils.custom_serializer_fields import ( + CustomIntegerField, + CustomCharField, + CustomListField +) class ListOptionSerializer(serializers.ModelSerializer): @@ -15,3 +20,24 @@ def to_representation(self, instance): class Meta: model = Options fields = ["id", "code", "name", "order", "color", "translations"] + + +class AddOptionSerializer(serializers.ModelSerializer): + question = CustomIntegerField(read_only=True) + id = CustomIntegerField() + name = CustomCharField() + order = CustomIntegerField() + code = CustomCharField(required=False, allow_null=True) + color = CustomCharField(required=False, allow_null=True) + translations = CustomListField(required=False, allow_null=True) + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + def create(self, validated_data): + opt = Options.objects.create(**validated_data) + return opt + + class Meta: + model = Options + fields = ["id", "code", "name", "order", "color", "translations"] From d3ebe3fee8a80aa81f4c67d28c5bbe72d4652013 Mon Sep 17 00:00:00 2001 From: wayangalihpratama Date: Tue, 5 Sep 2023 19:47:54 +0800 Subject: [PATCH 06/18] [#12] Create post example test & view --- .../core_forms/tests/test_form_endpoint.py | 13 ++++++ backend/akvo/core_forms/urls.py | 4 +- backend/akvo/core_forms/views/form.py | 45 ++++++++++++++++++- .../source/static/example_form_payload.json | 2 +- 4 files changed, 60 insertions(+), 4 deletions(-) diff --git a/backend/akvo/core_forms/tests/test_form_endpoint.py b/backend/akvo/core_forms/tests/test_form_endpoint.py index da5a122..ffc88ec 100644 --- a/backend/akvo/core_forms/tests/test_form_endpoint.py +++ b/backend/akvo/core_forms/tests/test_form_endpoint.py @@ -37,3 +37,16 @@ def test_endpoint_get_form_by_id(self): with open('./source/static/expected_form_definition.json', 'r') as f: expected_result = json.load(f) self.assertEqual(result, expected_result) + + def test_endpoint_post_form(self): + expected_payload = {} + with open('./source/static/example_form_payload.json', 'r') as f: + expected_payload = json.load(f) + data = self.client.post( + "/api/form", + data=expected_payload, + format="json" + ) + self.assertEqual(data.status_code, 200) + result = data.json() + self.assertEqual(result, {"message": "ok"}) diff --git a/backend/akvo/core_forms/urls.py b/backend/akvo/core_forms/urls.py index 6e3c3b7..050ace3 100644 --- a/backend/akvo/core_forms/urls.py +++ b/backend/akvo/core_forms/urls.py @@ -1,10 +1,12 @@ from django.urls import path, re_path from akvo.core_forms.views.form import ( - list_form, get_form_by_id + list_form, get_form_by_id, + FormManagementView ) urlpatterns = [ path('forms', list_form), re_path(r"form/(?P[0-9]+)", get_form_by_id), + path('form', FormManagementView.as_view()), ] diff --git a/backend/akvo/core_forms/views/form.py b/backend/akvo/core_forms/views/form.py index 00adefb..8fea211 100644 --- a/backend/akvo/core_forms/views/form.py +++ b/backend/akvo/core_forms/views/form.py @@ -1,14 +1,19 @@ from drf_spectacular.utils import extend_schema - +from rest_framework.views import APIView from rest_framework import status + from rest_framework.decorators import api_view from rest_framework.response import Response from rest_framework.generics import get_object_or_404 from akvo.core_forms.models import Forms from akvo.core_forms.serializers.form import ( - ListFormSerializer, FormDefinitionSerializer + ListFormSerializer, + FormDefinitionSerializer, + AddFormSerializer, ) +from akvo.utils.default_serializers import DefaultResponseSerializer +from akvo.utils.custom_serializer_fields import validate_serializers_message @extend_schema( @@ -41,3 +46,39 @@ def get_form_by_id(request, form_id): FormDefinitionSerializer(instance=instance).data, status=status.HTTP_200_OK ) + + +class FormManagementView(APIView): + @extend_schema( + responses={ + (200, "application/json"): DefaultResponseSerializer, + }, + tags=["Form"], + summary="Create form definition", + ) + def post(self, request): + serializer = AddFormSerializer(data=request.data) + if not serializer.is_valid(): + return Response( + { + "message": validate_serializers_message(serializer.errors), + "details": serializer.errors, + }, + status=status.HTTP_400_BAD_REQUEST, + ) + serializer.save() + return Response( + {"message": "ok"}, status=status.HTTP_200_OK + ) + + @extend_schema( + responses={ + (200, "application/json"): DefaultResponseSerializer, + }, + tags=["Form"], + summary="Update form definition", + ) + def put(self, request): + return Response( + {"message": "Update form success"}, status=status.HTTP_200_OK + ) diff --git a/backend/source/static/example_form_payload.json b/backend/source/static/example_form_payload.json index 8722d46..db06a39 100644 --- a/backend/source/static/example_form_payload.json +++ b/backend/source/static/example_form_payload.json @@ -1,7 +1,7 @@ { "id": 1693886026376, "name": "Community Culinary Survey 2021", - "description": null, + "description": "Lorem ipsum...", "languages": [ "en", "id" From b998772e7bb751a8f27b0b8feaa5d78da0fdfee4 Mon Sep 17 00:00:00 2001 From: wayangalihpratama Date: Tue, 5 Sep 2023 21:25:45 +0800 Subject: [PATCH 07/18] [#12] POST Form test debug (not done) --- backend/akvo/core_forms/serializers/form.py | 35 ++++++---- backend/akvo/core_forms/serializers/option.py | 8 +-- .../akvo/core_forms/serializers/question.py | 68 +++++++++++-------- .../core_forms/serializers/question_group.py | 33 ++++++--- .../core_forms/tests/test_form_endpoint.py | 10 ++- backend/akvo/core_forms/views/form.py | 1 + 6 files changed, 99 insertions(+), 56 deletions(-) diff --git a/backend/akvo/core_forms/serializers/form.py b/backend/akvo/core_forms/serializers/form.py index 9dfcc1e..c17f701 100644 --- a/backend/akvo/core_forms/serializers/form.py +++ b/backend/akvo/core_forms/serializers/form.py @@ -64,7 +64,7 @@ class Meta: ] -class AddFormSerializer(serializers.ModelSerializer): +class AddFormSerializer(serializers.Serializer): id = CustomIntegerField() name = CustomCharField() description = CustomCharField(required=False, allow_null=True) @@ -75,6 +75,7 @@ class AddFormSerializer(serializers.ModelSerializer): version = CustomIntegerField( required=False, allow_null=True, default=1) translations = CustomListField(required=False, allow_null=True) + question_group = AddQuestionGroupSerializer(many=True) def __init__(self, *args, **kwargs): # Get the value @@ -84,6 +85,16 @@ def __init__(self, *args, **kwargs): if default_language: self.fields['default_language'].initial = default_language + def validate_question_group(self, value): + serializer = AddQuestionGroupSerializer(data=value, many=True) + if not serializer.is_valid(): + print('QG ERROR', serializer.errors) + raise serializers.ValidationError({ + "message": validate_serializers_message(serializer.errors), + "details": serializer.errors, + }) + return value + def create(self, validated_data): question_groups_data = validated_data.pop("question_group", []) form = Forms.objects.create(**validated_data) @@ -99,14 +110,14 @@ def create(self, validated_data): return object return form - class Meta: - model = Forms - fields = [ - "id", - "name", - "description", - "default_language", - "languages", - "version", - "translations", - ] + # class Meta: + # model = Forms + # fields = [ + # "id", + # "name", + # "description", + # "default_language", + # "languages", + # "version", + # "translations", + # ] diff --git a/backend/akvo/core_forms/serializers/option.py b/backend/akvo/core_forms/serializers/option.py index b292f11..64d4ebe 100644 --- a/backend/akvo/core_forms/serializers/option.py +++ b/backend/akvo/core_forms/serializers/option.py @@ -22,7 +22,7 @@ class Meta: fields = ["id", "code", "name", "order", "color", "translations"] -class AddOptionSerializer(serializers.ModelSerializer): +class AddOptionSerializer(serializers.Serializer): question = CustomIntegerField(read_only=True) id = CustomIntegerField() name = CustomCharField() @@ -38,6 +38,6 @@ def create(self, validated_data): opt = Options.objects.create(**validated_data) return opt - class Meta: - model = Options - fields = ["id", "code", "name", "order", "color", "translations"] + # class Meta: + # model = Options + # fields = ["id", "code", "name", "order", "color", "translations"] diff --git a/backend/akvo/core_forms/serializers/question.py b/backend/akvo/core_forms/serializers/question.py index 742f2dc..20d9789 100644 --- a/backend/akvo/core_forms/serializers/question.py +++ b/backend/akvo/core_forms/serializers/question.py @@ -126,27 +126,26 @@ class Meta: ] -class AddQuestionSerializer(serializers.ModelSerializer): +class AddQuestionSerializer(serializers.Serializer): form = CustomIntegerField(read_only=True) question_group = CustomIntegerField(read_only=True) id = CustomIntegerField() name = CustomCharField() order = CustomIntegerField() - type = serializers.ChoiceField( - choices=list(QuestionTypes.FieldStr.values()), - required=True, - ) + type = CustomCharField() tooltip = CustomJSONField(required=False, allow_null=True) - required = CustomBooleanField() + required = CustomBooleanField( + required=False, allow_null=True, default=False) meta = CustomBooleanField( required=False, allow_null=True, default=False) rule = CustomJSONField(required=False, allow_null=True) dependency = CustomListField(required=False, allow_null=True) api = CustomJSONField(required=False, allow_null=True) - extra = CustomJSONField(required=False, allow_null=True) + extra = CustomListField(required=False, allow_null=True) autofield = CustomJSONField(required=False, allow_null=True) data_api_url = CustomCharField(required=False, allow_null=True) translations = CustomListField(required=False, allow_null=True) + option = AddOptionSerializer(many=True, required=False, allow_null=True) def __init__(self, *args, **kwargs): # Get the value @@ -157,15 +156,30 @@ def __init__(self, *args, **kwargs): if data_api_url: self.fields['data_api_url'].initial = data_api_url if autofield: - self.fields['autofield'].initial = data_api_url + self.fields['autofield'].initial = autofield def validate_type(self, value): qtype = getattr(QuestionTypes, value) if not qtype: raise serializers.ValidationError("Invalid question type") - return qtype + return value + + def validate_option(self, value): + if not value: + return None + serializer = AddOptionSerializer(data=value, many=True) + if not serializer.is_valid(): + print('OPT ERROR', serializer.errors) + raise serializers.ValidationError({ + "message": validate_serializers_message(serializer.errors), + "details": serializer.errors, + }) + return value def create(self, validated_data): + validated_data.pop("form", None) + qtype = validated_data.pop("type", None) + validated_data["type"] = getattr(QuestionTypes, qtype) options_data = validated_data.pop("option", []) q = Questions.objects.create(**validated_data) for opt in options_data: @@ -180,21 +194,21 @@ def create(self, validated_data): return object return q - class Meta: - model = Questions - fields = [ - "id", - "name", - "order", - "type", - "tooltip", - "required", - "dependency", - "meta", - "rule", - "api", - "extra", - "translations", - "data_api_url", - "autofield", - ] + # class Meta: + # model = Questions + # fields = [ + # "id", + # "name", + # "order", + # "type", + # "tooltip", + # "required", + # "dependency", + # "meta", + # "rule", + # "api", + # "extra", + # "translations", + # "data_api_url", + # "autofield", + # ] diff --git a/backend/akvo/core_forms/serializers/question_group.py b/backend/akvo/core_forms/serializers/question_group.py index 6aa6e21..57dcdf7 100644 --- a/backend/akvo/core_forms/serializers/question_group.py +++ b/backend/akvo/core_forms/serializers/question_group.py @@ -38,7 +38,7 @@ class Meta: ] -class AddQuestionGroupSerializer(serializers.ModelSerializer): +class AddQuestionGroupSerializer(serializers.Serializer): form = CustomIntegerField(read_only=True) id = CustomIntegerField() name = CustomCharField() @@ -47,10 +47,21 @@ class AddQuestionGroupSerializer(serializers.ModelSerializer): repeatable = CustomBooleanField( required=False, allow_null=True, default=False) translations = CustomListField(required=False, allow_null=True) + question = AddQuestionSerializer(many=True) def __init__(self, **kwargs): super().__init__(**kwargs) + def validate_question(self, value): + serializer = AddQuestionSerializer(data=value, many=True) + if not serializer.is_valid(): + print('Q ERROR', serializer.errors) + raise serializers.ValidationError({ + "message": validate_serializers_message(serializer.errors), + "details": serializer.errors, + }) + return value + def create(self, validated_data): questions_data = validated_data.pop("question", []) qg = QuestionGroups.objects.create(**validated_data) @@ -67,13 +78,13 @@ def create(self, validated_data): return object return qg - class Meta: - model = QuestionGroups - fields = [ - "id", - "name", - "description", - "order", - "repeatable", - "translations", - ] + # class Meta: + # model = QuestionGroups + # fields = [ + # "id", + # "name", + # "description", + # "order", + # "repeatable", + # "translations", + # ] diff --git a/backend/akvo/core_forms/tests/test_form_endpoint.py b/backend/akvo/core_forms/tests/test_form_endpoint.py index ffc88ec..d3c0290 100644 --- a/backend/akvo/core_forms/tests/test_form_endpoint.py +++ b/backend/akvo/core_forms/tests/test_form_endpoint.py @@ -39,14 +39,20 @@ def test_endpoint_get_form_by_id(self): self.assertEqual(result, expected_result) def test_endpoint_post_form(self): - expected_payload = {} with open('./source/static/example_form_payload.json', 'r') as f: expected_payload = json.load(f) data = self.client.post( "/api/form", data=expected_payload, - format="json" ) self.assertEqual(data.status_code, 200) result = data.json() self.assertEqual(result, {"message": "ok"}) + # get form after post + data = self.client.get( + f"/api/form/{expected_payload.get('id')}", + follow=True + ) + self.assertEqual(data.status_code, 200) + result = data.json() + print(json.dumps(result, indent=2)) diff --git a/backend/akvo/core_forms/views/form.py b/backend/akvo/core_forms/views/form.py index 8fea211..91dc515 100644 --- a/backend/akvo/core_forms/views/form.py +++ b/backend/akvo/core_forms/views/form.py @@ -59,6 +59,7 @@ class FormManagementView(APIView): def post(self, request): serializer = AddFormSerializer(data=request.data) if not serializer.is_valid(): + print('FORM ERROR', serializer.errors) return Response( { "message": validate_serializers_message(serializer.errors), From b3f86b9d313b2deabe69ea15c5c5502e2ecd1c9f Mon Sep 17 00:00:00 2001 From: wayangalihpratama Date: Tue, 5 Sep 2023 22:33:42 +0800 Subject: [PATCH 08/18] [#12] Fix post form --- backend/akvo/core_forms/serializers/form.py | 5 ++--- backend/akvo/core_forms/serializers/question.py | 3 +-- .../akvo/core_forms/serializers/question_group.py | 13 ++++++++----- backend/akvo/core_forms/tests/test_form_endpoint.py | 4 +++- .../akvo/core_forms/tests/test_form_serializers.py | 7 ------- 5 files changed, 14 insertions(+), 18 deletions(-) diff --git a/backend/akvo/core_forms/serializers/form.py b/backend/akvo/core_forms/serializers/form.py index c17f701..69406bd 100644 --- a/backend/akvo/core_forms/serializers/form.py +++ b/backend/akvo/core_forms/serializers/form.py @@ -75,7 +75,7 @@ class AddFormSerializer(serializers.Serializer): version = CustomIntegerField( required=False, allow_null=True, default=1) translations = CustomListField(required=False, allow_null=True) - question_group = AddQuestionGroupSerializer(many=True) + question_group = AddQuestionGroupSerializer(many=True, required=False) def __init__(self, *args, **kwargs): # Get the value @@ -99,14 +99,13 @@ def create(self, validated_data): question_groups_data = validated_data.pop("question_group", []) form = Forms.objects.create(**validated_data) for qg in question_groups_data: - qg["form"] = form serializer = AddQuestionGroupSerializer(data=qg) if not serializer.is_valid(): raise serializers.ValidationError({ "message": validate_serializers_message(serializer.errors), "details": serializer.errors, }) - serializer.save() + serializer.save(form=form) return object return form diff --git a/backend/akvo/core_forms/serializers/question.py b/backend/akvo/core_forms/serializers/question.py index 20d9789..1e01f7a 100644 --- a/backend/akvo/core_forms/serializers/question.py +++ b/backend/akvo/core_forms/serializers/question.py @@ -177,10 +177,9 @@ def validate_option(self, value): return value def create(self, validated_data): - validated_data.pop("form", None) + options_data = validated_data.pop("option", []) qtype = validated_data.pop("type", None) validated_data["type"] = getattr(QuestionTypes, qtype) - options_data = validated_data.pop("option", []) q = Questions.objects.create(**validated_data) for opt in options_data: opt["question"] = q diff --git a/backend/akvo/core_forms/serializers/question_group.py b/backend/akvo/core_forms/serializers/question_group.py index 57dcdf7..f24a256 100644 --- a/backend/akvo/core_forms/serializers/question_group.py +++ b/backend/akvo/core_forms/serializers/question_group.py @@ -49,8 +49,13 @@ class AddQuestionGroupSerializer(serializers.Serializer): translations = CustomListField(required=False, allow_null=True) question = AddQuestionSerializer(many=True) - def __init__(self, **kwargs): - super().__init__(**kwargs) + def __init__(self, *args, **kwargs): + # Get the value + form = kwargs.pop('form', None) + super(AddQuestionGroupSerializer, self).__init__(*args, **kwargs) + # Set the value + if form: + self.fields['form'].initial = form def validate_question(self, value): serializer = AddQuestionSerializer(data=value, many=True) @@ -66,15 +71,13 @@ def create(self, validated_data): questions_data = validated_data.pop("question", []) qg = QuestionGroups.objects.create(**validated_data) for q in questions_data: - q["form"] = validated_data.get("form") - q["question_group"] = qg serializer = AddQuestionSerializer(data=q) if not serializer.is_valid(): raise serializers.ValidationError({ "message": validate_serializers_message(serializer.errors), "details": serializer.errors, }) - serializer.save() + serializer.save(form=qg.form, question_group=qg) return object return qg diff --git a/backend/akvo/core_forms/tests/test_form_endpoint.py b/backend/akvo/core_forms/tests/test_form_endpoint.py index d3c0290..ca09388 100644 --- a/backend/akvo/core_forms/tests/test_form_endpoint.py +++ b/backend/akvo/core_forms/tests/test_form_endpoint.py @@ -44,6 +44,7 @@ def test_endpoint_post_form(self): data = self.client.post( "/api/form", data=expected_payload, + content_type="application/json" ) self.assertEqual(data.status_code, 200) result = data.json() @@ -55,4 +56,5 @@ def test_endpoint_post_form(self): ) self.assertEqual(data.status_code, 200) result = data.json() - print(json.dumps(result, indent=2)) + self.assertEqual(result, {}) + # print(json.dumps(result, indent=2)) diff --git a/backend/akvo/core_forms/tests/test_form_serializers.py b/backend/akvo/core_forms/tests/test_form_serializers.py index 93a8f1e..86e26df 100644 --- a/backend/akvo/core_forms/tests/test_form_serializers.py +++ b/backend/akvo/core_forms/tests/test_form_serializers.py @@ -7,7 +7,6 @@ ListFormSerializer, AddFormSerializer ) -from akvo.core_forms.constants import QuestionTypes class TestFormSerializers(TestCase): @@ -54,10 +53,4 @@ def test_add_form_serializer_valid(self): with open('./source/static/example_form_payload.json', 'r') as f: expected_payload = json.load(f) serializer = AddFormSerializer(data=expected_payload) - if not serializer.is_valid(): - print('[ ERROR ]', serializer.errors) - print([ - (key, value) for key, value - in QuestionTypes.FieldStr.items() - ]) self.assertTrue(serializer.is_valid()) From d0073cbcb572f8047593c9844d496dff4da23b66 Mon Sep 17 00:00:00 2001 From: wayangalihpratama Date: Wed, 6 Sep 2023 09:19:40 +0800 Subject: [PATCH 09/18] [#12] Fix ARF editor --- backend/akvo/core_forms/serializers/form.py | 1 - backend/akvo/core_forms/serializers/option.py | 2 +- .../akvo/core_forms/serializers/question.py | 3 +- .../core_forms/serializers/question_group.py | 1 - .../core_forms/tests/test_form_endpoint.py | 7 +- .../static/example_form_payload_result.json | 800 ++++++++++++++++++ 6 files changed, 807 insertions(+), 7 deletions(-) create mode 100644 backend/source/static/example_form_payload_result.json diff --git a/backend/akvo/core_forms/serializers/form.py b/backend/akvo/core_forms/serializers/form.py index 69406bd..ef81a77 100644 --- a/backend/akvo/core_forms/serializers/form.py +++ b/backend/akvo/core_forms/serializers/form.py @@ -106,7 +106,6 @@ def create(self, validated_data): "details": serializer.errors, }) serializer.save(form=form) - return object return form # class Meta: diff --git a/backend/akvo/core_forms/serializers/option.py b/backend/akvo/core_forms/serializers/option.py index 64d4ebe..0a861d4 100644 --- a/backend/akvo/core_forms/serializers/option.py +++ b/backend/akvo/core_forms/serializers/option.py @@ -24,7 +24,7 @@ class Meta: class AddOptionSerializer(serializers.Serializer): question = CustomIntegerField(read_only=True) - id = CustomIntegerField() + # id = CustomIntegerField() name = CustomCharField() order = CustomIntegerField() code = CustomCharField(required=False, allow_null=True) diff --git a/backend/akvo/core_forms/serializers/question.py b/backend/akvo/core_forms/serializers/question.py index 1e01f7a..6cf34c5 100644 --- a/backend/akvo/core_forms/serializers/question.py +++ b/backend/akvo/core_forms/serializers/question.py @@ -189,8 +189,7 @@ def create(self, validated_data): "message": validate_serializers_message(serializer.errors), "details": serializer.errors, }) - serializer.save() - return object + serializer.save(question=q) return q # class Meta: diff --git a/backend/akvo/core_forms/serializers/question_group.py b/backend/akvo/core_forms/serializers/question_group.py index f24a256..ed257a7 100644 --- a/backend/akvo/core_forms/serializers/question_group.py +++ b/backend/akvo/core_forms/serializers/question_group.py @@ -78,7 +78,6 @@ def create(self, validated_data): "details": serializer.errors, }) serializer.save(form=qg.form, question_group=qg) - return object return qg # class Meta: diff --git a/backend/akvo/core_forms/tests/test_form_endpoint.py b/backend/akvo/core_forms/tests/test_form_endpoint.py index ca09388..16d5620 100644 --- a/backend/akvo/core_forms/tests/test_form_endpoint.py +++ b/backend/akvo/core_forms/tests/test_form_endpoint.py @@ -56,5 +56,8 @@ def test_endpoint_post_form(self): ) self.assertEqual(data.status_code, 200) result = data.json() - self.assertEqual(result, {}) - # print(json.dumps(result, indent=2)) + with open( + './source/static/example_form_payload_result.json', 'r' + ) as f: + expected_result = json.load(f) + self.assertEqual(result, expected_result) diff --git a/backend/source/static/example_form_payload_result.json b/backend/source/static/example_form_payload_result.json new file mode 100644 index 0000000..4617274 --- /dev/null +++ b/backend/source/static/example_form_payload_result.json @@ -0,0 +1,800 @@ +{ + "id": 1693886026376, + "name": "Community Culinary Survey 2021", + "description": "Lorem ipsum...", + "defaultLanguage": "en", + "languages": [ + "en", + "id" + ], + "version": 1, + "translations": [ + { + "name": "Komunitas Kuliner Survey 2021", + "language": "id" + } + ], + "question_group": [ + { + "id": 1693886026370, + "name": "Registration", + "description": null, + "order": 1, + "repeatable": false, + "translations": [ + { + "name": "Registrasi", + "language": "id" + } + ], + "question": [ + { + "id": 1, + "name": "Geolocation", + "order": 1, + "type": "geo", + "tooltip": { + "text": "Please allow browser to access your test", + "translations": [ + { + "text": "Mohon izinkan peramban untuk mengakses lokasi saat ini", + "language": "id" + } + ] + }, + "required": true, + "meta": true, + "extra": [ + { + "content": "Please click on the maps or type it manually", + "placement": "after", + "translations": [ + { + "content": "Silakan Klik peta atau ketik secara manual", + "language": "id" + } + ] + } + ], + "translations": [ + { + "name": "Geolokasi", + "language": "id" + } + ] + }, + { + "id": 2, + "name": "Name", + "order": 2, + "type": "input", + "tooltip": { + "text": "Fullname or Nickname", + "translations": [ + { + "text": "Nama", + "language": "id" + } + ] + }, + "required": true, + "meta": true + }, + { + "id": 3, + "name": "Phone Number", + "order": 3, + "type": "number", + "required": true, + "meta": true, + "extra": [ + { + "content": "We will not share your phone number to public", + "placement": "before", + "translations": [ + { + "content": "Kita tidak akan mempublikasikan nomor anda", + "language": "id" + } + ] + } + ], + "translations": [ + { + "name": "Nomor Telepon", + "language": "id" + } + ] + }, + { + "id": 4, + "name": "Location (Using API)", + "order": 4, + "type": "cascade", + "required": true, + "meta": true, + "api": { + "list": "children", + "initial": 1, + "endpoint": "https://rtmis.akvotest.org/api/v1/administration" + }, + "extra": [ + { + "content": "Please select your current origin administration", + "placement": "before", + "translations": [ + { + "content": "Silakan pilih Kecamatan anda sekarang", + "language": "id" + } + ] + } + ], + "translations": [ + { + "name": "Lokasi (Menggunakan API)", + "language": "id" + } + ] + }, + { + "id": 5, + "name": "Birthdate", + "order": 5, + "type": "date", + "required": true, + "meta": true, + "translations": [ + { + "name": "Tanggal Lahir", + "language": "id" + } + ] + }, + { + "id": 6, + "name": "Gender", + "order": 6, + "type": "option", + "required": true, + "meta": true, + "translations": [ + { + "name": "Jenis Kelamin", + "language": "id" + } + ], + "option": [ + { + "id": 1, + "name": "Male", + "order": 1, + "translations": [ + { + "name": "Laki-Laki", + "language": "id" + } + ] + }, + { + "id": 2, + "name": "Female", + "order": 2, + "translations": [ + { + "name": "Perempuan", + "language": "id" + } + ] + }, + { + "id": 3, + "name": "Other", + "order": 3, + "translations": [ + { + "name": "Lainnya", + "language": "id" + } + ] + } + ] + }, + { + "id": 7, + "name": "Marital Status", + "order": 7, + "type": "option", + "required": true, + "dependency": [ + { + "id": 6, + "options": [ + "Female", + "Male" + ] + } + ], + "meta": false, + "translations": [ + { + "name": "Status Keluarga", + "language": "id" + } + ], + "option": [ + { + "id": 4, + "name": "Single", + "order": 1, + "translations": [ + { + "name": "Jomblo", + "language": "id" + } + ] + }, + { + "id": 5, + "name": "Maried", + "order": 2, + "translations": [ + { + "name": "Menikah", + "language": "id" + } + ] + }, + { + "id": 6, + "name": "Widowed", + "order": 3, + "translations": [ + { + "name": "Janda / Duda", + "language": "id" + } + ] + } + ] + } + ] + }, + { + "id": 1693886026375, + "name": "Culinary Group", + "description": "Example placeholder description text for Culinary Question Group. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus imperdiet orci in feugiat tincidunt. Donec tempor suscipit accumsan. Vestibulum orci risus, mattis vitae ex et, consectetur consequat augue. Nunc et ante vel massa sollicitudin posuere vel ornare ligula. Ut a mattis massa. Mauris pulvinar congue sem, et venenatis orci vulputate id. Praesent odio purus, ultricies non eros at, iaculis imperdiet turpis. Donec non massa ligula.", + "order": 2, + "repeatable": false, + "translations": [ + { + "name": "Pertanyaan Tentang Kuliner", + "language": "id", + "description": "Contoh teks pengganti deskripsi untuk Grup Pertanyaan Kuliner. Demikian pula, tidak adakah orang yang mencintai atau mengejar atau ingin mengalami penderitaan, bukan semata-mata karena penderitaan itu sendiri, tetapi karena sesekali terjadi keadaan di mana susah-payah dan penderitaan dapat memberikan kepadanya kesenangan yang besar." + } + ], + "question": [ + { + "id": 8, + "name": "How much do you spent for meals a day?", + "order": 1, + "type": "number", + "required": false, + "meta": false, + "translations": [ + { + "name": "Berapa biasanya uang yang anda habiskan per hari untuk makanan", + "language": "id" + } + ] + }, + { + "id": 9, + "name": "How many times do you usually eat in a day?", + "order": 2, + "type": "number", + "required": false, + "meta": false, + "translations": [ + { + "name": "Berapa kali anda biasanya makan dalam sehari", + "language": "id" + } + ] + }, + { + "id": 11, + "name": "Favorite Food", + "order": 4, + "type": "multiple_option", + "tooltip": { + "text": "Please mention the available options", + "translations": [ + { + "text": "Tolong pilih contoh yang ada", + "language": "id" + } + ] + }, + "required": false, + "meta": true, + "translations": [ + { + "name": "Makanan Favorit", + "language": "id", + "allowOtherText": "Silahkan menambahkan menu lain jika ada" + } + ], + "option": [ + { + "id": 7, + "name": "Asian", + "order": 1, + "translations": [ + { + "name": "Asia", + "language": "id" + } + ] + }, + { + "id": 8, + "name": "Western", + "order": 2, + "translations": [ + { + "name": "Barat", + "language": "id" + } + ] + }, + { + "id": 9, + "name": "Vegetarian", + "order": 3 + } + ] + }, + { + "id": 12, + "name": "Please specify", + "order": 5, + "type": "input", + "tooltip": { + "text": "Desert or snacks are allowed too", + "translations": [ + { + "text": "Makanan Penutup dan Makanan Ringan juga diperbolehkan", + "language": "id" + } + ] + }, + "required": true, + "dependency": [ + { + "id": 11, + "options": [ + "Asian", + "Western" + ] + } + ], + "meta": false, + "translations": [ + { + "name": "Tolong sebutkan", + "language": "id" + } + ] + }, + { + "id": 13, + "name": "Do you know beef rendang?", + "order": 6, + "type": "option", + "required": false, + "meta": false, + "translations": [ + { + "name": "Apakah anda tahu Rendang Daging?", + "language": "id", + "allowOtherText": "Jawaban Lain" + } + ], + "option": [ + { + "id": 10, + "name": "Yes", + "order": 1, + "translations": [ + { + "name": "Ya", + "language": "id" + } + ] + }, + { + "id": 11, + "name": "No", + "order": 2, + "translations": [ + { + "name": "Tidak", + "language": "id" + } + ] + } + ] + }, + { + "id": 14, + "name": "Weight", + "order": 7, + "type": "number", + "required": true, + "meta": false, + "rule": { + "max": 10, + "min": 5 + }, + "translations": [ + { + "name": "Berat Badan", + "language": "id" + } + ] + }, + { + "id": 15, + "name": "Where do you usually order Rendang from ?", + "order": 8, + "type": "option", + "required": true, + "dependency": [ + { + "id": 13, + "options": [ + "Yes" + ] + }, + { + "id": 14, + "min": 8 + } + ], + "meta": false, + "translations": [ + { + "name": "Dimana anda biasanya membeli Rendang?", + "language": "id" + } + ], + "option": [ + { + "id": 12, + "name": "Pagi Sore", + "order": 1 + }, + { + "id": 13, + "name": "Any Rendang Restaurant", + "order": 2, + "translations": [ + { + "name": "Restoran Rendang Manapun", + "language": "id" + } + ] + } + ] + }, + { + "id": 16, + "name": "Do you want to order Rendang from Pagi Sore now?", + "order": 9, + "type": "option", + "required": true, + "dependency": [ + { + "id": 15, + "options": [ + "Pagi Sore" + ] + } + ], + "meta": false, + "translations": [ + { + "name": "Apakah anda ingin memesan Rendang dari Pagi Sore?", + "language": "id" + } + ], + "option": [ + { + "id": 14, + "name": "Yes", + "order": 1, + "translations": [ + { + "name": "Ya", + "language": "id" + } + ] + }, + { + "id": 15, + "name": "No", + "order": 2, + "translations": [ + { + "name": "Tidak", + "language": "id" + } + ] + } + ] + } + ] + }, + { + "id": 1693886026377, + "name": "Other Questions", + "description": "This is an example of repeat group question", + "order": 3, + "repeatable": true, + "translations": [ + { + "name": "Pertanyaan Lain", + "language": "id", + "description": "Ini contoh dari pertanyaan berulang", + "repeat_text": "Isi lagi" + } + ], + "question": [ + { + "id": 18, + "name": "Comment", + "order": 1, + "type": "text", + "required": true, + "meta": false, + "extra": [ + { + "content": "Please write at least 10 words", + "placement": "after", + "translations": [ + { + "content": "Tolong tulis minimal 10 kata", + "language": "id" + } + ] + } + ], + "translations": [ + { + "name": "Komentar", + "language": "id" + } + ] + }, + { + "id": 19, + "name": "Job title", + "order": 2, + "type": "option", + "required": true, + "meta": false, + "translations": [ + { + "name": "Titel Pekerjaan", + "language": "id" + } + ], + "option": [ + { + "id": 16, + "name": "Director", + "order": 1, + "translations": [ + { + "name": "Direktur", + "language": "id" + } + ] + }, + { + "id": 17, + "name": "Manager", + "order": 2, + "translations": [ + { + "name": "Manajer", + "language": "id" + } + ] + }, + { + "id": 18, + "name": "Staff", + "order": 3, + "translations": [ + { + "name": "Staf", + "language": "id" + } + ] + } + ] + }, + { + "id": 20, + "name": "Dependency on Gender Male/Female", + "order": 3, + "type": "input", + "required": true, + "dependency": [ + { + "id": 6, + "options": [ + "Female", + "Male" + ] + } + ], + "meta": false + }, + { + "id": 21, + "name": "Dependency on Job Title Staff", + "order": 4, + "type": "option", + "required": true, + "dependency": [ + { + "id": 19, + "options": [ + "Staff" + ] + } + ], + "meta": false, + "option": [ + { + "id": 19, + "name": "Contract", + "order": 1 + }, + { + "id": 20, + "name": "Internship", + "order": 2 + } + ] + }, + { + "id": 23, + "name": "Please input any number start from 0 (no decimal)", + "order": 6, + "type": "number", + "required": true, + "meta": false, + "translations": [ + { + "name": "Masukkan angka berapapun, dimulai dari 0 (selain decimal)", + "language": "id" + } + ] + }, + { + "id": 24, + "name": "Please input any number start from 0 to 10 (allow decimal)", + "order": 7, + "type": "number", + "required": true, + "meta": false, + "rule": { + "max": 10, + "min": 0, + "allowDecimal": true + }, + "translations": [ + { + "name": "Masukkan angka berapapun, dimulai dari 1 sampai 10 (boleh decimal)", + "language": "id" + } + ] + }, + { + "id": 25, + "name": "Please input any number start from 0 to 5 (allow decimal)", + "order": 8, + "type": "number", + "required": false, + "meta": false, + "rule": { + "max": 5, + "min": 0, + "allowDecimal": true + }, + "translations": [ + { + "name": "Masukkan angka berapapun, dimulai dari 0 sampai 5 (boleh decimal)", + "language": "id" + } + ] + } + ] + }, + { + "id": 1693886026378, + "name": "Repeat Question", + "description": "This is an example of repeat group question", + "order": 4, + "repeatable": true, + "translations": [ + { + "name": "Pertanyaan Berulang", + "language": "id", + "repeatText": "Tambahkan Pertanyaan Berulang", + "description": "Ini contoh dari pertanyaan berulang" + } + ], + "question": [ + { + "id": 26, + "name": "Comment for Pagi Sore", + "order": 1, + "type": "text", + "required": false, + "dependency": [ + { + "id": 15, + "options": [ + "Pagi Sore" + ] + } + ], + "meta": false, + "translations": [ + { + "name": "Komentar", + "language": "id" + } + ] + }, + { + "id": 27, + "name": "Date with Rule", + "order": 2, + "type": "date", + "required": false, + "meta": false, + "rule": { + "maxDate": "2022-12-31", + "minDate": "2022-01-01" + }, + "translations": [ + { + "name": "Tanggal dengan ketentuan", + "language": "id" + } + ] + }, + { + "id": 28, + "name": "Question with Custom Params", + "order": 3, + "type": "text", + "required": false, + "meta": false, + "translations": [ + { + "name": "Pertanyaan dengan Parameter Khusus", + "language": "id" + } + ] + } + ] + } + ] +} \ No newline at end of file From d16398a2b58553f38a41b765b0bf4ffd8f26d15c Mon Sep 17 00:00:00 2001 From: wayangalihpratama Date: Wed, 6 Sep 2023 09:48:48 +0800 Subject: [PATCH 10/18] [#12] Remove commented code --- backend/akvo/core_forms/serializers/form.py | 12 ------------ backend/akvo/core_forms/serializers/option.py | 4 ---- .../akvo/core_forms/serializers/question.py | 19 ------------------- .../core_forms/serializers/question_group.py | 11 ----------- backend/akvo/core_forms/views/form.py | 2 ++ 5 files changed, 2 insertions(+), 46 deletions(-) diff --git a/backend/akvo/core_forms/serializers/form.py b/backend/akvo/core_forms/serializers/form.py index ef81a77..10881df 100644 --- a/backend/akvo/core_forms/serializers/form.py +++ b/backend/akvo/core_forms/serializers/form.py @@ -107,15 +107,3 @@ def create(self, validated_data): }) serializer.save(form=form) return form - - # class Meta: - # model = Forms - # fields = [ - # "id", - # "name", - # "description", - # "default_language", - # "languages", - # "version", - # "translations", - # ] diff --git a/backend/akvo/core_forms/serializers/option.py b/backend/akvo/core_forms/serializers/option.py index 0a861d4..7b1e9d6 100644 --- a/backend/akvo/core_forms/serializers/option.py +++ b/backend/akvo/core_forms/serializers/option.py @@ -37,7 +37,3 @@ def __init__(self, **kwargs): def create(self, validated_data): opt = Options.objects.create(**validated_data) return opt - - # class Meta: - # model = Options - # fields = ["id", "code", "name", "order", "color", "translations"] diff --git a/backend/akvo/core_forms/serializers/question.py b/backend/akvo/core_forms/serializers/question.py index 6cf34c5..21ab8ff 100644 --- a/backend/akvo/core_forms/serializers/question.py +++ b/backend/akvo/core_forms/serializers/question.py @@ -191,22 +191,3 @@ def create(self, validated_data): }) serializer.save(question=q) return q - - # class Meta: - # model = Questions - # fields = [ - # "id", - # "name", - # "order", - # "type", - # "tooltip", - # "required", - # "dependency", - # "meta", - # "rule", - # "api", - # "extra", - # "translations", - # "data_api_url", - # "autofield", - # ] diff --git a/backend/akvo/core_forms/serializers/question_group.py b/backend/akvo/core_forms/serializers/question_group.py index ed257a7..1abe766 100644 --- a/backend/akvo/core_forms/serializers/question_group.py +++ b/backend/akvo/core_forms/serializers/question_group.py @@ -79,14 +79,3 @@ def create(self, validated_data): }) serializer.save(form=qg.form, question_group=qg) return qg - - # class Meta: - # model = QuestionGroups - # fields = [ - # "id", - # "name", - # "description", - # "order", - # "repeatable", - # "translations", - # ] diff --git a/backend/akvo/core_forms/views/form.py b/backend/akvo/core_forms/views/form.py index 91dc515..5df59aa 100644 --- a/backend/akvo/core_forms/views/form.py +++ b/backend/akvo/core_forms/views/form.py @@ -50,6 +50,7 @@ def get_form_by_id(request, form_id): class FormManagementView(APIView): @extend_schema( + request=AddFormSerializer(), responses={ (200, "application/json"): DefaultResponseSerializer, }, @@ -73,6 +74,7 @@ def post(self, request): ) @extend_schema( + request=AddFormSerializer(), responses={ (200, "application/json"): DefaultResponseSerializer, }, From 846272800adf10e4c3c0b636d5df710229b5878d Mon Sep 17 00:00:00 2001 From: wayangalihpratama Date: Wed, 6 Sep 2023 12:12:52 +0800 Subject: [PATCH 11/18] [#12] Init update fn on serializer --- backend/akvo/core_forms/serializers/form.py | 16 ++++++++++ backend/akvo/core_forms/serializers/option.py | 14 ++++++++ .../akvo/core_forms/serializers/question.py | 32 +++++++++++++++++++ .../core_forms/serializers/question_group.py | 14 ++++++++ 4 files changed, 76 insertions(+) diff --git a/backend/akvo/core_forms/serializers/form.py b/backend/akvo/core_forms/serializers/form.py index 10881df..3d467aa 100644 --- a/backend/akvo/core_forms/serializers/form.py +++ b/backend/akvo/core_forms/serializers/form.py @@ -107,3 +107,19 @@ def create(self, validated_data): }) serializer.save(form=form) return form + + def update(self, instance, validated_data): + instance.name = validated_data.get( + 'name', instance.name) + instance.description = validated_data.get( + 'description', instance.description) + instance.version = validated_data.get( + 'version', instance.version) + instance.languages = validated_data.get( + 'languages', instance.languages) + instance.default_language = validated_data.get( + 'default_language', instance.default_language) + instance.translations = validated_data.get( + 'translations', instance.translations) + # check and delete question group + return instance diff --git a/backend/akvo/core_forms/serializers/option.py b/backend/akvo/core_forms/serializers/option.py index 7b1e9d6..94d6bbf 100644 --- a/backend/akvo/core_forms/serializers/option.py +++ b/backend/akvo/core_forms/serializers/option.py @@ -37,3 +37,17 @@ def __init__(self, **kwargs): def create(self, validated_data): opt = Options.objects.create(**validated_data) return opt + + def update(self, instance, validated_data): + instance.name = validated_data.get( + 'name', instance.name) + instance.order = validated_data.get( + 'order', instance.order) + instance.code = validated_data.get( + 'code', instance.code) + instance.color = validated_data.get( + 'color', instance.color) + instance.translations = validated_data.get( + 'translations', instance.translations) + instance.save() + return instance diff --git a/backend/akvo/core_forms/serializers/question.py b/backend/akvo/core_forms/serializers/question.py index 21ab8ff..0050474 100644 --- a/backend/akvo/core_forms/serializers/question.py +++ b/backend/akvo/core_forms/serializers/question.py @@ -191,3 +191,35 @@ def create(self, validated_data): }) serializer.save(question=q) return q + + def update(self, instance, validated_data): + instance.name = validated_data.get( + 'name', instance.name) + instance.order = validated_data.get( + 'order', instance.order) + instance.type = validated_data.get( + 'type', instance.type) + instance.tooltip = validated_data.get( + 'tooltip', instance.tooltip) + instance.required = validated_data.get( + 'required', instance.required) + instance.meta = validated_data.get( + 'meta', instance.meta) + instance.rule = validated_data.get( + 'rule', instance.rule) + instance.dependency = validated_data.get( + 'dependency', instance.dependency) + instance.api = validated_data.get( + 'api', instance.api) + instance.extra = validated_data.get( + 'extra', instance.extra) + instance.autofield = validated_data.get( + 'autofield', instance.autofield) + instance.data_api_url = validated_data.get( + 'data_api_url', instance.data_api_url) + instance.translations = validated_data.get( + 'translations', instance.translations) + # if question type changed from option, + # we should delete the old options + # check and delete options + return instance diff --git a/backend/akvo/core_forms/serializers/question_group.py b/backend/akvo/core_forms/serializers/question_group.py index 1abe766..d35f871 100644 --- a/backend/akvo/core_forms/serializers/question_group.py +++ b/backend/akvo/core_forms/serializers/question_group.py @@ -79,3 +79,17 @@ def create(self, validated_data): }) serializer.save(form=qg.form, question_group=qg) return qg + + def update(self, instance, validated_data): + instance.name = validated_data.get( + 'name', instance.name) + instance.description = validated_data.get( + 'description', instance.description) + instance.order = validated_data.get( + 'order', instance.order) + instance.repeatable = validated_data.get( + 'repeatable', instance.repeatable) + instance.translations = validated_data.get( + 'translations', instance.translations) + # check and delete question + return instance From c70b7a6529dbcd8250e2715ed4f52e91a73e7975 Mon Sep 17 00:00:00 2001 From: wayangalihpratama Date: Wed, 6 Sep 2023 12:52:08 +0800 Subject: [PATCH 12/18] [#12] Update / create question group & question --- backend/akvo/core_forms/serializers/form.py | 28 ++++++++++++++++++- .../core_forms/serializers/question_group.py | 20 ++++++++++++- .../core_forms/tests/test_form_endpoint.py | 10 ++++++- backend/akvo/core_forms/views/form.py | 14 ++++++++++ backend/db.sqlite3 | 0 5 files changed, 69 insertions(+), 3 deletions(-) create mode 100644 backend/db.sqlite3 diff --git a/backend/akvo/core_forms/serializers/form.py b/backend/akvo/core_forms/serializers/form.py index 3d467aa..90efd33 100644 --- a/backend/akvo/core_forms/serializers/form.py +++ b/backend/akvo/core_forms/serializers/form.py @@ -109,6 +109,7 @@ def create(self, validated_data): return form def update(self, instance, validated_data): + # update form instance.name = validated_data.get( 'name', instance.name) instance.description = validated_data.get( @@ -121,5 +122,30 @@ def update(self, instance, validated_data): 'default_language', instance.default_language) instance.translations = validated_data.get( 'translations', instance.translations) - # check and delete question group + + # TODO :: check and delete question group + current_qgs = QuestionGroups.objects.filter(form=instance).all() + current_qg_ids = [cqg.id for cqg in current_qgs] + + new_qg_data = validated_data.get('question_group') + new_qg_ids = [nqg.get('id') for nqg in new_qg_data] + missing_qg_ids = list(set(current_qg_ids) - set(new_qg_ids)) + print('MISSING QG IDS', missing_qg_ids) + + # update question group + for qg in new_qg_data: + current_qg = QuestionGroups.objects.filter(id=qg.get('id')).first() + serializer = AddQuestionGroupSerializer(data=qg) + if not serializer.is_valid(): + raise serializers.ValidationError({ + "message": validate_serializers_message(serializer.errors), + "details": serializer.errors, + }) + if not current_qg: + serializer.save(form=instance) + if current_qg: + serializer.update( + instance=current_qg, + validated_data=serializer.validated_data) + return instance diff --git a/backend/akvo/core_forms/serializers/question_group.py b/backend/akvo/core_forms/serializers/question_group.py index d35f871..3bc6360 100644 --- a/backend/akvo/core_forms/serializers/question_group.py +++ b/backend/akvo/core_forms/serializers/question_group.py @@ -1,7 +1,7 @@ from drf_spectacular.utils import extend_schema_field from rest_framework import serializers -from akvo.core_forms.models import QuestionGroups +from akvo.core_forms.models import QuestionGroups, Questions from akvo.core_forms.serializers.question import ( ListQuestionSerializer, AddQuestionSerializer @@ -91,5 +91,23 @@ def update(self, instance, validated_data): 'repeatable', instance.repeatable) instance.translations = validated_data.get( 'translations', instance.translations) + # check and delete question + new_q_data = validated_data.get('question', []) + for q in new_q_data: + current_q = Questions.objects.filter(id=q.get('id')).first() + serializer = AddQuestionSerializer(data=q) + if not serializer.is_valid(): + raise serializers.ValidationError({ + "message": validate_serializers_message(serializer.errors), + "details": serializer.errors, + }) + if not current_q: + serializer.save( + form=instance.form, question_group=instance) + if current_q: + serializer.update( + instance=current_q, + validated_data=serializer.validated_data) + return instance diff --git a/backend/akvo/core_forms/tests/test_form_endpoint.py b/backend/akvo/core_forms/tests/test_form_endpoint.py index 16d5620..b111ac6 100644 --- a/backend/akvo/core_forms/tests/test_form_endpoint.py +++ b/backend/akvo/core_forms/tests/test_form_endpoint.py @@ -38,7 +38,7 @@ def test_endpoint_get_form_by_id(self): expected_result = json.load(f) self.assertEqual(result, expected_result) - def test_endpoint_post_form(self): + def test_endpoint_post_put_form(self): with open('./source/static/example_form_payload.json', 'r') as f: expected_payload = json.load(f) data = self.client.post( @@ -61,3 +61,11 @@ def test_endpoint_post_form(self): ) as f: expected_result = json.load(f) self.assertEqual(result, expected_result) + # update form + data = self.client.put( + "/api/form", + follow=True, + data=expected_payload, + content_type="application/json" + ) + self.assertEqual(data.status_code, 200) diff --git a/backend/akvo/core_forms/views/form.py b/backend/akvo/core_forms/views/form.py index 5df59aa..60d315f 100644 --- a/backend/akvo/core_forms/views/form.py +++ b/backend/akvo/core_forms/views/form.py @@ -82,6 +82,20 @@ def post(self, request): summary="Update form definition", ) def put(self, request): + form_id = request.data.get('id') + instance = get_object_or_404(Forms, pk=form_id) + serializer = AddFormSerializer(data=request.data) + if not serializer.is_valid(): + print('FORM ERROR', serializer.errors) + return Response( + { + "message": validate_serializers_message(serializer.errors), + "details": serializer.errors, + }, + status=status.HTTP_400_BAD_REQUEST, + ) + serializer.update( + instance=instance, validated_data=serializer.validated_data) return Response( {"message": "Update form success"}, status=status.HTTP_200_OK ) diff --git a/backend/db.sqlite3 b/backend/db.sqlite3 new file mode 100644 index 0000000..e69de29 From a3599e7b36a0c9bc585433e120f214288d7c85a1 Mon Sep 17 00:00:00 2001 From: wayangalihpratama Date: Wed, 6 Sep 2023 13:35:28 +0800 Subject: [PATCH 13/18] [#12] Handle update question option --- .../akvo/core_forms/serializers/question.py | 20 +++++++++++++++---- .../core_forms/tests/test_form_endpoint.py | 13 ++++++++++++ 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/backend/akvo/core_forms/serializers/question.py b/backend/akvo/core_forms/serializers/question.py index 0050474..e7acc72 100644 --- a/backend/akvo/core_forms/serializers/question.py +++ b/backend/akvo/core_forms/serializers/question.py @@ -5,7 +5,10 @@ from rest_framework import serializers from akvo.core_forms.constants import QuestionTypes -from akvo.core_forms.models import Questions +from akvo.core_forms.models import ( + Questions, + Options +) from akvo.core_forms.serializers.option import ( ListOptionSerializer, AddOptionSerializer, @@ -182,7 +185,6 @@ def create(self, validated_data): validated_data["type"] = getattr(QuestionTypes, qtype) q = Questions.objects.create(**validated_data) for opt in options_data: - opt["question"] = q serializer = AddOptionSerializer(data=opt) if not serializer.is_valid(): raise serializers.ValidationError({ @@ -219,7 +221,17 @@ def update(self, instance, validated_data): 'data_api_url', instance.data_api_url) instance.translations = validated_data.get( 'translations', instance.translations) - # if question type changed from option, - # we should delete the old options + # check and delete options + # delete old options then create new + Options.objects.filter(question=instance).delete() + new_option_data = validated_data.get('option', []) + for opt in new_option_data: + serializer = AddOptionSerializer(data=opt) + if not serializer.is_valid(): + raise serializers.ValidationError({ + "message": validate_serializers_message(serializer.errors), + "details": serializer.errors, + }) + serializer.save(question=instance) return instance diff --git a/backend/akvo/core_forms/tests/test_form_endpoint.py b/backend/akvo/core_forms/tests/test_form_endpoint.py index b111ac6..ba5d81a 100644 --- a/backend/akvo/core_forms/tests/test_form_endpoint.py +++ b/backend/akvo/core_forms/tests/test_form_endpoint.py @@ -69,3 +69,16 @@ def test_endpoint_post_put_form(self): content_type="application/json" ) self.assertEqual(data.status_code, 200) + # get form after put + data = self.client.get( + f"/api/form/{expected_payload.get('id')}", + follow=True + ) + self.assertEqual(data.status_code, 200) + result = data.json() + # print(json.dumps(result, indent=2)) + with open( + './source/static/example_form_payload_result.json', 'r' + ) as f: + expected_result = json.load(f) + self.assertEqual(result, expected_result) From cfaad0669780e1276f3f7537542e7396b16b7638 Mon Sep 17 00:00:00 2001 From: wayangalihpratama Date: Wed, 6 Sep 2023 16:17:12 +0800 Subject: [PATCH 14/18] [#12] Handle delete question when update form def --- backend/akvo/core_forms/serializers/form.py | 4 +- .../core_forms/serializers/question_group.py | 14 +++ .../core_forms/tests/test_form_endpoint.py | 96 ++++++++++++++++--- 3 files changed, 100 insertions(+), 14 deletions(-) diff --git a/backend/akvo/core_forms/serializers/form.py b/backend/akvo/core_forms/serializers/form.py index 90efd33..fd6128f 100644 --- a/backend/akvo/core_forms/serializers/form.py +++ b/backend/akvo/core_forms/serializers/form.py @@ -127,12 +127,12 @@ def update(self, instance, validated_data): current_qgs = QuestionGroups.objects.filter(form=instance).all() current_qg_ids = [cqg.id for cqg in current_qgs] - new_qg_data = validated_data.get('question_group') + new_qg_data = validated_data.get('question_group', []) new_qg_ids = [nqg.get('id') for nqg in new_qg_data] missing_qg_ids = list(set(current_qg_ids) - set(new_qg_ids)) print('MISSING QG IDS', missing_qg_ids) - # update question group + # create or update question group for qg in new_qg_data: current_qg = QuestionGroups.objects.filter(id=qg.get('id')).first() serializer = AddQuestionGroupSerializer(data=qg) diff --git a/backend/akvo/core_forms/serializers/question_group.py b/backend/akvo/core_forms/serializers/question_group.py index 3bc6360..0f9678b 100644 --- a/backend/akvo/core_forms/serializers/question_group.py +++ b/backend/akvo/core_forms/serializers/question_group.py @@ -13,6 +13,7 @@ CustomBooleanField ) from akvo.utils.custom_serializer_fields import validate_serializers_message +from akvo.core_data.models import Answers class ListQuestionGroupSerializer(serializers.ModelSerializer): @@ -93,7 +94,20 @@ def update(self, instance, validated_data): 'translations', instance.translations) # check and delete question + current_qs = Questions.objects.filter(question_group=instance).all() + current_qs_ids = [cq.id for cq in current_qs] + new_q_data = validated_data.get('question', []) + new_q_ids = [nq.get('id') for nq in new_q_data] + missing_q_ids = list(set(current_qs_ids) - set(new_q_ids)) + print('MISSING QG IDS', missing_q_ids) + # check missing question ids with answer and delete + for qid in missing_q_ids: + answers = Answers.objects.filter(question_id=qid).count() + if not answers: + Questions.objects.filter(id=qid).delete() + + # create or update questions for q in new_q_data: current_q = Questions.objects.filter(id=q.get('id')).first() serializer = AddQuestionSerializer(data=q) diff --git a/backend/akvo/core_forms/tests/test_form_endpoint.py b/backend/akvo/core_forms/tests/test_form_endpoint.py index ba5d81a..6a2565f 100644 --- a/backend/akvo/core_forms/tests/test_form_endpoint.py +++ b/backend/akvo/core_forms/tests/test_form_endpoint.py @@ -38,7 +38,7 @@ def test_endpoint_get_form_by_id(self): expected_result = json.load(f) self.assertEqual(result, expected_result) - def test_endpoint_post_put_form(self): + def test_endpoint_post_form(self): with open('./source/static/example_form_payload.json', 'r') as f: expected_payload = json.load(f) data = self.client.post( @@ -61,24 +61,96 @@ def test_endpoint_post_put_form(self): ) as f: expected_result = json.load(f) self.assertEqual(result, expected_result) - # update form + + def test_endpoint_put_form_with_deleted_question(self): + payload_question = [{ + "id": 1693987349172, + "order": 1, + "questionGroupId": 1693987349171, + "name": "Phasellus amet suscipit ac tristique nisl", + "type": "input", + "required": False, + "meta": False + }, { + "id": 1693987361547, + "order": 2, + "questionGroupId": 1693987349171, + "name": "Tincidunt mauris tristique eu dapibus augue", + "type": "input", + "required": False, + "meta": False + }] + payload = { + "id": 1693987349170, + "name": "New Form", + "description": "New Form Description", + "question_group": [{ + "id": 1693987349171, + "name": "Lorem lorem Nam", + "order": 1, + "repeatable": False, + "question": payload_question + }] + } + # POST + data = self.client.post( + "/api/form", + data=payload, + content_type="application/json" + ) + self.assertEqual(data.status_code, 200) + result = data.json() + self.assertEqual(result, {"message": "ok"}) + # PUT (payload with question removed) + payload = { + "id": 1693987349170, + "name": "New Form", + "description": "New Form Description", + "question_group": [{ + "id": 1693987349171, + "name": "Lorem lorem Nam", + "order": 1, + "repeatable": False, + "question": [payload_question[1]] + }] + } data = self.client.put( "/api/form", - follow=True, - data=expected_payload, + data=payload, content_type="application/json" ) self.assertEqual(data.status_code, 200) - # get form after put + result = data.json() + self.assertEqual(result, {"message": "Update form success"}) + # GET data = self.client.get( - f"/api/form/{expected_payload.get('id')}", + "/api/form/1693987349170", follow=True ) self.assertEqual(data.status_code, 200) result = data.json() - # print(json.dumps(result, indent=2)) - with open( - './source/static/example_form_payload_result.json', 'r' - ) as f: - expected_result = json.load(f) - self.assertEqual(result, expected_result) + self.assertEqual(result, { + "id": 1693987349170, + "name": "New Form", + "description": "New Form Description", + "defaultLanguage": "en", + "languages": ["en"], + "version": 1, + "translations": None, + "question_group": [{ + "id": 1693987349171, + "name": "Lorem lorem Nam", + "description": None, + "order": 1, + "repeatable": False, + "translations": None, + "question": [{ + "id": 1693987361547, + "name": "Tincidunt mauris tristique eu dapibus augue", + "order": 2, + "type": "input", + "required": False, + "meta": False + }] + }] + }) From 51981956ad2bfb9fdad00c64339e60bccf3e5a29 Mon Sep 17 00:00:00 2001 From: wayangalihpratama Date: Wed, 6 Sep 2023 17:04:17 +0800 Subject: [PATCH 15/18] [#12] Handle update form def with removed question group --- backend/akvo/core_forms/serializers/form.py | 7 +- .../core_forms/serializers/question_group.py | 15 +-- .../core_forms/tests/test_form_endpoint.py | 93 +++++++++++++++++++ 3 files changed, 106 insertions(+), 9 deletions(-) diff --git a/backend/akvo/core_forms/serializers/form.py b/backend/akvo/core_forms/serializers/form.py index fd6128f..ca6f766 100644 --- a/backend/akvo/core_forms/serializers/form.py +++ b/backend/akvo/core_forms/serializers/form.py @@ -129,8 +129,6 @@ def update(self, instance, validated_data): new_qg_data = validated_data.get('question_group', []) new_qg_ids = [nqg.get('id') for nqg in new_qg_data] - missing_qg_ids = list(set(current_qg_ids) - set(new_qg_ids)) - print('MISSING QG IDS', missing_qg_ids) # create or update question group for qg in new_qg_data: @@ -148,4 +146,9 @@ def update(self, instance, validated_data): instance=current_qg, validated_data=serializer.validated_data) + missing_qg_ids = list(set(current_qg_ids) - set(new_qg_ids)) + print('MISSING QG IDS', missing_qg_ids) + # delete missing question groups + QuestionGroups.objects.filter(id__in=missing_qg_ids).delete() + return instance diff --git a/backend/akvo/core_forms/serializers/question_group.py b/backend/akvo/core_forms/serializers/question_group.py index 0f9678b..e9589ae 100644 --- a/backend/akvo/core_forms/serializers/question_group.py +++ b/backend/akvo/core_forms/serializers/question_group.py @@ -99,13 +99,6 @@ def update(self, instance, validated_data): new_q_data = validated_data.get('question', []) new_q_ids = [nq.get('id') for nq in new_q_data] - missing_q_ids = list(set(current_qs_ids) - set(new_q_ids)) - print('MISSING QG IDS', missing_q_ids) - # check missing question ids with answer and delete - for qid in missing_q_ids: - answers = Answers.objects.filter(question_id=qid).count() - if not answers: - Questions.objects.filter(id=qid).delete() # create or update questions for q in new_q_data: @@ -124,4 +117,12 @@ def update(self, instance, validated_data): instance=current_q, validated_data=serializer.validated_data) + missing_q_ids = list(set(current_qs_ids) - set(new_q_ids)) + print('MISSING Q IDS', missing_q_ids) + # check missing question ids with answer and delete + for qid in missing_q_ids: + answers = Answers.objects.filter(question_id=qid).count() + if not answers: + Questions.objects.filter(id=qid).delete() + return instance diff --git a/backend/akvo/core_forms/tests/test_form_endpoint.py b/backend/akvo/core_forms/tests/test_form_endpoint.py index 6a2565f..203bbce 100644 --- a/backend/akvo/core_forms/tests/test_form_endpoint.py +++ b/backend/akvo/core_forms/tests/test_form_endpoint.py @@ -154,3 +154,96 @@ def test_endpoint_put_form_with_deleted_question(self): }] }] }) + + def test_endpoint_put_form_with_deleted_question_group(self): + payload_question_group = [{ + "id": 1693988922938, + "name": "Ante aliquet lorem", + "order": 1, + "repeatable": False, + "question": [{ + "id": 1693988922939, + "order": 1, + "questionGroupId": 1693988922938, + "name": "Dolor ante augue adipiscing elit amet", + "type": "input", + "required": False, + "meta": False + }] + }, { + "id": 1693988928051, + "name": "Consequat Donec neque", + "order": 2, + "repeatable": False, + "question": [{ + "id": 1693988928052, + "order": 1, + "questionGroupId": 1693988928051, + "name": "Ornare consectetur neque Donec nisl lorem", + "type": "input", + "required": False, + "meta": False + }] + }] + payload = { + "id": 1693988922937, + "name": "New Form", + "description": "New Form Description", + "question_group": payload_question_group + } + # POST + data = self.client.post( + "/api/form", + data=payload, + content_type="application/json" + ) + self.assertEqual(data.status_code, 200) + result = data.json() + self.assertEqual(result, {"message": "ok"}) + # PUT (payload with question removed) + payload = { + "id": 1693988922937, + "name": "New Form", + "description": "New Form Description", + "question_group": [payload_question_group[0]] + } + data = self.client.put( + "/api/form", + data=payload, + content_type="application/json" + ) + self.assertEqual(data.status_code, 200) + result = data.json() + self.assertEqual(result, {"message": "Update form success"}) + # GET + data = self.client.get( + "/api/form/1693988922937", + follow=True + ) + self.assertEqual(data.status_code, 200) + result = data.json() + self.assertEqual(result, { + "id": 1693988922937, + "name": "New Form", + "description": "New Form Description", + "defaultLanguage": "en", + "languages": ["en"], + "version": 1, + "translations": None, + "question_group": [{ + "id": 1693988922938, + "name": "Ante aliquet lorem", + "description": None, + "order": 1, + "repeatable": False, + "translations": None, + "question": [{ + "id": 1693988922939, + "name": "Dolor ante augue adipiscing elit amet", + "order": 1, + "type": "input", + "required": False, + "meta": False + }] + }] + }) From 7cf2aad55ee1e37c4c691f58c2b613a411024241 Mon Sep 17 00:00:00 2001 From: wayangalihpratama Date: Wed, 6 Sep 2023 18:00:11 +0800 Subject: [PATCH 16/18] [#12] Update update option & improve form update test --- backend/akvo/core_forms/serializers/form.py | 3 +- backend/akvo/core_forms/serializers/option.py | 2 +- .../akvo/core_forms/serializers/question.py | 21 ++- .../core_forms/serializers/question_group.py | 1 - .../core_forms/tests/test_form_endpoint.py | 56 ++++++- .../tests/test_form_update_endpoint.py | 138 ++++++++++++++++++ .../source/static/example_form_payload.json | 40 ++--- 7 files changed, 228 insertions(+), 33 deletions(-) create mode 100644 backend/akvo/core_forms/tests/test_form_update_endpoint.py diff --git a/backend/akvo/core_forms/serializers/form.py b/backend/akvo/core_forms/serializers/form.py index ca6f766..aff45f8 100644 --- a/backend/akvo/core_forms/serializers/form.py +++ b/backend/akvo/core_forms/serializers/form.py @@ -123,7 +123,7 @@ def update(self, instance, validated_data): instance.translations = validated_data.get( 'translations', instance.translations) - # TODO :: check and delete question group + # check and delete question group current_qgs = QuestionGroups.objects.filter(form=instance).all() current_qg_ids = [cqg.id for cqg in current_qgs] @@ -147,7 +147,6 @@ def update(self, instance, validated_data): validated_data=serializer.validated_data) missing_qg_ids = list(set(current_qg_ids) - set(new_qg_ids)) - print('MISSING QG IDS', missing_qg_ids) # delete missing question groups QuestionGroups.objects.filter(id__in=missing_qg_ids).delete() diff --git a/backend/akvo/core_forms/serializers/option.py b/backend/akvo/core_forms/serializers/option.py index 94d6bbf..051cea9 100644 --- a/backend/akvo/core_forms/serializers/option.py +++ b/backend/akvo/core_forms/serializers/option.py @@ -24,7 +24,7 @@ class Meta: class AddOptionSerializer(serializers.Serializer): question = CustomIntegerField(read_only=True) - # id = CustomIntegerField() + id = CustomIntegerField() name = CustomCharField() order = CustomIntegerField() code = CustomCharField(required=False, allow_null=True) diff --git a/backend/akvo/core_forms/serializers/question.py b/backend/akvo/core_forms/serializers/question.py index e7acc72..d4f558e 100644 --- a/backend/akvo/core_forms/serializers/question.py +++ b/backend/akvo/core_forms/serializers/question.py @@ -195,6 +195,7 @@ def create(self, validated_data): return q def update(self, instance, validated_data): + # update question instance.name = validated_data.get( 'name', instance.name) instance.order = validated_data.get( @@ -223,15 +224,29 @@ def update(self, instance, validated_data): 'translations', instance.translations) # check and delete options - # delete old options then create new - Options.objects.filter(question=instance).delete() + current_options = Options.objects.filter(question=instance).all() + current_opt_ids = [co.id for co in current_options] + new_option_data = validated_data.get('option', []) + new_opt_ids = [no.get('id') for no in new_option_data] + for opt in new_option_data: + current_opt = Options.objects.filter(id=opt.get('id')).first() serializer = AddOptionSerializer(data=opt) if not serializer.is_valid(): raise serializers.ValidationError({ "message": validate_serializers_message(serializer.errors), "details": serializer.errors, }) - serializer.save(question=instance) + if not current_opt: + serializer.save(question=instance) + if current_opt: + serializer.update( + instance=current_opt, + validated_data=serializer.validated_data) + + missing_opt_ids = list(set(current_opt_ids) - set(new_opt_ids)) + # delete old options then create new + Options.objects.filter(id__in=missing_opt_ids).delete() + return instance diff --git a/backend/akvo/core_forms/serializers/question_group.py b/backend/akvo/core_forms/serializers/question_group.py index e9589ae..a6d5b1f 100644 --- a/backend/akvo/core_forms/serializers/question_group.py +++ b/backend/akvo/core_forms/serializers/question_group.py @@ -118,7 +118,6 @@ def update(self, instance, validated_data): validated_data=serializer.validated_data) missing_q_ids = list(set(current_qs_ids) - set(new_q_ids)) - print('MISSING Q IDS', missing_q_ids) # check missing question ids with answer and delete for qid in missing_q_ids: answers = Answers.objects.filter(question_id=qid).count() diff --git a/backend/akvo/core_forms/tests/test_form_endpoint.py b/backend/akvo/core_forms/tests/test_form_endpoint.py index 203bbce..7c7a6eb 100644 --- a/backend/akvo/core_forms/tests/test_form_endpoint.py +++ b/backend/akvo/core_forms/tests/test_form_endpoint.py @@ -111,7 +111,15 @@ def test_endpoint_put_form_with_deleted_question(self): "name": "Lorem lorem Nam", "order": 1, "repeatable": False, - "question": [payload_question[1]] + "question": [payload_question[0]] + [{ + "id": 1693987349188, + "order": 2, + "questionGroupId": 1693987349171, + "name": "Add new number question", + "type": "number", + "required": False, + "meta": False + }] }] } data = self.client.put( @@ -145,12 +153,19 @@ def test_endpoint_put_form_with_deleted_question(self): "repeatable": False, "translations": None, "question": [{ - "id": 1693987361547, - "name": "Tincidunt mauris tristique eu dapibus augue", - "order": 2, + "id": 1693987349172, + "name": "Phasellus amet suscipit ac tristique nisl", + "order": 1, "type": "input", "required": False, "meta": False + }, { + "id": 1693987349188, + "name": "Add new number question", + "order": 2, + "type": "number", + "required": False, + "meta": False }] }] }) @@ -200,12 +215,26 @@ def test_endpoint_put_form_with_deleted_question_group(self): self.assertEqual(data.status_code, 200) result = data.json() self.assertEqual(result, {"message": "ok"}) - # PUT (payload with question removed) + # PUT (payload with question group removed) payload = { "id": 1693988922937, "name": "New Form", "description": "New Form Description", - "question_group": [payload_question_group[0]] + "question_group": [payload_question_group[0]] + [{ + "id": 1693988922977, + "name": "New Question Group", + "order": 2, + "repeatable": False, + "question": [{ + "id": 1693988922955, + "order": 1, + "questionGroupId": 1693988922977, + "name": "New Question", + "type": "number", + "required": False, + "meta": False + }] + }] } data = self.client.put( "/api/form", @@ -245,5 +274,20 @@ def test_endpoint_put_form_with_deleted_question_group(self): "required": False, "meta": False }] + }, { + "id": 1693988922977, + "name": "New Question Group", + "description": None, + "order": 2, + "repeatable": False, + "translations": None, + "question": [{ + "id": 1693988922955, + "name": "New Question", + "order": 1, + "type": "number", + "required": False, + "meta": False + }] }] }) diff --git a/backend/akvo/core_forms/tests/test_form_update_endpoint.py b/backend/akvo/core_forms/tests/test_form_update_endpoint.py new file mode 100644 index 0000000..fad9087 --- /dev/null +++ b/backend/akvo/core_forms/tests/test_form_update_endpoint.py @@ -0,0 +1,138 @@ +from django.test import TestCase +from django.test.utils import override_settings + + +@override_settings(USE_TZ=False) +class TestFormUpdateEndpoint(TestCase): + def test_update_form_with_updated_options(self): + payload = { + "id": 1693992073895, + "name": "New Form", + "description": "New Form Description", + "question_group": [{ + "id": 1693992073896, + "name": "Sit quis dapibus", + "order": 1, + "repeatable": False, + "question": [{ + "id": 1693992073897, + "order": 1, + "questionGroupId": 1693992073896, + "name": "Sit ornare commodo sit ante consectetur", + "type": "option", + "required": False, + "meta": False, + "allowOther": False, + "option": [{ + "code": "NO1", + "name": "New Option 1", + "order": 1, + "id": 1693992338940, + "color": "#803838" + }, { + "code": "NO2", + "name": "New Option 2", + "order": 2, + "id": 1693992338941, + "color": "#5f5f5f" + }] + }] + }] + } + # POST + data = self.client.post( + "/api/form", + data=payload, + content_type="application/json" + ) + self.assertEqual(data.status_code, 200) + result = data.json() + self.assertEqual(result, {"message": "ok"}) + # PUT (payload with updated options) + payload = { + "id": 1693992073895, + "name": "New Form", + "description": "New Form Description", + "question_group": [{ + "id": 1693992073896, + "name": "Sit quis dapibus", + "order": 1, + "repeatable": False, + "question": [{ + "id": 1693992073897, + "order": 1, + "questionGroupId": 1693992073896, + "name": "Sit ornare commodo sit ante consectetur", + "type": "option", + "required": False, + "meta": False, + "allowOther": False, + "option": [{ + "code": "NO1", + "name": "New Option 1", + "order": 1, + "id": 1693992338940, + "color": "#803838" + }, { + "code": "NO3", + "name": "New Option 3", + "order": 2, + "id": 1693992338999, + "color": "#5f5f5f" + }] + }] + }] + } + data = self.client.put( + "/api/form", + data=payload, + content_type="application/json" + ) + self.assertEqual(data.status_code, 200) + result = data.json() + self.assertEqual(result, {"message": "Update form success"}) + # GET + data = self.client.get( + "/api/form/1693992073895", + follow=True + ) + self.assertEqual(data.status_code, 200) + result = data.json() + self.assertEqual(result, { + "id": 1693992073895, + "name": "New Form", + "description": "New Form Description", + "defaultLanguage": "en", + "languages": ["en"], + "version": 1, + "translations": None, + "question_group": [{ + "id": 1693992073896, + "name": "Sit quis dapibus", + "description": None, + "order": 1, + "repeatable": False, + "translations": None, + "question": [{ + "id": 1693992073897, + "name": "Sit ornare commodo sit ante consectetur", + "order": 1, + "type": "option", + "required": False, + "meta": False, + "option": [{ + "id": 1693992338940, + "code": "NO1", + "name": "New Option 1", + "order": 1, + "color": "#803838" + }, { + "id": 1693992338999, + "code": "NO3", + "name": "New Option 3", + "order": 2, + "color": "#5f5f5f" + }] + }] + }] + }) diff --git a/backend/source/static/example_form_payload.json b/backend/source/static/example_form_payload.json index db06a39..411a41a 100644 --- a/backend/source/static/example_form_payload.json +++ b/backend/source/static/example_form_payload.json @@ -170,7 +170,7 @@ "questionGroupId": 1693886026370, "option": [ { - "id": 7, + "id": 1, "name": "Male", "order": 1, "translations": [ @@ -181,7 +181,7 @@ ] }, { - "id": 8, + "id": 2, "name": "Female", "order": 2, "translations": [ @@ -192,7 +192,7 @@ ] }, { - "id": 9, + "id": 3, "name": "Other", "order": 3, "translations": [ @@ -228,7 +228,7 @@ "questionGroupId": 1693886026370, "option": [ { - "id": 8, + "id": 4, "name": "Single", "order": 1, "translations": [ @@ -239,7 +239,7 @@ ] }, { - "id": 9, + "id": 5, "name": "Maried", "order": 2, "translations": [ @@ -250,7 +250,7 @@ ] }, { - "id": 10, + "id": 6, "name": "Widowed", "order": 3, "translations": [ @@ -331,7 +331,7 @@ "questionGroupId": 1693886026375, "option": [ { - "id": 4, + "id": 7, "name": "Asian", "order": 1, "translations": [ @@ -342,7 +342,7 @@ ] }, { - "id": 5, + "id": 8, "name": "Western", "order": 2, "translations": [ @@ -353,7 +353,7 @@ ] }, { - "id": 6, + "id": 9, "name": "Vegetarian", "order": 3 } @@ -409,7 +409,7 @@ "questionGroupId": 1693886026375, "option": [ { - "id": 6, + "id": 10, "name": "Yes", "order": 1, "translations": [ @@ -420,7 +420,7 @@ ] }, { - "id": 7, + "id": 11, "name": "No", "order": 2, "translations": [ @@ -478,12 +478,12 @@ "questionGroupId": 1693886026375, "option": [ { - "id": 8, + "id": 12, "name": "Pagi Sore", "order": 1 }, { - "id": 9, + "id": 13, "name": "Any Rendang Restaurant", "order": 2, "translations": [ @@ -518,7 +518,7 @@ "questionGroupId": 1693886026375, "option": [ { - "id": 9, + "id": 14, "name": "Yes", "order": 1, "translations": [ @@ -529,7 +529,7 @@ ] }, { - "id": 10, + "id": 15, "name": "No", "order": 2, "translations": [ @@ -598,7 +598,7 @@ "questionGroupId": 1693886026377, "option": [ { - "id": 3, + "id": 16, "name": "Director", "order": 1, "translations": [ @@ -609,7 +609,7 @@ ] }, { - "id": 4, + "id": 17, "name": "Manager", "order": 2, "translations": [ @@ -620,7 +620,7 @@ ] }, { - "id": 5, + "id": 18, "name": "Staff", "order": 3, "translations": [ @@ -666,12 +666,12 @@ "questionGroupId": 1693886026377, "option": [ { - "id": 5, + "id": 19, "name": "Contract", "order": 1 }, { - "id": 6, + "id": 20, "name": "Internship", "order": 2 } From 28c0cf521fd7f5dfe83852eab8efafe13420071a Mon Sep 17 00:00:00 2001 From: wayangalihpratama Date: Wed, 6 Sep 2023 18:58:27 +0800 Subject: [PATCH 17/18] [#12] Test update question type from option to number --- backend/akvo/core_forms/serializers/form.py | 2 +- .../akvo/core_forms/serializers/question.py | 7 +- .../core_forms/serializers/question_group.py | 10 +- .../tests/test_form_update_endpoint.py | 104 ++++++++++++++++++ 4 files changed, 116 insertions(+), 7 deletions(-) diff --git a/backend/akvo/core_forms/serializers/form.py b/backend/akvo/core_forms/serializers/form.py index aff45f8..a194f03 100644 --- a/backend/akvo/core_forms/serializers/form.py +++ b/backend/akvo/core_forms/serializers/form.py @@ -149,5 +149,5 @@ def update(self, instance, validated_data): missing_qg_ids = list(set(current_qg_ids) - set(new_qg_ids)) # delete missing question groups QuestionGroups.objects.filter(id__in=missing_qg_ids).delete() - + instance.save() return instance diff --git a/backend/akvo/core_forms/serializers/question.py b/backend/akvo/core_forms/serializers/question.py index d4f558e..63303fd 100644 --- a/backend/akvo/core_forms/serializers/question.py +++ b/backend/akvo/core_forms/serializers/question.py @@ -196,12 +196,13 @@ def create(self, validated_data): def update(self, instance, validated_data): # update question + qtype = validated_data.get('type', instance.type) + instance.type = getattr(QuestionTypes, qtype) + instance.name = validated_data.get( 'name', instance.name) instance.order = validated_data.get( 'order', instance.order) - instance.type = validated_data.get( - 'type', instance.type) instance.tooltip = validated_data.get( 'tooltip', instance.tooltip) instance.required = validated_data.get( @@ -248,5 +249,5 @@ def update(self, instance, validated_data): missing_opt_ids = list(set(current_opt_ids) - set(new_opt_ids)) # delete old options then create new Options.objects.filter(id__in=missing_opt_ids).delete() - + instance.save() return instance diff --git a/backend/akvo/core_forms/serializers/question_group.py b/backend/akvo/core_forms/serializers/question_group.py index a6d5b1f..79a2a46 100644 --- a/backend/akvo/core_forms/serializers/question_group.py +++ b/backend/akvo/core_forms/serializers/question_group.py @@ -121,7 +121,11 @@ def update(self, instance, validated_data): # check missing question ids with answer and delete for qid in missing_q_ids: answers = Answers.objects.filter(question_id=qid).count() - if not answers: - Questions.objects.filter(id=qid).delete() - + if answers: + raise serializers.ValidationError({ + "message": "Can't delete question", + "details": f"Question {qid} has answers", + }) + Questions.objects.filter(id=qid).delete() + instance.save() return instance diff --git a/backend/akvo/core_forms/tests/test_form_update_endpoint.py b/backend/akvo/core_forms/tests/test_form_update_endpoint.py index fad9087..8977b91 100644 --- a/backend/akvo/core_forms/tests/test_form_update_endpoint.py +++ b/backend/akvo/core_forms/tests/test_form_update_endpoint.py @@ -1,6 +1,8 @@ from django.test import TestCase from django.test.utils import override_settings +from akvo.core_forms.models import Options + @override_settings(USE_TZ=False) class TestFormUpdateEndpoint(TestCase): @@ -136,3 +138,105 @@ def test_update_form_with_updated_options(self): }] }] }) + + def test_update_question_type_from_options_to_input(self): + payload = { + "id": 123, + "name": "New Form", + "description": "New Form Description", + "question_group": [{ + "id": 456, + "name": "Sit quis dapibus", + "order": 1, + "repeatable": False, + "question": [{ + "id": 789, + "order": 1, + "questionGroupId": 456, + "name": "Sit ornare commodo sit ante consectetur", + "type": "option", + "required": False, + "meta": False, + "allowOther": False, + "option": [{ + "code": "NO1", + "name": "New Option 1", + "order": 1, + "id": 911, + "color": "#803838" + }] + }] + }] + } + # POST + data = self.client.post( + "/api/form", + data=payload, + content_type="application/json" + ) + self.assertEqual(data.status_code, 200) + result = data.json() + self.assertEqual(result, {"message": "ok"}) + # PUT + payload = { + "id": 123, + "name": "Update Form", + "description": "Lorem ipsum", + "question_group": [{ + "id": 456, + "name": "Update Question Group", + "order": 1, + "repeatable": False, + "question": [{ + "id": 789, + "order": 1, + "questionGroupId": 456, + "name": "Update to number question", + "type": "number", + "required": False, + "meta": False + }] + }] + } + data = self.client.put( + "/api/form", + data=payload, + content_type="application/json" + ) + self.assertEqual(data.status_code, 200) + result = data.json() + self.assertEqual(result, {"message": "Update form success"}) + # GET + data = self.client.get( + "/api/form/123", + follow=True + ) + self.assertEqual(data.status_code, 200) + result = data.json() + self.assertEqual(result, { + "id": 123, + "name": "Update Form", + "description": "Lorem ipsum", + "defaultLanguage": "en", + "languages": ["en"], + "version": 1, + "translations": None, + "question_group": [{ + "id": 456, + "name": "Update Question Group", + "description": None, + "order": 1, + "repeatable": False, + "translations": None, + "question": [{ + "id": 789, + "name": "Update to number question", + "order": 1, + "type": "number", + "required": False, + "meta": False + }] + }] + }) + options = Options.objects.filter(question_id=789).all() + self.assertEqual(len(options), 0) From ed0242e5049cb0a35ad846b281c4e16de6dfc59b Mon Sep 17 00:00:00 2001 From: wayangalihpratama Date: Wed, 6 Sep 2023 19:23:24 +0800 Subject: [PATCH 18/18] [#12] Handle & test update question which has answers --- backend/akvo/core_forms/serializers/form.py | 19 ++- .../akvo/core_forms/serializers/question.py | 16 +- .../tests/test_form_update_endpoint.py | 160 ++++++++++++++++++ 3 files changed, 190 insertions(+), 5 deletions(-) diff --git a/backend/akvo/core_forms/serializers/form.py b/backend/akvo/core_forms/serializers/form.py index a194f03..7aa28e4 100644 --- a/backend/akvo/core_forms/serializers/form.py +++ b/backend/akvo/core_forms/serializers/form.py @@ -2,7 +2,11 @@ from drf_spectacular.utils import extend_schema_field from drf_spectacular.types import OpenApiTypes -from akvo.core_forms.models import Forms, QuestionGroups +from akvo.core_forms.models import ( + Forms, + QuestionGroups, + Questions +) from akvo.core_forms.serializers.question_group import ( ListQuestionGroupSerializer, AddQuestionGroupSerializer @@ -13,6 +17,7 @@ CustomListField ) from akvo.utils.custom_serializer_fields import validate_serializers_message +from akvo.core_data.models import Answers class ListFormSerializer(serializers.ModelSerializer): @@ -148,6 +153,16 @@ def update(self, instance, validated_data): missing_qg_ids = list(set(current_qg_ids) - set(new_qg_ids)) # delete missing question groups - QuestionGroups.objects.filter(id__in=missing_qg_ids).delete() + for qgid in missing_qg_ids: + questions = Questions.objects.filter(question_group_id=qgid).all() + qids = [q.id for q in questions] + # check answers + answers = Answers.objects.filter(question_id__in=qids).count() + if answers: + raise serializers.ValidationError({ + "message": "Can't delete question group", + "details": f"Question in group {qgid} has answers", + }) + QuestionGroups.objects.filter(id=qgid).delete() instance.save() return instance diff --git a/backend/akvo/core_forms/serializers/question.py b/backend/akvo/core_forms/serializers/question.py index 63303fd..dac2b28 100644 --- a/backend/akvo/core_forms/serializers/question.py +++ b/backend/akvo/core_forms/serializers/question.py @@ -21,6 +21,7 @@ CustomBooleanField, ) from akvo.utils.custom_serializer_fields import validate_serializers_message +from akvo.core_data.models import Answers class ListQuestionSerializer(serializers.ModelSerializer): @@ -195,10 +196,19 @@ def create(self, validated_data): return q def update(self, instance, validated_data): - # update question + # get question type qtype = validated_data.get('type', instance.type) - instance.type = getattr(QuestionTypes, qtype) - + qtype = getattr(QuestionTypes, qtype) + # check if question type change + if instance.type != qtype: + answers = Answers.objects.filter(question=instance).count() + if answers: + raise serializers.ValidationError({ + "message": "Can't update question type", + "details": f"Question {instance.id} has answers", + }) + # update question + instance.type = qtype instance.name = validated_data.get( 'name', instance.name) instance.order = validated_data.get( diff --git a/backend/akvo/core_forms/tests/test_form_update_endpoint.py b/backend/akvo/core_forms/tests/test_form_update_endpoint.py index 8977b91..e710926 100644 --- a/backend/akvo/core_forms/tests/test_form_update_endpoint.py +++ b/backend/akvo/core_forms/tests/test_form_update_endpoint.py @@ -240,3 +240,163 @@ def test_update_question_type_from_options_to_input(self): }) options = Options.objects.filter(question_id=789).all() self.assertEqual(len(options), 0) + + def test_update_question_which_has_answers(self): + payload = { + "id": 123, + "name": "Form", + "description": "Lorem ipsum..", + "question_group": [{ + "id": 456, + "name": "Question Group 1", + "order": 1, + "repeatable": False, + "question": [{ + "id": 789, + "order": 1, + "questionGroupId": 456, + "name": "Your Name", + "type": "input", + "required": False, + "meta": False, + }] + }] + } + # POST FORM + data = self.client.post( + "/api/form", + data=payload, + content_type="application/json" + ) + self.assertEqual(data.status_code, 200) + result = data.json() + self.assertEqual(result, {"message": "ok"}) + # POST DATA + payload = { + "data": { + "name": "Testing Data", + "geo": None, + "submitter": "Akvo", + }, + "answer": [{ + "question": 789, + "value": "Jane" + }], + } + data = self.client.post( + "/api/data/123", + payload, + content_type="application/json", + follow=True + ) + self.assertEqual(data.status_code, 200) + data = data.json() + self.assertEqual(data, {"message": "ok"}) + # PUT - Update Form change question type should error + payload = { + "id": 123, + "name": "Update Form", + "description": "Lorem ipsum", + "question_group": [{ + "id": 456, + "name": "Update Question Group", + "order": 1, + "repeatable": False, + "question": [{ + "id": 789, + "order": 1, + "questionGroupId": 456, + "name": "Update to number question", + "type": "number", + "required": False, + "meta": False + }] + }] + } + data = self.client.put( + "/api/form", + data=payload, + content_type="application/json" + ) + self.assertEqual(data.status_code, 400) + result = data.json() + print(result) + self.assertEqual( + result, + { + 'message': "Can't update question type", + 'details': 'Question 789 has answers' + } + ) + # PUT - Update Form delete question should error + payload = { + "id": 123, + "name": "Update Form", + "description": "Lorem ipsum", + "question_group": [{ + "id": 456, + "name": "Update Question Group", + "order": 1, + "repeatable": False, + "question": [{ + "id": 987, + "order": 1, + "questionGroupId": 456, + "name": "New Question", + "type": "input", + "required": False, + "meta": False + }] + }] + } + data = self.client.put( + "/api/form", + data=payload, + content_type="application/json" + ) + self.assertEqual(data.status_code, 400) + result = data.json() + print(result) + self.assertEqual( + result, + { + 'message': "Can't delete question", + 'details': 'Question 789 has answers' + } + ) + # PUT - Update Form delete question group should error + payload = { + "id": 123, + "name": "Update Form", + "description": "Lorem ipsum", + "question_group": [{ + "id": 654, + "name": "New Question Group", + "order": 1, + "repeatable": False, + "question": [{ + "id": 999, + "order": 1, + "questionGroupId": 456, + "name": "New Question", + "type": "input", + "required": False, + "meta": False + }] + }] + } + data = self.client.put( + "/api/form", + data=payload, + content_type="application/json" + ) + self.assertEqual(data.status_code, 400) + result = data.json() + print(result) + self.assertEqual( + result, + { + 'message': "Can't delete question group", + 'details': 'Question in group 456 has answers' + } + )