From 1609d2dfe9f8662b3c3c2147cf7e7e73e3cb001b Mon Sep 17 00:00:00 2001 From: fredkingham Date: Wed, 8 Jun 2022 17:57:40 +0100 Subject: [PATCH 01/14] We were getting errors in the console whenever the extract page loaded as it was looking up the field choices without a field. If we don't have a field, just return an empty array for the choices --- opal/core/search/static/js/search/controllers/extract.js | 5 ++++- opal/core/search/static/js/test/extract.controller.test.js | 5 +++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/opal/core/search/static/js/search/controllers/extract.js b/opal/core/search/static/js/search/controllers/extract.js index 9ed14176d..0f4e6b11a 100644 --- a/opal/core/search/static/js/search/controllers/extract.js +++ b/opal/core/search/static/js/search/controllers/extract.js @@ -85,7 +85,6 @@ angular.module('opal.controllers').controller( return criteria; }; - $scope.searchableFields = function(columnName){ var column = $scope.findColumn(columnName); if(column){ @@ -98,6 +97,7 @@ angular.module('opal.controllers').controller( } return c.type == 'token' || c.type == 'list' || c.type == "many_to_o";; }), + function(c){ return c; } ).sort(); } @@ -143,6 +143,9 @@ angular.module('opal.controllers').controller( }; $scope.getChoices = function(column, field){ + if(!field){ + return [] + } var modelField = $scope.findField(column, field); if(modelField.lookup_list && modelField.lookup_list.length){ diff --git a/opal/core/search/static/js/test/extract.controller.test.js b/opal/core/search/static/js/test/extract.controller.test.js index e3ed37232..43b844268 100644 --- a/opal/core/search/static/js/test/extract.controller.test.js +++ b/opal/core/search/static/js/test/extract.controller.test.js @@ -381,6 +381,11 @@ describe('ExtractCtrl', function(){ var result = $scope.getChoices("some", "field"); expect(result).toEqual([1, 2, 3]); }); + + it('should error if there is no field', function(){ + var result = $scope.getChoices("some", null); + expect(result).toEqual([]); + }); }); describe('refresh', function(){ From 6ce13f21e98879e7f64675ad4c697590e1365f5c Mon Sep 17 00:00:00 2001 From: fredkingham Date: Wed, 8 Jun 2022 17:57:40 +0100 Subject: [PATCH 02/14] We were getting errors in the console whenever the extract page loaded as it was looking up the field choices without a field. If we don't have a field, just return an empty array for the choices --- changelog.md | 2 +- opal/core/search/static/js/search/controllers/extract.js | 5 ++++- opal/core/search/static/js/test/extract.controller.test.js | 5 +++++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/changelog.md b/changelog.md index bea251c8d..a1364a361 100644 --- a/changelog.md +++ b/changelog.md @@ -1,6 +1,6 @@ ### 0.22.1 (Minor Release) -#### Exclude many to one relationships from the advanced Search +#### Exclude many to one relationships from the advanced search Many to one fields are no longer visible in the advanced search screen. diff --git a/opal/core/search/static/js/search/controllers/extract.js b/opal/core/search/static/js/search/controllers/extract.js index 9ed14176d..0f4e6b11a 100644 --- a/opal/core/search/static/js/search/controllers/extract.js +++ b/opal/core/search/static/js/search/controllers/extract.js @@ -85,7 +85,6 @@ angular.module('opal.controllers').controller( return criteria; }; - $scope.searchableFields = function(columnName){ var column = $scope.findColumn(columnName); if(column){ @@ -98,6 +97,7 @@ angular.module('opal.controllers').controller( } return c.type == 'token' || c.type == 'list' || c.type == "many_to_o";; }), + function(c){ return c; } ).sort(); } @@ -143,6 +143,9 @@ angular.module('opal.controllers').controller( }; $scope.getChoices = function(column, field){ + if(!field){ + return [] + } var modelField = $scope.findField(column, field); if(modelField.lookup_list && modelField.lookup_list.length){ diff --git a/opal/core/search/static/js/test/extract.controller.test.js b/opal/core/search/static/js/test/extract.controller.test.js index e3ed37232..43b844268 100644 --- a/opal/core/search/static/js/test/extract.controller.test.js +++ b/opal/core/search/static/js/test/extract.controller.test.js @@ -381,6 +381,11 @@ describe('ExtractCtrl', function(){ var result = $scope.getChoices("some", "field"); expect(result).toEqual([1, 2, 3]); }); + + it('should error if there is no field', function(){ + var result = $scope.getChoices("some", null); + expect(result).toEqual([]); + }); }); describe('refresh', function(){ From 6076f78a9800b1ad72e105eabe23c2e73c073beb Mon Sep 17 00:00:00 2001 From: fredkingham Date: Thu, 25 Aug 2022 17:30:04 +0100 Subject: [PATCH 03/14] Allows the contents a search result row to be overridden (#55) * Creates the setting of patient_summary_class on the DatabaseQuery * Changes the class to take in a patient and related episodes * Changes all serialization including demographics to be done within the class --- changelog.md | 3 + doc/docs/guides/search.md | 7 ++ opal/core/search/queries.py | 93 ++++++++------------- opal/core/search/tests/test_search_query.py | 83 ++++++++++++++---- opal/core/search/tests/test_views.py | 4 +- 5 files changed, 115 insertions(+), 75 deletions(-) diff --git a/changelog.md b/changelog.md index 9a9399d70..b9d255461 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,8 @@ ### 0.23.0 (Major Release) +#### Allows to the patient summary class that returns results to the front end to be overridden + +If you are using the default DatabaseQueryBackend, you can now return difference results to the front end by overriding the class that translates a patient and episodes to a result in the search results. ### 0.22.1 (Minor Release) diff --git a/doc/docs/guides/search.md b/doc/docs/guides/search.md index 8adb22b86..a0ca9a7ab 100644 --- a/doc/docs/guides/search.md +++ b/doc/docs/guides/search.md @@ -16,6 +16,13 @@ The backend takes in a dictionary with the following fields } ``` +## Overriding what is serialized in the search results +By default the class `opal.core.search.queries.PatientSummary` is used to serialize search results to the front end. It takes in a patient and some episodes and returns an element of the list to the front end. + +To override this declare a custom OPAL_SEARCH_BACKEND in settings and +override the `patient_summary_class` attribute. + + ## The Advanced search interface The Opal advanced search interface at `/#/extract` allows users to specify rules diff --git a/opal/core/search/queries.py b/opal/core/search/queries.py index ff8ae9a10..ddedcc768 100644 --- a/opal/core/search/queries.py +++ b/opal/core/search/queries.py @@ -3,6 +3,7 @@ """ import datetime import operator +from collections import defaultdict from functools import reduce from django.contrib.contenttypes.models import ContentType @@ -28,36 +29,35 @@ def get_model_from_api_name(column_name): class PatientSummary(object): - def __init__(self, episode): - self.start = episode.start - self.end = episode.end - self.episode_ids = set([episode.id]) - self.patient_id = episode.patient.id - self.categories = set([episode.category_name]) - - def update(self, episode): - if not self.start: - self.start = episode.start - elif episode.start: - if self.start > episode.start: - self.start = episode.start - - if not self.end: - self.end = episode.end - elif episode.end: - if self.end < episode.end: - self.end = episode.end - - self.episode_ids.add(episode.id) - self.categories.add(episode.category_name) + def __init__(self, patient, episodes): + start_dates = [i.start for i in episodes if i.start] + self.start = None + if start_dates: + self.start = min(start_dates) + + end_dates = [i.end for i in episodes if i.end] + self.end = None + if end_dates: + self.end = max(end_dates) + + self.patient_id = patient.id + demographics = patient.demographics_set.all()[0] + self.first_name = demographics.first_name + self.surname = demographics.surname + self.hospital_number = demographics.hospital_number + self.date_of_birth = demographics.date_of_birth + self.categories = list(sorted(set([ + episode.category_name for episode in episodes + ]))) + self.count = len(episodes) def to_dict(self): - result = {k: getattr(self, k) for k in [ - "patient_id", "start", "end" - ]} - result["categories"] = sorted(self.categories) - result["count"] = len(self.episode_ids) - return result + keys = [ + "patient_id", "start", "end", "first_name", + "surname", "hospital_number", "date_of_birth", + "categories", "count" + ] + return {key: getattr(self, key) for key in keys} def episodes_for_user(episodes, user): @@ -73,6 +73,7 @@ class QueryBackend(object): """ Base class for search implementations to inherit from """ + def __init__(self, user, query): self.user = user self.query = query @@ -109,6 +110,7 @@ class DatabaseQuery(QueryBackend): Finally we filter based on episode type level restrictions. """ + patient_summary_class = PatientSummary def fuzzy_query(self): """ @@ -403,37 +405,16 @@ def episodes_for_criteria(self, criteria): return eps def get_aggregate_patients_from_episodes(self, episodes): - # at the moment we use start/end only - patient_summaries = {} + patient_to_episodes = defaultdict(set) + result = [] for episode in episodes: - patient_id = episode.patient_id - if patient_id in patient_summaries: - patient_summaries[patient_id].update(episode) - else: - patient_summaries[patient_id] = PatientSummary(episode) - - patients = models.Patient.objects.filter( - id__in=list(patient_summaries.keys()) - ) - patients = patients.prefetch_related("demographics_set") - - results = [] + patient_to_episodes[episode.patient].add(episode) - for patient_id, patient_summary in patient_summaries.items(): - patient = next(p for p in patients if p.id == patient_id) - # Explicitly not using the .demographics property for performance - # note that we prefetch demographics_set a few lines earlier - demographic = patient.demographics_set.get() - - result = {k: getattr(demographic, k) for k in [ - "first_name", "surname", "hospital_number", "date_of_birth" - ]} - - result.update(patient_summary.to_dict()) - results.append(result) - - return results + for patient, episodes in patient_to_episodes.items(): + patient_summary = self.patient_summary_class(patient, episodes) + result.append(patient_summary.to_dict()) + return result def _episodes_without_restrictions(self): all_matches = [ diff --git a/opal/core/search/tests/test_search_query.py b/opal/core/search/tests/test_search_query.py index 5441b5fb3..67dd7eb00 100644 --- a/opal/core/search/tests/test_search_query.py +++ b/opal/core/search/tests/test_search_query.py @@ -26,23 +26,46 @@ class PatientSummaryTestCase(OpalTestCase): - def test_update_sets_start(self): - patient, episode = self.new_patient_and_episode_please() - summary = queries.PatientSummary(episode) - self.assertEqual(None, summary.start) - the_date = date(day=27, month=1, year=1972) - episode2 = patient.create_episode(start=the_date) - summary.update(episode2) - self.assertEqual(summary.start, the_date) - - def test_update_sets_end(self): - patient, episode = self.new_patient_and_episode_please() - summary = queries.PatientSummary(episode) - self.assertEqual(None, summary.start) - the_date = date(day=27, month=1, year=1972) - episode2 = patient.create_episode(end=the_date) - summary.update(episode2) - self.assertEqual(summary.end, the_date) + def setUp(self): + self.patient, self.episode_1 = self.new_patient_and_episode_please() + self.patient.demographics_set.update( + first_name='Sarah', + surname='Wilson', + hospital_number="2342232323", + date_of_birth=date(1984, 1, 2) + ) + self.episode_1.category_name = 'Inpatient' + self.episode_1.start = date(2022, 10, 1) + self.episode_1.end = date(2022, 10, 30) + + self.episode_2 = self.patient.episode_set.create( + category_name='Inpatient', + start=date(2022, 9, 10), + end=date(2022, 10, 10), + ) + + self.episode_3 = self.patient.episode_set.create( + category_name='Inpatient', + start=date(2022, 10, 12), + end=date(2022, 10, 13), + ) + + def test_patient_summary(self): + summary = queries.PatientSummary( + self.patient, [self.episode_1, self.episode_2, self.episode_3] + ) + expected = { + "patient_id": self.patient.id, + "start": date(2022, 9, 10), + "end": date(2022, 10, 30), + "first_name": 'Sarah', + "surname": 'Wilson', + "hospital_number": "2342232323", + "date_of_birth": date(1984, 1, 2), + "categories": ['Inpatient'], + "count": 3 + } + self.assertEqual(summary.to_dict(), expected) class QueryBackendTestCase(OpalTestCase): @@ -782,6 +805,32 @@ def test_update_patient_summaries(self): }] self.assertEqual(expected, summaries) + def test_allows_override_of_patient_summary_class(self): + class MyPatientSummary(queries.PatientSummary): + def to_dict(self): + result = super().to_dict() + result['overridden'] = True + return result + + class MySearchBackEnd(queries.DatabaseQuery): + patient_summary_class = MyPatientSummary + + query = MySearchBackEnd(self.user, self.name_criteria) + summaries = query.get_patient_summaries() + expected = [{ + 'count': 1, + 'hospital_number': u'0', + 'date_of_birth': self.DATE_OF_BIRTH, + 'first_name': u'Sally', + 'surname': u'Stevens', + 'end': self.DATE_OF_EPISODE, + 'start': self.DATE_OF_EPISODE, + 'patient_id': self.patient.id, + 'overridden': True, + 'categories': [u'Inpatient'] + }] + self.assertEqual(expected, summaries) + class CreateQueryTestCase(OpalTestCase): diff --git a/opal/core/search/tests/test_views.py b/opal/core/search/tests/test_views.py index 72fa4e959..82df3b7c9 100644 --- a/opal/core/search/tests/test_views.py +++ b/opal/core/search/tests/test_views.py @@ -256,7 +256,7 @@ def test_number_of_queries(self): "James", "Bond", str(i) ) - with self.assertNumQueries(26): + with self.assertNumQueries(24): self.get_response('{}/?query=Bond'.format(self.url)) for i in range(20): @@ -264,7 +264,7 @@ def test_number_of_queries(self): "James", "Blofelt", str(i) ) - with self.assertNumQueries(26): + with self.assertNumQueries(24): self.get_response('{}/?query=Blofelt'.format(self.url)) def test_with_multiple_patient_episodes(self): From 1b7a3208277f56ddc77492d2a816ce80e2ca9589 Mon Sep 17 00:00:00 2001 From: fredkingham Date: Fri, 26 Aug 2022 08:51:13 +0100 Subject: [PATCH 04/14] Updates the front end PatientSummary.js to include the full json payload Whatever is returned as a row from the search backend is put onto the patientSummary. This start_date and end_date are put onto the summary as startDate, endDate moments --- .../javascript/patient_summary_service.md | 10 +++ .../js/search/services/patient_summary.js | 64 +++++++++++-------- .../js/test/patient_summary.service.test.js | 10 +++ 3 files changed, 58 insertions(+), 26 deletions(-) diff --git a/doc/docs/reference/javascript/patient_summary_service.md b/doc/docs/reference/javascript/patient_summary_service.md index 0a49e19df..94ac9821a 100644 --- a/doc/docs/reference/javascript/patient_summary_service.md +++ b/doc/docs/reference/javascript/patient_summary_service.md @@ -11,3 +11,13 @@ that comes back from the Patient search JSON API. var patient_summary = new PatientSummary(json_data); +Whatever is on the JSON response is put onto it. The constructor adds in the below: + + * `hospitalNumber` + * `patientId` + * `link` A link to the patients patient detail page + * `dateOfBirth` The patient's date of birth cast to a moment + * `startDate` The patient's earliest episode.start cast to a moment + * `endDate` The patient's last episdoe.end cast to a moment + * `years` The span of years between startDate and EndDate e.g. "2020-2023" + * `categories` A comma joined string of the patient's episode categories, e.g. "Inpatient, ICU" diff --git a/opal/core/search/static/js/search/services/patient_summary.js b/opal/core/search/static/js/search/services/patient_summary.js index 57257c6ae..eed9ce71e 100644 --- a/opal/core/search/static/js/search/services/patient_summary.js +++ b/opal/core/search/static/js/search/services/patient_summary.js @@ -2,33 +2,45 @@ // This is the main PatientSummary class for OPAL. // angular.module('opal.services').factory('PatientSummary', function(UserProfile) { - var PatientSummary = function(jsonResponse){ - var startYear, endYear; - var self = this; - - if(jsonResponse.start_date){ - startYear= moment(jsonResponse.start_date, 'DD/MM/YYYY').format("YYYY"); - } - - if(jsonResponse.end_date){ - endYear = moment(jsonResponse.end_date, 'DD/MM/YYYY').format("YYYY"); - } - - if(startYear && endYear && startYear !== endYear){ - this.years = startYear + "-" + endYear; - } - else if(startYear){ - this.years = startYear; - } - this.first_name = jsonResponse.first_name; - this.surname = jsonResponse.surname; - this.patientId = jsonResponse.patient_id; - this.count = jsonResponse.count; + "use strict"; + var PatientSummary = function(jsonResponse){ + _.extend(this, jsonResponse); + var startYear, endYear + + if(jsonResponse.start_date){ + this.startDate = moment(jsonResponse.start_date, 'DD/MM/YYYY'); + startYear= this.start_date.format("YYYY"); + } + + if(jsonResponse.end_date){ + this.endDate = moment(jsonResponse.end_date, 'DD/MM/YYYY'); + endYear = this.end_date.format("YYYY"); + } + + if(startYear && endYear && startYear !== endYear){ + this.years = startYear + "-" + endYear; + } + else if(startYear){ + this.years = startYear; + } + + if(jsonResponse.date_of_birth){ this.dateOfBirth = moment(jsonResponse.date_of_birth, 'DD/MM/YYYY'); - this.categories = jsonResponse.categories.join(", "); + } + + if(jsonResponse.patient_id){ this.link = "/#/patient/" + jsonResponse.patient_id; + this.patientId = jsonResponse.patient_id; + } + + if(jsonResponse.categories){ + this.categories = jsonResponse.categories.join(", "); + } + + if(jsonResponse.hospital_number){ this.hospitalNumber = jsonResponse.hospital_number; - }; + } + }; - return PatientSummary; - }); + return PatientSummary; +}); diff --git a/opal/static/js/test/patient_summary.service.test.js b/opal/static/js/test/patient_summary.service.test.js index a106194de..c6f16129b 100644 --- a/opal/static/js/test/patient_summary.service.test.js +++ b/opal/static/js/test/patient_summary.service.test.js @@ -27,10 +27,20 @@ describe('PatientSummary', function (){ expect(patientSummary.dateOfBirth.toDate()).toEqual(new Date(1973, 4, 12)); expect(patientSummary.years).toEqual(undefined); expect(patientSummary.hospitalNumber).toEqual("11111111"); + expect(patientSummary.hospital_number).toEqual("11111111"); expect(patientSummary.first_name).toEqual("Isabella"); expect(patientSummary.surname).toEqual("King"); expect(patientSummary.link).toEqual("/#/patient/192"); expect(patientSummary.patientId).toEqual(192); + expect(patientSummary.patient_id).toEqual(192); + }); + + it('should cast start date and end date to moments', function(){ + testData.start_date = "10/10/1973"; + testData.end_date = "10/10/1974"; + var patientSummary = new PatientSummary(testData); + expect(patientSummary.start_date.toDate()).toEqual(new Date(1973, 9, 10)); + expect(patientSummary.end_date.toDate()).toEqual(new Date(1974, 9, 10)); }); it("should populate years if they exist", function(){ From 8a2d0be70c9a87f2d8e3ad055d9e8c73ba00b5b8 Mon Sep 17 00:00:00 2001 From: fredkingham Date: Sat, 27 Aug 2022 20:42:43 +0100 Subject: [PATCH 05/14] Improves the documentation and changelog around the patient summary class --- changelog.md | 27 +++++++++++++++++++++++++-- doc/docs/guides/search.md | 25 ++++++++++++++++++++++++- 2 files changed, 49 insertions(+), 3 deletions(-) diff --git a/changelog.md b/changelog.md index b9d255461..5f13e4e07 100644 --- a/changelog.md +++ b/changelog.md @@ -1,8 +1,31 @@ ### 0.23.0 (Major Release) -#### Allows to the patient summary class that returns results to the front end to be overridden +#### Allows the patient summary class to be overridden. + +The PatientSummary class is used to serialize episodes by the search functionality. + +The default is the DatabaseQueryBackend. You can now create a custom backend that inherits this but sets its own serialization class. + +e.g. adding title to the serialization + +``` +class MyPatientSummary(PatientSummary): + def __init__(self, patient, episodes): + super()__init__(patient, episodes) + self.patient_title = patient.demographics().title + + def to_json(self): + as_json = super().to_json() + as_json['title'] = self.patient_title + return as_json + + +class MyCustomBackend(DatabaseQueryBackend): + patient_summary_class = PatientSummary + +# change settings.py to include OPAL_SEARCH_BACKEND='{path to my backend}.MyCustomBackend' +``` -If you are using the default DatabaseQueryBackend, you can now return difference results to the front end by overriding the class that translates a patient and episodes to a result in the search results. ### 0.22.1 (Minor Release) diff --git a/doc/docs/guides/search.md b/doc/docs/guides/search.md index a0ca9a7ab..4c487e127 100644 --- a/doc/docs/guides/search.md +++ b/doc/docs/guides/search.md @@ -17,7 +17,30 @@ The backend takes in a dictionary with the following fields ``` ## Overriding what is serialized in the search results -By default the class `opal.core.search.queries.PatientSummary` is used to serialize search results to the front end. It takes in a patient and some episodes and returns an element of the list to the front end. +By default the class `opal.core.search.queries.PatientSummary` is used to serialize search results to the front end. This can be overridden by declaring a custom class on a custom backend. + +e.g. adding title to the serialization + +``` +class MyPatientSummary(PatientSummary): + def __init__(self, patient, episodes): + super()__init__(patient, episodes) + self.patient_title = patient.demographics().title + + def to_json(self): + as_json = super().to_json() + as_json['title'] = self.patient_title + return as_json + + +class MyCustomBackend(DatabaseQueryBackend): + patient_summary_class = PatientSummary + +# change settings.py to include OPAL_SEARCH_BACKEND='{path to my backend}.MyCustomBackend' +``` + + +It takes in a patient and the episodes that were returned by the search and returns an element of the list to the front end. To override this declare a custom OPAL_SEARCH_BACKEND in settings and override the `patient_summary_class` attribute. From a1fa409a6af001d7a1aa3c2002527683ef1d5eed Mon Sep 17 00:00:00 2001 From: fredkingham Date: Tue, 30 Aug 2022 08:47:07 +0100 Subject: [PATCH 06/14] Changelog tweaks --- changelog.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/changelog.md b/changelog.md index 2b6b70ec9..313c0f668 100644 --- a/changelog.md +++ b/changelog.md @@ -2,9 +2,9 @@ #### Allows the patient summary class to be overridden. -The PatientSummary class is used to serialize episodes by the search functionality. +You can now override the default episode serialization in the serach functionality. -The default is the DatabaseQueryBackend. You can now create a custom backend that inherits this but sets its own serialization class. +Serialization is done by default with the PatientSummary class. This can be overridden in a custom search backend. e.g. adding title to the serialization From 1226163ba0732c446d596f93a70ccb73de75e1cb Mon Sep 17 00:00:00 2001 From: fredkingham Date: Tue, 30 Aug 2022 08:47:41 +0100 Subject: [PATCH 07/14] documentation tweaks on the patient summary service --- doc/docs/reference/javascript/patient_summary_service.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/docs/reference/javascript/patient_summary_service.md b/doc/docs/reference/javascript/patient_summary_service.md index 94ac9821a..c8e357c18 100644 --- a/doc/docs/reference/javascript/patient_summary_service.md +++ b/doc/docs/reference/javascript/patient_summary_service.md @@ -6,8 +6,8 @@ functionality related to interacting with patient search results in the client. ### Constructor -The PatientSummary service is instantiated with the Patient search result data -that comes back from the Patient search JSON API. +The PatientSummary service is instantiated with the patient search result data +that comes back from the patient search JSON API. var patient_summary = new PatientSummary(json_data); From 3c28b89d5929bcd398a714aa604202b43da64a15 Mon Sep 17 00:00:00 2001 From: fredkingham Date: Tue, 30 Aug 2022 08:51:55 +0100 Subject: [PATCH 08/14] Improves the search documentation around the new PatientSummary serialization --- doc/docs/guides/search.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/doc/docs/guides/search.md b/doc/docs/guides/search.md index 4c487e127..28a395308 100644 --- a/doc/docs/guides/search.md +++ b/doc/docs/guides/search.md @@ -19,6 +19,11 @@ The backend takes in a dictionary with the following fields ## Overriding what is serialized in the search results By default the class `opal.core.search.queries.PatientSummary` is used to serialize search results to the front end. This can be overridden by declaring a custom class on a custom backend. +The class is initialised with the patient and the episodes and is then serialized with the `to_json` method. + +With a custom backend you can declare your own `patient_summary_class` allowing you to control how and what gets serialized. + + e.g. adding title to the serialization ``` @@ -40,11 +45,6 @@ class MyCustomBackend(DatabaseQueryBackend): ``` -It takes in a patient and the episodes that were returned by the search and returns an element of the list to the front end. - -To override this declare a custom OPAL_SEARCH_BACKEND in settings and -override the `patient_summary_class` attribute. - ## The Advanced search interface From ea330a5f48b6b2072cc34e9dc9a4e0c9c87e8b9a Mon Sep 17 00:00:00 2001 From: fredkingham Date: Wed, 31 Aug 2022 09:03:06 +0100 Subject: [PATCH 09/14] Bugfix for the patient summary. Only cast camelcase variables to moments --- opal/core/search/static/js/search/services/patient_summary.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/opal/core/search/static/js/search/services/patient_summary.js b/opal/core/search/static/js/search/services/patient_summary.js index a9d062c3c..66fb96ac9 100644 --- a/opal/core/search/static/js/search/services/patient_summary.js +++ b/opal/core/search/static/js/search/services/patient_summary.js @@ -9,12 +9,12 @@ angular.module('opal.services').factory('PatientSummary', function() { if(jsonResponse.start_date){ this.startDate = moment(jsonResponse.start_date, 'DD/MM/YYYY'); - startYear= this.start_date.format("YYYY"); + startYear= this.startDate.format("YYYY"); } if(jsonResponse.end_date){ this.endDate = moment(jsonResponse.end_date, 'DD/MM/YYYY'); - endYear = this.end_date.format("YYYY"); + endYear = this.endDate.format("YYYY"); } if(startYear && endYear && startYear !== endYear){ From 31f0a9dfff4a15134302d2d0e3d3617a6e83a069 Mon Sep 17 00:00:00 2001 From: fredkingham Date: Wed, 31 Aug 2022 09:03:38 +0100 Subject: [PATCH 10/14] Fixes unit tests for patientSummary It also adds an additional test to test that extra attributes on the populating data are put on the patient summary --- opal/static/js/test/patient_summary.service.test.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/opal/static/js/test/patient_summary.service.test.js b/opal/static/js/test/patient_summary.service.test.js index c6f16129b..1d6d258f2 100644 --- a/opal/static/js/test/patient_summary.service.test.js +++ b/opal/static/js/test/patient_summary.service.test.js @@ -39,8 +39,8 @@ describe('PatientSummary', function (){ testData.start_date = "10/10/1973"; testData.end_date = "10/10/1974"; var patientSummary = new PatientSummary(testData); - expect(patientSummary.start_date.toDate()).toEqual(new Date(1973, 9, 10)); - expect(patientSummary.end_date.toDate()).toEqual(new Date(1974, 9, 10)); + expect(patientSummary.startDate.toDate()).toEqual(new Date(1973, 9, 10)); + expect(patientSummary.endDate.toDate()).toEqual(new Date(1974, 9, 10)); }); it("should populate years if they exist", function(){ @@ -68,4 +68,10 @@ describe('PatientSummary', function (){ var patientSummary = new PatientSummary(testData); expect(patientSummary.years).toEqual("1973"); }); + + it("should put whatever is in the PatientSummary's initialising data onto the object", function(){ + testData.otherInfo = "other info" + var patientSummary = new PatientSummary(testData); + expect(patientSummary.otherInfo).toEqual("other info"); + }); }); From 9545d8f813e27d4ee851b2e812114ab97e6e10e9 Mon Sep 17 00:00:00 2001 From: fredkingham Date: Fri, 2 Sep 2022 17:33:20 +0100 Subject: [PATCH 11/14] Put the patient summary initializing data on patientSummary.data Previously we were looking at just extending the original object, actually to avoid convusion lets put it on patientSummary.data. Also make sure it does not error if we initialize the patientSummary with completely different parameters --- .../javascript/patient_summary_service.md | 4 ++-- .../static/js/search/services/patient_summary.js | 10 ++++++---- .../js/test/patient_summary.service.test.js | 16 +++++++--------- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/doc/docs/reference/javascript/patient_summary_service.md b/doc/docs/reference/javascript/patient_summary_service.md index c8e357c18..1ace55fb2 100644 --- a/doc/docs/reference/javascript/patient_summary_service.md +++ b/doc/docs/reference/javascript/patient_summary_service.md @@ -9,9 +9,9 @@ functionality related to interacting with patient search results in the client. The PatientSummary service is instantiated with the patient search result data that comes back from the patient search JSON API. - var patient_summary = new PatientSummary(json_data); + var patientSummary = new PatientSummary(json_data); -Whatever is on the JSON response is put onto it. The constructor adds in the below: +Whatever is on the JSON response is put onto patientSummary.data. The constructor adds in the below to the object itself: * `hospitalNumber` * `patientId` diff --git a/opal/core/search/static/js/search/services/patient_summary.js b/opal/core/search/static/js/search/services/patient_summary.js index 66fb96ac9..f66dda882 100644 --- a/opal/core/search/static/js/search/services/patient_summary.js +++ b/opal/core/search/static/js/search/services/patient_summary.js @@ -4,7 +4,7 @@ angular.module('opal.services').factory('PatientSummary', function() { "use strict"; var PatientSummary = function(jsonResponse){ - _.extend(this, jsonResponse); + this.data = jsonResponse; var startYear, endYear if(jsonResponse.start_date){ @@ -37,9 +37,11 @@ angular.module('opal.services').factory('PatientSummary', function() { this.categories = jsonResponse.categories.join(", "); } - if(jsonResponse.hospital_number){ - this.hospitalNumber = jsonResponse.hospital_number; - } + this.first_name = jsonResponse.first_name; + this.surname = jsonResponse.surname; + this.count = jsonResponse.count; + this.dateOfBirth = moment(jsonResponse.date_of_birth, 'DD/MM/YYYY'); + this.hospitalNumber = jsonResponse.hospital_number; }; return PatientSummary; diff --git a/opal/static/js/test/patient_summary.service.test.js b/opal/static/js/test/patient_summary.service.test.js index 1d6d258f2..f135541c4 100644 --- a/opal/static/js/test/patient_summary.service.test.js +++ b/opal/static/js/test/patient_summary.service.test.js @@ -1,4 +1,4 @@ -describe('PatientSummary', function (){ +fdescribe('PatientSummary', function (){ "use strict"; var PatientSummary; var testData; @@ -27,12 +27,16 @@ describe('PatientSummary', function (){ expect(patientSummary.dateOfBirth.toDate()).toEqual(new Date(1973, 4, 12)); expect(patientSummary.years).toEqual(undefined); expect(patientSummary.hospitalNumber).toEqual("11111111"); - expect(patientSummary.hospital_number).toEqual("11111111"); expect(patientSummary.first_name).toEqual("Isabella"); expect(patientSummary.surname).toEqual("King"); expect(patientSummary.link).toEqual("/#/patient/192"); expect(patientSummary.patientId).toEqual(192); - expect(patientSummary.patient_id).toEqual(192); + }); + + it("should not error if the json object we use to initialize it is unexpected", function(){ + var testData = {"title": "Dr"}; + var patientSummary = new PatientSummary(testData); + expect(patientSummary.data.title).toBe("Dr") }); it('should cast start date and end date to moments', function(){ @@ -68,10 +72,4 @@ describe('PatientSummary', function (){ var patientSummary = new PatientSummary(testData); expect(patientSummary.years).toEqual("1973"); }); - - it("should put whatever is in the PatientSummary's initialising data onto the object", function(){ - testData.otherInfo = "other info" - var patientSummary = new PatientSummary(testData); - expect(patientSummary.otherInfo).toEqual("other info"); - }); }); From fe9140746894602343c497a6b5162a4bd50be43a Mon Sep 17 00:00:00 2001 From: fredkingham Date: Fri, 2 Sep 2022 17:40:01 +0100 Subject: [PATCH 12/14] In the PatientSummary make the boolean checks on list more explicit --- opal/core/search/queries.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/opal/core/search/queries.py b/opal/core/search/queries.py index ddedcc768..00a34ba15 100644 --- a/opal/core/search/queries.py +++ b/opal/core/search/queries.py @@ -32,12 +32,12 @@ class PatientSummary(object): def __init__(self, patient, episodes): start_dates = [i.start for i in episodes if i.start] self.start = None - if start_dates: + if len(start_dates) > 0: self.start = min(start_dates) end_dates = [i.end for i in episodes if i.end] self.end = None - if end_dates: + if len(end_dates) > 0: self.end = max(end_dates) self.patient_id = patient.id From 878e020e0d73469f7e4811649ca998cfd41dc2d6 Mon Sep 17 00:00:00 2001 From: fredkingham Date: Fri, 2 Sep 2022 17:51:58 +0100 Subject: [PATCH 13/14] Change the search test for test_episode_fkorft_for_contains_synonym_name_and_ft to not worry about order --- opal/core/search/tests/test_search_query.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/opal/core/search/tests/test_search_query.py b/opal/core/search/tests/test_search_query.py index 67dd7eb00..d1a631943 100644 --- a/opal/core/search/tests/test_search_query.py +++ b/opal/core/search/tests/test_search_query.py @@ -545,7 +545,10 @@ def test_episode_fkorft_for_contains_synonym_name_and_ft(self): hound_owner.dog = "Dalwinion" hound_owner.save() query = queries.DatabaseQuery(self.user, [criteria]) - self.assertEqual([self.episode, episode_2], query.get_episodes()) + self.assertEqual( + set([self.episode, episode_2]), + set(query.get_episodes()) + ) def test_episode_fkorft_contains_distinct(self): criteria = dict( From 39e9f6dfd7fb171e12c4dbc55b9d2d4e91ce5d44 Mon Sep 17 00:00:00 2001 From: fredkingham Date: Mon, 5 Sep 2022 10:47:44 +0100 Subject: [PATCH 14/14] Remove fdescribe to fix unit test coverage --- opal/static/js/test/patient_summary.service.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opal/static/js/test/patient_summary.service.test.js b/opal/static/js/test/patient_summary.service.test.js index f135541c4..3a4605b6d 100644 --- a/opal/static/js/test/patient_summary.service.test.js +++ b/opal/static/js/test/patient_summary.service.test.js @@ -1,4 +1,4 @@ -fdescribe('PatientSummary', function (){ +describe('PatientSummary', function (){ "use strict"; var PatientSummary; var testData;