Coverage for tasks/cpft_lps.py: 51%

1"""

2 camcops_server/tasks/cpft_lps.py

3

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

5

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

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

8

9 This file is part of CamCOPS.

10

11 CamCOPS 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 CamCOPS 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 CamCOPS. If not, see <https://www.gnu.org/licenses/>.

23

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

25

26"""

27

28 import datetime

29 import logging

30 from typing import Any, List, Optional, Type

31

32 from cardinal_pythonlib.classes import classproperty

33 from cardinal_pythonlib.datetimefunc import format_datetime

34 from cardinal_pythonlib.logs import BraceStyleAdapter

35 import cardinal_pythonlib.rnc_web as ws

36 from pendulum import DateTime as Pendulum

37 import pyramid.httpexceptions as exc

38 from sqlalchemy.sql.expression import and_, exists, select

39 from sqlalchemy.sql.selectable import SelectBase

40 from sqlalchemy.orm import Mapped, mapped_column

41 from sqlalchemy.sql.sqltypes import UnicodeText

42

43 from camcops_server.cc_modules.cc_constants import (

44 CssClass,

45 DateFormat,

46 INVALID_VALUE,

47)

48 from camcops_server.cc_modules.cc_ctvinfo import CtvInfo

49 from camcops_server.cc_modules.cc_forms import (

50 LinkingIdNumSelector,

51 ReportParamSchema,

52)

53 from camcops_server.cc_modules.cc_html import (

54 answer,

55 get_yes_no_none,

56 subheading_spanning_four_columns,

57 subheading_spanning_two_columns,

58 tr_qa,

59 tr_span_col,

60)

61 from camcops_server.cc_modules.cc_nhs import (

62 get_nhs_dd_ethnic_category_code,

63 get_nhs_dd_person_marital_status,

64 PV_NHS_ETHNIC_CATEGORY,

65 PV_NHS_MARITAL_STATUS,

66)

67 from camcops_server.cc_modules.cc_patient import Patient

68 from camcops_server.cc_modules.cc_patientidnum import PatientIdNum

69 from camcops_server.cc_modules.cc_pyramid import ViewParam

70 from camcops_server.cc_modules.cc_report import Report

71 from camcops_server.cc_modules.cc_request import CamcopsRequest

72 from camcops_server.cc_modules.cc_sqla_coltypes import (

73 mapped_bool_column,

74 mapped_camcops_column,

75 CharColType,

76 PendulumDateTimeAsIsoTextColType,

77 DiagnosticCodeColType,

78 PermittedValueChecker,

79)

80 from camcops_server.cc_modules.cc_task import (

81 Task,

82 TaskHasClinicianMixin,

83 TaskHasPatientMixin,

84)

85 from camcops_server.tasks.psychiatricclerking import PsychiatricClerking

86

87 log = BraceStyleAdapter(logging.getLogger(__name__))

88

89

90# =============================================================================

91# CPFT_LPS_Referral

92# =============================================================================

93

94

95 class CPFTLPSReferral(TaskHasPatientMixin, Task): # type: ignore[misc]

96 """

97 Server implementation of the CPFT_LPS_Referral task.

98 """

99

100 __tablename__ = "cpft_lps_referral"

101 shortname = "CPFT_LPS_Referral"

102 info_filename_stem = "clinical"

103

104 referral_date_time: Mapped[Optional[Pendulum]] = mapped_column(

105 PendulumDateTimeAsIsoTextColType

106 )

107 lps_division: Mapped[Optional[str]] = mapped_camcops_column(

108 UnicodeText, exempt_from_anonymisation=True

109 )

110 referral_priority: Mapped[Optional[str]] = mapped_camcops_column(

111 UnicodeText, exempt_from_anonymisation=True

112 )

113 referral_method: Mapped[Optional[str]] = mapped_camcops_column(

114 UnicodeText, exempt_from_anonymisation=True

115 )

116 referrer_name: Mapped[Optional[str]] = mapped_column(UnicodeText)

117 referrer_contact_details: Mapped[Optional[str]] = mapped_column(

118 UnicodeText

119 )

120 referring_consultant: Mapped[Optional[str]] = mapped_column(UnicodeText)

121 referring_specialty: Mapped[Optional[str]] = mapped_camcops_column(

122 UnicodeText, exempt_from_anonymisation=True

123 )

124 referring_specialty_other: Mapped[Optional[str]] = mapped_column(

125 UnicodeText

126 )

127 patient_location: Mapped[Optional[str]] = mapped_column(UnicodeText)

128 admission_date: Mapped[Optional[datetime.date]] = mapped_column()

129 estimated_discharge_date: Mapped[Optional[datetime.date]] = mapped_column()

130 patient_aware_of_referral: Mapped[Optional[bool]] = mapped_bool_column(

131 "patient_aware_of_referral"

132 )

133 interpreter_required: Mapped[Optional[bool]] = mapped_bool_column(

134 "interpreter_required"

135 )

136 sensory_impairment: Mapped[Optional[bool]] = mapped_bool_column(

137 "sensory_impairment"

138 )

139 marital_status_code: Mapped[Optional[str]] = mapped_camcops_column(

140 CharColType,

141 permitted_value_checker=PermittedValueChecker(

142 permitted_values=PV_NHS_MARITAL_STATUS

143 ),

144 )

145 ethnic_category_code: Mapped[Optional[str]] = mapped_camcops_column(

146 CharColType,

147 permitted_value_checker=PermittedValueChecker(

148 permitted_values=PV_NHS_ETHNIC_CATEGORY

149 ),

150 )

151 admission_reason_overdose: Mapped[Optional[bool]] = mapped_bool_column(

152 "admission_reason_overdose"

153 )

154 admission_reason_self_harm_not_overdose: Mapped[Optional[bool]] = (

155 mapped_bool_column(

156 "admission_reason_self_harm_not_overdose",

157 constraint_name="ck_cpft_lps_referral_arshno",

158 )

159 )

160 admission_reason_confusion: Mapped[Optional[bool]] = mapped_bool_column(

161 "admission_reason_confusion"

162 )

163 admission_reason_trauma: Mapped[Optional[bool]] = mapped_bool_column(

164 "admission_reason_trauma"

165 )

166 admission_reason_falls: Mapped[Optional[bool]] = mapped_bool_column(

167 "admission_reason_falls"

168 )

169 admission_reason_infection: Mapped[Optional[bool]] = mapped_bool_column(

170 "admission_reason_infection"

171 )

172 admission_reason_poor_adherence: Mapped[Optional[bool]] = (

173 mapped_bool_column(

174 "admission_reason_poor_adherence",

175 constraint_name="ck_cpft_lps_referral_adpa",

176 )

177 )

178 admission_reason_other: Mapped[Optional[bool]] = mapped_bool_column(

179 "admission_reason_other"

180 )

181 existing_psychiatric_teams: Mapped[Optional[str]] = mapped_column(

182 UnicodeText

183 )

184 care_coordinator: Mapped[Optional[str]] = mapped_column(UnicodeText)

185 other_contact_details: Mapped[Optional[str]] = mapped_column(UnicodeText)

186 referral_reason: Mapped[Optional[str]] = mapped_column(UnicodeText)

187

188 @staticmethod

189 def longname(req: "CamcopsRequest") -> str:

190 _ = req.gettext

191 return _("CPFT LPS – referral")

192

193 def is_complete(self) -> bool:

194 return bool(

195 self.patient_location

196 and self.referral_reason

197 and self.field_contents_valid()

198 )

199

200 def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]:

201 return [

202 CtvInfo(

203 heading=ws.webify(self.wxstring(req, "f_referral_reason_t")),

204 content=self.referral_reason,

205 )

206 ]

207

208 @staticmethod

209 def four_column_row(

210 q1: str, a1: Any, q2: str, a2: Any, default: str = ""

211 ) -> str:

212 return f"""

213 <tr>

214 <td>{q1}</td><td>{answer(a1, default=default)}</td>

215 <td>{q2}</td><td>{answer(a2, default=default)}</td>

216 </tr>

217 """

218

219 @staticmethod

220 def tr_qa(q: str, a: Any, default: str = "") -> str:

221 return f"""

222 <tr>

223 <td colspan="2">{q}</td>

224 <td colspan="2"><b>{default if a is None else a}</b></td>

225 </tr>

226 """

227

228 def get_task_html(self, req: CamcopsRequest) -> str:

229 person_marital_status = get_nhs_dd_person_marital_status(req)

230 ethnic_category_code = get_nhs_dd_ethnic_category_code(req)

231 if self.lps_division == "G":

232 banner_class = CssClass.BANNER_REFERRAL_GENERAL_ADULT

233 division_name = self.wxstring(req, "service_G")

234 elif self.lps_division == "O":

235 banner_class = CssClass.BANNER_REFERRAL_OLD_AGE

236 division_name = self.wxstring(req, "service_O")

237 elif self.lps_division == "S":

238 banner_class = CssClass.BANNER_REFERRAL_SUBSTANCE_MISUSE

239 division_name = self.wxstring(req, "service_S")

240 else:

241 banner_class = ""

242 division_name = None

243

244 if self.referral_priority == "R":

245 priority_name = self.wxstring(req, "priority_R")

246 elif self.referral_priority == "U":

247 priority_name = self.wxstring(req, "priority_U")

248 elif self.referral_priority == "E":

249 priority_name = self.wxstring(req, "priority_E")

250 else:

251 priority_name = None

252

253 potential_admission_reasons = [

254 "admission_reason_overdose",

255 "admission_reason_self_harm_not_overdose",

256 "admission_reason_confusion",

257 "admission_reason_trauma",

258 "admission_reason_falls",

259 "admission_reason_infection",

260 "admission_reason_poor_adherence",

261 "admission_reason_other",

262 ]

263 admission_reasons = []

264 for r in potential_admission_reasons:

265 if getattr(self, r):

266 admission_reasons.append(self.wxstring(req, "f_" + r))

267

268 h = f"""

269 <div class="{CssClass.BANNER} {banner_class}">

270 {answer(division_name, default_for_blank_strings=True)}

271 referral at {

272 answer(format_datetime(

273 self.referral_date_time,

274 DateFormat.SHORT_DATETIME_WITH_DAY_NO_TZ,

275 default=None))}

276 </div>

277 <div class="{CssClass.SUMMARY}">

278 <table class="{CssClass.SUMMARY}">

279 {self.get_is_complete_tr(req)}

280 </table>

281 </div>

282 <table class="{CssClass.TASKDETAIL}">

283 <col width="25%">

284 <col width="25%">

285 <col width="25%">

286 <col width="25%">

287 """

288 h += subheading_spanning_four_columns(

289 self.wxstring(req, "t_about_referral")

290 )

291 h += """

292 <tr>

293 <td>{q_method}</td>

294 <td>{a_method}</td>

295 <td>{q_priority}</td>

296 <td class="{CssClass.HIGHLIGHT}">{a_priority}</td>

297 </tr>

298 """.format(

299 CssClass=CssClass,

300 q_method=self.wxstring(req, "f_referral_method"),

301 a_method=answer(self.referral_method),

302 q_priority=self.wxstring(req, "f_referral_priority"),

303 a_priority=(

304 answer(self.referral_priority, default_for_blank_strings=True)

305 + ": "

306 + answer(priority_name)

307 ),

308 )

309 h += self.four_column_row(

310 self.wxstring(req, "f_referrer_name"),

311 self.referrer_name,

312 self.wxstring(req, "f_referring_specialty"),

313 self.referring_specialty,

314 )

315 h += self.four_column_row(

316 self.wxstring(req, "f_referrer_contact_details"),

317 self.referrer_contact_details,

318 self.wxstring(req, "f_referring_specialty_other"),

319 self.referring_specialty_other,

320 )

321 h += self.four_column_row(

322 self.wxstring(req, "f_referring_consultant"),

323 self.referring_consultant,

324 "",

325 "",

326 )

327 h += subheading_spanning_four_columns(self.wxstring(req, "t_patient"))

328 h += """

329 <tr>

330 <td>{q_when}</td>

331 <td>{a_when}</td>

332 <td>{q_where}</td>

333 <td class="{CssClass.HIGHLIGHT}">{a_where}</td>

334 </tr>

335 """.format(

336 CssClass=CssClass,

337 q_when=self.wxstring(req, "f_admission_date"),

338 a_when=answer(

339 format_datetime(

340 self.admission_date, DateFormat.LONG_DATE, default=None

341 ),

342 "",

343 ),

344 q_where=self.wxstring(req, "f_patient_location"),

345 a_where=answer(self.patient_location),

346 )

347 h += self.four_column_row(

348 self.wxstring(req, "f_estimated_discharge_date"),

349 format_datetime(

350 self.estimated_discharge_date, DateFormat.LONG_DATE, ""

351 ),

352 self.wxstring(req, "f_patient_aware_of_referral"),

353 get_yes_no_none(req, self.patient_aware_of_referral),

354 )

355 h += self.four_column_row(

356 self.wxstring(req, "f_marital_status"),

357 person_marital_status.get(self.marital_status_code, INVALID_VALUE),

358 self.wxstring(req, "f_interpreter_required"),

359 get_yes_no_none(req, self.interpreter_required),

360 )

361 h += self.four_column_row(

362 self.wxstring(req, "f_ethnic_category"),

363 ethnic_category_code.get(self.ethnic_category_code, INVALID_VALUE),

364 self.wxstring(req, "f_sensory_impairment"),

365 get_yes_no_none(req, self.sensory_impairment),

366 )

367 h += subheading_spanning_four_columns(

368 self.wxstring(req, "t_admission_reason")

369 )

370 h += tr_span_col(answer(", ".join(admission_reasons), ""), cols=4)

371 h += subheading_spanning_four_columns(

372 self.wxstring(req, "t_other_people")

373 )

374 h += self.tr_qa(

375 self.wxstring(req, "f_existing_psychiatric_teams"),

376 self.existing_psychiatric_teams,

377 "",

378 )

379 h += self.tr_qa(

380 self.wxstring(req, "f_care_coordinator"), self.care_coordinator, ""

381 )

382 h += self.tr_qa(

383 self.wxstring(req, "f_other_contact_details"),

384 self.other_contact_details,

385 "",

386 )

387 h += subheading_spanning_four_columns(

388 self.wxstring(req, "t_referral_reason")

389 )

390 h += tr_span_col(answer(self.referral_reason, ""), cols=4)

391 h += """

392 </table>

393 """

394 return h

395

396

397# =============================================================================

398# CPFT_LPS_ResetResponseClock

399# =============================================================================

400

401

402 class CPFTLPSResetResponseClock( # type: ignore[misc]

403 TaskHasPatientMixin, TaskHasClinicianMixin, Task

404):

405 """

406 Server implementation of the CPFT_LPS_ResetResponseClock task.

407 """

408

409 __tablename__ = "cpft_lps_resetresponseclock"

410 shortname = "CPFT_LPS_ResetResponseClock"

411 info_filename_stem = "clinical"

412

413 reset_start_time_to: Mapped[Optional[Pendulum]] = mapped_column(

414 PendulumDateTimeAsIsoTextColType

415 )

416 reason: Mapped[Optional[str]] = mapped_column(UnicodeText)

417

418 @staticmethod

419 def longname(req: "CamcopsRequest") -> str:

420 _ = req.gettext

421 return _("CPFT LPS – reset response clock")

422

423 def is_complete(self) -> bool:

424 return bool(

425 self.reset_start_time_to

426 and self.reason

427 and self.field_contents_valid()

428 )

429

430 def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]:

431 return [CtvInfo(content=self.reason)]

432

433 def get_task_html(self, req: CamcopsRequest) -> str:

434 h = f"""

435 <div class="{CssClass.SUMMARY}">

436 <table class="{CssClass.SUMMARY}">

437 {self.get_is_complete_tr(req)}

438 </table>

439 </div>

440 <table class="{CssClass.TASKDETAIL}">

441 <col width="25%">

442 <col width="75%">

443 """

444 h += tr_qa(

445 self.wxstring(req, "to"),

446 format_datetime(

447 self.reset_start_time_to,

448 DateFormat.LONG_DATETIME_WITH_DAY,

449 default=None,

450 ),

451 )

452 h += tr_qa(self.wxstring(req, "reason"), self.reason)

453 h += """

454 </table>

455 """

456 return h

457

458

459# =============================================================================

460# CPFT_LPS_Discharge

461# =============================================================================

462

463

464 class CPFTLPSDischarge(TaskHasPatientMixin, TaskHasClinicianMixin, Task): # type: ignore[misc] # noqa: E501

465 """

466 Server implementation of the CPFT_LPS_Discharge task.

467 """

468

469 __tablename__ = "cpft_lps_discharge"

470 shortname = "CPFT_LPS_Discharge"

471 info_filename_stem = "clinical"

472

473 discharge_date: Mapped[Optional[datetime.date]] = mapped_column()

474 discharge_reason_code: Mapped[Optional[str]] = mapped_camcops_column(

475 UnicodeText, exempt_from_anonymisation=True

476 )

477

478 leaflet_or_discharge_card_given: Mapped[Optional[bool]] = (

479 mapped_bool_column(

480 "leaflet_or_discharge_card_given",

481 constraint_name="ck_cpft_lps_discharge_lodcg",

482 )

483 )

484 frequent_attender: Mapped[Optional[bool]] = mapped_bool_column(

485 "frequent_attender"

486 )

487 patient_wanted_copy_of_letter: Mapped[Optional[bool]] = mapped_bool_column(

488 # Was previously text! That wasn't right.

489 "patient_wanted_copy_of_letter"

490 )

491 gaf_at_first_assessment: Mapped[Optional[int]] = mapped_camcops_column(

492 permitted_value_checker=PermittedValueChecker(minimum=0, maximum=100),

493 )

494 gaf_at_discharge: Mapped[Optional[int]] = mapped_camcops_column(

495 permitted_value_checker=PermittedValueChecker(minimum=0, maximum=100),

496 )

497

498 referral_reason_self_harm_overdose: Mapped[Optional[bool]] = (

499 mapped_bool_column(

500 "referral_reason_self_harm_overdose",

501 constraint_name="ck_cpft_lps_discharge_rrshoverdose",

502 )

503 )

504 referral_reason_self_harm_other: Mapped[Optional[bool]] = (

505 mapped_bool_column(

506 "referral_reason_self_harm_other",

507 constraint_name="ck_cpft_lps_discharge_rrshother",

508 )

509 )

510 referral_reason_suicidal_ideas: Mapped[Optional[bool]] = (

511 mapped_bool_column(

512 "referral_reason_suicidal_ideas",

513 constraint_name="ck_cpft_lps_discharge_rrsuicidal",

514 )

515 )

516 referral_reason_behavioural_disturbance: Mapped[Optional[bool]] = (

517 mapped_bool_column(

518 "referral_reason_behavioural_disturbance",

519 constraint_name="ck_cpft_lps_discharge_behavdisturb",

520 )

521 )

522 referral_reason_low_mood: Mapped[Optional[bool]] = mapped_bool_column(

523 "referral_reason_low_mood"

524 )

525 referral_reason_elevated_mood: Mapped[Optional[bool]] = mapped_bool_column(

526 "referral_reason_elevated_mood"

527 )

528 referral_reason_psychosis: Mapped[Optional[bool]] = mapped_bool_column(

529 "referral_reason_psychosis"

530 )

531 referral_reason_pre_transplant: Mapped[Optional[bool]] = (

532 mapped_bool_column(

533 "referral_reason_pre_transplant",

534 constraint_name="ck_cpft_lps_discharge_pretransplant",

535 )

536 )

537 referral_reason_post_transplant: Mapped[Optional[bool]] = (

538 mapped_bool_column(

539 "referral_reason_post_transplant",

540 constraint_name="ck_cpft_lps_discharge_posttransplant",

541 )

542 )

543 referral_reason_delirium: Mapped[Optional[bool]] = mapped_bool_column(

544 "referral_reason_delirium"

545 )

546 referral_reason_anxiety: Mapped[Optional[bool]] = mapped_bool_column(

547 "referral_reason_anxiety"

548 )

549 referral_reason_somatoform_mus: Mapped[Optional[bool]] = (

550 mapped_bool_column(

551 "referral_reason_somatoform_mus",

552 constraint_name="ck_cpft_lps_discharge_mus",

553 )

554 )

555 referral_reason_motivation_adherence: Mapped[Optional[bool]] = (

556 mapped_bool_column(

557 "referral_reason_motivation_adherence",

558 constraint_name="ck_cpft_lps_discharge_motivadherence",

559 )

560 )

561 referral_reason_capacity: Mapped[Optional[bool]] = mapped_bool_column(

562 "referral_reason_capacity"

563 )

564 referral_reason_eating_disorder: Mapped[Optional[bool]] = (

565 mapped_bool_column(

566 "referral_reason_eating_disorder",

567 constraint_name="ck_cpft_lps_discharge_eatingdis",

568 )

569 )

570 referral_reason_safeguarding: Mapped[Optional[bool]] = mapped_bool_column(

571 "referral_reason_safeguarding"

572 )

573 referral_reason_discharge_placement: Mapped[Optional[bool]] = (

574 mapped_bool_column(

575 "referral_reason_discharge_placement",

576 constraint_name="ck_cpft_lps_discharge_dcplacement",

577 )

578 )

579 referral_reason_cognitive_problem: Mapped[Optional[bool]] = (

580 mapped_bool_column(

581 "referral_reason_cognitive_problem",

582 constraint_name="ck_cpft_lps_discharge_cognitiveprob",

583 )

584 )

585 referral_reason_substance_alcohol: Mapped[Optional[bool]] = (

586 mapped_bool_column(

587 "referral_reason_substance_alcohol",

588 constraint_name="ck_cpft_lps_discharge_alcohol",

589 )

590 )

591 referral_reason_substance_other: Mapped[Optional[bool]] = (

592 mapped_bool_column(

593 "referral_reason_substance_other",

594 constraint_name="ck_cpft_lps_discharge_substanceother",

595 )

596 )

597 referral_reason_other: Mapped[Optional[bool]] = mapped_bool_column(

598 "referral_reason_other"

599 )

600 referral_reason_transplant_organ: Mapped[Optional[str]] = (

601 mapped_camcops_column(

602 UnicodeText,

603 exempt_from_anonymisation=True,

604 )

605 )

606 referral_reason_other_detail: Mapped[Optional[str]] = mapped_column(

607 UnicodeText

608 )

609

610 diagnosis_no_active_mental_health_problem: Mapped[Optional[bool]] = (

611 mapped_bool_column(

612 "diagnosis_no_active_mental_health_problem",

613 constraint_name="ck_cpft_lps_discharge_nomhprob",

614 )

615 )

616 diagnosis_psych_1_icd10code: Mapped[Optional[str]] = mapped_column(

617 DiagnosticCodeColType

618 )

619 diagnosis_psych_1_description: Mapped[Optional[str]] = (

620 mapped_camcops_column(

621 UnicodeText,

622 exempt_from_anonymisation=True,

623 )

624 )

625 diagnosis_psych_2_icd10code: Mapped[Optional[str]] = mapped_column(

626 DiagnosticCodeColType

627 )

628 diagnosis_psych_2_description: Mapped[Optional[str]] = (

629 mapped_camcops_column(

630 UnicodeText,

631 exempt_from_anonymisation=True,

632 )

633 )

634 diagnosis_psych_3_icd10code: Mapped[Optional[str]] = mapped_column(

635 DiagnosticCodeColType

636 )

637 diagnosis_psych_3_description: Mapped[Optional[str]] = (

638 mapped_camcops_column(

639 UnicodeText,

640 exempt_from_anonymisation=True,

641 )

642 )

643 diagnosis_psych_4_icd10code: Mapped[Optional[str]] = mapped_column(

644 DiagnosticCodeColType

645 )

646 diagnosis_psych_4_description: Mapped[Optional[str]] = (

647 mapped_camcops_column(

648 UnicodeText,

649 exempt_from_anonymisation=True,

650 )

651 )

652 diagnosis_medical_1: Mapped[Optional[str]] = mapped_column(UnicodeText)

653 diagnosis_medical_2: Mapped[Optional[str]] = mapped_column(UnicodeText)

654 diagnosis_medical_3: Mapped[Optional[str]] = mapped_column(UnicodeText)

655 diagnosis_medical_4: Mapped[Optional[str]] = mapped_column(UnicodeText)

656

657 management_assessment_diagnostic: Mapped[Optional[bool]] = (

658 mapped_bool_column(

659 "management_assessment_diagnostic",

660 constraint_name="ck_cpft_lps_discharge_mx_ass_diag",

661 )

662 )

663 management_medication: Mapped[Optional[bool]] = mapped_bool_column(

664 "management_medication"

665 )

666 management_specialling_behavioural_disturbance: Mapped[Optional[bool]] = (

667 mapped_bool_column(

668 "management_specialling_behavioural_disturbance",

669 # Constraint name too long for MySQL unless we do this:

670 constraint_name="ck_cpft_lps_discharge_msbd",

671 )

672 )

673 management_supportive_patient: Mapped[Optional[bool]] = mapped_bool_column(

674 "management_supportive_patient"

675 )

676 management_supportive_carers: Mapped[Optional[bool]] = mapped_bool_column(

677 "management_supportive_carers"

678 )

679 management_supportive_staff: Mapped[Optional[bool]] = mapped_bool_column(

680 "management_supportive_staff"

681 )

682 management_nursing_management: Mapped[Optional[bool]] = mapped_bool_column(

683 "management_nursing_management"

684 )

685 management_therapy_cbt: Mapped[Optional[bool]] = mapped_bool_column(

686 "management_therapy_cbt"

687 )

688 management_therapy_cat: Mapped[Optional[bool]] = mapped_bool_column(

689 "management_therapy_cat"

690 )

691 management_therapy_other: Mapped[Optional[bool]] = mapped_bool_column(

692 "management_therapy_other"

693 )

694 management_treatment_adherence: Mapped[Optional[bool]] = (

695 mapped_bool_column(

696 "management_treatment_adherence",

697 constraint_name="ck_cpft_lps_discharge_mx_rx_adhere",

698 )

699 )

700 management_capacity: Mapped[Optional[bool]] = mapped_bool_column(

701 "management_capacity"

702 )

703 management_education_patient: Mapped[Optional[bool]] = mapped_bool_column(

704 "management_education_patient"

705 )

706 management_education_carers: Mapped[Optional[bool]] = mapped_bool_column(

707 "management_education_carers"

708 )

709 management_education_staff: Mapped[Optional[bool]] = mapped_bool_column(

710 "management_education_staff"

711 )

712 management_accommodation_placement: Mapped[Optional[bool]] = (

713 mapped_bool_column(

714 "management_accommodation_placement",

715 constraint_name="ck_cpft_lps_discharge_accom",

716 )

717 )

718 management_signposting_external_referral: Mapped[Optional[bool]] = (

719 mapped_bool_column(

720 "management_signposting_external_referral",

721 constraint_name="ck_cpft_lps_discharge_mx_signpostrefer",

722 )

723 )

724 management_mha_s136: Mapped[Optional[bool]] = mapped_bool_column(

725 "management_mha_s136"

726 )

727 management_mha_s5_2: Mapped[Optional[bool]] = mapped_bool_column(

728 "management_mha_s5_2"

729 )

730 management_mha_s2: Mapped[Optional[bool]] = mapped_bool_column(

731 "management_mha_s2"

732 )

733 management_mha_s3: Mapped[Optional[bool]] = mapped_bool_column(

734 "management_mha_s3"

735 )

736 management_complex_case_conference: Mapped[Optional[bool]] = (

737 mapped_bool_column(

738 "management_complex_case_conference",

739 constraint_name="ck_cpft_lps_discharge_caseconf",

740 )

741 )

742 management_other: Mapped[Optional[bool]] = mapped_bool_column(

743 "management_other"

744 )

745 management_other_detail: Mapped[Optional[str]] = mapped_column(UnicodeText)

746

747 outcome: Mapped[Optional[str]] = mapped_camcops_column(

748 UnicodeText, exempt_from_anonymisation=True

749 )

750 outcome_hospital_transfer_detail: Mapped[Optional[str]] = mapped_column(

751 UnicodeText

752 )

753 outcome_other_detail: Mapped[Optional[str]] = mapped_column(UnicodeText)

754

755 @staticmethod

756 def longname(req: "CamcopsRequest") -> str:

757 _ = req.gettext

758 return _("CPFT LPS – discharge")

759

760 def is_complete(self) -> bool:

761 return bool(

762 self.discharge_date

763 and self.discharge_reason_code

764 and

765 # self.outcome and # v2.0.0

766 self.field_contents_valid()

767 )

768

769 def get_discharge_reason(self, req: CamcopsRequest) -> Optional[str]:

770 if self.discharge_reason_code == "F":

771 return self.wxstring(req, "reason_code_F")

772 elif self.discharge_reason_code == "A":

773 return self.wxstring(req, "reason_code_A")

774 elif self.discharge_reason_code == "O":

775 return self.wxstring(req, "reason_code_O")

776 elif self.discharge_reason_code == "C":

777 return self.wxstring(req, "reason_code_C")

778 else:

779 return None

780

781 def get_referral_reasons(self, req: CamcopsRequest) -> List[str]:

782 potential_referral_reasons = [

783 "referral_reason_self_harm_overdose",

784 "referral_reason_self_harm_other",

785 "referral_reason_suicidal_ideas",

786 "referral_reason_behavioural_disturbance",

787 "referral_reason_low_mood",

788 "referral_reason_elevated_mood",

789 "referral_reason_psychosis",

790 "referral_reason_pre_transplant",

791 "referral_reason_post_transplant",

792 "referral_reason_delirium",

793 "referral_reason_anxiety",

794 "referral_reason_somatoform_mus",

795 "referral_reason_motivation_adherence",

796 "referral_reason_capacity",

797 "referral_reason_eating_disorder",

798 "referral_reason_safeguarding",

799 "referral_reason_discharge_placement",

800 "referral_reason_cognitive_problem",

801 "referral_reason_substance_alcohol",

802 "referral_reason_substance_other",

803 "referral_reason_other",

804 ]

805 referral_reasons = []

806 for r in potential_referral_reasons:

807 if getattr(self, r):

808 referral_reasons.append(self.wxstring(req, "" + r))

809 return referral_reasons

810

811 def get_managements(self, req: CamcopsRequest) -> List[str]:

812 potential_managements = [

813 "management_assessment_diagnostic",

814 "management_medication",

815 "management_specialling_behavioural_disturbance",

816 "management_supportive_patient",

817 "management_supportive_carers",

818 "management_supportive_staff",

819 "management_nursing_management",

820 "management_therapy_cbt",

821 "management_therapy_cat",

822 "management_therapy_other",

823 "management_treatment_adherence",

824 "management_capacity",

825 "management_education_patient",

826 "management_education_carers",

827 "management_education_staff",

828 "management_accommodation_placement",

829 "management_signposting_external_referral",

830 "management_mha_s136",

831 "management_mha_s5_2",

832 "management_mha_s2",

833 "management_mha_s3",

834 "management_complex_case_conference",

835 "management_other",

836 ]

837 managements = []

838 for r in potential_managements:

839 if getattr(self, r):

840 managements.append(self.wxstring(req, "" + r))

841 return managements

842

843 def get_psychiatric_diagnoses(self, req: CamcopsRequest) -> List[str]:

844 psychiatric_diagnoses = (

845 [self.wxstring(req, "diagnosis_no_active_mental_health_problem")]

846 if self.diagnosis_no_active_mental_health_problem

847 else []

848 )

849 for i in range(1, 4 + 1): # magic number

850 if getattr(self, "diagnosis_psych_" + str(i) + "_icd10code"):

851 psychiatric_diagnoses.append(

852 ws.webify(

853 getattr(

854 self, "diagnosis_psych_" + str(i) + "_icd10code"

855 )

856 )

857 + " – "

858 + ws.webify(

859 getattr(

860 self, "diagnosis_psych_" + str(i) + "_description"

861 )

862 )

863 )

864 return psychiatric_diagnoses

865

866 def get_medical_diagnoses(self) -> List[str]:

867 medical_diagnoses = []

868 for i in range(1, 4 + 1): # magic number

869 if getattr(self, "diagnosis_medical_" + str(i)):

870 medical_diagnoses.append(

871 ws.webify(getattr(self, "diagnosis_medical_" + str(i)))

872 )

873 return medical_diagnoses

874

875 def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]:

876 diagnoses = (

877 self.get_psychiatric_diagnoses(req) + self.get_medical_diagnoses()

878 )

879 return [

880 CtvInfo(

881 heading=ws.webify(self.wxstring(req, "discharge_reason")),

882 content=self.get_discharge_reason(req),

883 ),

884 CtvInfo(

885 heading=ws.webify(self.wxstring(req, "referral_reason_t")),

886 content=", ".join(self.get_referral_reasons(req)),

887 ),

888 CtvInfo(

889 heading=ws.webify(self.wxstring(req, "diagnoses_t")),

890 content=", ".join(diagnoses),

891 ),

892 CtvInfo(

893 heading=ws.webify(self.wxstring(req, "management_t")),

894 content=", ".join(self.get_managements(req)),

895 ),

896 CtvInfo(

897 heading=ws.webify(self.wxstring(req, "outcome_t")),

898 content=self.outcome,

899 ),

900 ]

901

902 def get_task_html(self, req: CamcopsRequest) -> str:

903 h = f"""

904 <div class="{CssClass.SUMMARY}">

905 <table class="{CssClass.SUMMARY}">

906 {self.get_is_complete_tr(req)}

907 </table>

908 </div>

909 <table class="{CssClass.TASKDETAIL}">

910 <col width="40%">

911 <col width="60%">

912 """

913 h += tr_qa(

914 self.wxstring(req, "discharge_date"),

915 format_datetime(

916 self.discharge_date,

917 DateFormat.LONG_DATE_WITH_DAY,

918 default=None,

919 ),

920 "",

921 )

922 h += tr_qa(

923 self.wxstring(req, "discharge_reason"),

924 self.get_discharge_reason(req),

925 "",

926 )

927 h += tr_qa(

928 self.wxstring(req, "leaflet_or_discharge_card_given"),

929 get_yes_no_none(req, self.leaflet_or_discharge_card_given),

930 "",

931 )

932 h += tr_qa(

933 self.wxstring(req, "frequent_attender"),

934 get_yes_no_none(req, self.frequent_attender),

935 "",

936 )

937 h += tr_qa(

938 self.wxstring(req, "patient_wanted_copy_of_letter"),

939 self.patient_wanted_copy_of_letter,

940 "",

941 )

942 h += tr_qa(

943 self.wxstring(req, "gaf_at_first_assessment"),

944 self.gaf_at_first_assessment,

945 "",

946 )

947 h += tr_qa(

948 self.wxstring(req, "gaf_at_discharge"), self.gaf_at_discharge, ""

949 )

950

951 h += subheading_spanning_two_columns(

952 self.wxstring(req, "referral_reason_t")

953 )

954 h += tr_span_col(

955 answer(", ".join(self.get_referral_reasons(req))), cols=2

956 )

957 h += tr_qa(

958 self.wxstring(req, "referral_reason_transplant_organ"),

959 self.referral_reason_transplant_organ,

960 "",

961 )

962 h += tr_qa(

963 self.wxstring(req, "referral_reason_other_detail"),

964 self.referral_reason_other_detail,

965 "",

966 )

967

968 h += subheading_spanning_two_columns(self.wxstring(req, "diagnoses_t"))

969 h += tr_qa(

970 self.wxstring(req, "psychiatric_t"),

971 "\n".join(self.get_psychiatric_diagnoses(req)),

972 "",

973 )

974 h += tr_qa(

975 self.wxstring(req, "medical_t"),

976 "\n".join(self.get_medical_diagnoses()),

977 "",

978 )

979

980 h += subheading_spanning_two_columns(

981 self.wxstring(req, "management_t")

982 )

983 h += tr_span_col(answer(", ".join(self.get_managements(req))), cols=2)

984 h += tr_qa(

985 self.wxstring(req, "management_other_detail"),

986 self.management_other_detail,

987 "",

988 )

989

990 h += subheading_spanning_two_columns(self.wxstring(req, "outcome_t"))

991 h += tr_qa(self.wxstring(req, "outcome_t"), self.outcome, "")

992 h += tr_qa(

993 self.wxstring(req, "outcome_hospital_transfer_detail"),

994 self.outcome_hospital_transfer_detail,

995 "",

996 )

997 h += tr_qa(

998 self.wxstring(req, "outcome_other_detail"),

999 self.outcome_other_detail,

1000 "",

1001 )

1002

1003 h += """

1004 </table>

1005 """

1006 return h

1007

1008

1009# =============================================================================

1010# Reports

1011# =============================================================================

1012

1013

1014 class LPSReportSchema(ReportParamSchema):

1015 which_idnum = LinkingIdNumSelector() # must match ViewParam.WHICH_IDNUM

1016

1017

1018 class LPSReportReferredNotDischarged(Report):

1019 # noinspection PyMethodParameters

1020 @classproperty

1021 def report_id(cls) -> str:

1022 return "cpft_lps_referred_not_subsequently_discharged"

1023

1024 @classmethod

1025 def title(cls, req: "CamcopsRequest") -> str:

1026 _ = req.gettext

1027 return _("CPFT LPS – referred but not yet discharged")

1028

1029 # noinspection PyMethodParameters

1030 @classproperty

1031 def superuser_only(cls) -> bool:

1032 return False

1033

1034 @staticmethod

1035 def get_paramform_schema_class() -> Type[ReportParamSchema]:

1036 return LPSReportSchema

1037

1038 # noinspection PyProtectedMember,PyUnresolvedReferences

1039 def get_query(self, req: CamcopsRequest) -> SelectBase:

1040 which_idnum = req.get_int_param(ViewParam.WHICH_IDNUM, 1)

1041 if which_idnum is None:

1042 raise exc.HTTPBadRequest(f"{ViewParam.WHICH_IDNUM} not specified")

1043

1044 group_ids = req.user.ids_of_groups_user_may_report_on

1045

1046 # Step 1: link referral and patient

1047 p1 = Patient.__table__.alias("p1")

1048 i1 = PatientIdNum.__table__.alias("i1")

1049 desc = req.get_id_shortdesc(which_idnum)

1050 select_fields = [

1051 CPFTLPSReferral.lps_division,

1052 CPFTLPSReferral.referral_date_time,

1053 CPFTLPSReferral.referral_priority,

1054 p1.c.surname,

1055 p1.c.forename,

1056 p1.c.dob,

1057 i1.c.idnum_value.label(desc),

1058 CPFTLPSReferral.patient_location,

1059 ]

1060 select_from = p1.join(

1061 CPFTLPSReferral.__table__,

1062 and_(

1063 p1.c._current == True, # noqa: E712

1064 CPFTLPSReferral.patient_id == p1.c.id,

1065 CPFTLPSReferral._device_id == p1.c._device_id,

1066 CPFTLPSReferral._era == p1.c._era,

1067 CPFTLPSReferral._current == True, # noqa: E712

1068 ),

1069 )

1070 select_from = select_from.join(

1071 i1,

1072 and_(

1073 i1.c.patient_id == p1.c.id,

1074 i1.c._device_id == p1.c._device_id,

1075 i1.c._era == p1.c._era,

1076 i1.c._current == True, # noqa: E712

1077 ),

1078 )

1079 wheres = [i1.c.which_idnum == which_idnum]

1080 if not req.user.superuser:

1081 # Restrict to accessible groups

1082 wheres.append(CPFTLPSReferral._group_id.in_(group_ids))

1083

1084 # Step 2: not yet discharged

1085 p2 = Patient.__table__.alias("p2")

1086 i2 = PatientIdNum.__table__.alias("i2")

1087 discharge = (

1088 select("*")

1089 .select_from(

1090 p2.join(

1091 CPFTLPSDischarge.__table__,

1092 and_(

1093 p2.c._current == True, # noqa: E712

1094 CPFTLPSDischarge.patient_id == p2.c.id,

1095 CPFTLPSDischarge._device_id == p2.c._device_id,

1096 CPFTLPSDischarge._era == p2.c._era,

1097 CPFTLPSDischarge._current == True, # noqa: E712

1098 ),

1099 ).join(

1100 i2,

1101 and_(

1102 i2.c.patient_id == p2.c.id,

1103 i2.c._device_id == p2.c._device_id,

1104 i2.c._era == p2.c._era,

1105 i2.c._current == True, # noqa: E712

1106 ),

1107 )

1108 )

1109 .where(

1110 and_(

1111 # Link on ID to main query: same patient

1112 i2.c.which_idnum == which_idnum,

1113 i2.c.idnum_value == i1.c.idnum_value,

1114 # Discharge later than referral

1115 (

1116 CPFTLPSDischarge.discharge_date

1117 >= CPFTLPSReferral.referral_date_time

1118 ),

1119 )

1120 )

1121 ) # nopep8

1122 if not req.user.superuser:

1123 # Restrict to accessible groups

1124 discharge = discharge.where(

1125 CPFTLPSDischarge._group_id.in_(group_ids)

1126 )

1127

1128 wheres.append(~exists(discharge))

1129

1130 # Finish up

1131 order_by = [

1132 CPFTLPSReferral.lps_division,

1133 CPFTLPSReferral.referral_date_time,

1134 CPFTLPSReferral.referral_priority,

1135 ]

1136 query = (

1137 select(*select_fields)

1138 .select_from(select_from)

1139 .where(and_(*wheres))

1140 .order_by(*order_by)

1141 )

1142 return query

1143

1144

1145 class LPSReportReferredNotClerkedOrDischarged(Report):

1146 # noinspection PyMethodParameters

1147 @classproperty

1148 def report_id(cls) -> str:

1149 return "cpft_lps_referred_not_subsequently_clerked_or_discharged"

1150

1151 @classmethod

1152 def title(cls, req: "CamcopsRequest") -> str:

1153 _ = req.gettext

1154 return _(

1155 "CPFT LPS – referred but not yet fully assessed or discharged"

1156 )

1157

1158 # noinspection PyMethodParameters

1159 @classproperty

1160 def superuser_only(cls) -> bool:

1161 return False

1162

1163 @staticmethod

1164 def get_paramform_schema_class() -> Type[ReportParamSchema]:

1165 return LPSReportSchema

1166

1167 # noinspection PyProtectedMember

1168 def get_query(self, req: CamcopsRequest) -> SelectBase:

1169 which_idnum = req.get_int_param(ViewParam.WHICH_IDNUM, 1)

1170 if which_idnum is None:

1171 raise exc.HTTPBadRequest(f"{ViewParam.WHICH_IDNUM} not specified")

1172

1173 group_ids = req.user.ids_of_groups_user_may_report_on

1174

1175 # Step 1: link referral and patient

1176 # noinspection PyUnresolvedReferences

1177 p1 = Patient.__table__.alias("p1")

1178 # noinspection PyUnresolvedReferences

1179 i1 = PatientIdNum.__table__.alias("i1")

1180 desc = req.get_id_shortdesc(which_idnum)

1181 select_fields = [

1182 CPFTLPSReferral.lps_division,

1183 CPFTLPSReferral.referral_date_time,

1184 CPFTLPSReferral.referral_priority,

1185 p1.c.surname,

1186 p1.c.forename,

1187 p1.c.dob,

1188 i1.c.idnum_value.label(desc),

1189 CPFTLPSReferral.patient_location,

1190 ]

1191 # noinspection PyUnresolvedReferences

1192 select_from = p1.join(

1193 CPFTLPSReferral.__table__,

1194 and_(

1195 p1.c._current == True, # noqa: E712

1196 CPFTLPSReferral.patient_id == p1.c.id,

1197 CPFTLPSReferral._device_id == p1.c._device_id,

1198 CPFTLPSReferral._era == p1.c._era,

1199 CPFTLPSReferral._current == True, # noqa: E712

1200 ),

1201 )

1202 select_from = select_from.join(

1203 i1,

1204 and_(

1205 i1.c.patient_id == p1.c.id,

1206 i1.c._device_id == p1.c._device_id,

1207 i1.c._era == p1.c._era,

1208 i1.c._current == True, # noqa: E712

1209 ),

1210 ) # nopep8

1211 wheres = [i1.c.which_idnum == which_idnum]

1212 if not req.user.superuser:

1213 # Restrict to accessible groups

1214 wheres.append(CPFTLPSReferral._group_id.in_(group_ids))

1215

1216 # Step 2: not yet discharged

1217 # noinspection PyUnresolvedReferences

1218 p2 = Patient.__table__.alias("p2")

1219 # noinspection PyUnresolvedReferences

1220 i2 = PatientIdNum.__table__.alias("i2")

1221 # noinspection PyUnresolvedReferences

1222 discharge = (

1223 select("*")

1224 .select_from(

1225 p2.join(

1226 CPFTLPSDischarge.__table__,

1227 and_(

1228 p2.c._current == True, # noqa: E712

1229 CPFTLPSDischarge.patient_id == p2.c.id,

1230 CPFTLPSDischarge._device_id == p2.c._device_id,

1231 CPFTLPSDischarge._era == p2.c._era,

1232 CPFTLPSDischarge._current == True, # noqa: E712

1233 ),

1234 ).join(

1235 i2,

1236 and_(

1237 i2.c.patient_id == p2.c.id,

1238 i2.c._device_id == p2.c._device_id,

1239 i2.c._era == p2.c._era,

1240 i2.c._current == True, # noqa: E712

1241 ),

1242 )

1243 )

1244 .where(

1245 and_(

1246 # Link on ID to main query: same patient

1247 i2.c.which_idnum == which_idnum,

1248 i2.c.idnum_value == i1.c.idnum_value,

1249 # Discharge later than referral

1250 (

1251 CPFTLPSDischarge.discharge_date

1252 >= CPFTLPSReferral.referral_date_time

1253 ),

1254 )

1255 )

1256 ) # nopep8

1257 if not req.user.superuser:

1258 # Restrict to accessible groups

1259 discharge = discharge.where(

1260 CPFTLPSDischarge._group_id.in_(group_ids)

1261 )

1262 wheres.append(~exists(discharge))

1263

1264 # Step 3: not yet clerked

1265 # noinspection PyUnresolvedReferences

1266 p3 = Patient.__table__.alias("p3")

1267 # noinspection PyUnresolvedReferences

1268 i3 = PatientIdNum.__table__.alias("i3")

1269 # noinspection PyUnresolvedReferences

1270 clerking = (

1271 select("*")

1272 .select_from(

1273 p3.join(

1274 PsychiatricClerking.__table__,

1275 and_(

1276 p3.c._current == True, # noqa: E712

1277 PsychiatricClerking.patient_id == p3.c.id,

1278 PsychiatricClerking._device_id == p3.c._device_id,

1279 PsychiatricClerking._era == p3.c._era,

1280 PsychiatricClerking._current == True, # noqa: E712

1281 ),

1282 ).join(

1283 i3,

1284 and_(

1285 i3.c.patient_id == p3.c.id,

1286 i3.c._device_id == p3.c._device_id,

1287 i3.c._era == p3.c._era,

1288 i3.c._current == True, # noqa: E712

1289 ),

1290 )

1291 )

1292 .where(

1293 and_(

1294 # Link on ID to main query: same patient

1295 i3.c.which_idnum == which_idnum,

1296 i3.c.idnum_value == i1.c.idnum_value,

1297 # Discharge later than referral

1298 (

1299 PsychiatricClerking.when_created

1300 >= CPFTLPSReferral.referral_date_time

1301 ),

1302 )

1303 )

1304 ) # nopep8

1305 if not req.user.superuser:

1306 # Restrict to accessible groups

1307 clerking = clerking.where(

1308 PsychiatricClerking._group_id.in_(group_ids)

1309 )

1310 wheres.append(~exists(clerking))

1311

1312 # Finish up

1313 order_by = [

1314 CPFTLPSReferral.lps_division,

1315 CPFTLPSReferral.referral_date_time,

1316 CPFTLPSReferral.referral_priority,

1317 ]

1318 query = (

1319 select(*select_fields)

1320 .select_from(select_from)

1321 .where(and_(*wheres))

1322 .order_by(*order_by)

1323 )

1324 return query