diff --git a/care/facility/api/serializers/daily_round.py b/care/facility/api/serializers/daily_round.py index 148cbdf94d..0ec6bda9af 100644 --- a/care/facility/api/serializers/daily_round.py +++ b/care/facility/api/serializers/daily_round.py @@ -139,7 +139,17 @@ def update(self, instance, validated_data): facility=instance.consultation.patient.facility, ).generate() - return super().update(instance, validated_data) + instance = super().update(instance, validated_data) + + create_consultation_events( + instance.consultation_id, + instance, + instance.created_by_id, + instance.created_date, + fields_to_store=set(validated_data.keys()), + ) + + return instance def update_last_daily_round(self, daily_round_obj): consultation = daily_round_obj.consultation diff --git a/care/facility/api/viewsets/events.py b/care/facility/api/viewsets/events.py index b99ad48565..4e0a31a5fe 100644 --- a/care/facility/api/viewsets/events.py +++ b/care/facility/api/viewsets/events.py @@ -20,7 +20,7 @@ class EventTypeViewSet(ReadOnlyModelViewSet): serializer_class = EventTypeSerializer - queryset = EventType.objects.all() + queryset = EventType.objects.filter(is_active=True) permission_classes = (IsAuthenticated,) def get_serializer_class(self) -> type[BaseSerializer]: diff --git a/care/facility/events/handler.py b/care/facility/events/handler.py index 4fac4efe03..66525603c0 100644 --- a/care/facility/events/handler.py +++ b/care/facility/events/handler.py @@ -1,60 +1,52 @@ from datetime import datetime -from celery import shared_task -from django.core import serializers -from django.db import models, transaction -from django.db.models import Model +from django.db import transaction +from django.db.models import Field, Model from django.db.models.query import QuerySet from django.utils.timezone import now from care.facility.models.events import ChangeType, EventType, PatientConsultationEvent -from care.utils.event_utils import get_changed_fields +from care.utils.event_utils import get_changed_fields, serialize_field -def transform(object_instance: Model, old_instance: Model): - fields = [] +def transform( + object_instance: Model, + old_instance: Model, + fields_to_store: set[str] | None = None, +) -> dict[str, any]: + fields: set[Field] = set() if old_instance: changed_fields = get_changed_fields(old_instance, object_instance) - fields = [ + fields = { field for field in object_instance._meta.fields if field.name in changed_fields - ] + } else: - fields = object_instance._meta.fields + fields = set(object_instance._meta.fields) - data = {} - for field in fields: - value = getattr(object_instance, field.name) - if isinstance(value, models.Model): - data[field.name] = serializers.serialize("python", [value])[0]["fields"] - elif issubclass(field.__class__, models.Field) and field.choices: - # serialize choice fields with display value - data[field.name] = getattr( - object_instance, f"get_{field.name}_display", lambda: value - )() - else: - data[field.name] = value - return data + if fields_to_store: + fields = {field for field in fields if field.name in fields_to_store} + + return {field.name: serialize_field(object_instance, field) for field in fields} -@shared_task def create_consultation_event_entry( consultation_id: int, object_instance: Model, caused_by: int, created_date: datetime, - old_instance: Model = None, + old_instance: Model | None = None, + fields_to_store: set[str] | None = None, ): change_type = ChangeType.UPDATED if old_instance else ChangeType.CREATED - data = transform(object_instance, old_instance) - - fields_to_store = set(data.keys()) + data = transform(object_instance, old_instance, fields_to_store) + fields_to_store = fields_to_store or set(data.keys()) batch = [] groups = EventType.objects.filter( - model=object_instance.__class__.__name__, fields__len__gt=0 + model=object_instance.__class__.__name__, fields__len__gt=0, is_active=True ).values_list("id", "fields") for group_id, group_fields in groups: if set(group_fields) & fields_to_store: @@ -103,6 +95,7 @@ def create_consultation_events( caused_by: int, created_date: datetime = None, old: Model | None = None, + fields_to_store: list[str] | set[str] | None = None, ): if created_date is None: created_date = now() @@ -115,9 +108,18 @@ def create_consultation_events( ) for obj in objects: create_consultation_event_entry( - consultation_id, obj, caused_by, created_date + consultation_id, + obj, + caused_by, + created_date, + fields_to_store=set(fields_to_store) if fields_to_store else None, ) else: create_consultation_event_entry( - consultation_id, objects, caused_by, created_date, old + consultation_id, + objects, + caused_by, + created_date, + old, + fields_to_store=set(fields_to_store) if fields_to_store else None, ) diff --git a/care/facility/management/commands/load_event_types.py b/care/facility/management/commands/load_event_types.py index 5d3e034295..06a82709ca 100644 --- a/care/facility/management/commands/load_event_types.py +++ b/care/facility/management/commands/load_event_types.py @@ -94,86 +94,39 @@ class Command(BaseCommand): "model": "DailyRound", "children": ( { - "name": "HEALTH", + "name": "DAILY_ROUND_DETAILS", + "fields": ( + "taken_at", + "round_type", + "other_details", + "action", + "review_after", + ), "children": ( { "name": "ROUND_SYMPTOMS", # todo resolve clash with consultation symptoms - "fields": ("additional_symptoms", "other_symptoms"), + "fields": ("additional_symptoms",), }, { "name": "PHYSICAL_EXAMINATION", "fields": ("physical_examination_info",), }, - {"name": "PATIENT_CATEGORY", "fields": ("patient_category",)}, + { + "name": "PATIENT_CATEGORY", + "fields": ("patient_category",), + }, ), }, { "name": "VITALS", "children": ( - { - "name": "TEMPERATURE", - "fields": ( - "temperature", - "temperature_measured_at", # todo remove field - ), - }, + {"name": "TEMPERATURE", "fields": ("temperature",)}, {"name": "SPO2", "fields": ("spo2",)}, {"name": "PULSE", "fields": ("pulse",)}, {"name": "BLOOD_PRESSURE", "fields": ("bp",)}, {"name": "RESPIRATORY_RATE", "fields": ("resp",)}, {"name": "RHYTHM", "fields": ("rhythm", "rhythm_details")}, - ), - }, - { - "name": "RESPIRATORY", - "children": ( - { - "name": "BILATERAL_AIR_ENTRY", - "fields": ("bilateral_air_entry",), - }, - ), - }, - { - "name": "INTAKE_OUTPUT", - "children": ( - {"name": "INFUSIONS", "fields": ("infusions",)}, - {"name": "IV_FLUIDS", "fields": ("iv_fluids",)}, - {"name": "FEEDS", "fields": ("feeds",)}, - { - "name": "TOTAL_INTAKE", - "fields": ("total_intake_calculated",), - }, - {"name": "OUTPUT", "fields": ("output",)}, - { - "name": "TOTAL_OUTPUT", - "fields": ("total_output_calculated",), - }, - ), - }, - { - "name": "VENTILATOR_MODES", - "fields": ( - "ventilator_interface", - "ventilator_mode", - "ventilator_peep", - "ventilator_pip", - "ventilator_mean_airway_pressure", - "ventilator_resp_rate", - "ventilator_pressure_support", - "ventilator_tidal_volume", - "ventilator_oxygen_modality", - "ventilator_oxygen_modality_oxygen_rate", - "ventilator_oxygen_modality_flow_rate", - "ventilator_fi02", - "ventilator_spo2", - ), - }, - { - "name": "DIALYSIS", - "fields": ( - "pressure_sore", - "dialysis_fluid_balance", - "dialysis_net_balance", + {"name": "PAIN_SCALE", "fields": ("pain_scale_enhanced",)}, ), }, { @@ -191,40 +144,84 @@ class Command(BaseCommand): "glasgow_verbal_response", "glasgow_motor_response", "glasgow_total_calculated", - "limb_response_upper_extremity_right", "limb_response_upper_extremity_left", + "limb_response_upper_extremity_right", "limb_response_lower_extremity_left", "limb_response_lower_extremity_right", "consciousness_level", "consciousness_level_detail", + "in_prone_position", ), }, { - "name": "BLOOD_GLUCOSE", - "fields": ("blood_sugar_level",), + "name": "RESPIRATORY_SUPPORT", + "fields": ( + "bilateral_air_entry", + "etco2", + "ventilator_fi02", + "ventilator_interface", + "ventilator_mean_airway_pressure", + "ventilator_mode", + "ventilator_oxygen_modality", + "ventilator_oxygen_modality_flow_rate", + "ventilator_oxygen_modality_oxygen_rate", + "ventilator_peep", + "ventilator_pip", + "ventilator_pressure_support", + "ventilator_resp_rate", + "ventilator_spo2", + "ventilator_tidal_volume", + ), }, { - "name": "DAILY_ROUND_DETAILS", + "name": "ARTERIAL_BLOOD_GAS_ANALYSIS", "fields": ( - "other_details", - "medication_given", - "in_prone_position", - "etco2", - "pain", - "pain_scale_enhanced", - "ph", - "pco2", - "po2", - "hco3", "base_excess", + "hco3", "lactate", - "sodium", + "pco2", + "ph", + "po2", "potassium", + "sodium", + ), + }, + { + "name": "BLOOD_GLUCOSE", + "fields": ( + "blood_sugar_level", "insulin_intake_dose", "insulin_intake_frequency", - "nursing", ), }, + { + "name": "IO_BALANCE", + "children": ( + {"name": "INFUSIONS", "fields": ("infusions",)}, + {"name": "IV_FLUIDS", "fields": ("iv_fluids",)}, + {"name": "FEEDS", "fields": ("feeds",)}, + {"name": "OUTPUT", "fields": ("output",)}, + { + "name": "TOTAL_INTAKE", + "fields": ("total_intake_calculated",), + }, + { + "name": "TOTAL_OUTPUT", + "fields": ("total_output_calculated",), + }, + ), + }, + { + "name": "DIALYSIS", + "fields": ( + "dialysis_fluid_balance", + "dialysis_net_balance", + ), + "children": ( + {"name": "PRESSURE_SORE", "fields": ("pressure_sore",)}, + ), + }, + {"name": "NURSING", "fields": ("nursing",)}, ), }, { @@ -239,6 +236,12 @@ class Command(BaseCommand): }, ) + inactive_event_types: Tuple[str, ...] = ( + "RESPIRATORY", + "INTAKE_OUTPUT", + "VENTILATOR_MODES", + ) + def create_objects( self, types: Tuple[EventType, ...], model: str = None, parent: EventType = None ): @@ -250,6 +253,7 @@ def create_objects( "parent": parent, "model": model, "fields": event_type.get("fields", []), + "is_active": True, }, ) if children := event_type.get("children"): @@ -258,6 +262,10 @@ def create_objects( def handle(self, *args, **options): self.stdout.write("Loading Event Types... ", ending="") + EventType.objects.filter(name__in=self.inactive_event_types).update( + is_active=False + ) + self.create_objects(self.consultation_event_types) self.stdout.write(self.style.SUCCESS("OK")) diff --git a/care/utils/event_utils.py b/care/utils/event_utils.py index 27ae5e715d..6105d07e26 100644 --- a/care/utils/event_utils.py +++ b/care/utils/event_utils.py @@ -2,7 +2,8 @@ from json import JSONEncoder from logging import getLogger -from django.db.models import Model +from django.core.serializers import serialize +from django.db.models import Field, Model from multiselectfield.db.fields import MSFList, MultiSelectField logger = getLogger(__name__) @@ -13,7 +14,7 @@ def is_null(data): def get_changed_fields(old: Model, new: Model) -> set[str]: - changed_fields = set() + changed_fields: set[str] = set() for field in new._meta.fields: field_name = field.name if isinstance(field, MultiSelectField): @@ -21,12 +22,22 @@ def get_changed_fields(old: Model, new: Model) -> set[str]: new_val = set(map(str, getattr(new, field_name, []))) if old_val != new_val: changed_fields.add(field_name) - continue - if getattr(old, field_name, None) != getattr(new, field_name, None): + elif getattr(old, field_name, None) != getattr(new, field_name, None): changed_fields.add(field_name) return changed_fields +def serialize_field(object: Model, field: Field): + value = getattr(object, field.name) + if isinstance(value, Model): + # serialize the fields of the related model + return serialize("python", [value])[0]["fields"] + if issubclass(field.__class__, Field) and field.choices: + # serialize choice fields with display value + return getattr(object, f"get_{field.name}_display", lambda: value)() + return value + + def model_diff(old, new): diff = {} for field in new._meta.fields: