Coverage for tasks/pcl5.py : 50%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1#!/usr/bin/env python
3"""
4camcops_server/tasks/pcl5.py
6===============================================================================
8 Copyright (C) 2012-2020 Rudolf Cardinal (rudolf@pobox.com).
10 This file is part of CamCOPS.
12 CamCOPS is free software: you can redistribute it and/or modify
13 it under the terms of the GNU General Public License as published by
14 the Free Software Foundation, either version 3 of the License, or
15 (at your option) any later version.
17 CamCOPS is distributed in the hope that it will be useful,
18 but WITHOUT ANY WARRANTY; without even the implied warranty of
19 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 GNU General Public License for more details.
22 You should have received a copy of the GNU General Public License
23 along with CamCOPS. If not, see <https://www.gnu.org/licenses/>.
25===============================================================================
27"""
29from typing import Any, Dict, List, Tuple, Type
31from cardinal_pythonlib.classes import classproperty
32from cardinal_pythonlib.stringfunc import strseq
33from semantic_version import Version
34from sqlalchemy.ext.declarative import DeclarativeMeta
35from sqlalchemy.sql.sqltypes import Boolean, Integer
37from camcops_server.cc_modules.cc_constants import CssClass
38from camcops_server.cc_modules.cc_ctvinfo import CtvInfo, CTV_INCOMPLETE
39from camcops_server.cc_modules.cc_db import add_multiple_columns
40from camcops_server.cc_modules.cc_html import (
41 answer, get_yes_no, subheading_spanning_two_columns, tr, tr_qa
42)
43from camcops_server.cc_modules.cc_request import CamcopsRequest
45from camcops_server.cc_modules.cc_summaryelement import SummaryElement
46from camcops_server.cc_modules.cc_task import (
47 get_from_dict,
48 Task,
49 TaskHasPatientMixin,
50)
51from camcops_server.cc_modules.cc_text import SS
52from camcops_server.cc_modules.cc_trackerhelpers import (
53 equally_spaced_int,
54 regular_tracker_axis_ticks_int,
55 TrackerInfo,
56 TrackerLabel,
57)
60# =============================================================================
61# PCL-5
62# =============================================================================
64class Pcl5Metaclass(DeclarativeMeta):
65 """
66 There is a multilayer metaclass problem; see hads.py for discussion.
67 """
68 # noinspection PyInitNewSignature
69 def __init__(cls: Type['Pcl5'],
70 name: str,
71 bases: Tuple[Type, ...],
72 classdict: Dict[str, Any]) -> None:
73 add_multiple_columns(
74 cls, "q", 1, cls.N_QUESTIONS,
75 minimum=0, maximum=4,
76 comment_fmt="Q{n} ({s}) (0 not at all - 4 extremely)",
77 comment_strings=[
78 "disturbing memories/thoughts/images",
79 "disturbing dreams",
80 "reliving",
81 "upset at reminders",
82 "physical reactions to reminders",
83 "avoid thinking/talking/feelings relating to experience",
84 "avoid activities/situations because they remind",
85 "trouble remembering important parts of stressful event",
86 "strong negative beliefs about self/others/world",
87 "blaming",
88 "strong negative emotions",
89 "loss of interest in previously enjoyed activities",
90 "feeling distant / cut off from people",
91 "feeling emotionally numb",
92 "irritable, angry and/or aggressive",
93 "risk-taking and/or self-harming behaviour",
94 "super alert/on guard",
95 "jumpy/easily startled",
96 "difficulty concentrating",
97 "hard to sleep",
98 ]
99 )
100 super().__init__(name, bases, classdict)
103class Pcl5(TaskHasPatientMixin, Task,
104 metaclass=Pcl5Metaclass):
105 """
106 Server implementation of the PCL-5 task.
107 """
108 __tablename__ = 'pcl5'
109 shortname = 'PCL-5'
110 provides_trackers = True
111 extrastring_taskname = "pcl5"
112 N_QUESTIONS = 20
113 SCORED_FIELDS = strseq("q", 1, N_QUESTIONS)
114 TASK_FIELDS = SCORED_FIELDS # may be overridden
115 MIN_SCORE = 0
116 MAX_SCORE = 4 * N_QUESTIONS
118 @staticmethod
119 def longname(req: "CamcopsRequest") -> str:
120 _ = req.gettext
121 return _('PTSD Checklist, DSM-5 version')
123 # noinspection PyMethodParameters
124 @classproperty
125 def minimum_client_version(cls) -> Version:
126 return Version("2.2.8")
128 def is_complete(self) -> bool:
129 return (
130 self.all_fields_not_none(self.TASK_FIELDS) and
131 self.field_contents_valid()
132 )
134 def total_score(self) -> int:
135 return self.sum_fields(self.SCORED_FIELDS)
137 def get_trackers(self, req: CamcopsRequest) -> List[TrackerInfo]:
138 line_step = 20
139 preliminary_cutoff = 33
140 return [TrackerInfo(
141 value=self.total_score(),
142 plot_label="PCL-5 total score",
143 axis_label=f"Total score ({self.MIN_SCORE}-{self.MAX_SCORE})",
144 axis_min=self.MIN_SCORE - 0.5,
145 axis_max=self.MAX_SCORE + 0.5,
146 axis_ticks=regular_tracker_axis_ticks_int(
147 self.MIN_SCORE,
148 self.MAX_SCORE,
149 step=line_step
150 ),
151 horizontal_lines=equally_spaced_int(
152 self.MIN_SCORE + line_step,
153 self.MAX_SCORE - line_step,
154 step=line_step
155 ) + [preliminary_cutoff],
156 horizontal_labels=[
157 TrackerLabel(preliminary_cutoff,
158 self.wxstring(req, "preliminary_cutoff")),
159 ]
160 )]
162 def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]:
163 if not self.is_complete():
164 return CTV_INCOMPLETE
165 return [CtvInfo(
166 content=f"PCL-5 total score {self.total_score()}"
167 )]
169 def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]:
170 return self.standard_task_summary_fields() + [
171 SummaryElement(
172 name="total",
173 coltype=Integer(),
174 value=self.total_score(),
175 comment=f"Total score ({self.MIN_SCORE}-{self.MAX_SCORE})"),
176 SummaryElement(
177 name="num_symptomatic",
178 coltype=Integer(),
179 value=self.num_symptomatic(),
180 comment="Total number of symptoms considered symptomatic "
181 "(meaning scoring 2 or more)"),
182 SummaryElement(
183 name="num_symptomatic_B",
184 coltype=Integer(),
185 value=self.num_symptomatic_b(),
186 comment="Number of group B symptoms considered symptomatic "
187 "(meaning scoring 2 or more)"),
188 SummaryElement(
189 name="num_symptomatic_C",
190 coltype=Integer(),
191 value=self.num_symptomatic_c(),
192 comment="Number of group C symptoms considered symptomatic "
193 "(meaning scoring 2 or more)"),
194 SummaryElement(
195 name="num_symptomatic_D",
196 coltype=Integer(),
197 value=self.num_symptomatic_d(),
198 comment="Number of group D symptoms considered symptomatic "
199 "(meaning scoring 2 or more)"),
200 SummaryElement(
201 name="num_symptomatic_E",
202 coltype=Integer(),
203 value=self.num_symptomatic_e(),
204 comment="Number of group D symptoms considered symptomatic "
205 "(meaning scoring 2 or more)"),
206 SummaryElement(
207 name="ptsd",
208 coltype=Boolean(),
209 value=self.ptsd(),
210 comment="Provisionally meets DSM-5 criteria for PTSD"),
211 ]
213 def get_num_symptomatic(self, first: int, last: int) -> int:
214 n = 0
215 for i in range(first, last + 1):
216 value = getattr(self, "q" + str(i))
217 if value is not None and value >= 2:
218 n += 1
219 return n
221 def num_symptomatic(self) -> int:
222 return self.get_num_symptomatic(1, self.N_QUESTIONS)
224 def num_symptomatic_b(self) -> int:
225 return self.get_num_symptomatic(1, 5)
227 def num_symptomatic_c(self) -> int:
228 return self.get_num_symptomatic(6, 7)
230 def num_symptomatic_d(self) -> int:
231 return self.get_num_symptomatic(8, 14)
233 def num_symptomatic_e(self) -> int:
234 return self.get_num_symptomatic(15, 20)
236 def ptsd(self) -> bool:
237 num_symptomatic_b = self.num_symptomatic_b()
238 num_symptomatic_c = self.num_symptomatic_c()
239 num_symptomatic_d = self.num_symptomatic_d()
240 num_symptomatic_e = self.num_symptomatic_e()
241 return (
242 num_symptomatic_b >= 1 and
243 num_symptomatic_c >= 1 and
244 num_symptomatic_d >= 2 and
245 num_symptomatic_e >= 2
246 )
248 def get_task_html(self, req: CamcopsRequest) -> str:
249 score = self.total_score()
250 num_symptomatic = self.num_symptomatic()
251 num_symptomatic_b = self.num_symptomatic_b()
252 num_symptomatic_c = self.num_symptomatic_c()
253 num_symptomatic_d = self.num_symptomatic_d()
254 num_symptomatic_e = self.num_symptomatic_e()
255 ptsd = self.ptsd()
256 answer_dict = {None: None}
257 for option in range(5):
258 answer_dict[option] = str(option) + " – " + \
259 self.wxstring(req, "a" + str(option))
260 q_a = ""
262 section_start = {
263 1: 'B (intrusion symptoms)',
264 6: 'C (avoidance)',
265 8: 'D (negative cognition/mood)',
266 15: 'E (arousal/reactivity)'
267 }
269 for q in range(1, self.N_QUESTIONS + 1):
270 if q in section_start:
271 section = section_start[q]
272 q_a += subheading_spanning_two_columns(
273 f"DSM-5 section {section}"
274 )
276 q_a += tr_qa(
277 self.wxstring(req, "q" + str(q) + "_s"),
278 get_from_dict(answer_dict, getattr(self, "q" + str(q)))
279 )
281 h = """
282 <div class="{CssClass.SUMMARY}">
283 <table class="{CssClass.SUMMARY}">
284 {tr_is_complete}
285 {total_score}
286 {num_symptomatic}
287 {dsm_criteria_met}
288 </table>
289 </div>
290 <table class="{CssClass.TASKDETAIL}">
291 <tr>
292 <th width="70%">Question</th>
293 <th width="30%">Answer</th>
294 </tr>
295 {q_a}
296 </table>
297 <div class="{CssClass.FOOTNOTES}">
298 [1] Questions with scores ≥2 are considered symptomatic; see
299 https://www.ptsd.va.gov/professional/assessment/adult-sr/ptsd-checklist.asp
300 [2] ≥1 ‘B’ symptoms and ≥1 ‘C’ symptoms and ≥2 ‘D’ symptoms
301 and ≥2 ‘E’ symptoms.
302 </div>
303 """.format( # noqa
304 CssClass=CssClass,
305 tr_is_complete=self.get_is_complete_tr(req),
306 total_score=tr_qa(
307 f"{req.sstring(SS.TOTAL_SCORE)} (0–80)",
308 score
309 ),
310 num_symptomatic=tr(
311 "Number symptomatic <sup>[1]</sup>: B, C, D, E (total)",
312 answer(num_symptomatic_b) + ", " +
313 answer(num_symptomatic_c) + ", " +
314 answer(num_symptomatic_d) + ", " +
315 answer(num_symptomatic_e) + " (" + answer(num_symptomatic) + ")" # noqa
316 ),
317 dsm_criteria_met=tr_qa(
318 self.wxstring(req, "dsm_criteria_met") + " <sup>[2]</sup>",
319 get_yes_no(req, ptsd)
320 ),
321 q_a=q_a,
322 )
323 return h