Coverage for tasks/cpft_lps.py: 51%
367 statements
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-15 14:23 +0100
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-15 14:23 +0100
1"""
2camcops_server/tasks/cpft_lps.py
4===============================================================================
6 Copyright (C) 2012, University of Cambridge, Department of Psychiatry.
7 Created by Rudolf Cardinal (rnc1001@cam.ac.uk).
9 This file is part of CamCOPS.
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.
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.
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/>.
24===============================================================================
26"""
28import datetime
29import logging
30from typing import Any, List, Optional, Type
32from cardinal_pythonlib.classes import classproperty
33from cardinal_pythonlib.datetimefunc import format_datetime
34from cardinal_pythonlib.logs import BraceStyleAdapter
35import cardinal_pythonlib.rnc_web as ws
36from pendulum import DateTime as Pendulum
37import pyramid.httpexceptions as exc
38from sqlalchemy.sql.expression import and_, exists, select
39from sqlalchemy.sql.selectable import SelectBase
40from sqlalchemy.orm import Mapped, mapped_column
41from sqlalchemy.sql.sqltypes import UnicodeText
43from camcops_server.cc_modules.cc_constants import (
44 CssClass,
45 DateFormat,
46 INVALID_VALUE,
47)
48from camcops_server.cc_modules.cc_ctvinfo import CtvInfo
49from camcops_server.cc_modules.cc_forms import (
50 LinkingIdNumSelector,
51 ReportParamSchema,
52)
53from 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)
61from 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)
67from camcops_server.cc_modules.cc_patient import Patient
68from camcops_server.cc_modules.cc_patientidnum import PatientIdNum
69from camcops_server.cc_modules.cc_pyramid import ViewParam
70from camcops_server.cc_modules.cc_report import Report
71from camcops_server.cc_modules.cc_request import CamcopsRequest
72from 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)
80from camcops_server.cc_modules.cc_task import (
81 Task,
82 TaskHasClinicianMixin,
83 TaskHasPatientMixin,
84)
85from camcops_server.tasks.psychiatricclerking import PsychiatricClerking
87log = BraceStyleAdapter(logging.getLogger(__name__))
90# =============================================================================
91# CPFT_LPS_Referral
92# =============================================================================
95class CPFTLPSReferral(TaskHasPatientMixin, Task): # type: ignore[misc]
96 """
97 Server implementation of the CPFT_LPS_Referral task.
98 """
100 __tablename__ = "cpft_lps_referral"
101 shortname = "CPFT_LPS_Referral"
102 info_filename_stem = "clinical"
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)
188 @staticmethod
189 def longname(req: "CamcopsRequest") -> str:
190 _ = req.gettext
191 return _("CPFT LPS – referral")
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 )
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 ]
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 """
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 """
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
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
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))
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
397# =============================================================================
398# CPFT_LPS_ResetResponseClock
399# =============================================================================
402class CPFTLPSResetResponseClock( # type: ignore[misc]
403 TaskHasPatientMixin, TaskHasClinicianMixin, Task
404):
405 """
406 Server implementation of the CPFT_LPS_ResetResponseClock task.
407 """
409 __tablename__ = "cpft_lps_resetresponseclock"
410 shortname = "CPFT_LPS_ResetResponseClock"
411 info_filename_stem = "clinical"
413 reset_start_time_to: Mapped[Optional[Pendulum]] = mapped_column(
414 PendulumDateTimeAsIsoTextColType
415 )
416 reason: Mapped[Optional[str]] = mapped_column(UnicodeText)
418 @staticmethod
419 def longname(req: "CamcopsRequest") -> str:
420 _ = req.gettext
421 return _("CPFT LPS – reset response clock")
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 )
430 def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]:
431 return [CtvInfo(content=self.reason)]
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
459# =============================================================================
460# CPFT_LPS_Discharge
461# =============================================================================
464class CPFTLPSDischarge(TaskHasPatientMixin, TaskHasClinicianMixin, Task): # type: ignore[misc] # noqa: E501
465 """
466 Server implementation of the CPFT_LPS_Discharge task.
467 """
469 __tablename__ = "cpft_lps_discharge"
470 shortname = "CPFT_LPS_Discharge"
471 info_filename_stem = "clinical"
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 )
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 )
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 )
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)
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)
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)
755 @staticmethod
756 def longname(req: "CamcopsRequest") -> str:
757 _ = req.gettext
758 return _("CPFT LPS – discharge")
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 )
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
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
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
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
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
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 ]
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 )
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 )
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 )
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 )
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 )
1003 h += """
1004 </table>
1005 """
1006 return h
1009# =============================================================================
1010# Reports
1011# =============================================================================
1014class LPSReportSchema(ReportParamSchema):
1015 which_idnum = LinkingIdNumSelector() # must match ViewParam.WHICH_IDNUM
1018class LPSReportReferredNotDischarged(Report):
1019 # noinspection PyMethodParameters
1020 @classproperty
1021 def report_id(cls) -> str:
1022 return "cpft_lps_referred_not_subsequently_discharged"
1024 @classmethod
1025 def title(cls, req: "CamcopsRequest") -> str:
1026 _ = req.gettext
1027 return _("CPFT LPS – referred but not yet discharged")
1029 # noinspection PyMethodParameters
1030 @classproperty
1031 def superuser_only(cls) -> bool:
1032 return False
1034 @staticmethod
1035 def get_paramform_schema_class() -> Type[ReportParamSchema]:
1036 return LPSReportSchema
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")
1044 group_ids = req.user.ids_of_groups_user_may_report_on
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))
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 )
1128 wheres.append(~exists(discharge))
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
1145class LPSReportReferredNotClerkedOrDischarged(Report):
1146 # noinspection PyMethodParameters
1147 @classproperty
1148 def report_id(cls) -> str:
1149 return "cpft_lps_referred_not_subsequently_clerked_or_discharged"
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 )
1158 # noinspection PyMethodParameters
1159 @classproperty
1160 def superuser_only(cls) -> bool:
1161 return False
1163 @staticmethod
1164 def get_paramform_schema_class() -> Type[ReportParamSchema]:
1165 return LPSReportSchema
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")
1173 group_ids = req.user.ids_of_groups_user_may_report_on
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))
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))
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))
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