Coverage for crateweb/consent/lookup_rio.py: 12%

210 statements  

« prev     ^ index     » next       coverage.py v7.8.0, created at 2025-08-27 10:34 -0500

1""" 

2crate_anon/crateweb/consent/lookup_rio.py 

3 

4=============================================================================== 

5 

6 Copyright (C) 2015, University of Cambridge, Department of Psychiatry. 

7 Created by Rudolf Cardinal (rnc1001@cam.ac.uk). 

8 

9 This file is part of CRATE. 

10 

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. 

15 

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. 

20 

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/>. 

23 

24=============================================================================== 

25 

26**Functions to look up patient details from various versions of a Servelec 

27RiO clinical database.** 

28 

29""" 

30 

31from typing import Generator, List, Optional, Tuple 

32 

33from cardinal_pythonlib.dbfunc import dictfetchall, dictfetchone, genrows 

34from django.db import connections 

35 

36from crate_anon.crateweb.consent.lookup_common import ( 

37 get_team_details, 

38 pick_best_clinician, 

39 SignatoryTitles, 

40) 

41from crate_anon.crateweb.consent.models import ( 

42 ClinicianInfoHolder, 

43 ConsentMode, 

44 PatientLookup, 

45) 

46from crate_anon.crateweb.consent.utils import to_date 

47from crate_anon.preprocess.rio_constants import ( 

48 CRATE_COL_RIO_NUMBER, 

49 RCEP_COL_PATIENT_ID, 

50) 

51 

52 

53# ============================================================================= 

54# Look up patient IDs 

55# ============================================================================= 

56 

57# ----------------------------------------------------------------------------- 

58# CPFT RiO (raw -> preprocessed by CRATE) 

59# ----------------------------------------------------------------------------- 

60 

61 

62def lookup_cpft_rio_crate_preprocessed( 

63 lookup: PatientLookup, decisions: List[str], secret_decisions: List[str] 

64) -> None: 

65 """ 

66 Look up patient details from a CRATE-preprocessed RiO database. 

67 

68 Args: 

69 lookup: a :class:`crate_anon.crateweb.consent.models.PatientLookup` 

70 decisions: list of human-readable decisions; will be modified 

71 secret_decisions: list of human-readable decisions containing secret 

72 (identifiable) information; will be modified 

73 

74 Here, we use the version of RiO preprocessed by the CRATE preprocessor. 

75 This is almost identical to the RCEP version, saving us some thought and 

76 lots of repetition of complex JOIN code to deal with the raw RiO database. 

77 

78 However, the CRATE preprocessor does this with views. We would need to 

79 index the underlying tables; however, the CRATE processor has also done 

80 this for us for the lookup tables, so we don't need so many. 

81 

82 .. code-block:: sql 

83 

84 USE my_database_name; 

85 

86 CREATE INDEX _idx_cdd_nhs ON ClientIndex (NNN); -- already in RiO source 

87 

88 CREATE INDEX _idx_cnh_id ON ClientName (ClientID); -- already in RiO source 

89 CREATE INDEX _idx_cnh_eff ON ClientName (EffectiveDate); -- ignored 

90 CREATE INDEX _idx_cnh_end ON ClientName (EndDate); -- ignored 

91 

92 CREATE INDEX _idx_cah_id ON ClientAddress (ClientID); -- already in RiO source as part of composite index 

93 CREATE INDEX _idx_cah_from ON ClientAddress (FromDate); -- ignored 

94 CREATE INDEX _idx_cah_to ON ClientAddress (ToDate); -- ignored 

95 

96 CREATE INDEX _idx_cch_id ON ClientTelecom (ClientID); -- already in RiO source as part of composite index 

97 

98 CREATE INDEX _idx_cgh_id ON ClientHealthCareProvider (ClientID); -- already in RiO source 

99 CREATE INDEX _idx_cgh_from ON ClientHealthCareProvider (FromDate); -- ignored 

100 CREATE INDEX _idx_cgh_to ON ClientHealthCareProvider (ToDate); -- ignored 

101 

102 CREATE INDEX _idx_cc_id ON CPACareCoordinator (ClientID); -- preprocessor adds this 

103 CREATE INDEX _idx_cc_start ON CPACareCoordinator (StartDate); -- ignored 

104 CREATE INDEX _idx_cc_end ON CPACareCoordinator (EndDate); -- ignored 

105 

106 CREATE INDEX _idx_ref_id ON AmsReferral (ClientID); -- already in RiO source as part of composite index 

107 CREATE INDEX _idx_ref_recv ON AmsReferral (ReferralReceivedDate); -- ignored 

108 CREATE INDEX _idx_ref_removal ON AmsReferral (RemovalDateTime); -- ignored 

109 

110 CREATE INDEX _idx_rsh_id ON AmsReferralAllocation (ClientID); -- already in RiO source as part of composite index 

111 CREATE INDEX _idx_rsh_start ON AmsReferralAllocation (StartDate); -- ignored 

112 CREATE INDEX _idx_rsh_end ON AmsReferralAllocation (EndDate); -- ignored 

113 

114 CREATE INDEX _idx_rth_id ON AmsReferralTeam (ClientID); -- already in RiO source as part of composite index 

115 CREATE INDEX _idx_rth_start ON AmsReferralTeam (StartDate); -- ignored 

116 CREATE INDEX _idx_rth_end ON AmsReferralTeam (EndDate); -- ignored 

117 

118 ... or alternative RiO number indexes on CRATE_COL_RIO_NUMBER field. 

119 

120 Then, the only field name differences from RCEP are: 

121 

122 .. code-block:: none 

123 

124 Client_Name_History.End_Date -- not End_Date_ 

125 """ # noqa: E501 

126 lookup_cpft_rio_generic( 

127 lookup, decisions, secret_decisions, as_crate_not_rcep=True 

128 ) 

129 

130 

131# ----------------------------------------------------------------------------- 

132# CPFT RiO as preprocessed by Servelec RCEP tool 

133# ----------------------------------------------------------------------------- 

134 

135 

136def lookup_cpft_rio_rcep( 

137 lookup: PatientLookup, decisions: List[str], secret_decisions: List[str] 

138) -> None: 

139 """ 

140 Look up patient details from a RiO database that's been preprocessed 

141 through Servelec's RCEP (RiO CRIS Extraction Program) tool. 

142 

143 Args: 

144 lookup: a :class:`crate_anon.crateweb.consent.models.PatientLookup` 

145 decisions: list of human-readable decisions; will be modified 

146 secret_decisions: list of human-readable decisions containing secret 

147 (identifiable) information; will be modified 

148 

149 **RiO notes, 2015-05-19** 

150 

151 ... ADDENDUM 2017-02-27: this is the RiO database as modified by Servelec's 

152 RiO CRIS Extraction Program (RCEP). See also lookup_cpft_rio_raw(). 

153 

154 For speed, RiO-RCEP needs these indexes: 

155 

156 .. code-block:: sql 

157 

158 USE my_database_name; 

159 

160 CREATE INDEX _idx_cdd_nhs ON Client_Demographic_Details (NHS_Number); 

161 

162 CREATE INDEX _idx_cnh_id ON Client_Name_History (Client_ID); 

163 CREATE INDEX _idx_cnh_eff ON Client_Name_History (Effective_Date); 

164 CREATE INDEX _idx_cnh_end ON Client_Name_History (End_Date_); 

165 

166 CREATE INDEX _idx_cah_id ON Client_Address_History (Client_ID); 

167 CREATE INDEX _idx_cah_from ON Client_Address_History (Address_From_Date); 

168 CREATE INDEX _idx_cah_to ON Client_Address_History (Address_To_Date); 

169 

170 CREATE INDEX _idx_cch_id ON Client_Communications_History (Client_ID); 

171 

172 CREATE INDEX _idx_cgh_id ON Client_GP_History (Client_ID); 

173 CREATE INDEX _idx_cgh_from ON Client_GP_History (GP_From_Date); 

174 CREATE INDEX _idx_cgh_to ON Client_GP_History (GP_To_Date); 

175 

176 CREATE INDEX _idx_cc_id ON CPA_CareCoordinator (Client_ID); 

177 CREATE INDEX _idx_cc_start ON CPA_CareCoordinator (Start_Date); 

178 CREATE INDEX _idx_cc_end ON CPA_CareCoordinator (End_Date); 

179 

180 CREATE INDEX _idx_ref_id ON Main_Referral_Data (Client_ID); 

181 CREATE INDEX _idx_ref_recv ON Main_Referral_Data (Referral_Received_Date); 

182 CREATE INDEX _idx_ref_removal ON Main_Referral_Data (Removal_DateTime); 

183 

184 CREATE INDEX _idx_rsh_id ON Referral_Staff_History (Client_ID); 

185 CREATE INDEX _idx_rsh_start ON Referral_Staff_History (Start_Date); 

186 CREATE INDEX _idx_rsh_end ON Referral_Staff_History (End_Date); 

187 

188 CREATE INDEX _idx_rth_id ON Referral_Team_History (Client_ID); 

189 CREATE INDEX _idx_rth_start ON Referral_Team_History (Start_Date); 

190 CREATE INDEX _idx_rth_end ON Referral_Team_History (End_Date); 

191 

192 -- CREATE INDEX _idx_rth_teamdesc ON Referral_Team_History (Team_Description); 

193 """ # noqa: E501 

194 lookup_cpft_rio_generic( 

195 lookup, decisions, secret_decisions, as_crate_not_rcep=False 

196 ) 

197 

198 

199# ----------------------------------------------------------------------------- 

200# CPFT RiO: function that copes with either the RCEP or the CRATE version, 

201# which are extremely similar. 

202# ----------------------------------------------------------------------------- 

203 

204 

205def lookup_cpft_rio_generic( 

206 lookup: PatientLookup, 

207 decisions: List[str], 

208 secret_decisions: List[str], 

209 as_crate_not_rcep: bool, 

210) -> None: 

211 """ 

212 Look up patient details from a RiO database, either as a CRATE-processed 

213 or an RCEP-processed version. (They are very similar.) 

214 

215 Args: 

216 lookup: a :class:`crate_anon.crateweb.consent.models.PatientLookup` 

217 decisions: list of human-readable decisions; will be modified 

218 secret_decisions: list of human-readable decisions containing secret 

219 (identifiable) information; will be modified 

220 as_crate_not_rcep: is it a CRATE-preprocessed, rather than an 

221 RCEP-preprocessed, database? 

222 

223 Main: 

224 

225 .. code-block:: none 

226 

227 Client_Demographic_Details 

228 Client_ID -- PK; RiO number; integer in VARCHAR(15) field 

229 Date_of_Birth -- DATETIME 

230 Date_of_Death -- DATETIME; NULL if not dead 

231 Death_Flag -- INT; 0 for alive, 1 for dead 

232 Deleted_Flag -- INT; 0 normally; 1 for deleted 

233 NHS_Number -- CHAR(10) 

234 Gender_Code -- 'F', 'M', 'U', 'X' 

235 Gender_Description -- 'Male', 'Female', ... 

236 

237 Then, linked to it: 

238 

239 .. code-block:: none 

240 

241 Client_Name_History 

242 Client_ID -- integer in VARCHAR(15) 

243 Effective_Date -- DATETIME 

244 End_Date_ -- DATETIME, typically NULL 

245 -- in the CRATE version, this is End_Date instead 

246 Name_Type_Code -- '1' for 'usual name', '2' for 'Alias', '3' 

247 for 'Preferred name', '4' for 'Birth name', '5' for 

248 'Maiden name', '7' for 'Other', 'CM' for 'Client Merge'; 

249 NVARCHAR(10) 

250 Name_Type_Description -- e.g. 'Usual name', 'Alias' 

251 Deleted_Flag -- INT 

252 

253 title 

254 Given_Name_1 -- through to Given_Name_5 

255 Family_Name 

256 suffix 

257 ... 

258 

259 Client_Address_History 

260 Client_ID -- integer in VARCHAR(15) 

261 Address_Type_Code -- e.g. 'PRIMARY' but also 'CA', 'FCH'... 

262 Address_Type_Description 

263 Address_From_Date -- DATETIME 

264 Address_To_Date -- DATETIME; NULL for active ones 

265 

266 Address_Line_1 

267 Address_Line_2 

268 Address_Line_3 

269 Address_Line_4 

270 Address_Line_5 

271 Post_Code 

272 ... -- no e-mail address field 

273 

274 Client_GP_History 

275 Client_ID -- integer in VARCHAR(15) 

276 GP_From_Date -- DATETIME 

277 GP_To_Date -- DATETIME; NULL for active ones 

278 

279 GP_Name -- e.g. 'Smith JT' 

280 GP_Practice_Address_Line1 

281 GP_Practice_Address_Line2 

282 GP_Practice_Address_Line3 

283 GP_Practice_Address_Line4 

284 GP_Practice_Address_Line5 

285 GP_Practice_Post_code 

286 ... 

287 

288 CPFT clinician details/?discharged info appear to be here: 

289 

290 .. code-block:: none 

291 

292 CPA_CareCoordinator 

293 Client_ID -- integer in VARCHAR(15) 

294 Start_Date -- DATETIME 

295 End_Date -- DATETIME 

296 End_Reason_Code 

297 End_Reason_Description 

298 End_Reason_National_Code 

299 

300 Care_Coordinator_User_title 

301 Care_Coordinator_User_first_name 

302 Care_Coordinator_User_surname 

303 Care_Coordinator_User_email 

304 Care_Coordinator_User_Consultant_Flag -- INT; 0 or 1 (or NULL?) 

305 

306 Main_Referral_Data 

307 Client_ID -- integer in VARCHAR(15) 

308 Referral_Received_Date -- DATETIME 

309 Removal_DateTime -- DATETIME 

310 # Care_Spell_Start_Date 

311 # Care_Spell_End_Date -- never non-NULL in our data set 

312 # Discharge_HCP -- ??user closing the referral 

313 

314 Referred_Consultant_User_title 

315 Referred_Consultant_User_first_name 

316 Referred_Consultant_User_surname 

317 Referred_Consultant_User_email 

318 Referred_Consultant_User_Consultant_Flag -- 0, 1, NULL 

319 

320 Referral_Staff_History 

321 Client_ID -- integer in VARCHAR(15) 

322 Start_Date -- DATETIME 

323 End_Date -- DATETIME 

324 Current_At_Discharge -- INT -- ? -- 1 or NULL 

325 

326 HCP_User_title 

327 HCP_User_first_name 

328 HCP_User_surname 

329 HCP_User_email 

330 HCP_User_Consultant_Flag -- 0, 1, NULL 

331 

332 Referral_Team_History 

333 -- similar, but for teams; no individual info 

334 Client_ID -- integer in VARCHAR(15) 

335 Start_Date -- DATETIME 

336 End_Date -- DATETIME 

337 Current_At_Discharge -- INT -- ? -- 1 or NULL 

338 

339 Team_Code -- NVARCHAR -- e.g. 'TCGMH712' 

340 Team_Description -- NVARCHAR -- e.g. 'George Mackenzie' 

341 Team_Classification_Group_Code -- NVARCHAR -- e.g. 'FS' 

342 Team_Classification_Group_Description -- NVARCHAR -- e.g. 

343 'Forensic Service' 

344 

345 Not obviously relevant: 

346 

347 .. code-block:: none 

348 

349 Client_CPA -- records CPA start/end, etc. 

350 Client_Professional_Contacts -- empty table! 

351 

352 Added 2017-02-27: 

353 

354 .. code-block:: none 

355 

356 Client_Communications_History -- email/phone 

357 Client_ID -- integer in VARCHAR(15) 

358 Method_Code -- NVARCHAR(10); '1' for 'Telephone number', '3' 

359 for 'Email address', '4' for 'Minicom/textphone number' 

360 Method_Description 

361 Context_Code -- e.g. '1' for 'Communication address at home', 

362 other codes for 'Vacation home...', etc. 

363 Context_Description 

364 Contact_Details -- NVARCHAR(80) 

365 

366 """ 

367 cursor = connections[lookup.source_db].cursor() 

368 rio_number_field = ( 

369 CRATE_COL_RIO_NUMBER if as_crate_not_rcep else RCEP_COL_PATIENT_ID 

370 ) 

371 

372 # ------------------------------------------------------------------------- 

373 # RiO/RCEP: 1. Get RiO PK 

374 # ------------------------------------------------------------------------- 

375 cursor.execute( 

376 f""" 

377 SELECT 

378 {rio_number_field}, -- RiO number (PK) 

379 -- NHS_Number, 

380 Date_of_Birth, 

381 Date_of_Death, 

382 Death_Flag, 

383 -- Deleted_Flag, 

384 Gender_Code 

385 -- Gender_Description, 

386 FROM Client_Demographic_Details 

387 WHERE 

388 NHS_Number = %s -- CHAR comparison 

389 AND (Deleted_Flag IS NULL OR Deleted_Flag = 0) 

390 """, 

391 [str(lookup.nhs_number)], 

392 ) 

393 # Can't use "NOT Deleted_Flag" with SQL Server; you get 

394 # "An expression of non-boolean type specified in a context where a 

395 # condition is expected, near 'Deleted_Flag'." 

396 # The field is of type INTEGER NULL, but SQL Server won't auto-cast it 

397 # to something boolean. 

398 rows = dictfetchall(cursor) 

399 if not rows: 

400 decisions.append( 

401 "NHS number not found in Client_Demographic_Details table." 

402 ) 

403 return 

404 if len(rows) > 1: 

405 decisions.append("Two patients found with that NHS number; aborting.") 

406 return 

407 row = rows[0] 

408 rio_client_id = row[rio_number_field] 

409 lookup.pt_local_id_description = "CPFT RiO number" 

410 lookup.pt_local_id_number = rio_client_id 

411 secret_decisions.append(f"RiO number: {rio_client_id}.") 

412 lookup.pt_dob = to_date(row["Date_of_Birth"]) 

413 lookup.pt_dod = to_date(row["Date_of_Death"]) 

414 lookup.pt_dead = bool(lookup.pt_dod or row["Death_Flag"]) 

415 lookup.pt_sex = "?" if row["Gender_Code"] == "U" else row["Gender_Code"] 

416 

417 # ------------------------------------------------------------------------- 

418 # RiO/RCEP: 2. Name 

419 # ------------------------------------------------------------------------- 

420 cursor.execute( 

421 f""" 

422 SELECT 

423 title, 

424 Given_Name_1, 

425 Family_Name 

426 FROM Client_Name_History 

427 WHERE 

428 {rio_number_field} = %s 

429 AND Effective_Date <= GETDATE() 

430 AND ({'End_Date' if as_crate_not_rcep else 'End_Date_'} IS NULL 

431 OR {'End_Date' if as_crate_not_rcep else 'End_Date_'} > GETDATE()) 

432 AND (Deleted_Flag IS NULL OR Deleted_Flag = 0) 

433 ORDER BY Name_Type_Code 

434 """, # noqa: E501 

435 [rio_client_id], 

436 ) 

437 row = dictfetchone(cursor) 

438 if not row: 

439 decisions.append( 

440 "No name/address information found in Client_Name_History." 

441 ) 

442 return 

443 lookup.pt_found = True 

444 lookup.pt_title = row["title"] or "" 

445 lookup.pt_first_name = row["Given_Name_1"] or "" 

446 lookup.pt_last_name = row["Family_Name"] or "" 

447 # Deal with dodgy case 

448 lookup.pt_title = lookup.pt_title.title() 

449 lookup.pt_first_name = lookup.pt_first_name.title() 

450 lookup.pt_last_name = lookup.pt_last_name.title() 

451 

452 # ------------------------------------------------------------------------- 

453 # RiO/RCEP: 3. Address 

454 # ------------------------------------------------------------------------- 

455 cursor.execute( 

456 f""" 

457 SELECT 

458 Address_Line_1, 

459 Address_Line_2, 

460 Address_Line_3, 

461 Address_Line_4, 

462 Address_Line_5, 

463 Post_Code 

464 FROM Client_Address_History 

465 WHERE 

466 {rio_number_field} = %s 

467 AND Address_From_Date <= GETDATE() 

468 AND (Address_To_Date IS NULL 

469 OR Address_To_Date > GETDATE()) 

470 ORDER BY CASE WHEN Address_Type_Code = 'PRIMARY' THEN '1' 

471 ELSE Address_Type_Code END 

472 """, 

473 [rio_client_id], 

474 ) 

475 row = dictfetchone(cursor) 

476 if not row: 

477 decisions.append("No address found in Client_Address_History table.") 

478 else: 

479 lookup.pt_address_1 = row["Address_Line_1"] or "" 

480 lookup.pt_address_2 = row["Address_Line_2"] or "" 

481 lookup.pt_address_3 = row["Address_Line_3"] or "" 

482 lookup.pt_address_4 = row["Address_Line_4"] or "" 

483 lookup.pt_address_5 = row["Address_Line_5"] or "" 

484 lookup.pt_address_6 = row["Post_Code"] or "" 

485 

486 # ------------------------------------------------------------------------- 

487 # RiO/RCEP: 3b. Patient's e-mail address 

488 # ------------------------------------------------------------------------- 

489 cursor.execute( 

490 f""" 

491 SELECT 

492 Contact_Details -- an e-mail address if Method_Code = 3 

493 FROM Client_Communications_History 

494 WHERE 

495 {rio_number_field} = %s 

496 AND Method_Code = 3 -- e-mail address 

497 AND Valid_From <= GETDATE() 

498 AND (Valid_To IS NULL 

499 OR Valid_To > GETDATE()) 

500 ORDER BY Context_Code 

501 -- 1 = Communication address at home 

502 -- 2 = Primary home (after business hours) 

503 -- 3 = Vacation home (when person on holiday) 

504 -- 4 = Office address 

505 -- 6 = Emergency contact 

506 -- 8 = Mobile device 

507 """, 

508 [rio_client_id], 

509 ) 

510 rows = dictfetchall(cursor) 

511 if rows: 

512 row = rows[0] 

513 lookup.pt_email = row["Contact_Details"] 

514 

515 # ------------------------------------------------------------------------- 

516 # RiO/RCEP: 4. GP 

517 # ------------------------------------------------------------------------- 

518 if as_crate_not_rcep: 

519 cursor.execute( 

520 f""" 

521 SELECT 

522 GP_Title, 

523 GP_Forename, 

524 GP_Surname, 

525 GP_Practice_Address_Line_1, 

526 GP_Practice_Address_Line_2, 

527 GP_Practice_Address_Line_3, 

528 GP_Practice_Address_Line_4, 

529 GP_Practice_Address_Line_5, 

530 GP_Practice_Post_Code 

531 FROM Client_GP_History 

532 WHERE 

533 {rio_number_field} = %s 

534 AND GP_From_Date <= GETDATE() 

535 AND (GP_To_Date IS NULL OR GP_To_Date > GETDATE()) 

536 """, 

537 [rio_client_id], 

538 ) 

539 row = dictfetchone(cursor) 

540 if not row: 

541 decisions.append("No GP found in Client_GP_History table.") 

542 else: 

543 lookup.gp_found = True 

544 lookup.gp_title = row["GP_Title"] or "Dr" 

545 lookup.gp_first_name = row["GP_Forename"] or "" 

546 lookup.gp_last_name = row["GP_Surname"] or "" 

547 lookup.gp_address_1 = row["GP_Practice_Address_Line_1"] or "" 

548 lookup.gp_address_2 = row["GP_Practice_Address_Line_2"] or "" 

549 lookup.gp_address_3 = row["GP_Practice_Address_Line_3"] or "" 

550 lookup.gp_address_4 = row["GP_Practice_Address_Line_4"] or "" 

551 lookup.gp_address_5 = row["GP_Practice_Address_Line_5"] or "" 

552 lookup.gp_address_6 = row["GP_Practice_Post_Code"] 

553 else: 

554 cursor.execute( 

555 f""" 

556 SELECT 

557 GP_Name, 

558 GP_Practice_Address_Line1, 

559 GP_Practice_Address_Line2, 

560 GP_Practice_Address_Line3, 

561 GP_Practice_Address_Line4, 

562 GP_Practice_Address_Line5, 

563 GP_Practice_Post_code 

564 FROM Client_GP_History 

565 WHERE 

566 {rio_number_field} = %s 

567 AND GP_From_Date <= GETDATE() 

568 AND (GP_To_Date IS NULL OR GP_To_Date > GETDATE()) 

569 """, 

570 [rio_client_id], 

571 ) 

572 row = dictfetchone(cursor) 

573 if not row: 

574 decisions.append("No GP found in Client_GP_History table.") 

575 else: 

576 lookup.gp_found = True 

577 lookup.set_gp_name_components( 

578 row["GP_Name"] or "", decisions, secret_decisions 

579 ) 

580 lookup.gp_address_1 = row["GP_Practice_Address_Line1"] or "" 

581 lookup.gp_address_2 = row["GP_Practice_Address_Line2"] or "" 

582 lookup.gp_address_3 = row["GP_Practice_Address_Line3"] or "" 

583 lookup.gp_address_4 = row["GP_Practice_Address_Line4"] or "" 

584 lookup.gp_address_5 = row["GP_Practice_Address_Line5"] or "" 

585 lookup.gp_address_6 = row["GP_Practice_Post_code"] 

586 

587 # ------------------------------------------------------------------------- 

588 # RiO/RCEP: 5. Clinician, active v. discharged 

589 # ------------------------------------------------------------------------- 

590 clinicians = [] # type: List[ClinicianInfoHolder] 

591 # 

592 # (a) Care coordinator? 

593 # 

594 if as_crate_not_rcep: 

595 care_co_title_field = "Care_Coordinator_Title" 

596 care_co_forename_field = "Care_Coordinator_First_Name" 

597 care_co_surname_field = "Care_Coordinator_Surname" 

598 care_co_email_field = "Care_Coordinator_Email" 

599 care_co_consultant_flag_field = "Care_Coordinator_Consultant_Flag" 

600 care_co_table = "CPA_Care_Coordinator" 

601 else: 

602 care_co_title_field = "Care_Coordinator_User_title" 

603 care_co_forename_field = "Care_Coordinator_User_first_name" 

604 care_co_surname_field = "Care_Coordinator_User_surname" 

605 care_co_email_field = "Care_Coordinator_User_email" 

606 care_co_consultant_flag_field = "Care_Coordinator_User_Consultant_Flag" 

607 care_co_table = "CPA_CareCoordinator" 

608 cursor.execute( 

609 f""" 

610 SELECT 

611 {care_co_title_field}, 

612 {care_co_forename_field}, 

613 {care_co_surname_field}, 

614 {care_co_email_field}, 

615 {care_co_consultant_flag_field}, 

616 Start_Date, 

617 End_Date 

618 FROM {care_co_table} 

619 WHERE 

620 {rio_number_field} = %s 

621 AND Start_Date <= GETDATE() 

622 """, 

623 [rio_client_id], 

624 ) 

625 for row in dictfetchall(cursor): 

626 clinicians.append( 

627 ClinicianInfoHolder( 

628 clinician_type=ClinicianInfoHolder.CARE_COORDINATOR, 

629 title=row[care_co_title_field] or "", 

630 first_name=row[care_co_forename_field] or "", 

631 surname=row[care_co_surname_field] or "", 

632 email=row[care_co_email_field] or "", 

633 signatory_title=SignatoryTitles.CARE_COORDINATOR, 

634 is_consultant=bool(row[care_co_consultant_flag_field]), 

635 start_date=row["Start_Date"], 

636 end_date=row["End_Date"], 

637 ) 

638 ) 

639 # 

640 # (b) Active named consultant referral? 

641 # 

642 if as_crate_not_rcep: 

643 cons_title_field = "Referred_Consultant_Title" 

644 cons_forename_field = "Referred_Consultant_First_Name" 

645 cons_surname_field = "Referred_Consultant_Surname" 

646 cons_email_field = "Referred_Consultant_Email" 

647 cons_consultant_flag_field = "Referred_Consultant_Consultant_Flag" 

648 referral_table = "Referral" 

649 else: 

650 cons_title_field = "Referred_Consultant_User_title" 

651 cons_forename_field = "Referred_Consultant_User_first_name" 

652 cons_surname_field = "Referred_Consultant_User_surname" 

653 cons_email_field = "Referred_Consultant_User_email" 

654 cons_consultant_flag_field = "Referred_Consultant_User_Consultant_Flag" 

655 referral_table = "Main_Referral_Data" 

656 cursor.execute( 

657 f""" 

658 SELECT 

659 {cons_title_field}, 

660 {cons_forename_field}, 

661 {cons_surname_field}, 

662 {cons_email_field}, 

663 {cons_consultant_flag_field}, 

664 Referral_Received_Date, 

665 Removal_DateTime 

666 FROM {referral_table} 

667 WHERE 

668 {rio_number_field} = %s 

669 AND Referral_Received_Date <= GETDATE() 

670 """, 

671 [rio_client_id], 

672 ) 

673 for row in dictfetchall(cursor): 

674 clinicians.append( 

675 ClinicianInfoHolder( 

676 clinician_type=ClinicianInfoHolder.CONSULTANT, 

677 title=row[cons_title_field] or "", 

678 first_name=row[cons_forename_field] or "", 

679 surname=row[cons_surname_field] or "", 

680 email=row[cons_email_field] or "", 

681 signatory_title=SignatoryTitles.CONS_PSYCHIATRIST, 

682 is_consultant=bool(row[cons_consultant_flag_field]), 

683 # ... would be odd if this were not true! 

684 start_date=row["Referral_Received_Date"], 

685 end_date=row["Removal_DateTime"], 

686 ) 

687 ) 

688 # 

689 # (c) Active other named staff referral? 

690 # 

691 if as_crate_not_rcep: 

692 hcp_title_field = "HCP_Title" 

693 hcp_forename_field = "HCP_First_Name" 

694 hcp_surname_field = "HCP_Surname" 

695 hcp_email_field = "HCP_Email" 

696 hcp_consultant_flag_field = "HCP_Consultant_Flag" 

697 else: 

698 hcp_title_field = "HCP_User_title" 

699 hcp_forename_field = "HCP_User_first_name" 

700 hcp_surname_field = "HCP_User_surname" 

701 hcp_email_field = "HCP_User_email" 

702 hcp_consultant_flag_field = "HCP_User_Consultant_Flag" 

703 cursor.execute( 

704 f""" 

705 SELECT 

706 {hcp_title_field}, 

707 {hcp_forename_field}, 

708 {hcp_surname_field}, 

709 {hcp_email_field}, 

710 {hcp_consultant_flag_field}, 

711 Start_Date, 

712 End_Date 

713 FROM Referral_Staff_History 

714 WHERE 

715 {rio_number_field} = %s 

716 AND Start_Date <= GETDATE() 

717 """, 

718 [rio_client_id], 

719 ) 

720 for row in dictfetchall(cursor): 

721 clinicians.append( 

722 ClinicianInfoHolder( 

723 clinician_type=ClinicianInfoHolder.HCP, 

724 title=row[hcp_title_field] or "", 

725 first_name=row[hcp_forename_field] or "", 

726 surname=row[hcp_surname_field] or "", 

727 email=row[hcp_email_field] or "", 

728 signatory_title=SignatoryTitles.CLINICIAN, 

729 is_consultant=bool(row[hcp_consultant_flag_field]), 

730 start_date=row["Start_Date"], 

731 end_date=row["End_Date"], 

732 ) 

733 ) 

734 # 

735 # (d) Active team referral? 

736 # 

737 cursor.execute( 

738 f""" 

739 SELECT 

740 Team_Description, 

741 Start_Date, 

742 End_Date 

743 FROM Referral_Team_History 

744 WHERE 

745 {rio_number_field} = %s 

746 AND Start_Date <= GETDATE() 

747 """, 

748 [rio_client_id], 

749 ) 

750 for row in dictfetchall(cursor): 

751 team_info = get_team_details( 

752 team_name=row["Team_Description"], 

753 start_date=row["Start_Date"], 

754 end_date=row["End_Date"], 

755 decisions=decisions, 

756 ) 

757 clinicians.append(team_info) 

758 # We append it even if we can't find a representative, because it still 

759 # carries information about whether the patient is discharged or not. 

760 

761 # Re CLINICIAN ADDRESSES: 

762 # Candidate tables in RiO: 

763 # - OrgContactAddress +/- OrgContactAddressHistory 

764 # - OrgOrganisation 

765 # - GenPerson <-- THIS. From GenHCP: "This table contains about all HCPs 

766 # registered in RiO. HCP’s personal details (name, address etc.) are 

767 # stored in GenPerson. 

768 # - ??GenLocation; ??GenNHSLocation 

769 # 

770 # So, GenPerson is correct. However, in CPFT, when we 

771 # SELECT * FROM GenPerson WHERE AddressLine2 IS NOT NULL 

772 # we get lots of things saying "Agency Staff", "leaves Trust 17/02/15", 

773 # "changed name from Smith", "Medical student", and so on. 

774 # 

775 # Therefore, our source is simply duff; people are using the fields for 

776 # a different purpose. 

777 # Therefore, the set_from_clinician_info_holder() function will default 

778 # to the RDBM's address. 

779 

780 # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 

781 # Now pick a clinician. 

782 # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 

783 

784 pick_best_clinician(lookup, clinicians, decisions) 

785 

786 

787# ============================================================================= 

788# Look up choices about research consent 

789# ============================================================================= 

790 

791# Constant strings used in database: 

792ADULT_WITH_CAPACITY_TEXT = "16 or over, has capacity" 

793ADULT_WITH_CAPACITY_CODE = "a" 

794ADULT_LACKS_CAPACITY_TEXT = "16 or over, lacks capacity" 

795ADULT_LACKS_CAPACITY_CODE = "b" 

796CHILD_PARENT_TEXT = "Under 16, parent/guardian consent" 

797CHILD_PARENT_CODE = "c" 

798CHILD_GILLICK_TEXT = "Under 16, “Gillick competent”" 

799CHILD_GILLICK_CODE = "d" # by inference; none live yet in raw20171128 DB 

800DECISION_METHOD_CODE_TO_TEXT = { 

801 ADULT_WITH_CAPACITY_CODE: ADULT_WITH_CAPACITY_TEXT, 

802 ADULT_LACKS_CAPACITY_CODE: ADULT_LACKS_CAPACITY_TEXT, 

803 CHILD_PARENT_CODE: CHILD_PARENT_TEXT, 

804 CHILD_GILLICK_CODE: CHILD_GILLICK_TEXT, 

805} 

806NOT_APPLICABLE_UPPER = "N/A" 

807 

808 

809def get_latest_consent_mode_from_rio_generic( 

810 nhs_number: int, 

811 source_db: str, 

812 decisions: List[str], 

813 raw_rio: bool = False, 

814 cpft_datamart: bool = False, 

815) -> Optional[ConsentMode]: 

816 """ 

817 Returns the latest consent mode for a patient, from some style of RiO 

818 database. 

819 

820 Args: 

821 nhs_number: NHS number 

822 source_db: the type of the source database; see 

823 :class:`crate_anon.crateweb.config.constants.ClinicalDatabaseType` 

824 decisions: list of human-readable decisions; will be modified 

825 raw_rio: is the source database a raw copy of RiO? 

826 cpft_datamart: is the source database the version from the CPFT 

827 data warehouse? 

828 

829 Returns: 

830 a :class:`crate_anon.crateweb.consent.models.ConsentMode`, or ``None`` 

831 

832 Shared function as very similar for the various copies of RiO data. 

833 

834 In raw RiO at CPFT, the traffic-light table is UserAssessConsentrd. 

835 This is processed regularly into the CPFT Data Warehouse, so that 

836 contains very fresh data and is a good choice. 

837 

838 For CPFT's custom consent mode, built into RiO v6, the table copy in 

839 the Data Warehouse is: 

840 

841 .. code-block:: sql 

842 

843 SELECT 

844 [ClientID] -- VARCHAR(15) NOT NULL; RiO number as text 

845 ,[AssessmentDate] -- DATETIME 

846 ,[ReferralNumber] -- NVARCHAR(20); a small integer as text 

847 ,[ResearchContact] -- NVARCHAR(20); 'RED', 'YELLOW', 'GREEN' 

848 ,[OptOut] 

849 -- BIT; 1 for opt out; NULL (or potentially 0?) for not 

850 -- opted out 

851 ,[OptOutFromMedicalResearch_AfterDetailsRemoved] 

852 -- VARCHAR(1); 'Y' for opt out; 9 for not opted out 

853 -- identical information to OptOut; OptOut is the simpler 

854 ,[PersonActingonBehalf_of_Patient] 

855 -- NTEXT, e.g. 'Mr Smith' or NULL 

856 ,[PersonActingonBehalf_of_Patient_Relation] 

857 -- NTEXT, e.g. 'Husband' or NULL or 'N/A' or 'n/a' 

858 ,[Personactingonbehalf_address] 

859 -- NTEXT, e.g. an address or NULL or 'N/A' or 'n/a' 

860 ,[WhoMakesDecisionFor_Patient] 

861 -- NVARCHAR(40), e.g. one of: 

862 -- '16 or over, has capacity' 

863 -- '16 or over, lacks capacity' 

864 -- 'Under 16, “Gillick competent”' 

865 -- note left/right double quotes 

866 -- 'Under 16, parent/guardian consent' 

867 FROM [CPFT_DATAMART].[dbo].[ConsentToResearch] 

868 -- NB this is on a different CPFT server; see databases.txt 

869 

870 Note also: the CPFT_DATAMART database does not provide patient- 

871 identifiable information, except PatientOverview_RiO: 

872 

873 .. code-block:: sql 

874 

875 SELECT 

876 [NHSNumber] -- VARCHAR(15); NHS number as text 

877 ,[ClientID] -- VARCHAR(15) NOT NULL; RiO number as text 

878 ,[PatientName] -- VARCHAR(202); e.g. 'Smith, John' 

879 ,[Surname] -- VARCHAR(100); e.g. 'Smith' 

880 ,[DOB] -- DATETIME; time part is zero 

881 ,[TeamStartDate] -- DATETIME 

882 ,[TeamName] -- NVARCHAR(50) 

883 ,[ReferralNumber] -- INT NOT NULL; a small integer 

884 ,[LastAttendedApptDate_WithTeam] -- DATE 

885 ,[FutureAppts_WithTeam] -- DATE; just one despite the name 

886 ,[HCPs] 

887 -- NVARCHAR(MAX); semicolon-delimited list; e.g. 

888 -- '; Alice Smith' 

889 -- '; Alice Smith; Bob Jones' 

890 -- '; Unknown Consultant' 

891 FROM [CPFT_DATAMART].[dbo].[PatientOverviewRiO] 

892 

893 In raw RiO at CPFT, the traffic-light table is UserAssessConsentrd. 

894 

895 .. code-block:: sql 

896 

897 SELECT 

898 [ClientID] -- VARCHAR(15); RiO number as text 

899 ,[AssessmentDate] -- DATETIME 

900 ,[system_ValidationData] -- NTEXT; XML style 

901 ,[ResearchContact] -- NVARCHAR(20); 'RED', 'YELLOW', 'GREEN', NULL 

902 ,[useemail] -- BIT (1, 0, NULL) 

903 ,[optout] -- BIT 

904 ,[capname] -- NTEXT 

905 ,[capaddress] -- NTEXT 

906 ,[caprelation] -- NTEXT 

907 ,[capacity] -- VARCHAR(20); e.g. 'a', 'b' 

908 ,[type12_NoteID] -- INT NOT NULL; small integer 

909 ,[type12_OriginalNoteID] -- INT; usually NULL 

910 ,[type12_DeletedDate] -- DATETIME; NULL if not deleted 

911 ,[type12_UpdatedBy] -- NVARCHAR(20); username 

912 ,[type12_UpdatedDate] -- DATETIME 

913 ,[formref] -- NVARCHAR(20); typically a small integer as text 

914 

915 -- If it's been preprocessed, also these: 

916 ,[crate_pk] -- in CRATE preprocessed version only; BIGINT 

917 ,[crate_rio_number] -- in CRATE preprocessed version only; BIGINT 

918 FROM [dbo].[UserAssessconsentrd] 

919 

920 The NHS number table is: 

921 

922 .. code-block:: sql 

923 

924 SELECT 

925 [ClientID] -- VARCHAR(15); RiO number as text 

926 ,[NNN] -- CHAR(10); NHS number as text 

927 -- and lots more 

928 FROM [dbo].ClientIndex] 

929 

930 """ 

931 assert ( 

932 sum([raw_rio, cpft_datamart]) == 1 

933 ), "Specify exactly one database type to look up from" 

934 if raw_rio: 

935 sql = """ 

936 SELECT TOP 1 -- guaranteed to be running SQL Server 

937 ci.NNN AS nhs_number_text, 

938 ci.ClientID AS rio_number_text, 

939 cr.AssessmentDate AS decision_date, 

940 cr.ResearchContact AS traffic_light, 

941 -- 'RED', 'YELLOW', 'GREEN', NULL 

942 cr.useemail AS use_email, 

943 cr.optout AS opt_out, -- 1, 0 (possibly), NULL 

944 cr.capacity AS decision_method_code, 

945 cr.capname AS representative_name, 

946 cr.caprelation AS representative_relation 

947 FROM 

948 UserAssessconsentrd AS cr 

949 INNER JOIN 

950 ClientIndex AS ci 

951 ON cr.ClientID = ci.ClientID 

952 WHERE 

953 ci.NNN = %s -- string comparison 

954 ORDER BY 

955 cr.AssessmentDate DESC 

956 """ 

957 elif cpft_datamart: 

958 # Old, discarded 2018-06-28: 

959 _ = """ 

960 SELECT TOP 1 -- guaranteed to be running SQL Server 

961 po.NHSNumber AS nhs_number_text, 

962 po.ClientID AS rio_number_text, 

963 cr.AssessmentDate AS decision_date, 

964 cr.ResearchContact AS traffic_light, 

965 -- 'RED', 'YELLOW', 'GREEN', NULL 

966 0 AS use_email, -- not in CPFT data warehouse copy 

967 cr.OptOut AS opt_out, -- 1, 0 (possibly), NULL 

968 cr.WhoMakesDecisionFor_Patient AS decision_method, 

969 cr.PersonActingonBehalf_of_Patient AS representative_name, 

970 cr.PersonActingonBehalf_of_Patient_Relation AS representative_relation 

971 FROM 

972 ConsentToResearch AS cr 

973 INNER JOIN 

974 PatientOverviewRiO AS po 

975 ON cr.ClientID = po.ClientID 

976 WHERE 

977 po.NHSNumber = %s -- string comparison 

978 ORDER BY 

979 cr.AssessmentDate DESC 

980 """ # noqa: E501 

981 # BEWARE "%s" IN SQL COMMENTS! The database backend will crash because 

982 # the number of substituted parameters will be wrong. 

983 # New as of 2018-06-28: 

984 sql = """ 

985 SELECT TOP 1 -- guaranteed to be running SQL Server 

986 cr.NHSNumber AS nhs_number_text, 

987 cr.ClientID AS rio_number_text, 

988 cr.AssessmentDate AS decision_date, 

989 cr.ResearchContact AS traffic_light, 

990 -- 'RED', 'YELLOW', 'GREEN', NULL 

991 0 AS use_email, -- not in CPFT data warehouse copy 

992 cr.OptOut AS opt_out, -- 1, 0 (possibly), NULL 

993 cr.WhoMakesDecisionFor_Patient AS decision_method, 

994 cr.PersonActingonBehalf_of_Patient AS representative_name, 

995 cr.PersonActingonBehalf_of_Patient_Relation AS representative_relation 

996 FROM 

997 ConsentToResearch AS cr 

998 WHERE 

999 cr.NHSNumber = %s -- string comparison 

1000 ORDER BY 

1001 cr.AssessmentDate DESC 

1002 """ # noqa: E501 

1003 else: 

1004 assert False, "Internal bug" # makes type checker happy 

1005 

1006 cursor = connections[source_db].cursor() 

1007 cursor.execute(sql, [str(nhs_number)]) 

1008 row = dictfetchone(cursor) 

1009 if not row: 

1010 return None 

1011 

1012 decision_date = row["decision_date"] 

1013 exclude_entirely = row["opt_out"] == 1 

1014 use_email = row["use_email"] == 1 

1015 representative_name = row["representative_name"] 

1016 

1017 traffic_light = row["traffic_light"] 

1018 if traffic_light: 

1019 traffic_light = traffic_light.lower() 

1020 if traffic_light not in ConsentMode.VALID_CONSENT_MODES: 

1021 decisions.append( 

1022 f"Invalid traffic light {traffic_light!r}; ignoring" 

1023 ) 

1024 return None 

1025 

1026 if raw_rio: 

1027 # Raw RiO contains codes. 

1028 dmc = row["decision_method_code"] 

1029 if dmc not in DECISION_METHOD_CODE_TO_TEXT.keys(): 

1030 decisions.append(f"Decision method code {dmc!r} unknown; ignoring") 

1031 return None 

1032 dm = DECISION_METHOD_CODE_TO_TEXT[dmc] 

1033 else: 

1034 # The CPFT Data Warehouse version contains text. 

1035 dm = row["decision_method"] 

1036 if dm not in DECISION_METHOD_CODE_TO_TEXT.values(): 

1037 decisions.append(f"Decision method {dm!r} unknown; ignoring") 

1038 return None 

1039 

1040 decision_by_other = ( 

1041 representative_name 

1042 and representative_name.upper() != NOT_APPLICABLE_UPPER 

1043 ) 

1044 # Compare what follows with decision_valid() 

1045 decision_signed_by_patient = False 

1046 decision_otherwise_directly_authorized_by_patient = dm in [ 

1047 ADULT_WITH_CAPACITY_TEXT, 

1048 CHILD_GILLICK_TEXT, 

1049 CHILD_PARENT_TEXT, 

1050 ] 

1051 decision_under16_signed_by_parent = dm == CHILD_PARENT_TEXT 

1052 decision_under16_signed_by_clinician = dm == CHILD_GILLICK_TEXT 

1053 # ... the clinician has had to verify Gillick competence 

1054 decision_lack_capacity_signed_by_representative = ( 

1055 # not strictly "signed", but authorized directly by 

1056 dm == ADULT_LACKS_CAPACITY_TEXT 

1057 and decision_by_other 

1058 ) 

1059 decision_lack_capacity_signed_by_clinician = ( 

1060 dm == ADULT_LACKS_CAPACITY_TEXT 

1061 ) # ... similarly verified by clinician 

1062 

1063 cm = ConsentMode( 

1064 nhs_number=nhs_number, 

1065 created_at=decision_date, 

1066 # ... important that this date matches the source, not "now". For 

1067 # example, if the clinical record copy comes from time T1, and 

1068 # we check it at T3, but then data from T2 comes in later, we 

1069 # want to recognize that T2 data is newer than what we have (so 

1070 # the CRATE copy should be timestamped T2, not T3). 

1071 exclude_entirely=exclude_entirely, 

1072 consent_mode=traffic_light, 

1073 prefers_email=use_email, 

1074 decision_signed_by_patient=decision_signed_by_patient, 

1075 decision_otherwise_directly_authorized_by_patient=decision_otherwise_directly_authorized_by_patient, # noqa: E501 

1076 decision_under16_signed_by_parent=decision_under16_signed_by_parent, # noqa: E501 

1077 decision_under16_signed_by_clinician=decision_under16_signed_by_clinician, # noqa: E501 

1078 decision_lack_capacity_signed_by_representative=decision_lack_capacity_signed_by_representative, # noqa: E501 

1079 decision_lack_capacity_signed_by_clinician=decision_lack_capacity_signed_by_clinician, # noqa: E501 

1080 ) 

1081 return cm 

1082 

1083 

1084def get_latest_consent_mode_from_rio_cpft_datamart( 

1085 nhs_number: int, source_db: str, decisions: List[str] 

1086) -> Optional[ConsentMode]: 

1087 """ 

1088 Returns the latest consent mode for a patient from the copy of RiO in 

1089 the CPFT data warehouse. 

1090 

1091 Args: 

1092 nhs_number: NHS number 

1093 source_db: the type of the source database; see 

1094 :class:`crate_anon.crateweb.config.constants.ClinicalDatabaseType` 

1095 decisions: list of human-readable decisions; will be modified 

1096 

1097 Returns: 

1098 a :class:`crate_anon.crateweb.consent.models.ConsentMode`, or ``None`` 

1099 

1100 """ 

1101 return get_latest_consent_mode_from_rio_generic( 

1102 nhs_number=nhs_number, 

1103 source_db=source_db, 

1104 decisions=decisions, 

1105 cpft_datamart=True, 

1106 ) 

1107 

1108 

1109def get_latest_consent_mode_from_rio_raw( 

1110 nhs_number: int, source_db: str, decisions: List[str] 

1111) -> Optional[ConsentMode]: 

1112 """ 

1113 Returns the latest consent mode for a patient from a raw copy of RiO. 

1114 

1115 Args: 

1116 nhs_number: NHS number 

1117 source_db: the type of the source database; see 

1118 :class:`crate_anon.crateweb.config.constants.ClinicalDatabaseType` 

1119 decisions: list of human-readable decisions; will be modified 

1120 

1121 Returns: 

1122 a :class:`crate_anon.crateweb.consent.models.ConsentMode`, or ``None`` 

1123 

1124 """ 

1125 return get_latest_consent_mode_from_rio_generic( 

1126 nhs_number=nhs_number, 

1127 source_db=source_db, 

1128 decisions=decisions, 

1129 raw_rio=True, 

1130 ) 

1131 

1132 

1133def gen_opt_out_pids_mpids_rio_generic( 

1134 source_db: str, raw_rio: bool = False, cpft_datamart: bool = False 

1135) -> Generator[Tuple[str, str], None, None]: 

1136 """ 

1137 Generates PIDs/MPIDs from all patients opting out, from a RiO database of 

1138 some sort. 

1139 

1140 Args: 

1141 source_db: the type of the source database; see 

1142 :class:`crate_anon.crateweb.config.constants.ClinicalDatabaseType` 

1143 raw_rio: is the source database a raw copy of RiO? 

1144 cpft_datamart: is the source database the version from the CPFT 

1145 data warehouse? 

1146 

1147 Yields: 

1148 tuple: ``rio_number, nhs_number`` for each patient opting out; both are 

1149 in string format 

1150 """ 

1151 assert ( 

1152 sum([raw_rio, cpft_datamart]) == 1 

1153 ), "Specify exactly one database type to look up from" 

1154 if raw_rio: 

1155 sql = """ 

1156 SELECT 

1157 ci.ClientID AS rio_number_text, 

1158 ci.NNN AS nhs_number_text 

1159 FROM 

1160 UserAssessconsentrd AS cr 

1161 INNER JOIN 

1162 ClientIndex AS ci 

1163 ON cr.ClientID = ci.ClientID 

1164 WHERE 

1165 cr.optout = 1 

1166 ORDER BY 

1167 cr.ClientID 

1168 """ 

1169 elif cpft_datamart: 

1170 sql = """ 

1171 SELECT 

1172 po.ClientID AS rio_number_text, 

1173 po.NHSNumber AS nhs_number_text 

1174 FROM 

1175 ConsentToResearch AS cr 

1176 INNER JOIN 

1177 PatientOverviewRiO AS po 

1178 ON cr.ClientID = po.ClientID 

1179 WHERE 

1180 cr.OptOut = 1 

1181 ORDER BY 

1182 cr.ClientID 

1183 """ 

1184 else: 

1185 assert False, "Internal bug" # makes type checker happy 

1186 

1187 cursor = connections[source_db].cursor() 

1188 cursor.execute(sql) 

1189 for row in genrows(cursor): 

1190 pid = row[0] # RiO number, as text 

1191 mpid = row[1] # NHS number, as text 

1192 yield pid, mpid 

1193 

1194 

1195def gen_opt_out_pids_mpids_rio_cpft_datamart( 

1196 source_db: str, 

1197) -> Generator[Tuple[str, str], None, None]: 

1198 """ 

1199 Generates PIDs/MPIDs from all patients opting out, from a RiO database that 

1200 is the version in the CPFT data warehouse. 

1201 

1202 Args: 

1203 source_db: the type of the source database; see 

1204 :class:`crate_anon.crateweb.config.constants.ClinicalDatabaseType` 

1205 

1206 Yields: 

1207 tuple: ``rio_number, nhs_number`` for each patient opting out; both are 

1208 in string format 

1209 """ 

1210 return gen_opt_out_pids_mpids_rio_generic( 

1211 source_db=source_db, cpft_datamart=True 

1212 ) 

1213 

1214 

1215def gen_opt_out_pids_mpids_rio_raw( 

1216 source_db: str, 

1217) -> Generator[Tuple[str, str], None, None]: 

1218 """ 

1219 Generates PIDs/MPIDs from all patients opting out, from a raw RiO database. 

1220 

1221 Args: 

1222 source_db: the type of the source database; see 

1223 :class:`crate_anon.crateweb.config.constants.ClinicalDatabaseType` 

1224 

1225 Yields: 

1226 tuple: ``rio_number, nhs_number`` for each patient opting out; both are 

1227 in string format 

1228 """ 

1229 return gen_opt_out_pids_mpids_rio_generic( 

1230 source_db=source_db, raw_rio=True 

1231 )