From 6488a4372a1e036758daada92fd8360737eb814d Mon Sep 17 00:00:00 2001 From: abhishek Date: Tue, 6 Feb 2024 20:55:41 +0530 Subject: [PATCH 1/4] [#1144] Data Editing Column Width --- frontend/src/pages/approvals/ApprovalDetail.jsx | 2 ++ frontend/src/pages/manage-data/DataDetail.jsx | 2 ++ frontend/src/pages/submissions/SubmissionEditing.jsx | 2 ++ frontend/src/pages/submissions/UploadDetail.jsx | 2 ++ 4 files changed, 8 insertions(+) diff --git a/frontend/src/pages/approvals/ApprovalDetail.jsx b/frontend/src/pages/approvals/ApprovalDetail.jsx index ecb2897d7..c6583f1e4 100644 --- a/frontend/src/pages/approvals/ApprovalDetail.jsx +++ b/frontend/src/pages/approvals/ApprovalDetail.jsx @@ -453,6 +453,7 @@ const ApprovalDetail = ({ { title: "Question", dataIndex: "name", + width: "50%", }, { title: "Response", @@ -467,6 +468,7 @@ const ApprovalDetail = ({ resetButton={resetButton} /> ), + width: "50%", }, Table.EXPAND_COLUMN, ]} diff --git a/frontend/src/pages/manage-data/DataDetail.jsx b/frontend/src/pages/manage-data/DataDetail.jsx index 80e4b20ac..9db85dda1 100644 --- a/frontend/src/pages/manage-data/DataDetail.jsx +++ b/frontend/src/pages/manage-data/DataDetail.jsx @@ -214,6 +214,7 @@ const DataDetail = ({ { title: "Question", dataIndex: "name", + width: "50%", }, { title: "Response", @@ -228,6 +229,7 @@ const DataDetail = ({ resetButton={resetButton} /> ), + width: "50%", }, Table.EXPAND_COLUMN, ]} diff --git a/frontend/src/pages/submissions/SubmissionEditing.jsx b/frontend/src/pages/submissions/SubmissionEditing.jsx index 044f1923b..438fb9b94 100644 --- a/frontend/src/pages/submissions/SubmissionEditing.jsx +++ b/frontend/src/pages/submissions/SubmissionEditing.jsx @@ -61,6 +61,7 @@ const SubmissionEditing = ({ { title: text?.QuestionCol, dataIndex: "name", + width: "50%", }, { title: text?.responseCol, @@ -75,6 +76,7 @@ const SubmissionEditing = ({ resetButton={resetButton} /> ), + width: "50%", }, Table.EXPAND_COLUMN, ]} diff --git a/frontend/src/pages/submissions/UploadDetail.jsx b/frontend/src/pages/submissions/UploadDetail.jsx index a3288bdfb..f761f98f1 100644 --- a/frontend/src/pages/submissions/UploadDetail.jsx +++ b/frontend/src/pages/submissions/UploadDetail.jsx @@ -411,6 +411,7 @@ const UploadDetail = ({ record, setReload }) => { { title: "Question", dataIndex: "name", + width: "50%", }, { title: "Response", @@ -425,6 +426,7 @@ const UploadDetail = ({ record, setReload }) => { resetButton={resetButton} /> ), + width: "50%", }, Table.EXPAND_COLUMN, ]} From 1541f84536fab1b6ae19fde4cd51788c8676b3c4 Mon Sep 17 00:00:00 2001 From: dedenbangkit Date: Wed, 7 Feb 2024 13:35:07 +0700 Subject: [PATCH 2/4] [#1113] Soft delete migration on user --- .../0009_alter_systemuser_deleted_at.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 backend/api/v1/v1_users/migrations/0009_alter_systemuser_deleted_at.py diff --git a/backend/api/v1/v1_users/migrations/0009_alter_systemuser_deleted_at.py b/backend/api/v1/v1_users/migrations/0009_alter_systemuser_deleted_at.py new file mode 100644 index 000000000..854d9645c --- /dev/null +++ b/backend/api/v1/v1_users/migrations/0009_alter_systemuser_deleted_at.py @@ -0,0 +1,18 @@ +# Generated by Django 4.0.4 on 2024-02-07 06:04 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('v1_users', '0008_reset_organisation_id_seq'), + ] + + operations = [ + migrations.AlterField( + model_name='systemuser', + name='deleted_at', + field=models.DateTimeField(blank=True, null=True), + ), + ] From ae3db78bc63a0a449e40b9cec5f611e8c3889d0e Mon Sep 17 00:00:00 2001 From: dedenbangkit Date: Wed, 7 Feb 2024 15:13:46 +0700 Subject: [PATCH 3/4] [#1150] Seed Monitoring Data when it gets Approved --- .../management/commands/fake_data_seeder.py | 34 ++++++-- .../migrations/0029_formdata_parent.py | 19 +++++ backend/api/v1/v1_data/models.py | 4 + backend/api/v1/v1_data/tasks.py | 7 ++ .../api/v1/v1_data/tests/tests_monitoring.py | 85 +++++++++++++++++++ 5 files changed, 140 insertions(+), 9 deletions(-) create mode 100644 backend/api/v1/v1_data/migrations/0029_formdata_parent.py create mode 100644 backend/api/v1/v1_data/tests/tests_monitoring.py diff --git a/backend/api/v1/v1_data/management/commands/fake_data_seeder.py b/backend/api/v1/v1_data/management/commands/fake_data_seeder.py index 5e0008548..db071cfee 100644 --- a/backend/api/v1/v1_data/management/commands/fake_data_seeder.py +++ b/backend/api/v1/v1_data/management/commands/fake_data_seeder.py @@ -7,7 +7,7 @@ from faker import Faker -from api.v1.v1_data.models import FormData, Answers +from api.v1.v1_data.models import FormData, Answers, PendingAnswers from api.v1.v1_forms.constants import QuestionTypes, FormTypes from api.v1.v1_forms.models import Forms from api.v1.v1_profile.constants import UserRoleTypes @@ -51,7 +51,9 @@ def set_answer_data(data, question): return name, value, option -def add_fake_answers(data: FormData, form_type): +def add_fake_answers(data: FormData, + form_type=FormTypes.county, + pending=False): form = data.form meta_name = [] for question in form.form_questions.all().order_by('question_group__order', @@ -73,20 +75,34 @@ def add_fake_answers(data: FormData, form_type): seed = True if question.dependency: for d in question.dependency: - prev_answer = Answers.objects.filter( + if not pending: + prev_answer = Answers.objects.filter( data=data, question_id=d.get('id')).first() + else: + prev_answer = PendingAnswers.objects.filter( + pending_data=data, question_id=d.get('id')).first() if prev_answer: seed = False for o in prev_answer.options: if o in d.get("options"): seed = True if seed: - Answers.objects.create(data=data, - question=question, - name=name, - value=value, - options=option, - created_by=data.created_by) + if not pending: + Answers.objects.create(data=data, + question=question, + name=name, + value=value, + options=option, + created_by=data.created_by) + else: + PendingAnswers.objects.create( + pending_data=data, + question=question, + name=name, + value=value, + options=option, + created_by=data.created_by + ) data.name = ' - '.join(meta_name) if \ form_type != FormTypes.national else data.name data.save() diff --git a/backend/api/v1/v1_data/migrations/0029_formdata_parent.py b/backend/api/v1/v1_data/migrations/0029_formdata_parent.py new file mode 100644 index 000000000..6fabb948d --- /dev/null +++ b/backend/api/v1/v1_data/migrations/0029_formdata_parent.py @@ -0,0 +1,19 @@ +# Generated by Django 4.0.4 on 2024-02-07 06:41 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('v1_data', '0028_rename_deleted_pendingformdata_deleted_at'), + ] + + operations = [ + migrations.AddField( + model_name='formdata', + name='parent', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='children', to='v1_data.formdata'), + ), + ] diff --git a/backend/api/v1/v1_data/models.py b/backend/api/v1/v1_data/models.py index 04e9c793b..f932e9ea1 100644 --- a/backend/api/v1/v1_data/models.py +++ b/backend/api/v1/v1_data/models.py @@ -14,6 +14,10 @@ class FormData(models.Model): + parent = models.ForeignKey('self', + on_delete=models.CASCADE, + related_name='children', + null=True, blank=True) name = models.TextField() form = models.ForeignKey(to=Forms, on_delete=models.CASCADE, diff --git a/backend/api/v1/v1_data/tasks.py b/backend/api/v1/v1_data/tasks.py index 05bf80eba..15bc51058 100644 --- a/backend/api/v1/v1_data/tasks.py +++ b/backend/api/v1/v1_data/tasks.py @@ -4,8 +4,14 @@ def seed_approved_data(data): + parent_data = FormData.objects.filter( + form=data.form, + uuid=data.uuid, + parent=None, + ).first() if data.data: form_data: FormData = data.data + form_data.parent = parent_data form_data.name = data.name form_data.uuid = data.uuid form_data.form = data.form @@ -28,6 +34,7 @@ def seed_approved_data(data): form_answer.delete() else: form_data = FormData.objects.create( + parent=parent_data, name=data.name, uuid=data.uuid, form=data.form, diff --git a/backend/api/v1/v1_data/tests/tests_monitoring.py b/backend/api/v1/v1_data/tests/tests_monitoring.py new file mode 100644 index 000000000..194fdcbd5 --- /dev/null +++ b/backend/api/v1/v1_data/tests/tests_monitoring.py @@ -0,0 +1,85 @@ +from django.test import TestCase +from django.core.management import call_command +from api.v1.v1_users.models import SystemUser +from api.v1.v1_data.models import ( + FormData, + PendingFormData, + PendingDataBatch +) +from api.v1.v1_forms.models import Forms +from api.v1.v1_forms.constants import FormTypes +from api.v1.v1_profile.models import Administration, Access +from api.v1.v1_profile.constants import UserRoleTypes +from api.v1.v1_data.management.commands.fake_data_seeder import ( + add_fake_answers +) +from api.v1.v1_data.tasks import seed_approved_data + + +class MonitoringDataTestCase(TestCase): + def setUp(self): + call_command('administration_seeder', '--test') + call_command('form_seeder', '--test') + self.user = SystemUser.objects.create_user( + email='test@test.org', + password='test1234', + first_name='test', + last_name='testing', + ) + self.administration = Administration.objects.filter( + parent__isnull=True + ).first() + role = UserRoleTypes.user + self.user_access = Access.objects.create( + user=self.user, role=role, administration=self.administration + ) + self.uuid = '1234567890' + self.form = Forms.objects.filter(type=FormTypes.county).first() + self.data = FormData.objects.create( + parent=None, + uuid=self.uuid, + form=self.form, + administration=self.administration, + created_by=self.user, + ) + add_fake_answers(self.data, FormTypes.county) + + def test_parent_data(self): + self.assertTrue(self.data.name) + self.assertEqual(self.data.parent, None) + self.assertEqual(self.data.uuid, self.uuid) + self.assertEqual(self.data.form, self.form) + self.assertEqual(self.data.administration, self.administration) + self.assertEqual(self.data.created_by, self.user) + self.assertTrue(self.data.data_answer.count() > 0) + + def test_seed_monitoring_batch(self): + for i in range(2): + pending_data = PendingFormData.objects.create( + uuid=self.uuid if i == 0 else f'{self.uuid}{i}', + form=self.form, + administration=self.administration, + created_by=self.user, + ) + add_fake_answers(pending_data, + form_type=FormTypes.county, + pending=True) + self.assertTrue(PendingFormData.objects.count() == 2) + batch = PendingDataBatch.objects.create( + name='test batch', + administration=self.administration, + form=self.form, + user=self.user, + approved=True + ) + batch.batch_pending_data_batch.add(*PendingFormData.objects.all()) + self.assertTrue(batch.batch_pending_data_batch.count() == 2) + for pending_data in batch.batch_pending_data_batch.all(): + seed_approved_data(pending_data) + self.assertTrue(FormData.objects.count() == 3) + child_data = FormData.objects.filter( + parent__isnull=False + ).first() + self.assertEqual(child_data.parent.uuid, self.uuid) + self.assertEqual(self.data.children.first().id, child_data.id) + self.assertEqual(self.data.children.count(), 1) From dcc0d608cbe5d41fe43352272ad4af41130f069a Mon Sep 17 00:00:00 2001 From: dedenbangkit Date: Wed, 7 Feb 2024 18:50:48 +0700 Subject: [PATCH 4/4] Cleanup unused pages --- frontend/src/App.js | 12 -- frontend/src/pages/index.js | 2 - .../pages/questionnaires/Questionnaires.jsx | 188 ----------------- .../questionnaires/QuestionnairesAdmin.jsx | 193 ------------------ frontend/src/pages/questionnaires/style.scss | 43 ---- 5 files changed, 438 deletions(-) delete mode 100644 frontend/src/pages/questionnaires/Questionnaires.jsx delete mode 100644 frontend/src/pages/questionnaires/QuestionnairesAdmin.jsx delete mode 100644 frontend/src/pages/questionnaires/style.scss diff --git a/frontend/src/App.js b/frontend/src/App.js index ddedaf949..b4ee18234 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -9,8 +9,6 @@ import { AddUser, Forms, ManageData, - Questionnaires, - QuestionnairesAdmin, Approvals, ApproversTree, Profile, @@ -192,16 +190,6 @@ const RouteList = () => { path="approvals" element={} /> - } - /> - - } - /> } diff --git a/frontend/src/pages/index.js b/frontend/src/pages/index.js index 6a90eb794..9ecf48ba6 100644 --- a/frontend/src/pages/index.js +++ b/frontend/src/pages/index.js @@ -6,8 +6,6 @@ export { default as ControlCenter } from "./control-center/ControlCenter"; export { default as Users } from "./users/Users"; export { default as AddUser } from "./add-user/AddUser"; export { default as ManageData } from "./manage-data/ManageData"; -export { default as Questionnaires } from "./questionnaires/Questionnaires"; -export { default as QuestionnairesAdmin } from "./questionnaires/QuestionnairesAdmin"; export { default as Approvals } from "./approvals/Approvals"; export { default as ApproversTree } from "./approvers-tree/ApproversTree"; export { default as Profile } from "./profile/Profile"; diff --git a/frontend/src/pages/questionnaires/Questionnaires.jsx b/frontend/src/pages/questionnaires/Questionnaires.jsx deleted file mode 100644 index fc5ec4e68..000000000 --- a/frontend/src/pages/questionnaires/Questionnaires.jsx +++ /dev/null @@ -1,188 +0,0 @@ -import React, { useMemo, useEffect, useState } from "react"; -import "./style.scss"; -import { - Row, - Col, - Button, - Table, - ConfigProvider, - Checkbox, - Empty, - Space, -} from "antd"; -import { api, store, uiText } from "../../lib"; -import { Breadcrumbs } from "../../components"; -import { reloadData } from "../../util/form"; -import { useNotification } from "../../util/hooks"; - -const Questionnaires = () => { - const { forms, user } = store.useState((s) => s); - const [dataset, setDataset] = useState([]); - const [loading, setLoading] = useState(false); - const { notify } = useNotification(); - - const { language } = store.useState((s) => s); - - const { active: activeLang } = language; - const text = useMemo(() => { - return uiText[activeLang]; - }, [activeLang]); - - const pagePath = [ - { - title: text.controlCenter, - link: "/control-center", - }, - { - title: text.approvalsTitle, - link: "/control-center/approvals", - }, - { - title: text.manageQnApproval, - }, - ]; - - useEffect(() => { - if (forms.length) { - setDataset([...forms]); - } - }, [forms]); - - const columns = [ - { - title: "Questionnaire", - dataIndex: "name", - key: "name", - }, - { - title: "Questionnaire Description", - dataIndex: "description", - render: (cell) => cell || -, - }, - { - title: "National", - render: (row) => ( - { - handleChecked(row.id, 2); - }} - /> - ), - }, - { - title: "County", - render: (row) => ( - { - handleChecked(row.id, 1); - }} - /> - ), - }, - ]; - - const handleChecked = (id, val) => { - const pos = dataset.findIndex((d) => d.id === id); - if (pos !== -1) { - const cloned = JSON.parse(JSON.stringify(dataset)); - cloned[pos].type = val; - setDataset(cloned); - } - }; - - const handleSubmit = () => { - const data = dataset.map((d) => ({ - form_id: d.id, - type: d.type, - })); - setLoading(true); - api - .post("form/type", data) - .then(() => { - setLoading(false); - notify({ - type: "success", - message: "Questionnaires updated", - }); - reloadData(user, dataset); - }) - .catch(() => { - notify({ - type: "error", - message: "Could not update Questionnaires", - }); - setLoading(false); - }); - }; - - const handleChange = () => { - // setCurrentPage(e.current); - }; - - const isPristine = useMemo(() => { - return JSON.stringify(dataset) === JSON.stringify(forms); - }, [dataset, forms]); - - return ( -
-
- - - - - - - - - - - -
-
-
-
- }> - - - - - - - ); -}; - -export default React.memo(Questionnaires); diff --git a/frontend/src/pages/questionnaires/QuestionnairesAdmin.jsx b/frontend/src/pages/questionnaires/QuestionnairesAdmin.jsx deleted file mode 100644 index 4339deae1..000000000 --- a/frontend/src/pages/questionnaires/QuestionnairesAdmin.jsx +++ /dev/null @@ -1,193 +0,0 @@ -import React, { useMemo, useState, useEffect } from "react"; -import "./style.scss"; -import { - Row, - Col, - Table, - ConfigProvider, - Empty, - Checkbox, - Space, - Button, -} from "antd"; -import { api, store } from "../../lib"; -import { Breadcrumbs } from "../../components"; -import { reloadData } from "../../util/form"; -import { useNotification } from "../../util/hooks"; - -const pagePath = [ - { - title: "Control Center", - link: "/control-center", - }, - { - title: "Approvals", - link: "/control-center/approvals", - }, - { - title: "Manage Questionnaires Approvals", - }, -]; - -const QuestionnairesAdmin = () => { - const { forms, levels, user } = store.useState((s) => s); - const [loading, setLoading] = useState(true); - const [saving, setSaving] = useState(false); - const [dataset, setDataset] = useState([]); - const [dataOriginal, setDataOriginal] = useState(""); - const { notify } = useNotification(); - - const columns = useMemo(() => { - const handleChecked = (id, val) => { - const pos = dataset.findIndex((d) => d.form_id === id); - if (pos !== -1) { - const cloned = JSON.parse(JSON.stringify(dataset)); - const exists = dataset[pos].levels?.includes(val); - if (exists) { - cloned[pos].levels = cloned[pos].levels.filter((i) => i !== val); - } else { - cloned[pos].levels.push(val); - } - setDataset(cloned); - } - }; - return [ - { - title: "Questionnaire", - dataIndex: "form_id", - render: (cell) => forms.find((f) => f.id === cell)?.name || "", - }, - { - title: "Questionnaire Description", - dataIndex: "description", - render: (cell) => cell || -, - }, - ].concat( - levels - .filter((lv) => lv.level !== 0) - .map((level) => { - return { - title: level.name, - key: `lvl-${level.level}`, - render: (row) => ( - { - handleChecked(row.form_id, level.level); - }} - /> - ), - }; - }) - ); - }, [levels, forms, dataset]); - - useEffect(() => { - if (forms.length) { - setLoading(true); - api - .get("form/approval-level") - .then((res) => { - setDataset(res.data); - setDataOriginal(JSON.stringify(res.data)); - setLoading(false); - }) - .catch((err) => { - console.error(err); - setLoading(false); - }); - } - }, [forms]); - - const handleChange = () => { - // setCurrentPage(e.current); - }; - - const handleSubmit = () => { - const data = dataset.map((d) => ({ - form_id: d.form_id, - level_id: d.levels, - })); - setSaving(true); - api - .put("form/approval", data) - .then(() => { - setSaving(false); - notify({ - type: "success", - message: "Questionnaires updated", - }); - reloadData(user); - }) - .catch(() => { - notify({ - type: "error", - message: "Could not update Questionnaires", - }); - setSaving(false); - }); - }; - - const isPristine = useMemo(() => { - return JSON.stringify(dataset) === dataOriginal; - }, [dataset, dataOriginal]); - - return ( -
-
- -
- - - - - - - - - - -
-
-
- }> -
- - - - - - ); -}; - -export default React.memo(QuestionnairesAdmin); diff --git a/frontend/src/pages/questionnaires/style.scss b/frontend/src/pages/questionnaires/style.scss deleted file mode 100644 index 4da3029a5..000000000 --- a/frontend/src/pages/questionnaires/style.scss +++ /dev/null @@ -1,43 +0,0 @@ -@import "../../variables"; - -#root { - #questionnaires { - .ant-table { - th, - td { - text-align: center; - } - th { - font-weight: 900; - font-size: 15px; - color: #323132; - } - tr td:nth-child(2) span { - color: #c2c2c7; - } - } - } - #approvers { - .ant-table { - .ant-table-cell > .ant-select { - width: 100%; - .ant-select-selector { - border: 0 none; - .ant-select-selection-item:before { - display: inline-block; - width: 0; - height: 0; - vertical-align: middle; - margin-right: 10px; - content: ""; - width: 0; - height: 0; - border-style: solid; - border-width: 5px 4px 0 4px; - border-color: #4d4d4d transparent transparent transparent; - } - } - } - } - } -}