Skip to content

Commit

Permalink
Merge pull request #204 from uwcirg/feature/deactivate-account-fix
Browse files Browse the repository at this point in the history
Feature/deactivate account fix
  • Loading branch information
Filienko authored Jan 18, 2024
2 parents 2ae8f73 + 4e40df9 commit 4fc6feb
Show file tree
Hide file tree
Showing 21 changed files with 1,265 additions and 206 deletions.
95 changes: 90 additions & 5 deletions patientsearch/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import jwt
import requests
from werkzeug.exceptions import Unauthorized, Forbidden
from copy import deepcopy

from patientsearch.audit import audit_entry, audit_HAPI_change
from patientsearch.models import (
Expand All @@ -24,6 +25,7 @@
internal_patient_search,
new_resource_hook,
sync_bundle,
restore_patient,
)
from patientsearch.extensions import oidc
from patientsearch.jsonify_abort import jsonify_abort
Expand Down Expand Up @@ -228,6 +230,7 @@ def bundle_getpages():
return jsonify_abort(status_code=400, message=str(error))


@api_blueprint.route("/fhir/<string:resource_type>", methods=["GET"])
@api_blueprint.route("/fhir/<string:resource_type>", methods=["GET"])
def resource_bundle(resource_type):
"""Query HAPI for resource_type and return as JSON FHIR Bundle
Expand All @@ -241,13 +244,23 @@ def resource_bundle(resource_type):
"""
token = validate_auth()
# Check for the store's configurations
active_patient_flag = current_app.config.get("ACTIVE_PATIENT_FLAG")
params = dict(deepcopy(request.args)) # Necessary on ImmutableMultiDict

# Override if the search is specifically for inactive objects
if request.args.get("inactive_search") in {"true", "1"}:
del params["inactive_search"]
elif active_patient_flag:
params["active"] = "true"

try:
return jsonify(
HAPI_request(
token=token,
method="GET",
resource_type=resource_type,
params=request.args,
params=params,
)
)
except (RuntimeError, ValueError) as error:
Expand All @@ -266,6 +279,9 @@ def post_resource(resource_type):
"""
token = validate_auth()
# Check for the store's configurations
active_patient_flag = current_app.config.get("ACTIVE_PATIENT_FLAG")

try:
resource = request.get_json()
if not resource:
Expand All @@ -278,6 +294,10 @@ def post_resource(resource_type):

resource = new_resource_hook(resource)
method = request.method
if active_patient_flag and resource_type == "Patient":
# Ensure it is an active patient
resource["active"] = True

audit_HAPI_change(
user_info=current_user_info(token),
method=method,
Expand Down Expand Up @@ -311,9 +331,52 @@ def update_resource_by_id(resource_type, resource_id):
redirect. Client should watch for 401 and redirect appropriately.
"""
token = validate_auth()
# Check for the store's configurations
active_patient_flag = current_app.config.get("ACTIVE_PATIENT_FLAG")
params = dict(deepcopy(request.args)) # Necessary on ImmutableMultiDict
resource = request.get_json()

# This portion of code is only invoked when restoring a patient
# and returns 500 error if patient's phone number is already in use
if resource_type == "Patient" and active_patient_flag:
try:
# Get our patient in order to access his phone number
patient = HAPI_request(
method="GET",
resource_type=resource_type,
token=token,
resource_id=resource_id,
)
telecom = patient.get("telecom")
if telecom:
# Assuming there is one telecom, looking for phone number among active patients
telecom_entry = telecom[0]
telecom_value = telecom_entry.get("value")
params = {
"telecom": telecom_value,
"active": "true",
}

duplicates = HAPI_request(
token=token,
method="GET",
resource_type=resource_type,
params=params,
)

# Raise a 500 error if active patients with the same phone number have been found
if duplicates["total"] > 0:
first = duplicates["entry"][0]["resource"]["name"][0]["given"][0]
last = duplicates["entry"][0]["resource"]["name"][0]["family"]
error_message = f"""The account can't be restored because
it's phone number, {telecom_value} is now used by another
account {first} {last}"""
raise RuntimeError(error_message)

except (RuntimeError, ValueError) as error:
return jsonify_abort(status_code=500, message=str(error))

try:
resource = request.get_json()
if not resource:
return jsonify_abort(
status_code=400,
Expand All @@ -337,6 +400,9 @@ def update_resource_by_id(resource_type, resource_id):
else:
ignorable_id_increment_audit = True
resource["identifier"] = identifiers
if active_patient_flag:
# Ensure it is an active patient
resource["active"] = True

method = "PUT"
if not ignorable_id_increment_audit:
Expand All @@ -356,6 +422,7 @@ def update_resource_by_id(resource_type, resource_id):
token=token,
)
)

except (RuntimeError, ValueError) as error:
return jsonify_abort(status_code=400, message=str(error))

Expand Down Expand Up @@ -463,10 +530,18 @@ def external_search(resource_type):
"""
token = validate_auth()
active_patient_flag = current_app.config.get("ACTIVE_PATIENT_FLAG")
reactivate_patient = current_app.config.get("REACTIVATE_PATIENT")

params = dict(deepcopy(request.args)) # Necessary on ImmutableMultiDict
if active_patient_flag and resource_type == "Patient":
# Only consider active external patients
params["active"] = "true"

# Tag any matching results with identifier naming source
try:
external_search_bundle = add_identifier_to_resource_type(
bundle=external_request(token, resource_type, request.args),
bundle=external_request(token, resource_type, params),
resource_type=resource_type,
identifier={
"system": "https://github.com/uwcirg/script-fhir-facade",
Expand All @@ -491,7 +566,9 @@ def external_search(resource_type):
if external_match_count:
# Merge result details with internal resources
try:
local_fhir_patient = sync_bundle(token, external_search_bundle)
local_fhir_patient = sync_bundle(
token, external_search_bundle, active_patient_flag
)
except ValueError:
return jsonify_abort(message="Error in local sync", status_code=400)
if local_fhir_patient:
Expand All @@ -500,12 +577,18 @@ def external_search(resource_type):
# See if local match already exists
patient = resource_from_args(resource_type, request.args)
try:
internal_bundle = internal_patient_search(token, patient)
internal_bundle = internal_patient_search(
token, patient, not reactivate_patient
)
except (RuntimeError, ValueError) as error:
return jsonify_abort(status_code=400, message=str(error))
local_fhir_patient = None
if internal_bundle["total"] > 0:
local_fhir_patient = internal_bundle["entry"][0]["resource"]
active = local_fhir_patient.get("active", True)
if reactivate_patient and not active:
local_fhir_patient = restore_patient(token, local_fhir_patient)

if internal_bundle["total"] > 1:
audit_entry(
f"found multiple internal matches ({patient}), return first",
Expand All @@ -525,6 +608,8 @@ def external_search(resource_type):
resource_type=resource_type,
resource=patient,
)
if active_patient_flag:
patient["active"] = True
local_fhir_patient = HAPI_request(
token=token, method=method, resource_type="Patient", resource=patient
)
Expand Down
2 changes: 2 additions & 0 deletions patientsearch/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,3 +139,5 @@ def load_json_config(potential_json_string):
PROJECT_NAME = os.getenv("PROJECT_NAME", "COSRI")
REQUIRED_ROLES = json.loads(os.getenv("REQUIRED_ROLES", "[]"))
UDS_LAB_TYPES = json.loads(os.getenv("UDS_LAB_TYPES", "[]"))
ACTIVE_PATIENT_FLAG = os.getenv("ACTIVE_PATIENT_FLAG", "false").lower() == "true"
REACTIVATE_PATIENT = os.getenv("REACTIVATE_PATIENT", "false").lower() == "true"
2 changes: 2 additions & 0 deletions patientsearch/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
internal_patient_search,
new_resource_hook,
sync_bundle,
restore_patient,
)

__all__ = [
Expand All @@ -16,4 +17,5 @@
"internal_patient_search",
"new_resource_hook",
"sync_bundle",
"restore_patient",
]
66 changes: 56 additions & 10 deletions patientsearch/models/sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ def external_request(token, resource_type, params):
return resp.json()


def sync_bundle(token, bundle):
def sync_bundle(token, bundle, consider_active=False):
"""Given FHIR bundle, insert or update all contained resources
:param token: valid JWT token for use in auth calls
Expand All @@ -167,13 +167,13 @@ def sync_bundle(token, bundle):
if entry["resourceType"] != "Patient":
raise ValueError(f"Can't sync resourceType {entry['resourceType']}")

patient = sync_patient(token, entry)
patient = sync_patient(token, entry, consider_active)
# TODO handle multiple external matches (if it ever happens!)
# currently returning first
return patient


def _merge_patient(src_patient, internal_patient, token):
def _merge_patient(src_patient, internal_patient, token, consider_active=False):
"""Helper used to push details from src into internal patient"""
# TODO consider additional patient attributes beyond identifiers

Expand All @@ -194,10 +194,27 @@ def different(src, dest):
return True

if not different(src_patient, internal_patient):
return internal_patient
# If patient is active, proceed. If not, re-activate
if not consider_active or internal_patient.get("active", False):
return internal_patient

params = patient_as_search_params(internal_patient)
# Ensure it is active
internal_patient["active"] = True
return HAPI_request(
token=token,
method="PUT",
params=params,
resource_type="Patient",
resource=internal_patient,
resource_id=internal_patient["id"],
)
else:
internal_patient["identifier"] = src_patient["identifier"]
params = patient_as_search_params(internal_patient)
# Ensure it is active, skip if active parameter is not considered
if consider_active:
internal_patient["active"] = True
return HAPI_request(
token=token,
method="PUT",
Expand All @@ -208,7 +225,7 @@ def different(src, dest):
)


def patient_as_search_params(patient):
def patient_as_search_params(patient, active_only=False):
"""Generate HAPI search params from patient resource"""

# Use same parameters sent to external src looking for existing Patient
Expand All @@ -221,6 +238,10 @@ def patient_as_search_params(patient):
("name[0].given[0]", "given", ""),
("birthDate", "birthdate", "eq"),
)
if active_only:
# Change the search params if we are considering only active patients in search
search_map = search_map + (("active", True, ""),)

search_params = {}

for path, queryterm, compstr in search_map:
Expand All @@ -230,9 +251,10 @@ def patient_as_search_params(patient):
return search_params


def internal_patient_search(token, patient):
def internal_patient_search(token, patient, active_only=False):
"""Look up given patient from "internal" HAPI store, returns bundle"""
params = patient_as_search_params(patient)
params = patient_as_search_params(patient, active_only)

return HAPI_request(
token=token, method="GET", resource_type="Patient", params=params
)
Expand All @@ -259,7 +281,7 @@ def new_resource_hook(resource):
return resource


def sync_patient(token, patient):
def sync_patient(token, patient, consider_active=False):
"""Sync single patient resource - insert or update as needed"""

internal_search = internal_patient_search(token, patient)
Expand All @@ -274,12 +296,36 @@ def sync_patient(token, patient):

internal_patient = internal_search["entry"][0]["resource"]
merged_patient = _merge_patient(
src_patient=patient, internal_patient=internal_patient, token=token
src_patient=patient,
internal_patient=internal_patient,
token=token,
consider_active=consider_active,
)
return merged_patient

# No match, insert and return
patient = new_resource_hook(resource=patient)
if consider_active:
patient["active"] = True
return HAPI_request(
token=token,
method="POST",
resource_type="Patient",
resource=patient,
)


def restore_patient(token, patient):
"""Restore single internal patient resource"""
# If the patient is already active, bail
if patient.get("active", False):
return patient
patient["active"] = True

return HAPI_request(
token=token, method="POST", resource_type="Patient", resource=patient
token=token,
method="PUT",
resource_type="Patient",
resource=patient,
resource_id=patient["id"],
)
Loading

0 comments on commit 4fc6feb

Please sign in to comment.