Skip to content

Commit

Permalink
Merge e78d2e2 into 4d4c48d
Browse files Browse the repository at this point in the history
  • Loading branch information
fredkingham committed Apr 13, 2023
2 parents 4d4c48d + e78d2e2 commit a8eef6c
Show file tree
Hide file tree
Showing 7 changed files with 668 additions and 7 deletions.
3 changes: 3 additions & 0 deletions intrahospital_api/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,6 @@

SEND_TO_EPR = "send_to_epr"
EXTERNAL_SYSTEM = "RFH Database"

MERGE_LOAD_MINUTES = "MERGE_LOAD_MINUTES"
TOTAL_MERGE_COUNT = "TOTAL_MERGE_COUNT"
166 changes: 166 additions & 0 deletions intrahospital_api/management/commands/merge_all_patients.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
from django.utils import timezone
from django.core.management.base import BaseCommand
from django.db import transaction
from opal.models import Patient
from intrahospital_api.apis.prod_api import ProdApi as ProdAPI
from intrahospital_api import logger, loader, merge_patient, update_demographics
from elcid import episode_categories as elcid_episode_categories
from elcid.utils import timing
from elcid import models


# Returns all active merged patients
# used by the merge_all_patients mgmt
# command
GET_ALL_ACTIVE_MERGED_MRNS = """
SELECT Patient_Number FROM CRS_Patient_Masterfile
WHERE MERGED = 'Y'
AND ACTIVE_INACTIVE = 'ACTIVE'
"""

def get_all_active_merged_mrns():
api = ProdAPI()
query_result = api.execute_hospital_query(GET_ALL_ACTIVE_MERGED_MRNS)
return [i["Patient_Number"] for i in query_result]



@transaction.atomic
def our_merge_patient(*, old_patient, new_patient):
"""
All elcid native non-singleton entries to be converted to
the equivalent episode category on the wining MRN, with a reference
created in the original_mrn field where they have been moved
Copy teams from the old infection service episode? To the new
Infection service episode
Copy over any episode categories that do not exist, iterate
over subrecord attached to these and add the PreviousMRN
Singleton entries to pick the latest but create a reversion
history entry for the non-oldest, with a reference to the original_mrn
Non-singletons entries are moved from the old parent to the
new parent.
This is the same as merge_patient.merge_patient but
does not call load_patient.
"""
old_mrn = old_patient.demographics().hospital_number
for patient_related_model in merge_patient.PATIENT_RELATED_MODELS:
merge_patient.move_record(
patient_related_model,
old_patient,
new_patient,
old_mrn,
)
for old_episode in old_patient.episode_set.all():
# Note: if the old episode has multiple episode
# categories of the same category name
# this will merge those.
new_episode, _ = new_patient.episode_set.get_or_create(
category_name=old_episode.category_name
)
merge_patient.update_tagging(old_episode, new_episode)
for episode_related_model in merge_patient.EPISODE_RELATED_MODELS:
merge_patient.move_record(
episode_related_model,
old_episode,
new_episode,
old_mrn,
)
old_patient.delete()
new_patient.mergedmrn_set.filter(mrn=old_mrn).update(
our_merge_datetime=timezone.now()
)
merge_patient.updates_statuses(new_patient)



def check_and_handle_upstream_merges_for_mrns(mrns):
"""
Takes in a list of MRNs.
Filters those not related to elCID.
If they are inactive, creates a Patient for the
active MRN and creates MergedMRN for all related inactive
MRNs.
If they are active, creates MergedMRN for all
related inactive MRNs.
This function looks the same as
update_demographics.check_and_handle_upstream_merges_for_mrns
but instead of running merge_patient.merge_patient
it runs the above merge_patient function that
does not call load_patient.
"""
cache = update_demographics.get_mrn_to_upstream_merge_data()
now = timezone.now()
active_mrn_to_merged_dicts = {}
# it is possible that the MRNs passed
# in will link to the same active MRN
# so make sure we only have one per
# active MRN
for mrn in mrns:
active_mrn, merged_dicts = update_demographics.get_active_mrn_and_merged_mrn_data(
mrn, cache
)
active_mrn_to_merged_dicts[active_mrn] = merged_dicts

logger.info('Generating merged MRNs')
for active_mrn, merged_dicts in active_mrn_to_merged_dicts.items():
to_create = []
merged_mrns = [i["mrn"] for i in merged_dicts]
active_patient = Patient.objects.filter(
demographics__hospital_number=active_mrn
).first()
merged_mrn_objs = models.MergedMRN.objects.filter(
mrn__in=merged_mrns
)
unmerged_patients = Patient.objects.filter(
demographics__hospital_number__in=merged_mrns
)
# If we have patients that are inactive we need to do a merge.
if len(unmerged_patients) > 0:
if not active_patient:
active_patient, _ = loader.get_or_create_patient(
active_mrn,
elcid_episode_categories.InfectionService,
run_async=False
)
for unmerged_patient in unmerged_patients:
if active_patient:
our_merge_patient(
old_patient=unmerged_patient,
new_patient=active_patient
)

# If there is an active patient then we need to create merged MRNs.
if active_patient:
# we don't delete and write anew to preserve the our_merge_datetime field
existing_merged_mrns = set([i.mrn for i in merged_mrn_objs])
new_merged_mrns = set(i["mrn"] for i in merged_dicts)
to_add_merged_mrns = new_merged_mrns - existing_merged_mrns

for merged_dict in merged_dicts:
if merged_dict["mrn"] in to_add_merged_mrns:
to_create.append(
models.MergedMRN(
patient=active_patient,
our_merge_datetime=now,
**merged_dict
)
)
logger.info('Saving merged MRNs')
models.MergedMRN.objects.bulk_create(to_create)
logger.info(f'Saved {len(to_create)} merged MRNs')


class Command(BaseCommand):
@timing
def handle(self, *args, **options):
mrns = get_all_active_merged_mrns()
check_and_handle_upstream_merges_for_mrns(mrns)
147 changes: 147 additions & 0 deletions intrahospital_api/management/commands/merge_dup_episode_categories.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
from django.utils import timezone
from django.core.management.base import BaseCommand
from django.db import transaction
from opal.models import Patient
from intrahospital_api.apis.prod_api import ProdApi as ProdAPI
from intrahospital_api import logger, merge_patient
from elcid.utils import timing


import os
from intrahospital_api import merge_patient
from opal import models as opal_models
from django.db import transaction
from django.db.models import Count
import reversion
from elcid.utils import timing

DELETE_FILE = "deleted_episodes.txt"


def update_singleton(subrecord_cls, old_parent, new_parent):
if new_parent.__class__ == opal_models.Episode:
is_episode_subrecord = True
else:
is_episode_subrecord = False

if is_episode_subrecord:
old_singleton = subrecord_cls.objects.get(episode=old_parent)
new_singleton = subrecord_cls.objects.get(episode=new_parent)
else:
old_singleton = subrecord_cls.objects.get(patient=old_parent)
new_singleton = subrecord_cls.objects.get(patient=new_parent)
if not old_singleton.updated:
# the old singleton was never editted, we can skip
return
if not new_singleton.updated:
# the new singleton was never editted, we can delete
# it and replace it with the old one.
new_singleton.delete()
if is_episode_subrecord:
old_singleton.episode = new_parent
else:
old_singleton.patient = new_parent
with reversion.create_revision():
old_singleton.save()
else:
if new_singleton.updated < old_singleton.updated:
# the old singleton is new than the new singleton
# stamp the new singleton as reversion
# then copy over all the fields from the old
# onto the new
for field in old_singleton._meta.get_fields():
field_name = field.name
if field_name in merge_patient.IGNORED_FIELDS:
continue
setattr(new_singleton, field_name, getattr(old_singleton, field_name))
with reversion.create_revision():
new_singleton.save()
else:
# the old singleton is older than the new singleton
# create a reversion record with the data of the old
# singleton, then continue with the more recent data
more_recent_data = {}
for field in new_singleton._meta.get_fields():
field_name = field.name
if field_name in merge_patient.IGNORED_FIELDS:
continue
more_recent_data[field_name] = getattr(new_singleton, field_name)
setattr(new_singleton, field_name, getattr(old_singleton, field_name))
new_singleton.save()
for field, value in more_recent_data.items():
setattr(new_singleton, field, value)
with reversion.create_revision():
new_singleton.save()


def move_non_singletons(subrecord_cls, old_parent, new_parent):
"""
Moves the old_subrecords query set onto the new parent (a patient or episode).
In doing so it updates the previous_mrn field to be that of the old_mrn
"""
if new_parent.__class__ == opal_models.Episode:
is_episode_subrecord = True
else:
is_episode_subrecord = False
if is_episode_subrecord:
old_subrecords = subrecord_cls.objects.filter(episode=old_parent)
else:
old_subrecords = subrecord_cls.objects.filter(patient=old_parent)
for old_subrecord in old_subrecords:
if is_episode_subrecord:
old_subrecord.episode = new_parent
else:
old_subrecord.patient = new_parent
with reversion.create_revision():
old_subrecord.save()


def move_record(subrecord_cls, old_parent, new_parent):
if getattr(subrecord_cls, "_is_singleton", False):
update_singleton(subrecord_cls, old_parent, new_parent)
else:
move_non_singletons(subrecord_cls, old_parent, new_parent)


def merge_episode(*, old_episode, new_episode):
for episode_related_model in merge_patient.EPISODE_RELATED_MODELS:
move_record(
episode_related_model,
old_episode,
new_episode,
)

@transaction.atomic
def merge_patient_episodes(patient_id):
patient = opal_models.Patient.objects.get(id=patient_id)
episodes = patient.episode_set.all()
category_names = list(set([i.category_name for i in episodes]))
for category in category_names:
category_episodes = [i for i in episodes if i.category_name==category]
if len(category_episodes) > 1:
root = category_episodes[0]
for category_episode in category_episodes[1:]:
merge_patient.update_tagging(category_episode, root)
merge_episode(old_episode=category_episode, new_episode=root)
category_episode_id = category_episode.id
patient_id = category_episode.patient_id
category_episode.delete()
with open(DELETE_FILE, 'a') as w:
w.write(f'\n{patient_id},{category_episode_id}')


def patient_ids_with_duplicate_episode_categories():
dups = opal_models.Episode.objects.values('patient_id', 'category_name').annotate(
cnt=Count('id')
).filter(cnt__gte=2)
return [i["patient_id"] for i in dups]


class Command(BaseCommand):
@timing
def handle(self, *args, **options):
dups = patient_ids_with_duplicate_episode_categories()
logger.info(f'Looking at {len(dups)}')
for idx, patient_id in enumerate(dups):
logger.info(f'Merging {patient_id} ({idx+1}/{len(dups)})')
merge_patient_episodes(patient_id)
27 changes: 27 additions & 0 deletions intrahospital_api/management/commands/merge_patients_since.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import datetime
import time
import traceback
from django.core.management.base import BaseCommand
from django.utils import timezone
from opal.models import Patient
from intrahospital_api import update_demographics, logger, constants
from plugins.monitoring.models import Fact


class Command(BaseCommand):
def handle(self, *args, **options):
try:
start = time.time()
since = timezone.now() - datetime.timedelta(2)
upstream_merged_mrns = update_demographics.get_all_merged_mrns_since(since)
update_demographics.check_and_handle_upstream_merges_for_mrns(upstream_merged_mrns)
finished = time.time()
now = timezone.now()
minutes_taken = int(int(finished-start)/60)
total_merge_count = Patient.objects.filter(mergedmrn__isnull=False).count()
Fact(when=now, label=constants.MERGE_LOAD_MINUTES, value_int=minutes_taken).save()
Fact(when=now, label=constants.TOTAL_MERGE_COUNT, value_int=total_merge_count).save()
except Exception:
msg = "Loading merges: \n{}".format(traceback.format_exc())
logger.error(msg)
return
Loading

0 comments on commit a8eef6c

Please sign in to comment.