Skip to content

Commit

Permalink
Merge da56c44 into e81199d
Browse files Browse the repository at this point in the history
  • Loading branch information
fredkingham committed Mar 28, 2023
2 parents e81199d + da56c44 commit b41a9d2
Show file tree
Hide file tree
Showing 6 changed files with 522 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"
167 changes: 167 additions & 0 deletions intrahospital_api/management/commands/merge_all_patients.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
from django.utils import timezone
from django.core.management.base import BaseCommand
from django.db import transaction
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 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

demographics = models.Demographics.objects.all().select_related('patient')
mrn_to_patient = {i.hospital_number: i.patient for i in demographics}

logger.info('Generating merged MRNs')
to_create = []
for active_mrn, merged_dicts in active_mrn_to_merged_dicts.items():
merged_mrns = [i["mrn"] for i in merged_dicts]
active_patient = mrn_to_patient.get(active_mrn)
merged_mrn_objs = models.MergedMRN.objects.filter(
mrn__in=merged_mrns
)
unmerged_patients = [
mrn_to_patient.get(i) for i in merged_mrns if i in mrn_to_patient
]

# 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:
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()
update_demographics.check_and_handle_upstream_merges_for_mrns(mrns)
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
139 changes: 139 additions & 0 deletions intrahospital_api/test/test_update_demographics.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import datetime
import copy
from opal.core.test import OpalTestCase
from opal.models import Patient
from django.utils import timezone
from elcid.models import Demographics, GPDetails
from intrahospital_api.test.test_loader import ApiTestCase
Expand Down Expand Up @@ -473,6 +474,125 @@ def test_multiple_results(self):
)


@mock.patch("intrahospital_api.update_demographics.get_mrn_to_upstream_merge_data")
@mock.patch("intrahospital_api.update_demographics.loader.get_or_create_patient")
class CheckAndHandleUpstreamMergesForMRNsTestCase(OpalTestCase):
def test_handles_an_inactive_mrn(self, get_or_create_patient, get_mrn_to_upstream_merge_data):
get_mrn_to_upstream_merge_data.return_value = {
"123": {
"ACTIVE_INACTIVE": "INACTIVE",
"MERGED": "Y",
"MRN": "123",
"MERGE_COMMENTS": "Merged with MRN 234 on Oct 20 2014 4:44PM",
},
"234": {
"ACTIVE_INACTIVE": "ACTIVE",
"MERGED": "Y",
"MRN": "234",
"MERGE_COMMENTS": "Merged with MRN 123 on Oct 20 2014 4:44PM",
},
}
patient, _ = self.new_patient_and_episode_please()
patient.demographics_set.update(hospital_number='123')
def create_patient(*args, **kwargs):
patient = Patient.objects.create()
patient.demographics_set.update(hospital_number="234")
return patient, True
get_or_create_patient.side_effect = create_patient
update_demographics.check_and_handle_upstream_merges_for_mrns(["234"])
patient = Patient.objects.get()
self.assertEqual(
patient.demographics().hospital_number, "234"
)
merged_mrn = patient.mergedmrn_set.get()
self.assertEqual(
merged_mrn.mrn, "123"
)


def test_handles_an_active_mrn(self, get_or_create_patient, get_mrn_to_upstream_merge_data):
get_mrn_to_upstream_merge_data.return_value = {
"123": {
"ACTIVE_INACTIVE": "ACTIVE",
"MERGED": "Y",
"MRN": "123",
"MERGE_COMMENTS": "Merged with MRN 234 on Oct 20 2014 4:44PM",
},
"234": {
"ACTIVE_INACTIVE": "INACTIVE",
"MERGED": "Y",
"MRN": "234",
"MERGE_COMMENTS": "Merged with MRN 123 on Oct 20 2014 4:44PM",
},
}
patient, _ = self.new_patient_and_episode_please()
patient.demographics_set.update(hospital_number='123')
update_demographics.check_and_handle_upstream_merges_for_mrns(["234"])
patient = Patient.objects.get()
self.assertEqual(
patient.demographics().hospital_number, "123"
)
self.assertFalse(
get_or_create_patient.called
)
merged_mrn = patient.mergedmrn_set.get()
self.assertEqual(
merged_mrn.mrn, "234"
)

def test_handles_new_inactive_mrns(self, get_or_create_patient, get_mrn_to_upstream_merge_data):
get_mrn_to_upstream_merge_data.return_value = {
"123": {
"ACTIVE_INACTIVE": "ACTIVE",
"MERGED": "Y",
"MRN": "123",
"MERGE_COMMENTS": " ".join([
"Merged with MRN 234 on Oct 20 2014 4:44PM",
"Merged with MRN 345 on Oct 21 2014 5:44PM",
])
},
"234": {
"ACTIVE_INACTIVE": "INACTIVE",
"MERGED": "Y",
"MRN": "234",
"MERGE_COMMENTS": "Merged with MRN 123 on Oct 20 2014 4:44PM",
},
"345": {
"ACTIVE_INACTIVE": "INACTIVE",
"MERGED": "Y",
"MRN": "345",
"MERGE_COMMENTS": "Merged with MRN 123 on Oct 20 2014 5:44PM",
},
}
patient, _ = self.new_patient_and_episode_please()
patient.demographics_set.update(hospital_number='123')
before = timezone.now() - datetime.timedelta(1)
patient.mergedmrn_set.create(
mrn="234",
our_merge_datetime=before,
merge_comments="Merged with MRN 123 on Oct 20 2014 4:44PM"
)
update_demographics.check_and_handle_upstream_merges_for_mrns(["345"])
patient = Patient.objects.get()
self.assertEqual(
patient.demographics().hospital_number, "123"
)
self.assertFalse(get_or_create_patient.called)
# Make sure that we haven't deleted the old one
self.assertTrue(
patient.mergedmrn_set.filter(
mrn="234", our_merge_datetime=before, merge_comments="Merged with MRN 123 on Oct 20 2014 4:44PM"
).exists()
)

# Make sure wehave created a new one
self.assertTrue(
patient.mergedmrn_set.filter(
mrn="345", merge_comments="Merged with MRN 123 on Oct 20 2014 5:44PM"
).exists()
)


class GetActiveMrnAndMergedMrnDataTestCase(OpalTestCase):
BASIC_MAPPING = {
"123": {
Expand Down Expand Up @@ -552,6 +672,25 @@ def test_basic_active_case(self, get_masterfile_row):
}]
)

@mock.patch('intrahospital_api.update_demographics.get_masterfile_row')
def test_uses_mrn_to_upstream_merge_data(self, get_masterfile_row):
"""
Tests that if we use the mrn_to_upstream_merge_data dict the database is not called
"""
get_masterfile_row.side_effect = lambda x: self.BASIC_MAPPING[x]
active_mrn, merged_mrn_and_dates = update_demographics.get_active_mrn_and_merged_mrn_data(
'123', self.BASIC_MAPPING
)
self.assertEqual(active_mrn, "234")
self.assertEqual(
merged_mrn_and_dates,
[{
"mrn": "123",
"merge_comments": self.BASIC_MAPPING["123"]["MERGE_COMMENTS"],
}]
)
self.assertFalse(get_masterfile_row.called)

@mock.patch('intrahospital_api.update_demographics.get_masterfile_row')
def test_crawls_nested_rows_from_branch(self, get_masterfile_row):
"""
Expand Down
Loading

0 comments on commit b41a9d2

Please sign in to comment.