Skip to content

Commit

Permalink
Added CSV support To API listFacilityDischargedPatients (#2601)
Browse files Browse the repository at this point in the history
Added CSV support To API listFacilityDischargedPatients (#2601)

Co-authored-by: Aakash Singh <[email protected]>
  • Loading branch information
AnveshNalimela and sainak authored Nov 25, 2024
1 parent d23cbcb commit 7454ab9
Show file tree
Hide file tree
Showing 4 changed files with 135 additions and 68 deletions.
73 changes: 5 additions & 68 deletions care/facility/api/viewsets/patient.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
from django.db.models.query import QuerySet
from django.utils import timezone
from django_filters import rest_framework as filters
from djqscsv import render_to_csv_response
from drf_spectacular.utils import extend_schema, extend_schema_view
from dry_rest_permissions.generics import DRYPermissionFiltersBase, DRYPermissions
from rest_framework import filters as rest_framework_filters
Expand Down Expand Up @@ -80,6 +79,7 @@
from care.facility.models.patient_consultation import PatientConsultation
from care.users.models import User
from care.utils.cache.cache_allowed_facilities import get_accessible_facilities
from care.utils.exports.mixins import CSVExportViewSetMixin
from care.utils.filters.choicefilter import CareChoiceFilter
from care.utils.filters.multiselect import MultiSelectFilter
from care.utils.notification_handler import NotificationGenerator
Expand Down Expand Up @@ -376,6 +376,7 @@ def filter_queryset(self, request, queryset, view):

@extend_schema_view(history=extend_schema(tags=["patient"]))
class PatientViewSet(
CSVExportViewSetMixin,
HistoryMixin,
mixins.CreateModelMixin,
mixins.ListModelMixin,
Expand Down Expand Up @@ -475,7 +476,6 @@ class PatientViewSet(
"last_consultation_encounter_date",
"last_consultation_discharge_date",
]
CSV_EXPORT_LIMIT = 7

def get_queryset(self):
queryset = super().get_queryset().order_by("modified_date")
Expand Down Expand Up @@ -520,71 +520,6 @@ def filter_queryset(self, queryset: QuerySet) -> QuerySet:

return super().filter_queryset(queryset)

def list(self, request, *args, **kwargs):
"""
Patient List
`without_facility` accepts boolean - default is false -
if true: shows only patients without a facility mapped
if false (default behaviour): shows only patients with a facility mapped
`disease_status` accepts - string and int -
SUSPECTED = 1
POSITIVE = 2
NEGATIVE = 3
RECOVERY = 4
RECOVERED = 5
EXPIRED = 6
"""
if settings.CSV_REQUEST_PARAMETER in request.GET:
# Start Date Validation
temp = filters.DjangoFilterBackend().get_filterset(
self.request, self.queryset, self
)
temp.is_valid()
within_limits = False
for field in self.date_range_fields:
slice_obj = temp.form.cleaned_data.get(field)
if slice_obj:
if not slice_obj.start or not slice_obj.stop:
raise ValidationError(
{
field: "both starting and ending date must be provided for export"
}
)
days_difference = (
temp.form.cleaned_data.get(field).stop
- temp.form.cleaned_data.get(field).start
).days
if days_difference <= self.CSV_EXPORT_LIMIT:
within_limits = True
else:
raise ValidationError(
{
field: f"Cannot export more than {self.CSV_EXPORT_LIMIT} days at a time"
}
)
if not within_limits:
raise ValidationError(
{
"date": f"Atleast one date field must be filtered to be within {self.CSV_EXPORT_LIMIT} days"
}
)
# End Date Limiting Validation
queryset = (
self.filter_queryset(self.get_queryset())
.annotate(**PatientRegistration.CSV_ANNOTATE_FIELDS)
.values(*PatientRegistration.CSV_MAPPING.keys())
)
return render_to_csv_response(
queryset,
field_header_map=PatientRegistration.CSV_MAPPING,
field_serializer_map=PatientRegistration.CSV_MAKE_PRETTY,
)

return super().list(request, *args, **kwargs)

@extend_schema(tags=["patient"])
@action(detail=True, methods=["POST"])
def transfer(self, request, *args, **kwargs):
Expand Down Expand Up @@ -678,7 +613,9 @@ def filter_by_bed_type(self, queryset, name, value):


@extend_schema_view(tags=["patient"])
class FacilityDischargedPatientViewSet(GenericViewSet, mixins.ListModelMixin):
class FacilityDischargedPatientViewSet(
CSVExportViewSetMixin, GenericViewSet, mixins.ListModelMixin
):
permission_classes = (IsAuthenticated, DRYPermissions)
lookup_field = "external_id"
serializer_class = PatientListSerializer
Expand Down
Empty file added care/utils/exports/__init__.py
Empty file.
129 changes: 129 additions & 0 deletions care/utils/exports/mixins.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
from django.conf import settings
from django.db import models
from django_filters import rest_framework as filters
from djqscsv import render_to_csv_response
from rest_framework.exceptions import ValidationError


class CSVExportViewSetMixin:
"""Mixin that adds CSV export functionality to a viewset"""

csv_export_limit = 7
date_range_fields = []

def get_model(self):
"""Get model class from viewset's queryset or model attribute"""
if hasattr(self, "queryset"):
return self.queryset.model
if hasattr(self, "model"):
return self.model
msg = (
"Cannot determine model class from viewset, set model or queryset attribute"
)
raise ValueError(msg)

def get_date_range_fields(self):
"""Get date range fields from model and filterset"""
if self.date_range_fields:
return self.date_range_fields

model = self.get_model()
date_fields = []

# Get fields from model that are DateField/DateTimeField
for field in model._meta.fields: # noqa: SLF001
if isinstance(field, (models.DateField, models.DateTimeField)):
date_fields.append(field.name)

# Get date range fields from filterset if defined
if hasattr(self, "filterset_class"):
for name, field in self.filterset_class.declared_filters.items():
if isinstance(field, filters.DateFromToRangeFilter):
date_fields.append(name)

return list(set(date_fields))

def get_csv_settings(self):
"""Get CSV export configuration from model"""
model = self.get_model()

# Try to get settings from model
annotations = getattr(model, "CSV_ANNOTATE_FIELDS", {})
field_mapping = getattr(model, "CSV_MAPPING", {})
field_serializers = getattr(model, "CSV_MAKE_PRETTY", {})

if not field_mapping:
# Auto-generate field mapping from model fields
field_mapping = {f.name: f.verbose_name.title() for f in model._meta.fields} # noqa: SLF001

fields = list(field_mapping.keys())

return {
"annotations": annotations,
"field_mapping": field_mapping,
"field_serializers": field_serializers,
"fields": fields,
}

def validate_date_ranges(self, request):
"""Validates that at least one date range filter is within limits"""
filterset = filters.DjangoFilterBackend().get_filterset(
request, self.queryset, self
)
if not filterset.is_valid():
raise ValidationError(filterset.errors)

within_limits = False
for field in self.get_date_range_fields():
slice_obj = filterset.form.cleaned_data.get(field)
if slice_obj:
if not slice_obj.start or not slice_obj.stop:
raise ValidationError(
{
field: "both starting and ending date must be provided for export"
}
)

days_difference = (
filterset.form.cleaned_data.get(field).stop
- filterset.form.cleaned_data.get(field).start
).days

if days_difference <= self.csv_export_limit:
within_limits = True
else:
raise ValidationError(
{
field: f"Cannot export more than {self.csv_export_limit} days at a time"
}
)

if not within_limits:
raise ValidationError(
{
"date": f"At least one date field must be filtered to be within {self.csv_export_limit} days"
}
)

def export_as_csv(self, request):
"""Exports queryset as CSV"""
self.validate_date_ranges(request)

csv_settings = self.get_csv_settings()
queryset = self.filter_queryset(self.get_queryset())

if csv_settings["annotations"]:
queryset = queryset.annotate(**csv_settings["annotations"])

queryset = queryset.values(*csv_settings["fields"])

return render_to_csv_response(
queryset,
field_header_map=csv_settings["field_mapping"],
field_serializer_map=csv_settings["field_serializers"],
)

def list(self, request, *args, **kwargs):
if settings.CSV_REQUEST_PARAMETER in request.GET:
return self.export_as_csv(request)
return super().list(request, *args, **kwargs)
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ ignore = [
"FBT001", # why not!
"S106",
"S105",
"UP038" # this results in slower code
]


Expand Down

0 comments on commit 7454ab9

Please sign in to comment.