Coverage for crateweb/consent/lookup_crs.py: 8%
86 statements
« prev ^ index » next coverage.py v7.8.0, created at 2025-08-27 10:34 -0500
« prev ^ index » next coverage.py v7.8.0, created at 2025-08-27 10:34 -0500
1"""
2crate_anon/crateweb/consent/lookup_crs.py
4===============================================================================
6 Copyright (C) 2015, University of Cambridge, Department of Psychiatry.
7 Created by Rudolf Cardinal (rnc1001@cam.ac.uk).
9 This file is part of CRATE.
11 CRATE is free software: you can redistribute it and/or modify
12 it under the terms of the GNU General Public License as published by
13 the Free Software Foundation, either version 3 of the License, or
14 (at your option) any later version.
16 CRATE is distributed in the hope that it will be useful,
17 but WITHOUT ANY WARRANTY; without even the implied warranty of
18 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 GNU General Public License for more details.
21 You should have received a copy of the GNU General Public License
22 along with CRATE. If not, see <https://www.gnu.org/licenses/>.
24===============================================================================
26**Function to look up patient details from CPFT's now-defunct
27CRS (Care Records System) database.**
29"""
31from typing import List
33from cardinal_pythonlib.dbfunc import dictfetchall, dictfetchone
34from django.db import connections
36from crate_anon.crateweb.consent.lookup_common import SignatoryTitles
37from crate_anon.crateweb.consent.models import PatientLookup
38from crate_anon.crateweb.consent.utils import make_cpft_email_address
41# =============================================================================
42# CPFT Care Records System (CRS)
43# =============================================================================
46def lookup_cpft_crs(
47 lookup: PatientLookup, decisions: List[str], secret_decisions: List[str]
48) -> None:
49 """
50 Looks up patient details from the (defunct) CPFT CRS database.
52 Args:
53 lookup: a :class:`crate_anon.crateweb.consent.models.PatientLookup`
54 decisions: list of human-readable decisions; will be modified
55 secret_decisions: list of human-readable decisions containing secret
56 (identifiable) information; will be modified
57 """
58 cursor = connections[lookup.source_db].cursor()
59 # -------------------------------------------------------------------------
60 # CRS 1. Fetch basic details
61 # -------------------------------------------------------------------------
62 # Incoming nhs_number will be a number. However, the database has a VARCHAR
63 # field (nhs_identifier) that may include spaces. So we compare a
64 # whitespace-stripped field to our value converted to a VARCHAR:
65 # WHERE REPLACE(nhs_identifier, ' ', '') = CAST(%s AS VARCHAR)
66 # ... or the other way round:
67 # WHERE CAST(nhs_identifier AS BIGINT) = %s
68 cursor.execute(
69 """
70 SELECT
71 patient_id, -- M number (PK)
72 -- nhs_identifier,
73 title,
74 forename,
75 surname,
76 gender,
77 -- ethnicity,
78 -- marital_status,
79 -- religion,
80 dttm_of_birth,
81 dttm_of_death
82 FROM mpi
83 WHERE CAST(nhs_identifier AS BIGINT) = %s
84 """,
85 [lookup.nhs_number],
86 )
87 rows = dictfetchall(cursor)
88 if not rows:
89 decisions.append("NHS number not found in mpi table.")
90 return
91 if len(rows) > 1:
92 decisions.append("Two patients found with that NHS number; aborting.")
93 return
94 row = rows[0]
95 crs_patient_id = row["patient_id"]
96 lookup.pt_local_id_description = "CPFT M number"
97 lookup.pt_local_id_number = crs_patient_id
98 secret_decisions.append(f"CPFT M number: {crs_patient_id}.")
99 lookup.pt_found = True
100 lookup.pt_title = row["title"] or ""
101 lookup.pt_first_name = row["forename"] or ""
102 lookup.pt_last_name = row["surname"] or ""
103 lookup.pt_sex = row["gender"] or ""
104 lookup.pt_dob = row["dttm_of_birth"]
105 lookup.pt_dod = row["dttm_of_death"]
106 lookup.pt_dead = bool(lookup.pt_dod)
107 # Deal with dodgy case
108 lookup.pt_title = lookup.pt_title.title()
109 lookup.pt_first_name = lookup.pt_first_name.title()
110 lookup.pt_last_name = lookup.pt_last_name.title()
111 # -------------------------------------------------------------------------
112 # CRS 2. Address
113 # -------------------------------------------------------------------------
114 cursor.execute(
115 """
116 SELECT
117 -- document_id, -- PK
118 address1,
119 address2,
120 address3,
121 address4,
122 postcode,
123 email
124 -- startdate
125 -- enddate
126 -- patient_id
128 FROM Address
129 WHERE
130 patient_id = %s
131 AND enddate IS NULL
132 """,
133 [crs_patient_id],
134 )
135 row = dictfetchone(cursor)
136 if not row:
137 decisions.append("No address found in Address table.")
138 else:
139 lookup.pt_address_1 = row["address1"] or ""
140 lookup.pt_address_2 = row["address2"] or ""
141 lookup.pt_address_3 = row["address3"] or ""
142 lookup.pt_address_4 = row["address4"] or ""
143 lookup.pt_address_6 = row["postcode"] or ""
144 lookup.pt_email = row["email"] or ""
145 # -------------------------------------------------------------------------
146 # CRS 3. GP
147 # -------------------------------------------------------------------------
148 cursor.execute(
149 """
150 SELECT
151 -- sourcesystempk, # PK
152 -- patient_id, # FK
153 -- national_gp_id,
154 gpname,
155 -- national_practice_id,
156 practicename,
157 address1,
158 address2,
159 address3,
160 address4,
161 address5,
162 postcode,
163 telno
164 -- startdate,
165 -- enddate,
166 FROM PracticeGP
167 WHERE
168 patient_id = %s
169 AND enddate IS NULL
170 """,
171 [crs_patient_id],
172 )
173 row = dictfetchone(cursor)
174 if not row:
175 decisions.append("No GP found in PracticeGP table.")
176 else:
177 lookup.gp_found = True
178 lookup.set_gp_name_components(
179 row["gpname"] or "", decisions, secret_decisions
180 )
181 lookup.gp_address_1 = row["practicename"] or ""
182 lookup.gp_address_2 = row["address1"] or ""
183 lookup.gp_address_3 = row["address2"] or ""
184 lookup.gp_address_4 = row["address3"] or ""
185 lookup.gp_address_5 = ", ".join(
186 [row["address4"] or "", row["address5"] or ""]
187 )
188 lookup.gp_address_6 = row["postcode"]
189 lookup.gp_telephone = row["telno"]
190 # -------------------------------------------------------------------------
191 # CRS 4. Clinician
192 # -------------------------------------------------------------------------
193 cursor.execute(
194 """
195 SELECT
196 -- patient_id, # PK
197 -- trustarea,
198 consultanttitle,
199 consultantfirstname,
200 consultantlastname,
201 carecoordinatortitle,
202 carecoordinatorfirstname,
203 carecoordinatorlastname,
204 carecoordinatoraddress1,
205 carecoordinatoraddress2,
206 carecoordinatoraddress3,
207 carecoordinatortown,
208 carecoordinatorcounty,
209 carecoordinatorpostcode,
210 carecoordinatoremailaddress,
211 carecoordinatormobilenumber,
212 carecoordinatorlandlinenumber
213 FROM CDLPatient
214 WHERE
215 patient_id = %s
216 """,
217 [crs_patient_id],
218 )
219 row = dictfetchone(cursor)
220 if not row:
221 decisions.append("No clinician info found in CDLPatient table.")
222 else:
223 lookup.clinician_address_1 = row["carecoordinatoraddress1"] or ""
224 lookup.clinician_address_2 = row["carecoordinatoraddress2"] or ""
225 lookup.clinician_address_3 = row["carecoordinatoraddress3"] or ""
226 lookup.clinician_address_4 = row["carecoordinatortown"] or ""
227 lookup.clinician_address_5 = row["carecoordinatorcounty"] or ""
228 lookup.clinician_address_6 = row["carecoordinatorpostcode"] or ""
229 lookup.clinician_telephone = " / ".join(
230 [
231 row["carecoordinatorlandlinenumber"] or "",
232 row["carecoordinatormobilenumber"] or "",
233 ]
234 )
235 careco_email = row[
236 "carecoordinatoremailaddress"
237 ] or make_cpft_email_address(
238 row["carecoordinatorfirstname"], row["carecoordinatorlastname"]
239 )
240 cons_email = make_cpft_email_address(
241 row["consultantfirstname"], row["consultantlastname"]
242 )
243 if careco_email:
244 # Use care coordinator information
245 lookup.clinician_found = True
246 lookup.clinician_title = row["carecoordinatortitle"] or ""
247 lookup.clinician_first_name = row["carecoordinatorfirstname"] or ""
248 lookup.clinician_last_name = row["carecoordinatorlastname"] or ""
249 lookup.clinician_email = careco_email
250 lookup.clinician_signatory_title = SignatoryTitles.CARE_COORDINATOR
251 decisions.append("Clinician found: care coordinator (CDL).")
252 elif cons_email:
253 # Use consultant information
254 lookup.clinician_found = True
255 lookup.clinician_title = row["consultanttitle"] or ""
256 lookup.clinician_first_name = row["consultantfirstname"] or ""
257 lookup.clinician_last_name = row["consultantlastname"] or ""
258 lookup.clinician_email = cons_email
259 lookup.clinician_signatory_title = (
260 SignatoryTitles.CONS_PSYCHIATRIST
261 )
262 lookup.clinician_is_consultant = True
263 decisions.append("Clinician found: consultant psychiatrist (CDL).")
264 else:
265 # Don't know
266 decisions.append(
267 "No/insufficient clinician information found (CDL)."
268 )