diff --git a/backend/api/v1/v1_data/migrations/0031_alter_formdata_parent.py b/backend/api/v1/v1_data/migrations/0031_alter_formdata_parent.py new file mode 100644 index 000000000..34c1288ed --- /dev/null +++ b/backend/api/v1/v1_data/migrations/0031_alter_formdata_parent.py @@ -0,0 +1,19 @@ +# Generated by Django 4.0.4 on 2024-02-20 04:02 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('v1_data', '0030_alter_formdata_parent'), + ] + + operations = [ + migrations.AlterField( + 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 bc462e6af..0d4860386 100644 --- a/backend/api/v1/v1_data/models.py +++ b/backend/api/v1/v1_data/models.py @@ -38,14 +38,6 @@ class FormData(models.Model): created = models.DateTimeField(auto_now_add=True) updated = models.DateTimeField(default=None, null=True) - parent = models.ForeignKey( - 'self', - on_delete=models.PROTECT, - related_name='children', - default=None, - null=True - ) - def __str__(self): return self.name diff --git a/backend/api/v1/v1_data/serializers.py b/backend/api/v1/v1_data/serializers.py index 0fdcd5184..6f61ec600 100644 --- a/backend/api/v1/v1_data/serializers.py +++ b/backend/api/v1/v1_data/serializers.py @@ -234,7 +234,7 @@ class ListFormDataRequestSerializer(serializers.Serializer): required=False, ) parent = CustomPrimaryKeyRelatedField( - queryset=FormData.objects.filter(parent=None).none(), + queryset=FormData.objects.none(), required=False ) @@ -245,8 +245,7 @@ def __init__(self, **kwargs): self.fields.get("questions").child.queryset = Questions.objects.all() form_id = self.context.get('form_id') self.fields.get("parent").queryset = FormData.objects.filter( - form_id=form_id, - parent=None, + form_id=form_id ).all() @@ -257,7 +256,6 @@ class ListFormDataSerializer(serializers.ModelSerializer): updated = serializers.SerializerMethodField() administration = serializers.ReadOnlyField(source="administration.name") pending_data = serializers.SerializerMethodField() - children_count = serializers.SerializerMethodField() @extend_schema_field(OpenApiTypes.STR) def get_created_by(self, instance: FormData): @@ -298,10 +296,6 @@ def get_pending_data(self, instance: FormData): } return None - @extend_schema_field(OpenApiTypes.NUMBER) - def get_children_count(self, instance: FormData): - return instance.children.count() - class Meta: model = FormData fields = [ @@ -315,8 +309,7 @@ class Meta: "updated_by", "created", "updated", - "pending_data", - "children_count", + "pending_data" ] diff --git a/backend/api/v1/v1_data/tests/tests_data.py b/backend/api/v1/v1_data/tests/tests_data.py index dad6ac2d9..e5a1f49e4 100644 --- a/backend/api/v1/v1_data/tests/tests_data.py +++ b/backend/api/v1/v1_data/tests/tests_data.py @@ -32,7 +32,6 @@ def test_list_form_data(self): self.assertEqual(list(result['data'][0]), [ 'id', 'uuid', 'name', 'form', 'administration', 'geo', 'created_by', 'updated_by', 'created', 'updated', 'pending_data', - 'children_count' ]) self.assertIsNotNone(result['data'][0]['uuid']) diff --git a/backend/api/v1/v1_data/tests/tests_monitoring_endpoints.py b/backend/api/v1/v1_data/tests/tests_monitoring_endpoints.py new file mode 100644 index 000000000..f654d7e6e --- /dev/null +++ b/backend/api/v1/v1_data/tests/tests_monitoring_endpoints.py @@ -0,0 +1,146 @@ +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 +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 +) + + +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.admin + 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) + + # Login as an admin + admin = {"email": self.user.email, "password": 'test1234'} + admin = self.client.post( + '/api/v1/login', + admin, + content_type='application/json' + ) + admin = admin.json() + self.token = admin.get("token") + + def test_parent_data(self): + data = self.client.get( + f"/api/v1/form-data/1/{self.form.id}", + content_type='application/json', + **{'HTTP_AUTHORIZATION': f'Bearer {self.token}'} + ) + self.assertEqual(data.status_code, 200) + data = data.json() + self.assertEqual(data['total'], 1) + self.assertEqual(data['data'][0]['uuid'], self.uuid) + + def test_update_parent_data(self): + payload = [ + { + "question": 101, + "value": "Edit" + } + ] + edit = self.client.put( + f'/api/v1/form-data/{self.form.id}?data_id={self.data.id}', + payload, + content_type='application/json', + **{'HTTP_AUTHORIZATION': f'Bearer {self.token}'} + ) + self.assertEqual(edit.status_code, 200) + + data = self.client.get( + f"/api/v1/form-data/1/{self.form.id}", + content_type='application/json', + **{'HTTP_AUTHORIZATION': f'Bearer {self.token}'} + ) + self.assertEqual(data.status_code, 200) + data = data.json() + self.assertEqual(data['total'], 1) + + answers = self.client.get( + f'/api/v1/data/{self.data.id}', + content_type='application/json', + **{'HTTP_AUTHORIZATION': f'Bearer {self.token}'} + ) + self.assertEqual(answers.status_code, 200) + answers = answers.json() + find_answer = list(filter(lambda a: a['question'] == 101, answers)) + self.assertEqual(len(find_answer), 1) + self.assertNotEqual(find_answer[0]['history'], None) + self.assertEqual(find_answer[0]['value'], 'Edit') + + def test_add_new_monitoring(self): + monitoring = FormData.objects.create( + uuid=self.uuid, + form=self.form, + administration=self.administration, + created_by=self.user, + ) + add_fake_answers(monitoring, form_type=FormTypes.county) + + data = self.client.get( + f"/api/v1/form-data/1/{self.form.id}", + content_type='application/json', + **{'HTTP_AUTHORIZATION': f'Bearer {self.token}'} + ) + self.assertEqual(data.status_code, 200) + data = data.json() + self.assertEqual(data['total'], 1) + + data_parent = self.client.get( + f"/api/v1/form-data/1/{self.form.id}?parent={monitoring.id}", + content_type='application/json', + **{'HTTP_AUTHORIZATION': f'Bearer {self.token}'} + ) + self.assertEqual(data_parent.status_code, 200) + data_parent = data_parent.json() + self.assertEqual(data_parent['total'], 2) + + self.assertEqual(data_parent['data'][0]['name'], monitoring.name) + + def test_get_latest_data(self): + for m in range(2): + monitoring = FormData.objects.create( + uuid=self.uuid, + form=self.form, + administration=self.administration, + created_by=self.user, + ) + add_fake_answers(monitoring, form_type=FormTypes.county) + lastest = FormData.objects.order_by('-created').first() + data = self.client.get( + f"/api/v1/form-data/1/{self.form.id}", + content_type='application/json', + **{'HTTP_AUTHORIZATION': f'Bearer {self.token}'} + ) + self.assertEqual(data.status_code, 200) + data = data.json() + self.assertEqual(data['total'], 1) + self.assertEqual(data['data'][0]['name'], lastest.name) diff --git a/backend/api/v1/v1_data/views.py b/backend/api/v1/v1_data/views.py index 29e7f850f..be7b6a41c 100644 --- a/backend/api/v1/v1_data/views.py +++ b/backend/api/v1/v1_data/views.py @@ -1,14 +1,14 @@ # Create your views here. from math import ceil from collections import defaultdict -from datetime import datetime, date, timedelta +from datetime import datetime, date from wsgiref.util import FileWrapper from django.utils import timezone # from operator import or_ # from functools import reduce from django.contrib.postgres.aggregates import StringAgg -from django.db.models import Count, TextField, Value, F, Sum, Avg, Max, Q +from django.db.models import Count, TextField, F, Sum, Avg, Max from django.db.models.functions import Cast, Coalesce from django.http import HttpResponse from django_q.tasks import async_task @@ -114,22 +114,41 @@ def get(self, request, form_id, version): serializer.errors)}, status=status.HTTP_400_BAD_REQUEST ) + page_size = REST_FRAMEWORK.get('PAGE_SIZE') + + paginator = PageNumberPagination() + parent = serializer.validated_data.get('parent') - filter_data = {} if parent: - latest_created_per_uuid = FormData.objects.filter( - Q(form_id=form_id, parent=parent) | - Q(pk=parent.id) - ).values('uuid').annotate(latest_created=Max('created')) - filter_data['uuid__in'] = latest_created_per_uuid.values('uuid') - else: - filter_data['parent'] = None + queryset = form.form_form_data.filter(uuid=parent.uuid).order_by( + '-created' + ) + instance = paginator.paginate_queryset(queryset, request) + data = { + "current": int(request.GET.get('page', '1')), + "total": queryset.count(), + "total_page": ceil(queryset.count() / page_size), + "data": ListFormDataSerializer( + instance=instance, context={ + 'questions': serializer.validated_data.get( + 'questions')}, + many=True).data, + } + return Response(data, status=status.HTTP_200_OK) + + filter_data = {} + latest_ids_per_uuid = form.form_form_data.values('uuid').annotate( + latest_id=Max('id') + ).values_list('latest_id', flat=True) + filter_data["pk__in"] = latest_ids_per_uuid if serializer.validated_data.get('administration'): filter_administration = serializer.validated_data.get( 'administration') if filter_administration.path: - filter_path = '{0}{1}.'.format(filter_administration.path, - filter_administration.id) + filter_path = '{0}{1}.'.format( + filter_administration.path, + filter_administration.id + ) else: filter_path = f"{filter_administration.id}." filter_descendants = list(Administration.objects.filter( @@ -145,34 +164,11 @@ def get(self, request, form_id, version): administration_id=request.GET.get("administration"), options=request.GET.getlist('options')) filter_data["pk__in"] = data_ids - - page_size = REST_FRAMEWORK.get('PAGE_SIZE') - - the_past = datetime.now() - timedelta(days=10 * 365) - queryset = form.form_form_data.filter(**filter_data) - if parent: - queryset = queryset.annotate( - last_updated=Coalesce('updated', Value(the_past)), - latest_created=Coalesce('updated', 'created') \ - # Use updated time if available, otherwise use created time - ).filter( - created=F('latest_created') \ - # Filter by objects where created equals latest_created - ).order_by( - '-last_updated', - '-created' - ) - else: - queryset = queryset.annotate( - last_updated=Coalesce('updated', Value(the_past)), - children_count=Count('children') - ).order_by( - '-children_count', - '-last_updated', + queryset = form.form_form_data.filter(**filter_data) \ + .order_by( '-created' ) - paginator = PageNumberPagination() instance = paginator.paginate_queryset(queryset, request) data = { "current": int(request.GET.get('page', '1')), diff --git a/frontend/src/App.js b/frontend/src/App.js index e9741b83d..bd4fbc1d3 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -116,7 +116,7 @@ const RouteList = () => { element={} /> } /> { const [deleting, setDeleting] = useState(false); const navigate = useNavigate(); + const { administration, selectedForm } = store.useState((state) => state); const { language, advancedFilters } = store.useState((s) => s); const { active: activeLang } = language; const text = useMemo(() => { @@ -51,11 +52,9 @@ const ManageData = () => { store.update((s) => { s.selectedFormData = record; }); - navigate(`/control-center/data/monitoring/${record.id}`); + navigate(`/control-center/data/${selectedForm}/monitoring/${record.id}`); }; - const { administration, selectedForm } = store.useState((state) => state); - const isAdministrationLoaded = administration.length; const selectedAdministration = administration.length > 0 @@ -71,12 +70,6 @@ const ManageData = () => { filteredValue: query.trim() === "" ? [] : [query], onFilter: (value, filters) => filters.name.toLowerCase().includes(value.toLowerCase()), - render: (value) => ( - - - {value} - - ), }, { title: "Last Updated", @@ -142,6 +135,9 @@ const ManageData = () => { .then((res) => { setDataset(res.data.data); setTotalCount(res.data.total); + if (res.data.total < currentPage) { + setCurrentPage(1); + } setUpdateRecord(null); setLoading(false); }) diff --git a/frontend/src/pages/manage-data/MonitoringDetail.jsx b/frontend/src/pages/manage-data/MonitoringDetail.jsx index d83948412..76529e63e 100644 --- a/frontend/src/pages/manage-data/MonitoringDetail.jsx +++ b/frontend/src/pages/manage-data/MonitoringDetail.jsx @@ -15,7 +15,6 @@ import { import { LeftCircleOutlined, DownCircleOutlined, - ExclamationCircleOutlined, DeleteOutlined, ArrowLeftOutlined, } from "@ant-design/icons"; @@ -38,7 +37,7 @@ const MonitoringDetail = () => { const [deleting, setDeleting] = useState(false); const [editedRecord, setEditedRecord] = useState({}); const [editable, setEditable] = useState(false); - const { parentId } = useParams(); + const { form, parentId } = useParams(); const navigate = useNavigate(); const { language, selectedFormData } = store.useState((s) => s); @@ -61,11 +60,7 @@ const MonitoringDetail = () => { }, ]; - const { - selectedForm, - questionGroups, - user: authUser, - } = store.useState((state) => state); + const { questionGroups, user: authUser } = store.useState((state) => state); useEffect(() => { const currentUser = config.roles.find( @@ -84,12 +79,6 @@ const MonitoringDetail = () => { title: "Name", dataIndex: "name", key: "name", - render: (value) => ( - - - {value} - - ), }, { title: "User", @@ -129,9 +118,9 @@ const MonitoringDetail = () => { }; useEffect(() => { - if (selectedForm && !updateRecord) { + if (form && !updateRecord) { setLoading(true); - const url = `/form-data/${selectedForm}/?page=${currentPage}&parent=${parentId}`; + const url = `/form-data/${form}/?page=${currentPage}&parent=${parentId}`; api .get(url) .then((res) => { @@ -146,7 +135,7 @@ const MonitoringDetail = () => { setLoading(false); }); } - }, [selectedForm, currentPage, updateRecord, parentId]); + }, [form, currentPage, updateRecord, parentId]); return ( @@ -173,19 +162,13 @@ const MonitoringDetail = () => { {text.backManageData} - {selectedFormData?.name || ""} + {selectedFormData?.name || dataset?.[0]?.name} ( - - )} + renderEmpty={() => } >