From e126261177781d0a09c3d05156c1c828e15fe8cc Mon Sep 17 00:00:00 2001 From: fredkingham Date: Wed, 7 Dec 2022 10:54:53 +0000 Subject: [PATCH] Changes create_rfh_patient_from_hospital_number to created MergedMRN create_rfh_patient_from_hospital_number now looks through the upstream database for the assosicated MRNs. Creates an MRN for whichever is the active MRN. Creates MergedMRNs for any inactive MRNs. Returns the patient. It also changes utils.get_or_create patient to not check upstream merged mrns as this is no longer necessary. --- elcid/models.py | 3 + elcid/test/test_utils.py | 72 +++++---- elcid/utils.py | 26 +--- intrahospital_api/loader.py | 56 +++++-- intrahospital_api/test/test_loader.py | 137 +++++++++++++++++- .../test/test_update_demographics.py | 52 +++++++ intrahospital_api/update_demographics.py | 56 ++++--- 7 files changed, 316 insertions(+), 86 deletions(-) diff --git a/elcid/models.py b/elcid/models.py index fd2afa70f..cb920b206 100644 --- a/elcid/models.py +++ b/elcid/models.py @@ -34,6 +34,9 @@ class MergedMRN(models.Model): e.g. if MRN 77456 was merged into patient 123 Patient 123 would have a patient merge object with MRN 77456 """ + ACTIVE = "ACTIVE" + INACTIVE = "INACTIVE" + patient = models.ForeignKey(Patient, on_delete=models.CASCADE) mrn = models.CharField(max_length=256) merge_comments = models.TextField(blank=True, null=True, default="") diff --git a/elcid/test/test_utils.py b/elcid/test/test_utils.py index 9a49a0457..6cc8cf0a2 100644 --- a/elcid/test/test_utils.py +++ b/elcid/test/test_utils.py @@ -2,6 +2,8 @@ from unittest.mock import patch from opal.core.test import OpalTestCase from elcid import utils +from elcid import episode_categories +from plugins.tb import episode_categories as tb_episode_categories class ModelMethodLoggingTestCase(OpalTestCase): @@ -35,49 +37,61 @@ def some_method(self): result, "some_var" ) -class GetPatientTestCase(OpalTestCase): + +class GetOrCreatePatientTestCase(OpalTestCase): def setUp(self): self.patient, _ = self.new_patient_and_episode_please() def test_get_existing_patient(self): self.patient.demographics_set.update(hospital_number="123") - patient = utils.get_patient('123') - self.assertEqual(self.patient, patient) - - def test_get_existing_merged_patient(self): - self.patient.demographics_set.update(hospital_number="123") - self.patient.mergedmrn_set.create( - mrn="234" + self.patient.episode_set.update( + category_name=episode_categories.InfectionService.display_name + ) + patient, created = utils.get_or_create_patient( + '123', episode_categories.InfectionService ) - patient = utils.get_patient('234') self.assertEqual(self.patient, patient) + episode = self.patient.episode_set.get() + self.assertEqual( + episode.category_name, + episode_categories.InfectionService.display_name + ) + self.assertFalse(created) -class GetOrCreatePatientTestCase(OpalTestCase): - def setUp(self): - self.patient, _ = self.new_patient_and_episode_please() - - def test_get_existing_patient(self): + def test_create_new_episode_on_existing_patient(self): self.patient.demographics_set.update(hospital_number="123") - patient, created = utils.get_or_create_patient('123') + patient, created = utils.get_or_create_patient( + '123', tb_episode_categories.TbEpisode + ) self.assertEqual(self.patient, patient) + self.assertTrue(self.patient.episode_set.filter( + category_name=tb_episode_categories.TbEpisode.display_name + ).exists()) self.assertFalse(created) - @patch('intrahospital_api.update_demographics.upstream_merged_mrn') @patch('intrahospital_api.loader.create_rfh_patient_from_hospital_number') - def test_create_new_patient(self, create_rfh_patient, upstream_merged_mrn): - create_rfh_patient.return_value = self.patient - upstream_merged_mrn.return_value = None - patient, created = utils.get_or_create_patient('123') - create_rfh_patient.assert_called_once_with('123', run_async=None) - self.assertEqual(self.patient, patient) - self.assertTrue(created) + def test_get_merged_patient(self, create_rfh_patient_from_hospital_number): + self.patient.demographics_set.update(hospital_number="234") + self.patient.mergedmrn_set.create(mrn="123") + patient, created = utils.get_or_create_patient( + '123', episode_categories.InfectionService + ) + self.assertEqual( + patient.id, self.patient.id + ) + self.assertFalse(create_rfh_patient_from_hospital_number.called) + self.assertFalse(created) - @patch('intrahospital_api.update_demographics.upstream_merged_mrn') @patch('intrahospital_api.loader.create_rfh_patient_from_hospital_number') - def test_create_merged_patient(self, create_rfh_patient, upstream_merged_mrn): - create_rfh_patient.return_value = self.patient - upstream_merged_mrn.return_value = "234" - patient, created = utils.get_or_create_patient('123') - create_rfh_patient.assert_called_once_with('234', run_async=None) + def test_create_new_patient(self, create_rfh_patient_from_hospital_number): + create_rfh_patient_from_hospital_number.return_value = self.patient + patient, created = utils.get_or_create_patient( + '123', episode_categories.InfectionService + ) + create_rfh_patient_from_hospital_number.assert_called_once_with( + '123', + episode_categories.InfectionService, + run_async=None + ) self.assertEqual(self.patient, patient) self.assertTrue(created) diff --git a/elcid/utils.py b/elcid/utils.py index 698e0b395..ff2cf8487 100644 --- a/elcid/utils.py +++ b/elcid/utils.py @@ -81,30 +81,20 @@ def natural_keys(text): return [ atoi(c) for c in re.split(r'(\d+)', text) ] -def get_patient(mrn): +def get_or_create_patient(mrn, episode_category, run_async=None): + from intrahospital_api import loader patient = opal_models.Patient.objects.filter( demographics__hospital_number=mrn ).first() - if patient: - return patient - return opal_models.Patient.objects.filter( + if not patient: + patient = opal_models.Patient.objects.filter( mergedmrn__mrn=mrn ).first() - -def get_or_create_patient(mrn, run_async=None): - from intrahospital_api import loader - from intrahospital_api import update_demographics - patient = get_patient(mrn) if patient: - return (patient, False) - merged_mrn = update_demographics.upstream_merged_mrn(mrn) - if merged_mrn: - patient = loader.create_rfh_patient_from_hospital_number( - merged_mrn, run_async=run_async - ) - else: - patient = loader.create_rfh_patient_from_hospital_number( - mrn, run_async=run_async + patient.episode_set.get_or_create( + category_name=episode_category.display_name ) + return (patient, False) + patient = loader.create_rfh_patient_from_hospital_number(mrn, episode_category, run_async=run_async) return patient, True diff --git a/intrahospital_api/loader.py b/intrahospital_api/loader.py index f350c0932..185d42ce5 100644 --- a/intrahospital_api/loader.py +++ b/intrahospital_api/loader.py @@ -12,6 +12,7 @@ from opal.models import Patient from elcid import models as emodels +from elcid import episode_categories as elcid_episode_categories from elcid.utils import timing from plugins.admissions.loader import load_encounters, load_transfer_history_for_patient from plugins.appointments.loader import load_appointments @@ -103,21 +104,52 @@ def create_rfh_patient_from_hospital_number(hospital_number, episode_category, r if emodels.MergedMRN.objects.filter(mrn=hospital_number): raise ValueError('MRN has already been merged into another MRN') - patient = Patient.objects.create() + rows = update_demographics.get_related_rows_for_mrn(hospital_number) - demographics = patient.demographics() - demographics.hospital_number = hospital_number - demographics.save() + if not rows: + patient = Patient.objects.create() + patient.demographics_set.update( + hospital_number=hospital_number + ) + patient.episode_set.create( + category_name=episode_category.display_name + ) + load_patient(patient, run_async=run_async) + return patient + + actives = [row for row in rows if row["ACTIVE_INACTIVE"] == emodels.MergedMRN.ACTIVE] + if len(actives) > 1 or len(actives) == 0: + # If there are multiple active rows, we can't create merges, so just create the patient + # If there are no active patients, we just create the patient + patient = Patient.objects.create() + patient.demographics_set.update( + hospital_number=hospital_number + ) + patient.episode_set.create( + category_name=episode_category.display_name + ) + load_patient(patient, run_async=run_async) + return patient - patient.create_episode( - category_name=episode_category.display_name, - start=datetime.date.today() + active_row = actives[0] + patient = Patient.objects.create() + patient.demographics_set.update( + hospital_number=active_row["PATIENT_NUMBER"] ) - - if run_async is None: - load_patient(patient, run_async=run_async) - else: - load_patient(patient) + merged_mrns = [] + for row in rows: + if row["ACTIVE_INACTIVE"] == emodels.MergedMRN.INACTIVE: + _, merge_dt = update_demographics.get_mrn_and_date_from_merge_comment( + row["MERGE_COMMENTS"] + )[0] + merged_mrns.append(emodels.MergedMRN( + patient = patient, + mrn=row["PATIENT_NUMBER"], + upstream_merge_datetime=merge_dt, + merge_comments=row["MERGE_COMMENTS"] + )) + emodels.MergedMRN.objects.bulk_create(merged_mrns) + load_patient(patient, run_async=run_async) return patient diff --git a/intrahospital_api/test/test_loader.py b/intrahospital_api/test/test_loader.py index fca9c7516..5b3cf6e4d 100644 --- a/intrahospital_api/test/test_loader.py +++ b/intrahospital_api/test/test_loader.py @@ -682,8 +682,14 @@ def test_synch_patient( ) +@mock.patch('intrahospital_api.loader.update_demographics.get_related_rows_for_mrn') class CreateRfhPatientFromHospitalNumberTestCase(OpalTestCase): - def test_creates_patient_and_episode(self): + def test_creates_patient_and_episode(self, get_related_rows_for_mrn): + """ + A patient has no merged MRNs. Create the patient with + the episode category and return it. + """ + get_related_rows_for_mrn.return_value = None patient = loader.create_rfh_patient_from_hospital_number( '111', episode_categories.InfectionService ) @@ -695,7 +701,10 @@ def test_creates_patient_and_episode(self): episode_categories.InfectionService.display_name ) - def test_errors_if_the_hospital_number_starts_with_a_zero(self): + def test_errors_if_the_hospital_number_starts_with_a_zero(self, get_related_rows_for_mrn): + """ + The MRN starts with a zero, raise a ValueError + """ with self.assertRaises(ValueError) as v: loader.create_rfh_patient_from_hospital_number( '0111', episode_categories.InfectionService @@ -706,7 +715,10 @@ def test_errors_if_the_hospital_number_starts_with_a_zero(self): ]) self.assertEqual(str(v.exception), expected) - def test_errors_if_the_hospital_number_has_already_been_merged(self): + def test_errors_if_the_hospital_number_has_already_been_merged(self, get_related_rows_for_mrn): + """ + The MRN has already been merged, raise a Value Error + """ patient, _ = self.new_patient_and_episode_please() patient.mergedmrn_set.create( mrn="111" @@ -719,3 +731,122 @@ def test_errors_if_the_hospital_number_has_already_been_merged(self): str(v.exception), "MRN has already been merged into another MRN" ) + + def test_create_rfh_patient_from_hospital_number(self, get_related_rows_for_mrn): + """ + The MRN passed in is inactive and has + an associated active MRN. Create a patient with the + active MRN and the associated mergedMRNs for the + MRN passed in. + """ + RETURN_VALUE = [ + { + "PATIENT_NUMBER": "123", + "MERGE_COMMENTS": " ".join([ + "Merged with MRN 234 on Oct 21 2014 4:44PM", + ]), + "ACTIVE_INACTIVE": "INACTIVE" + }, + { + "PATIENT_NUMBER": "234", + "MERGE_COMMENTS": " ".join([ + "Merged with MRN 123 Oct 17 2014 11:03AM", + "Merged with MRN 123 on Oct 21 2014 4:44PM", + "Merged with MRN 456 on Apr 14 2018 1:40PM" + ]), + "ACTIVE_INACTIVE": "INACTIVE", + }, + { + "PATIENT_NUMBER": "456", + "MERGE_COMMENTS": " ".join([ + "Merged with MRN 234 on Apr 14 2018 1:40PM", + ]), + "ACTIVE_INACTIVE": "ACTIVE" + } + ] + get_related_rows_for_mrn.return_value = RETURN_VALUE + patient = loader.create_rfh_patient_from_hospital_number( + '123', episode_category=episode_categories.InfectionService + ) + self.assertEqual( + patient.demographics_set.get().hospital_number, "456" + ) + self.assertEqual(patient.mergedmrn_set.count(), 2) + + self.assertTrue( + patient.mergedmrn_set.filter( + mrn="123", + merge_comments=RETURN_VALUE[0]["MERGE_COMMENTS"], + upstream_merge_datetime=timezone.make_aware( + datetime.datetime(2014, 10, 21, 16, 44) + ) + ).exists() + ) + self.assertTrue( + patient.mergedmrn_set.filter( + mrn="234", + merge_comments=RETURN_VALUE[1]["MERGE_COMMENTS"], + upstream_merge_datetime=timezone.make_aware( + datetime.datetime(2018, 4, 14, 13, 40) + ) + ).exists() + ) + + def test_active_mrn_with_inactive_associated_mrns(self, get_related_rows_for_mrn): + """ + The MRN passed in is active and has an associated inactive MRN. + Create a patient with the active MRN and the associated mergedMRNs for the + other MRNs + """ + RETURN_VALUE = [ + { + "PATIENT_NUMBER": "123", + "MERGE_COMMENTS": " ".join([ + "Merged with MRN 234 on Oct 21 2014 4:44PM", + ]), + "ACTIVE_INACTIVE": "INACTIVE" + }, + { + "PATIENT_NUMBER": "234", + "MERGE_COMMENTS": " ".join([ + "Merged with MRN 123 Oct 17 2014 11:03AM", + "Merged with MRN 123 on Oct 21 2014 4:44PM", + "Merged with MRN 456 on Apr 14 2018 1:40PM" + ]), + "ACTIVE_INACTIVE": "INACTIVE", + }, + { + "PATIENT_NUMBER": "456", + "MERGE_COMMENTS": " ".join([ + "Merged with MRN 234 on Apr 14 2018 1:40PM", + ]), + "ACTIVE_INACTIVE": "ACTIVE" + } + ] + get_related_rows_for_mrn.return_value = RETURN_VALUE + patient = loader.create_rfh_patient_from_hospital_number( + '456', episode_category=episode_categories.InfectionService + ) + self.assertEqual( + patient.demographics_set.get().hospital_number, "456" + ) + self.assertEqual(patient.mergedmrn_set.count(), 2) + + self.assertTrue( + patient.mergedmrn_set.filter( + mrn="123", + merge_comments=RETURN_VALUE[0]["MERGE_COMMENTS"], + upstream_merge_datetime=timezone.make_aware( + datetime.datetime(2014, 10, 21, 16, 44) + ) + ).exists() + ) + self.assertTrue( + patient.mergedmrn_set.filter( + mrn="234", + merge_comments=RETURN_VALUE[1]["MERGE_COMMENTS"], + upstream_merge_datetime=timezone.make_aware( + datetime.datetime(2018, 4, 14, 13, 40) + ) + ).exists() + ) diff --git a/intrahospital_api/test/test_update_demographics.py b/intrahospital_api/test/test_update_demographics.py index cde7ccaaa..f0d06d549 100644 --- a/intrahospital_api/test/test_update_demographics.py +++ b/intrahospital_api/test/test_update_demographics.py @@ -470,3 +470,55 @@ def test_multiple_results(self): datetime.datetime(2016, 12, 5, 15, 49) ) ) + +@mock.patch('intrahospital_api.update_demographics.api') +class GetRelatedRowsForMRNTestCase(OpalTestCase): + """ + Tests a real world case (with dates and MRNs changed) + to make sure we crawl the tree of Merged MRNs correctly. + """ + MAPPING = { + "123": { + "PATIENT_NUMBER": "123", + "MERGE_COMMENTS": " ".join([ + "Merged with MRN 234 on Oct 21 2014 4:44PM", + ]), + "ACTIVE_INACTIVE": "INACTIVE" + }, + "234": { + "PATIENT_NUMBER": "234", + "MERGE_COMMENTS": " ".join([ + "Merged with MRN 123 Oct 17 2014 11:03AM", + "Merged with MRN 123 on Oct 21 2014 4:44PM", + "Merged with MRN 456 on Apr 14 2018 1:40PM" + ]), + "ACTIVE_INACTIVE": "INACTIVE" + }, + "456": { + "PATIENT_NUMBER": "456", + "MERGE_COMMENTS": " ".join([ + "Merged with MRN 234 on Apr 14 2018 1:40PM", + ]), + "ACTIVE_INACTIVE": "ACTIVE" + } + } + + def test_crawls_nested_rows_from_branch(self, api): + api.execute_hospital_query.side_effect = lambda x, y: self.MAPPING[y["mrn"]] + result = update_demographics.get_related_rows_for_mrn("123") + self.assertEqual( + sorted(list(self.MAPPING.values()), key=lambda x: x["PATIENT_NUMBER"]), + sorted(result, key=lambda x: x["PATIENT_NUMBER"]), + ) + + def test_crawls_nested_rows_from_trunk(self, api): + api.execute_hospital_query.side_effect = lambda x, y: self.MAPPING[y["mrn"]] + result = update_demographics.get_related_rows_for_mrn("234") + self.assertEqual( + sorted(list(self.MAPPING.values()), key=lambda x: x["PATIENT_NUMBER"]), + sorted(result, key=lambda x: x["PATIENT_NUMBER"]), + ) + + def test_no_results(self, api): + api.execute_hospital_query.return_value = [] + self.assertIsNone(update_demographics.get_related_rows_for_mrn("234")) diff --git a/intrahospital_api/update_demographics.py b/intrahospital_api/update_demographics.py index a105929db..4829620bd 100644 --- a/intrahospital_api/update_demographics.py +++ b/intrahospital_api/update_demographics.py @@ -194,6 +194,7 @@ def update_if_changed(instance, update_dict): instance.external_system = EXTERNAL_SYSTEM instance.save() + def get_mrn_and_date_from_merge_comment(merge_comment): """ Takes in an merge comment e.g. @@ -211,43 +212,50 @@ def get_mrn_and_date_from_merge_comment(merge_comment): # return by merged date return sorted(result, key=lambda x: x[1], reverse=True) -def upstream_merged_mrn(mrn): + +def get_related_rows_for_mrn(mrn): """ - Takes an MRN - Returns the MRN its merged into if it exists. + Returns all merged MRNs related to the MRN including the row + for the MRN from the CRS_Patient_Masterfile. - If there are multiple upstream active MRNs (a rare edge case) - return None as we do not know who to merge into. + The merged comments can be nested for for MRN x + we can have MERGE_COMMENT "Merged with y on 21 Jan" + Then for y we can have the merge comment "Merged with z on 30 Mar" + This will return the rows for x, y and z - Also if there is no merge comment, or no active MRN we can match - in the comment return None + If there are no related rows, ie the patient + is not merged, return None. """ query = """ - SELECT Patient_Number, MERGED, MERGE_COMMENTS, ACTIVE_INACTIVE + SELECT * FROM CRS_Patient_Masterfile WHERE Patient_Number = @mrn AND MERGED = 'Y' - AND ACTIVE_INACTIVE = @active_flag AND MERGE_COMMENTS <> '' AND MERGE_COMMENTS is not null """ - result = api.execute_hospital_query( - query, {"mrn": mrn, "active_flag": models.MasterFileMeta.INACTIVE} + query_result = api.execute_hospital_query( + query, {"mrn": mrn} ) - if len(result) == 0: - return - merges = get_mrn_and_date_from_merge_comment(result["MERGE_COMMENTS"]) - - active_upstream_merged_mrns_and_dts = [] - - for merged_mrn, merged_dt in merges: - upstream_active = api.execute_hospital_query( - query, {"mrn": merged_mrn, "active_flag": models.MasterFileMeta.ACTIVE} + if not query_result: + return None + mrn_to_row = {mrn: query_result} + related_mrns = [i[0] for i in get_mrn_and_date_from_merge_comment(query_result["MERGE_COMMENTS"])] + for related_mrn in related_mrns: + related_result = api.execute_hospital_query( + query, {"mrn": related_mrn} ) - if len(upstream_active) == 1: - active_upstream_merged_mrns_and_dts.append((merged_mrn, merged_dt,)) - if len(active_upstream_merged_mrns_and_dts) == 1: - return active_upstream_merged_mrns_and_dts[0] + mrn_to_row[related_mrn] = related_result + related_related_mrns = [ + i[0] for i in get_mrn_and_date_from_merge_comment(related_result["MERGE_COMMENTS"]) + ] + for related_related_mrn in related_related_mrns: + if related_related_mrn not in mrn_to_row: + mrn_to_row[related_related_mrn] = api.execute_hospital_query( + query, {"mrn": related_related_mrn} + ) + return list(mrn_to_row.values()) + def update_patient_subrecords_from_upstream_dict(patient, upstream_patient_information):