Coverage for tasks/pcl.py: 58%
111 statements
« prev ^ index » next coverage.py v6.5.0, created at 2022-11-08 23:14 +0000
« prev ^ index » next coverage.py v6.5.0, created at 2022-11-08 23:14 +0000
1#!/usr/bin/env python
3"""
4camcops_server/tasks/pcl.py
6===============================================================================
8 Copyright (C) 2012, University of Cambridge, Department of Psychiatry.
9 Created by Rudolf Cardinal (rnc1001@cam.ac.uk).
11 This file is part of CamCOPS.
13 CamCOPS is free software: you can redistribute it and/or modify
14 it under the terms of the GNU General Public License as published by
15 the Free Software Foundation, either version 3 of the License, or
16 (at your option) any later version.
18 CamCOPS is distributed in the hope that it will be useful,
19 but WITHOUT ANY WARRANTY; without even the implied warranty of
20 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 GNU General Public License for more details.
23 You should have received a copy of the GNU General Public License
24 along with CamCOPS. If not, see <https://www.gnu.org/licenses/>.
26===============================================================================
28"""
30from abc import ABCMeta, ABC
31from typing import Any, Dict, List, Tuple, Type
33from cardinal_pythonlib.stringfunc import strseq
34from sqlalchemy.ext.declarative import DeclarativeMeta
35from sqlalchemy.sql.schema import Column
36from sqlalchemy.sql.sqltypes import Boolean, Integer, UnicodeText
38from camcops_server.cc_modules.cc_constants import CssClass
39from camcops_server.cc_modules.cc_ctvinfo import CTV_INCOMPLETE, CtvInfo
40from camcops_server.cc_modules.cc_db import add_multiple_columns
41from camcops_server.cc_modules.cc_html import (
42 answer,
43 get_yes_no,
44 subheading_spanning_two_columns,
45 tr,
46 tr_qa,
47)
48from camcops_server.cc_modules.cc_request import CamcopsRequest
49from camcops_server.cc_modules.cc_summaryelement import SummaryElement
50from camcops_server.cc_modules.cc_task import (
51 get_from_dict,
52 Task,
53 TaskHasPatientMixin,
54)
55from camcops_server.cc_modules.cc_text import SS
56from camcops_server.cc_modules.cc_trackerhelpers import TrackerInfo
59# =============================================================================
60# PCL
61# =============================================================================
64class PclMetaclass(DeclarativeMeta, ABCMeta):
65 """
66 There is a multilayer metaclass problem; see hads.py for discussion.
67 """
69 # noinspection PyInitNewSignature
70 def __init__(
71 cls: Type["PclCommon"],
72 name: str,
73 bases: Tuple[Type, ...],
74 classdict: Dict[str, Any],
75 ) -> None:
76 add_multiple_columns(
77 cls,
78 "q",
79 1,
80 cls.NQUESTIONS,
81 minimum=1,
82 maximum=5,
83 comment_fmt="Q{n} ({s}) (1 not at all - 5 extremely)",
84 comment_strings=[
85 "disturbing memories/thoughts/images",
86 "disturbing dreams",
87 "reliving",
88 "upset at reminders",
89 "physical reactions to reminders",
90 "avoid thinking/talking/feelings relating to experience",
91 "avoid activities/situations because they remind",
92 "trouble remembering important parts of stressful event",
93 "loss of interest in previously enjoyed activities",
94 "feeling distant/cut off from people",
95 "feeling emotionally numb",
96 "feeling future will be cut short",
97 "hard to sleep",
98 "irritable",
99 "difficulty concentrating",
100 "super alert/on guard",
101 "jumpy/easily startled",
102 ],
103 )
104 super().__init__(name, bases, classdict)
107class PclCommon(TaskHasPatientMixin, Task, ABC, metaclass=PclMetaclass):
108 __abstract__ = True
109 provides_trackers = True
110 extrastring_taskname = "pcl"
111 info_filename_stem = extrastring_taskname
113 NQUESTIONS = 17
114 SCORED_FIELDS = strseq("q", 1, NQUESTIONS)
115 TASK_FIELDS = SCORED_FIELDS # may be overridden
116 TASK_TYPE = "?" # will be overridden
117 # ... not really used; we display the generic question forms on the server
118 MIN_SCORE = NQUESTIONS
119 MAX_SCORE = 5 * NQUESTIONS
121 def is_complete(self) -> bool:
122 return (
123 self.all_fields_not_none(self.TASK_FIELDS)
124 and self.field_contents_valid()
125 )
127 def total_score(self) -> int:
128 return self.sum_fields(self.SCORED_FIELDS)
130 def get_trackers(self, req: CamcopsRequest) -> List[TrackerInfo]:
131 return [
132 TrackerInfo(
133 value=self.total_score(),
134 plot_label="PCL total score",
135 axis_label=f"Total score ({self.MIN_SCORE}-{self.MAX_SCORE})",
136 axis_min=self.MIN_SCORE - 0.5,
137 axis_max=self.MAX_SCORE + 0.5,
138 )
139 ]
141 def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]:
142 if not self.is_complete():
143 return CTV_INCOMPLETE
144 return [CtvInfo(content=f"PCL total score {self.total_score()}")]
146 def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]:
147 return self.standard_task_summary_fields() + [
148 SummaryElement(
149 name="total",
150 coltype=Integer(),
151 value=self.total_score(),
152 comment=f"Total score ({self.MIN_SCORE}-{self.MAX_SCORE})",
153 ),
154 SummaryElement(
155 name="num_symptomatic",
156 coltype=Integer(),
157 value=self.num_symptomatic(),
158 comment="Total number of symptoms considered symptomatic "
159 "(meaning scoring 3 or more)",
160 ),
161 SummaryElement(
162 name="num_symptomatic_B",
163 coltype=Integer(),
164 value=self.num_symptomatic_b(),
165 comment="Number of group B symptoms considered symptomatic "
166 "(meaning scoring 3 or more)",
167 ),
168 SummaryElement(
169 name="num_symptomatic_C",
170 coltype=Integer(),
171 value=self.num_symptomatic_c(),
172 comment="Number of group C symptoms considered symptomatic "
173 "(meaning scoring 3 or more)",
174 ),
175 SummaryElement(
176 name="num_symptomatic_D",
177 coltype=Integer(),
178 value=self.num_symptomatic_d(),
179 comment="Number of group D symptoms considered symptomatic "
180 "(meaning scoring 3 or more)",
181 ),
182 SummaryElement(
183 name="ptsd",
184 coltype=Boolean(),
185 value=self.ptsd(),
186 comment="Meets DSM-IV criteria for PTSD",
187 ),
188 ]
190 def get_num_symptomatic(self, first: int, last: int) -> int:
191 n = 0
192 for i in range(first, last + 1):
193 value = getattr(self, "q" + str(i))
194 if value is not None and value >= 3:
195 n += 1
196 return n
198 def num_symptomatic(self) -> int:
199 return self.get_num_symptomatic(1, self.NQUESTIONS)
201 def num_symptomatic_b(self) -> int:
202 return self.get_num_symptomatic(1, 5)
204 def num_symptomatic_c(self) -> int:
205 return self.get_num_symptomatic(6, 12)
207 def num_symptomatic_d(self) -> int:
208 return self.get_num_symptomatic(13, 17)
210 def ptsd(self) -> bool:
211 num_symptomatic_b = self.num_symptomatic_b()
212 num_symptomatic_c = self.num_symptomatic_c()
213 num_symptomatic_d = self.num_symptomatic_d()
214 return (
215 num_symptomatic_b >= 1
216 and num_symptomatic_c >= 3
217 and num_symptomatic_d >= 2
218 )
220 def get_task_html(self, req: CamcopsRequest) -> str:
221 score = self.total_score()
222 num_symptomatic = self.num_symptomatic()
223 num_symptomatic_b = self.num_symptomatic_b()
224 num_symptomatic_c = self.num_symptomatic_c()
225 num_symptomatic_d = self.num_symptomatic_d()
226 ptsd = self.ptsd()
227 answer_dict = {None: None}
228 for option in range(1, 6):
229 answer_dict[option] = (
230 str(option)
231 + " – "
232 + self.wxstring(req, "option" + str(option))
233 )
234 q_a = ""
235 if hasattr(self, "event") and hasattr(self, "eventdate"):
236 # PCL-S
237 q_a += tr_qa(self.wxstring(req, "s_event_s"), self.event)
238 q_a += tr_qa(self.wxstring(req, "s_eventdate_s"), self.eventdate)
239 for q in range(1, self.NQUESTIONS + 1):
240 if q == 1 or q == 6 or q == 13:
241 section = "B" if q == 1 else ("C" if q == 6 else "D")
242 q_a += subheading_spanning_two_columns(
243 f"DSM-IV-TR section {section}"
244 )
245 q_a += tr_qa(
246 self.wxstring(req, "q" + str(q) + "_s"),
247 get_from_dict(answer_dict, getattr(self, "q" + str(q))),
248 )
249 h = """
250 <div class="{CssClass.SUMMARY}">
251 <table class="{CssClass.SUMMARY}">
252 {tr_is_complete}
253 {total_score}
254 {num_symptomatic}
255 {dsm_criteria_met}
256 </table>
257 </div>
258 <table class="{CssClass.TASKDETAIL}">
259 <tr>
260 <th width="70%">Question</th>
261 <th width="30%">Answer</th>
262 </tr>
263 {q_a}
264 </table>
265 <div class="{CssClass.FOOTNOTES}">
266 [1] Questions with scores ≥3 are considered symptomatic.
267 [2] ≥1 ‘B’ symptoms and ≥3 ‘C’ symptoms and
268 ≥2 ‘D’ symptoms.
269 </div>
270 """.format(
271 CssClass=CssClass,
272 tr_is_complete=self.get_is_complete_tr(req),
273 total_score=tr_qa(f"{req.sstring(SS.TOTAL_SCORE)} (17–85)", score),
274 num_symptomatic=tr(
275 "Number symptomatic <sup>[1]</sup>: B, C, D (total)",
276 answer(num_symptomatic_b)
277 + ", "
278 + answer(num_symptomatic_c)
279 + ", "
280 + answer(num_symptomatic_d)
281 + " ("
282 + answer(num_symptomatic)
283 + ")",
284 ),
285 dsm_criteria_met=tr_qa(
286 self.wxstring(req, "dsm_criteria_met") + " <sup>[2]</sup>",
287 get_yes_no(req, ptsd),
288 ),
289 q_a=q_a,
290 )
291 return h
294# =============================================================================
295# PCL-C
296# =============================================================================
299class PclC(PclCommon, metaclass=PclMetaclass):
300 """
301 Server implementation of the PCL-C task.
302 """
304 __tablename__ = "pclc"
305 shortname = "PCL-C"
307 TASK_TYPE = "C"
309 @staticmethod
310 def longname(req: "CamcopsRequest") -> str:
311 _ = req.gettext
312 return _("PTSD Checklist, Civilian version")
315# =============================================================================
316# PCL-M
317# =============================================================================
320class PclM(PclCommon, metaclass=PclMetaclass):
321 """
322 Server implementation of the PCL-M task.
323 """
325 __tablename__ = "pclm"
326 shortname = "PCL-M"
328 TASK_TYPE = "M"
330 @staticmethod
331 def longname(req: "CamcopsRequest") -> str:
332 _ = req.gettext
333 return _("PTSD Checklist, Military version")
336# =============================================================================
337# PCL-S
338# =============================================================================
341class PclS(PclCommon, metaclass=PclMetaclass):
342 """
343 Server implementation of the PCL-S task.
344 """
346 __tablename__ = "pcls"
347 shortname = "PCL-S"
349 event = Column("event", UnicodeText, comment="Traumatic event")
350 eventdate = Column(
351 "eventdate", UnicodeText, comment="Date of traumatic event (free text)"
352 )
354 TASK_FIELDS = PclCommon.SCORED_FIELDS + ["event", "eventdate"]
355 TASK_TYPE = "S"
357 @staticmethod
358 def longname(req: "CamcopsRequest") -> str:
359 _ = req.gettext
360 return _("PTSD Checklist, Stressor-specific version")