Skip to content

Commit

Permalink
Include status in observation handling for all APIs. Previously
Browse files Browse the repository at this point in the history
the shortcuts were overlooking this detail, and although the value
quantity would return the set value, the status value of `unknown`
needs to be respected for when the user's value isn't yet known.

Was previously only working when setting the observation by id.
  • Loading branch information
pbugni committed Feb 9, 2018
1 parent 7cc8767 commit bb27f6a
Show file tree
Hide file tree
Showing 9 changed files with 197 additions and 26 deletions.
42 changes: 33 additions & 9 deletions portal/models/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -621,7 +621,8 @@ def add_observation(self, fhir, audit):

issued = fhir.get('issued') and\
parser.parse(fhir.get('issued')) or None
observation = self.save_observation(cc, vq, audit, issued)
status = fhir.get('status')
observation = self.save_observation(cc, vq, audit, status, issued)
if 'performer' in fhir:
for p in fhir['performer']:
performer = Performer.from_fhir(p)
Expand Down Expand Up @@ -676,13 +677,30 @@ def add_service_account(self):
self.add_relationship(service_user, RELATIONSHIP.SPONSOR)
return service_user

def fetch_values_for_concept(self, codeable_concept):
"""Return any matching ValueQuantities for this user"""
def fetch_value_status_for_concept(self, codeable_concept):
"""Return matching ValueQuantity & status for this user
Expected to be used on constrained concepts, where a user
should have zero or one defined. More than one will raise
a value error
:returns: (value_quantity, status) tuple for the observation
if found on the user, else (None, None)
"""
# User may not have persisted concept - do so now for match
codeable_concept = codeable_concept.add_if_not_found()

return [obs.value_quantity for obs in self.observations if
obs.codeable_concept_id == codeable_concept.id]
matching_obs = [
obs for obs in self.observations if
obs.codeable_concept_id == codeable_concept.id]
if not matching_obs:
return None, None
if len(matching_obs) > 1:
raise ValueError(
"multiple observations for {} on constrianed {}".format(
self, codeable_concept))
return matching_obs[0].value_quantity, matching_obs[0].status

def fetch_datetime_for_concept(self, codeable_concept):
"""Return newest issued timestamp from matching observation"""
Expand All @@ -697,7 +715,8 @@ def fetch_datetime_for_concept(self, codeable_concept):
return newest

def save_constrained_observation(
self, codeable_concept, value_quantity, audit, issued=None):
self, codeable_concept, value_quantity, audit, status,
issued=None):
"""Add or update the value for given concept as observation
We can store any number of observations for a patient, and
Expand All @@ -717,23 +736,28 @@ def save_constrained_observation(

if existing:
if existing[0].value_quantity_id == value_quantity.id:
# perfect match -- update audit info
# perfect match -- update audit info, setting status
# and issued as given
existing.status = status
if issued:
existing.issued = issued
existing[0].audit = audit
return
else:
# We don't want multiple observations for this concept
# with different values. Delete old and add new
self.observations.remove(existing[0])

self.save_observation(codeable_concept, value_quantity, audit, issued)
self.save_observation(codeable_concept, value_quantity, audit, status, issued)

def save_observation(self, cc, vq, audit, issued):
def save_observation(self, cc, vq, audit, status, issued):
"""Helper method for creating new observations"""
# avoid cyclical imports
from .assessment_status import invalidate_assessment_status_cache

observation = Observation(
codeable_concept_id=cc.id,
status=status,
issued=issued,
value_quantity_id=vq.id).add_if_not_found(True)
# The audit defines the acting user, to which the current
Expand Down
27 changes: 19 additions & 8 deletions portal/views/clinical.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,9 @@ def biopsy_set(patient_id):
value:
type: boolean
description: has the patient undergone a biopsy
status:
type: string
description: optional status such as 'final' or 'unknown'
responses:
200:
description: successful operation
Expand Down Expand Up @@ -207,6 +210,9 @@ def pca_diag_set(patient_id):
value:
type: boolean
description: the patient's PCa diagnosis
status:
type: string
description: optional status such as 'final' or 'unknown'
responses:
200:
description: successful operation
Expand Down Expand Up @@ -262,6 +268,9 @@ def pca_localized_set(patient_id):
value:
type: boolean
description: the patient's PCaLocalized diagnosis
status:
type: string
description: optional status such as 'final' or 'unknown'
responses:
200:
description: successful operation
Expand Down Expand Up @@ -488,10 +497,12 @@ def clinical_api_shortcut_set(patient_id, codeable_concept):
abort(400, "Expecting boolean for 'value'")

truthiness = ValueQuantity(value=value, units='boolean')
patient.save_constrained_observation(codeable_concept=codeable_concept,
value_quantity=truthiness,
audit=Audit(user_id=current_user().id,
subject_id=patient_id, context='observation'))
audit = Audit(user_id=current_user().id,
subject_id=patient_id, context='observation')
patient.save_constrained_observation(
codeable_concept=codeable_concept, value_quantity=truthiness,
audit=audit, status=request.json.get('status'))

db.session.commit()
auditable_event("set {0} {1} on user {2}".format(
codeable_concept, truthiness, patient_id), user_id=current_user().id,
Expand All @@ -505,9 +516,9 @@ def clinical_api_shortcut_get(patient_id, codeable_concept):
patient = get_user(patient_id)
if patient.deleted:
abort(400, "deleted user - operation not permitted")
value_quantities = patient.fetch_values_for_concept(codeable_concept)
if value_quantities:
assert len(value_quantities) == 1
return jsonify(value=value_quantities[0].value)
value_quantity, status = patient.fetch_value_status_for_concept(
codeable_concept)
if value_quantity and status != 'unknown':
return jsonify(value=value_quantity.value)

return jsonify(value='unknown')
2 changes: 1 addition & 1 deletion tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ def add_required_clinical_data(self, backdate=None):
for cc in CC.BIOPSY, CC.PCaDIAG, CC.PCaLocalized:
get_user(TEST_USER_ID).save_constrained_observation(
codeable_concept=cc, value_quantity=CC.TRUE_VALUE,
audit=audit, issued=timestamp)
audit=audit, status='preliminary', issued=timestamp)

def add_procedure(self, code='367336001', display='Chemotherapy',
system=SNOMED, setdate=None):
Expand Down
3 changes: 2 additions & 1 deletion tests/test_assessment_status.py
Original file line number Diff line number Diff line change
Expand Up @@ -647,6 +647,7 @@ def test_no_start_date(self):
self.test_user = db.session.merge(self.test_user)
self.test_user.save_constrained_observation(
codeable_concept=CC.BIOPSY, value_quantity=CC.FALSE_VALUE,
audit=Audit(user_id=TEST_USER_ID, subject_id=TEST_USER_ID))
audit=Audit(user_id=TEST_USER_ID, subject_id=TEST_USER_ID),
status='final')
self.assertFalse(
QuestionnaireBank.qbs_for_user(self.test_user, 'baseline'))
128 changes: 128 additions & 0 deletions tests/test_clinical.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,25 @@ def test_empty_clinical_get(self):
rv = self.client.get('/api/patient/%s/clinical' % TEST_USER_ID)
self.assert200(rv)

def test_unknown_clinical_post(self):
self.login()
data = {
"resourceType": "Observation",
"code":{"coding":[{
"code":"121",
"display":"PCa diagnosis",
"system":"http://us.truenth.org/clinical-codes"}]},
"status":"unknown",
"valueQuantity":{"units":"boolean","value":"false"}}
rv = self.client.post('/api/patient/{}/clinical'.format(
TEST_USER_ID), content_type='application/json',
data=json.dumps(data))

# confirm status unknown sticks
self.assert200(rv)
rv = self.client.get('/api/patient/%s/clinical' % TEST_USER_ID)
self.assertEquals('unknown', rv.json['entry'][0]['content']['status'])

def test_empty_biopsy_get(self):
"""Access biopsy on user w/o any clinical info"""
self.login()
Expand Down Expand Up @@ -180,6 +199,53 @@ def test_clinical_biopsy_put(self):
user = User.query.get(TEST_USER_ID)
self.assertEquals(user.observations.count(), 1)

def test_clinical_biopsy_unknown(self):
"""Shortcut API - biopsy data w status unknown"""
self.login()
rv = self.client.post(
'/api/patient/%s/clinical/biopsy' % TEST_USER_ID,
content_type='application/json',
data=json.dumps({'value': True, 'status': 'unknown'}))
self.assert200(rv)
result = json.loads(rv.data)
self.assertEquals(result['message'], 'ok')

# Can we get it back in FHIR?
rv = self.client.get('/api/patient/%s/clinical' % TEST_USER_ID)
data = json.loads(rv.data)
coding = data['entry'][0]['content']['code']['coding'][0]
vq = data['entry'][0]['content']['valueQuantity']
status = data['entry'][0]['content']['status']

self.assertEquals(coding['code'], '111')
self.assertEquals(coding['display'], 'biopsy')
self.assertEquals(coding['system'],
'http://us.truenth.org/clinical-codes')
self.assertEquals(vq['value'], 'true')
self.assertEquals(status, 'unknown')

# Access the direct biopsy value
rv = self.client.get('/api/patient/%s/clinical/biopsy' % TEST_USER_ID)
data = json.loads(rv.data)
self.assertEquals(data['value'], 'unknown')

# Can we alter the value?
rv = self.client.post('/api/patient/%s/clinical/biopsy' % TEST_USER_ID,
content_type='application/json',
data=json.dumps({'value': False}))
self.assert200(rv)
result = json.loads(rv.data)
self.assertEquals(result['message'], 'ok')

# Confirm it's altered
rv = self.client.get('/api/patient/%s/clinical/biopsy' % TEST_USER_ID)
data = json.loads(rv.data)
self.assertEquals(data['value'], 'false')

# Confirm the db is clean
user = User.query.get(TEST_USER_ID)
self.assertEquals(user.observations.count(), 1)

def test_clinical_pca_diag(self):
"""Shortcut API - just PCa diagnosis w/o FHIR overhead"""
self.login()
Expand Down Expand Up @@ -209,6 +275,37 @@ def test_clinical_pca_diag(self):
data = json.loads(rv.data)
self.assertEquals(data['value'], 'true')

def test_clinical_pca_diag_unknown(self):
"""Shortcut API - PCa diagnosis w/ status unknown"""
self.login()
rv = self.client.post(
'/api/patient/%s/clinical/pca_diag' % TEST_USER_ID,
content_type='application/json',
data=json.dumps({'value': True, 'status': 'unknown'}))
self.assert200(rv)
result = json.loads(rv.data)
self.assertEquals(result['message'], 'ok')

# Can we get it back in FHIR?
rv = self.client.get('/api/patient/%s/clinical' % TEST_USER_ID)
data = json.loads(rv.data)
coding = data['entry'][0]['content']['code']['coding'][0]
vq = data['entry'][0]['content']['valueQuantity']
status = data['entry'][0]['content']['status']

self.assertEquals(coding['code'], '121')
self.assertEquals(coding['display'], 'PCa diagnosis')
self.assertEquals(coding['system'],
'http://us.truenth.org/clinical-codes')
self.assertEquals(vq['value'], 'true')
self.assertEquals(status, 'unknown')

# Access the direct pca_diag value
rv = self.client.get(
'/api/patient/%s/clinical/pca_diag' % TEST_USER_ID)
data = json.loads(rv.data)
self.assertEquals(data['value'], 'unknown')

def test_clinical_pca_localized(self):
"""Shortcut API - just PCa localized diagnosis w/o FHIR overhead"""
self.login()
Expand Down Expand Up @@ -237,6 +334,37 @@ def test_clinical_pca_localized(self):
data = json.loads(rv.data)
self.assertEquals(data['value'], 'true')

def test_clinical_pca_localized_unknown(self):
"""Shortcut API - PCa localized diagnosis w status unknown"""
self.login()
rv = self.client.post(
'/api/patient/%s/clinical/pca_localized' % TEST_USER_ID,
content_type='application/json',
data=json.dumps({'value': False, 'status': 'unknown'}))
self.assert200(rv)
result = json.loads(rv.data)
self.assertEquals(result['message'], 'ok')

# Can we get it back in FHIR?
rv = self.client.get('/api/patient/%s/clinical' % TEST_USER_ID)
data = json.loads(rv.data)
coding = data['entry'][0]['content']['code']['coding'][0]
vq = data['entry'][0]['content']['valueQuantity']
status = data['entry'][0]['content']['status']

self.assertEquals(coding['code'], '141')
self.assertEquals(coding['display'], 'PCa localized diagnosis')
self.assertEquals(coding['system'],
'http://us.truenth.org/clinical-codes')
self.assertEquals(vq['value'], 'false')
self.assertEquals(status, 'unknown')

# Access the direct pca_localized value
rv = self.client.get(
'/api/patient/%s/clinical/pca_localized' % TEST_USER_ID)
data = json.loads(rv.data)
self.assertEquals(data['value'], 'unknown')

def test_weight(self):
with open(os.path.join(os.path.dirname(__file__),
'weight_example.json'), 'r') as fhir_data:
Expand Down
3 changes: 2 additions & 1 deletion tests/test_communication.py
Original file line number Diff line number Diff line change
Expand Up @@ -382,7 +382,8 @@ def test_st_metastatic(self):
self.test_user = db.session.merge(self.test_user)
self.test_user.save_constrained_observation(
codeable_concept=CC.PCaLocalized, value_quantity=CC.FALSE_VALUE,
audit=Audit(user_id=TEST_USER_ID, subject_id=TEST_USER_ID))
audit=Audit(user_id=TEST_USER_ID, subject_id=TEST_USER_ID),
status='final')

# Confirm test user doesn't qualify for ST QB
self.assertFalse(
Expand Down
12 changes: 8 additions & 4 deletions tests/test_intervention.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,8 @@ def test_diag_stategy(self):
self.login()
user.save_constrained_observation(
codeable_concept=CC.PCaDIAG, value_quantity=CC.TRUE_VALUE,
audit=Audit(user_id=TEST_USER_ID, subject_id=TEST_USER_ID))
audit=Audit(user_id=TEST_USER_ID, subject_id=TEST_USER_ID),
status='registered')
with SessionScope(db):
db.session.commit()
user, cp = map(db.session.merge, (user, cp))
Expand Down Expand Up @@ -746,7 +747,8 @@ def test_p3p_conditions(self):
self.login()
user.save_constrained_observation(
codeable_concept=CC.PCaLocalized, value_quantity=CC.TRUE_VALUE,
audit=Audit(user_id=TEST_USER_ID, subject_id=TEST_USER_ID))
audit=Audit(user_id=TEST_USER_ID, subject_id=TEST_USER_ID),
status='preliminary')
with SessionScope(db):
db.session.commit()
user, ds_p3p = map(db.session.merge, (user, ds_p3p))
Expand Down Expand Up @@ -875,7 +877,8 @@ def test_eproms_p3p_conditions(self):
self.login()
user.save_constrained_observation(
codeable_concept=CC.PCaLocalized, value_quantity=CC.TRUE_VALUE,
audit=Audit(user_id=TEST_USER_ID, subject_id=TEST_USER_ID))
audit=Audit(user_id=TEST_USER_ID, subject_id=TEST_USER_ID),
status='final')
with SessionScope(db):
db.session.commit()
user, ds_p3p = map(db.session.merge, (user, ds_p3p))
Expand Down Expand Up @@ -969,7 +972,8 @@ def test_truenth_st_conditions(self):
self.login()
user.save_constrained_observation(
codeable_concept=CC.BIOPSY, value_quantity=CC.TRUE_VALUE,
audit=Audit(user_id=TEST_USER_ID, subject_id=TEST_USER_ID))
audit=Audit(user_id=TEST_USER_ID, subject_id=TEST_USER_ID),
status='unknown')
with SessionScope(db):
db.session.commit()
user, sm = map(db.session.merge, (user, sm))
Expand Down
3 changes: 2 additions & 1 deletion tests/test_questionnaire_bank.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,8 @@ def test_intervention_trigger_date(self):
self.login()
self.test_user.save_constrained_observation(
codeable_concept=CC.BIOPSY, value_quantity=CC.TRUE_VALUE,
audit=Audit(user_id=TEST_USER_ID, subject_id=TEST_USER_ID))
audit=Audit(user_id=TEST_USER_ID, subject_id=TEST_USER_ID),
status='')
self.test_user = db.session.merge(self.test_user)
obs = self.test_user.observations.first()
self.assertEquals(obs.codeable_concept.codings[0].display, 'biopsy')
Expand Down
3 changes: 2 additions & 1 deletion tests/test_site_persistence.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,8 @@ def testP3Pstrategy(self):
code='424313000', display='Started active surveillance')
get_user(TEST_USER_ID).save_constrained_observation(
codeable_concept=CC.PCaLocalized, value_quantity=CC.TRUE_VALUE,
audit=Audit(user_id=TEST_USER_ID, subject_id=TEST_USER_ID))
audit=Audit(user_id=TEST_USER_ID, subject_id=TEST_USER_ID),
status=None)
self.promote_user(user, role_name=ROLE.PATIENT)
with SessionScope(db):
db.session.commit()
Expand Down

0 comments on commit bb27f6a

Please sign in to comment.