diff --git a/apps/cases/constants.py b/apps/cases/constants.py index 10af9fb25fc99f99b5112abf74b76bbcd713d7b3..999cbda51b25a392178a32b5f40d239591f660e1 100644 --- a/apps/cases/constants.py +++ b/apps/cases/constants.py @@ -133,7 +133,7 @@ SUB_DISTRICT_CHOICES = [ (BAKTI_JAYA, 'Bakti Jaya'), (CISALAK, 'Cisalak'), (MEKAR_JAYA, 'Mekar Jaya'), - (SUKAMAJU, 'Sukmajaya'), + (SUKMAJAYA, 'Sukmajaya'), (TIRTAJAYA, 'Tirtajaya'), (CILANGKAP, 'Cilangkap'), (CIMPAEUN, 'Cimpaeun'), @@ -146,6 +146,7 @@ SUB_DISTRICT_CHOICES = [ SUB_DISTRICT_MAPPING = { BEJI: [ + BEJI, BEJI_TIMUR, KEMIRIMUKA, KUKUSAN, diff --git a/apps/cases/tests/test_units/test_utils.py b/apps/cases/tests/test_units/test_utils.py index 03603c8dcf2319c951e99318262323b1e02a32c3..08eb422a0daaa251399932d5c258f1a390ed7bd2 100644 --- a/apps/cases/tests/test_units/test_utils.py +++ b/apps/cases/tests/test_units/test_utils.py @@ -14,7 +14,14 @@ from ...utils import ( AllCaseFilter, CaseFilter, CreatedDateFilterDecorator, - DiagramConverter + DiagramConverter, + CSVConverter, + XLSConverter, +) + + +from apps.exportables.constants import ( + INVESTIGATION_CASE_RENDERER_FIELDS, ) @@ -269,3 +276,69 @@ class DiagramConverterTest(TestCase): def test(self): DiagramConverter(AllCaseFilter().filter()).convert() + + +class CSVConverterTest(TestCase): + + def setUp(self): + self.case_subjects = [ + CaseSubjectFactory( + age=25, + district="Beji" + ), + CaseSubjectFactory( + age=35, + district="Beji" + ), + CaseSubjectFactory( + age=15, + district="Beji" + ), + CaseSubjectFactory( + age=20, + district="Tapos" + ), + CaseSubjectFactory( + age=30, + district="Tapos" + ) + ] + + def test(self): + data = [case.__dict__ for case in AllCaseFilter().filter( + ) if 'investigation_case__case_relation' in case.__dict__.keys()] + result = CSVConverter(data).convert() + self.assertIsNotNone(result) + + +class XLSConverterTest(TestCase): + + def setUp(self): + self.case_subjects = [ + CaseSubjectFactory( + age=25, + district="Beji" + ), + CaseSubjectFactory( + age=35, + district="Beji" + ), + CaseSubjectFactory( + age=15, + district="Beji" + ), + CaseSubjectFactory( + age=20, + district="Tapos" + ), + CaseSubjectFactory( + age=30, + district="Tapos" + ) + ] + + def test(self): + data = [case.__dict__ for case in AllCaseFilter().filter( + ) if 'investigation_case__case_relation' in case.__dict__.keys()] + result = XLSConverter(data).convert() + self.assertIsNotNone(result) diff --git a/apps/cases/utils.py b/apps/cases/utils.py index ffd02a0f0b6f80bdc64dc0325e180678332b864a..07aa431058b080872d53840e3e81373766dd7c51 100644 --- a/apps/cases/utils.py +++ b/apps/cases/utils.py @@ -24,8 +24,17 @@ from apps.exportables.constants import ( FEMALE, MALE, UNDETERMINED, + INVESTIGATION_CASE_HEADER_FIELDS, + INVESTIGATION_CASE_RENDERER_FIELDS, ) +from django.utils import timezone + +import csv +import json +import tempfile +import pandas as pd + class CaseFilter: def filter(self): @@ -162,3 +171,63 @@ class DiagramConverter(ConverterStrategy): TOTAL: len(self.latest_raw_data) } return data + + +class CSVConverter(ConverterStrategy): + + def convert(self): + output = [] + with tempfile.TemporaryFile(mode="w+") as csv_file: + writer = csv.writer(csv_file) + # Header + writer.writerow(INVESTIGATION_CASE_HEADER_FIELDS) + FIELDS_LENGTH = len(INVESTIGATION_CASE_RENDERER_FIELDS) + for data in self.raw_data: + formatted_row = {} + + for field_name_index in range(FIELDS_LENGTH): + original_field_name = INVESTIGATION_CASE_RENDERER_FIELDS[field_name_index] + target_field_name = INVESTIGATION_CASE_HEADER_FIELDS[field_name_index] + + formatted_row[target_field_name] = data[original_field_name] + + gejala = json.loads(formatted_row.get("gejala_medis", "{}")) + gejala = [key for key in gejala.keys() if gejala[key]] + faktor = json.loads(formatted_row.get("faktor_resiko", "{}")) + faktor = [key for key in faktor.keys() if faktor[key]] + + output.append([ + formatted_row.get('nama', ""), + formatted_row.get('alamat', ""), + formatted_row.get('usia', ""), + "Laki-laki" if formatted_row.get('jenis_kelamin', "") else "Perempuan", + formatted_row.get('kecamatan', ""), + formatted_row.get('kelurahan', ""), + formatted_row.get('jenis_kontak', ""), + formatted_row.get('subyek_kasus_acuan', ""), + formatted_row.get('hasil_pemeriksaan', ""), + "; ".join(gejala), + "; ".join(faktor), + formatted_row.get('rujukan_fasilitas_kesehatan', ""), + timezone.localtime(formatted_row.get("tanggal_pencatatan", timezone.now())), + formatted_row.get('nama_pencatat', ""), + "Admin" if formatted_row.get('peran_pencatat', "") else "Kader", + ]) + + writer.writerows(output) + csv_file.seek(0) + return csv_file.read() + + +class XLSConverter(ConverterStrategy): + + def convert(self): + with tempfile.TemporaryFile(mode="wb+") as xls_file, \ + tempfile.TemporaryFile(mode="w+") as csv_file: + csv_data = CSVConverter(self.raw_data).convert() + csv_file.write(csv_data) + csv_file.seek(0) + csv_data_pandas = pd.read_csv(csv_file) + csv_data_pandas.to_excel(xls_file, index=None, header=True) + xls_file.seek(0) + return xls_file.read() diff --git a/apps/exportables/constants.py b/apps/exportables/constants.py index fc75e588a20e08cdd495f6095d91eee95bf10b6f..fcc41fcb1cec9f40df043b445db575869e31128e 100644 --- a/apps/exportables/constants.py +++ b/apps/exportables/constants.py @@ -97,7 +97,7 @@ SUB_DISTRICTS = ( "Cimpaeun", "Jatijajar", "Leuwinanggung", - "Sukmajaya Baru", + "Sukamaju Baru", "Sukatani", "Tapos" ) diff --git a/apps/exportables/serializers.py b/apps/exportables/serializers.py index 22a379ea75aca6857e1b3d45b22878b66855bf6e..d03a0e324c0591645a37cd91a219390e4e7f8d09 100644 --- a/apps/exportables/serializers.py +++ b/apps/exportables/serializers.py @@ -1,3 +1,4 @@ +from django.http import request from rest_framework import serializers from django.utils.translation import gettext_lazy as _ from django.core.exceptions import ValidationError @@ -13,6 +14,9 @@ class ExportInvestigationCaseQuerySerializer(serializers.Serializer): max_age = serializers.IntegerField(required=False, min_value=0) district = serializers.CharField(required=False) sub_district = serializers.CharField(required=False) + download_as_xls = serializers.NullBooleanField(required=False) + start_date = serializers.DateField(required=False) + end_date = serializers.DateField(required=False) def validate(self, data): data = super().validate(data) @@ -24,12 +28,25 @@ class ExportInvestigationCaseQuerySerializer(serializers.Serializer): class CaseCountsQuerySerializer(serializers.Serializer): + is_male = serializers.NullBooleanField(required=False) + min_age = serializers.IntegerField(required=False, min_value=0) + max_age = serializers.IntegerField(required=False, min_value=0) + district = serializers.CharField(required=False) + sub_district = serializers.CharField(required=False) + download_as_xls = serializers.NullBooleanField(required=False) start_date = serializers.DateField(default=datetime(1, 1, 2).date()) end_date = serializers.DateField(default=datetime(9999, 12, 30).date()) def validate(self, data): - data['start_date'] = get_datetime_from_iso_format( - str(data['start_date']), pytz.timezone(settings.TIME_ZONE), 'start') - data['end_date'] = get_datetime_from_iso_format( - str(data['end_date']), pytz.timezone(settings.TIME_ZONE), 'end') + try: + data['start_date'] = get_datetime_from_iso_format( + str(data['start_date']), pytz.timezone(settings.TIME_ZONE), 'start') + data['end_date'] = get_datetime_from_iso_format( + str(data['end_date']), pytz.timezone(settings.TIME_ZONE), 'end') + except ValueError as e: + raise ValidationError(_("Date format invalid, must be using iso format 'YYYY-MM-DD'.")) + + if data['end_date'] < data['start_date']: + raise ValidationError(_("Invalid date range.")) + return data diff --git a/apps/exportables/tests/test_units/data.py b/apps/exportables/tests/test_units/data.py index 65c413e3e7e6c392f3b79c771e0f3f5b23991e11..3294f74578e0c433ad65b9ab328c551c692ad7ca 100644 --- a/apps/exportables/tests/test_units/data.py +++ b/apps/exportables/tests/test_units/data.py @@ -448,7 +448,7 @@ data = { "undetermined": 0, "total_count": 0 }, - "Sukmajaya Baru": { + "Sukamaju Baru": { "positive": 0, "negative": 0, "undetermined": 0, diff --git a/apps/exportables/tests/test_units/test_exportables.py b/apps/exportables/tests/test_units/test_exportables.py index e15d447f49ffb688fec24e5ec23c2e0ba7265b8c..371987b8fc674a73f201dc12c8d3677394b2002e 100644 --- a/apps/exportables/tests/test_units/test_exportables.py +++ b/apps/exportables/tests/test_units/test_exportables.py @@ -101,7 +101,7 @@ class ExportableViewTest(APITestCase): def test_url_filter_exportable(self): start_date = datetime.now(tz=pytz.timezone(settings.TIME_ZONE)) - timedelta(days=1) end_date = datetime.now(tz=pytz.timezone(settings.TIME_ZONE)) + timedelta(days=1) - url = "/exportables/?start-date={}&end-date={}".format( + url = "/exportables/?start_date={}&end_date={}".format( start_date.isoformat()[0:10], end_date.isoformat()[0:10]) response = self.client.get(url) @@ -110,7 +110,7 @@ class ExportableViewTest(APITestCase): def test_url_filter_exportable_invalid_date_range(self): start_date = datetime.now(tz=pytz.timezone(settings.TIME_ZONE)) + timedelta(days=1) end_date = datetime.now(tz=pytz.timezone(settings.TIME_ZONE)) - timedelta(days=1) - url = "/exportables/?start-date={}&end-date={}".format( + url = "/exportables/?start_date={}&end_date={}".format( start_date.isoformat()[0:10], end_date.isoformat()[0:10]) response = self.client.get(url) @@ -119,7 +119,7 @@ class ExportableViewTest(APITestCase): def test_url_filter_exportable_invalid_date_format(self): start_date = datetime.now(tz=pytz.timezone(settings.TIME_ZONE)) + timedelta(days=1) end_date = datetime.now(tz=pytz.timezone(settings.TIME_ZONE)) - timedelta(days=1) - url = "/exportables/?start-date={}&end-date={}".format( + url = "/exportables/?start_date={}&end_date={}".format( start_date.isoformat()[0:8], end_date.isoformat()[0:10]) response = self.client.get(url) @@ -128,7 +128,7 @@ class ExportableViewTest(APITestCase): def test_filter_exportable_data_return_values(self): start_date = datetime.now(tz=pytz.timezone(settings.TIME_ZONE)) - timedelta(days=1) end_date = datetime.now(tz=pytz.timezone(settings.TIME_ZONE)) + timedelta(days=1) - url = "/exportables/?start-date={}&end-date={}".format( + url = "/exportables/?start_date={}&end_date={}".format( start_date.isoformat()[0:10], end_date.isoformat()[0:10]) response = self.client.get(url) response_data = json.loads(response.content) @@ -137,7 +137,7 @@ class ExportableViewTest(APITestCase): def test_filter_exportable_empty_return_values(self): start_date = datetime.now(tz=pytz.timezone(settings.TIME_ZONE)) + timedelta(days=1) end_date = datetime.now(tz=pytz.timezone(settings.TIME_ZONE)) + timedelta(days=2) - url = "/exportables/?start-date={}&end-date={}".format( + url = "/exportables/?start_date={}&end_date={}".format( start_date.isoformat()[0:10], end_date.isoformat()[0:10]) response = self.client.get(url) response_data = json.loads(response.content) @@ -201,6 +201,10 @@ class ExportInvestigationCaseViewTest(APITestCase): body = self.export_csv_test_util("?sub_district=Sukatani") self.assertEqual(len(body), 1) + def test_export_investigation_data_to_csv_filter_date(self): + body = self.export_csv_test_util("?start_date=2020-12-31&end_date=2021-12-31") + self.assertEqual(len(body), 5) + def test_export_investigation_data_to_csv_wrong_gender_format(self): self.wrong_query_param_test_util("?is_male=asdf") @@ -210,6 +214,9 @@ class ExportInvestigationCaseViewTest(APITestCase): def test_export_investigation_data_to_csv_wrong_max_age_format(self): self.wrong_query_param_test_util("?min_age=10&max_age=asd") + def test_export_investigation_data_to_csv_wrong_date(self): + self.wrong_query_param_test_util("?start_date=2020-31-12&end_date=2021-31-12") + class UtilsFunctionsTest(TestCase): @@ -240,3 +247,69 @@ class UtilsFunctionsTest(TestCase): error_msg = "Date format invalid, must be using iso format 'YYYY-MM-DD'." self.assertEqual(str(err.exception), error_msg) + + +class CaseCountsViewTest(TestCase): + @classmethod + def setUpTestData(cls): + cls.user = UserFactory(username="user", password="justpass") + cls.admin = AccountFactory(admin=True, user=cls.user) + cls.token, _ = Token.objects.get_or_create(user=cls.user) + cls.BASE_URL = "/exportables/" + + init_data() + + def setUp(self): + self.client = APIClient(HTTP_AUTHORIZATION=HEADER_PREFIX + self.token.key) + + def get_case_count_util(self, filter): + url = self.BASE_URL + filter + response = self.client.get(url) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + + body = json.loads(response.content.decode('utf-8')) + + return body + + def wrong_query_param_test_util(self, filter): + url = self.BASE_URL + filter + response = self.client.get(url) + + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + def test_get_case_count_all(self): + body = self.get_case_count_util("") + self.assertEqual(body['total_count'], 5) + + def test_get_case_count_filter_gender(self): + body = self.get_case_count_util("?is_male=false") + self.assertEqual(body['total_count'], 2) + + def test_get_case_count_filter_age(self): + body = self.get_case_count_util("?min_age=21&max_age=21") + self.assertEqual(body['total_count'], 2) + + def test_get_case_count_filter_district(self): + body = self.get_case_count_util("?district=Beji") + self.assertEqual(body['total_count'], 2) + + def test_get_case_count_filter_sub_district(self): + body = self.get_case_count_util("?sub_district=Sukatani") + self.assertEqual(body['total_count'], 1) + + def test_get_case_count_filter_date(self): + body = self.get_case_count_util("?start_date=2020-12-31&end_date=2021-12-31") + self.assertEqual(body['total_count'], 5) + + def test_get_case_count_wrong_gender_format(self): + self.wrong_query_param_test_util("?is_male=asdf") + + def test_get_case_count_wrong_min_age_format(self): + self.wrong_query_param_test_util("?min_age=abc&max_age=12") + + def test_get_case_count_wrong_max_age_format(self): + self.wrong_query_param_test_util("?min_age=10&max_age=asd") + + def test_get_case_count_wrong_date(self): + self.wrong_query_param_test_util("?start_date=2020-31-12&end_date=2021-31-12") diff --git a/apps/exportables/views.py b/apps/exportables/views.py index 55dd9425429b20cb57617f74ab2477957ab2541d..b82ff72a5231908c0389282dbedc129d290a56db 100644 --- a/apps/exportables/views.py +++ b/apps/exportables/views.py @@ -1,5 +1,6 @@ from django.http import JsonResponse from django.db.models import Max +from django.http.response import HttpResponse from django.utils import timezone from rest_framework import status, serializers from rest_framework.response import Response @@ -13,7 +14,9 @@ from ..cases.utils import ( AgeFilterDecorator, SubDistrictFilterDecorator, CreatedDateFilterDecorator, - DiagramConverter + DiagramConverter, + CSVConverter, + XLSConverter, ) from apps.cases.models import ( CaseSubject, @@ -37,48 +40,45 @@ from apps.exportables.constants import ( START_DATE, END_DATE ) -from apps.exportables.utils import ( - format_custom_csv_rows, - generate_initial_counts, - generate_initial_group_counts, - map_outcome_value, - map_sex_value, - get_datetime_from_iso_format, -) from apps.exportables.renderers import InvestigationCaseCSVRenderer from django.conf import settings -from datetime import datetime, timedelta -import pytz class CaseCountsView(APIView): permission_classes = (IsAuthenticated,) def get(self, request, format=None): - query = {} - if 'start-date' in request.GET: - query['start_date'] = request.GET['start-date'] - if 'end-date' in request.GET: - query['end_date'] = request.GET['end-date'] - query = CaseCountsQuerySerializer(data=query) - if not query.is_valid(): - return JsonResponse( - {"error": "Date format invalid, must be using iso format 'YYYY-MM-DD'."}, - status=status.HTTP_400_BAD_REQUEST - ) + query = ExportInvestigationCaseQuerySerializer(data=self.request.query_params) + query.is_valid(raise_exception=True) query = query.validated_data - if query['end_date'] < query['start_date']: - return JsonResponse( - {"error": "Invalid date range."}, - status=status.HTTP_400_BAD_REQUEST - ) + query = CaseCountsQuerySerializer(data=query) + query.is_valid(raise_exception=True) + query = query.validated_data - case_subjects = CaseSubject.objects.filter( - created_at__range=[query['start_date'], query['end_date']]) + cases = AllCaseFilter() + if 'is_male' in query: + cases = SexFilterDecorator(cases, query['is_male']) + if 'min_age' in query and 'max_age' in query: + cases = AgeFilterDecorator(cases, query['min_age'], query['max_age']) + if 'district' in query: + cases = DistrictFilterDecorator(cases, query['district']) + if 'sub_district' in query: + cases = SubDistrictFilterDecorator(cases, query['sub_district']) + if 'start_date' in query and 'end_date' in query: + cases = CreatedDateFilterDecorator( + cases, start_date=query['start_date'], end_date=query['end_date']) + + data = cases.filter().prefetch_related( + "investigation_case", + "investigation_case__reference_case", + "author" + ).values( + *INVESTIGATION_CASE_RENDERER_FIELDS + ) - data = DiagramConverter(case_subjects).convert() + data = DiagramConverter(data).convert() return JsonResponse(data=data, status=status.HTTP_200_OK) @@ -92,6 +92,10 @@ class ExportInvestigationCaseView(APIView): query.is_valid(raise_exception=True) query = query.validated_data + query = CaseCountsQuerySerializer(data=query) + query.is_valid(raise_exception=True) + query = query.validated_data + cases = AllCaseFilter() if 'is_male' in query: cases = SexFilterDecorator(cases, query['is_male']) @@ -101,6 +105,9 @@ class ExportInvestigationCaseView(APIView): cases = DistrictFilterDecorator(cases, query['district']) if 'sub_district' in query: cases = SubDistrictFilterDecorator(cases, query['sub_district']) + if 'start_date' in query and 'end_date' in query: + cases = CreatedDateFilterDecorator( + cases, start_date=query['start_date'], end_date=query['end_date']) data = list( cases.filter().prefetch_related( @@ -111,9 +118,16 @@ class ExportInvestigationCaseView(APIView): *INVESTIGATION_CASE_RENDERER_FIELDS ) ) - formatted_data = format_custom_csv_rows(data) - filename = "cases-{}.csv".format(timezone.localtime(timezone.now())) - headers = {"Content-Disposition": 'attachment; filename="{}"'.format(filename)} + if 'download_as_xls' in query: + filename = "cases-{}.xls".format(timezone.localtime(timezone.now())) + data = XLSConverter(data).convert() + response = HttpResponse(data, content_type='text/ms-excel') + else: # Default is csv + filename = "cases-{}.csv".format(timezone.localtime(timezone.now())) + data = CSVConverter(data).convert() + response = HttpResponse(data, content_type='text/csv') + + response['Content-Disposition'] = 'attachment; filename="{}"'.format(filename) - return Response(formatted_data, headers=headers) + return response diff --git a/requirements.txt b/requirements.txt index 342a229dd561f92a9e0821e22a17383bef48f447..d3c7af9535b392ec18ba2ad90eb91ecd837dc276 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,11 +12,15 @@ django-sendgrid-v5==0.8.1 djangorestframework==3.11.0 djangorestframework-csv==2.1.0 entrypoints==0.3 +et-xmlfile==1.1.0 factory-boy==2.12.0 Faker==4.0.1 future==0.18.2 gunicorn==20.0.4 mccabe==0.6.1 +numpy==1.19.5 +openpyxl==3.0.7 +pandas==1.1.5 pathspec==0.7.0 psycopg2-binary==2.8.4 pycodestyle==2.5.0