Skip to content

Commit

Permalink
Merge 878e020 into b05e3d9
Browse files Browse the repository at this point in the history
  • Loading branch information
fredkingham committed Sep 2, 2022
2 parents b05e3d9 + 878e020 commit a882872
Show file tree
Hide file tree
Showing 8 changed files with 234 additions and 106 deletions.
26 changes: 26 additions & 0 deletions changelog.md
@@ -1,5 +1,31 @@
### 0.23.0 (Major Release)

#### Allows the patient summary class to be overridden.

You can now override the default episode serialization in the serach functionality.

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

```
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'
```

#### The future filter now accepts strings

The filter `future` now accepts a string as outputted by the Opal date serialization
Expand Down
30 changes: 30 additions & 0 deletions doc/docs/guides/search.md
Expand Up @@ -16,6 +16,36 @@ 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

```
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'
```



## The Advanced search interface

The Opal advanced search interface at `/#/extract` allows users to specify rules
Expand Down
16 changes: 13 additions & 3 deletions doc/docs/reference/javascript/patient_summary_service.md
Expand Up @@ -6,8 +6,18 @@ 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);
var patientSummary = new PatientSummary(json_data);

Whatever is on the JSON response is put onto patientSummary.data. The constructor adds in the below to the object itself:

* `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"
93 changes: 37 additions & 56 deletions opal/core/search/queries.py
Expand Up @@ -3,6 +3,7 @@
"""
import datetime
import operator
from collections import defaultdict
from functools import reduce

from django.contrib.contenttypes.models import ContentType
Expand All @@ -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 len(start_dates) > 0:
self.start = min(start_dates)

end_dates = [i.end for i in episodes if i.end]
self.end = None
if len(end_dates) > 0:
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):
Expand All @@ -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
Expand Down Expand Up @@ -109,6 +110,7 @@ class DatabaseQuery(QueryBackend):
Finally we filter based on episode type level restrictions.
"""
patient_summary_class = PatientSummary

def fuzzy_query(self):
"""
Expand Down Expand Up @@ -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 = [
Expand Down
67 changes: 41 additions & 26 deletions opal/core/search/static/js/search/services/patient_summary.js
Expand Up @@ -2,32 +2,47 @@
// This is the main PatientSummary class for OPAL.
//
angular.module('opal.services').factory('PatientSummary', function() {
var PatientSummary = function(jsonResponse){
var startYear, endYear;

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){
this.data = jsonResponse;
var startYear, endYear

if(jsonResponse.start_date){
this.startDate = moment(jsonResponse.start_date, 'DD/MM/YYYY');
startYear= this.startDate.format("YYYY");
}

if(jsonResponse.end_date){
this.endDate = moment(jsonResponse.end_date, 'DD/MM/YYYY');
endYear = this.endDate.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.hospitalNumber = jsonResponse.hospital_number;
};
this.patientId = jsonResponse.patient_id;
}

if(jsonResponse.categories){
this.categories = jsonResponse.categories.join(", ");
}

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;
});
return PatientSummary;
});

0 comments on commit a882872

Please sign in to comment.