Coverage for tasks/suppsp.py: 60%
81 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/suppsp.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**Short UPPS-P Impulsive Behaviour Scale (SUPPS-P) task.**
28"""
30from typing import Any, cast, List, Type
32from cardinal_pythonlib.stringfunc import strseq
33from sqlalchemy import Integer
35from camcops_server.cc_modules.cc_constants import CssClass
36from camcops_server.cc_modules.cc_html import tr_qa, tr, answer
37from camcops_server.cc_modules.cc_request import CamcopsRequest
38from camcops_server.cc_modules.cc_sqla_coltypes import (
39 camcops_column,
40 ONE_TO_FOUR_CHECKER,
41)
43from camcops_server.cc_modules.cc_summaryelement import SummaryElement
44from camcops_server.cc_modules.cc_task import (
45 TaskHasPatientMixin,
46 Task,
47 get_from_dict,
48)
49from camcops_server.cc_modules.cc_text import SS
52class Suppsp( # type: ignore[misc]
53 TaskHasPatientMixin,
54 Task,
55):
56 __tablename__ = "suppsp"
57 shortname = "SUPPS-P"
59 N_QUESTIONS = 20
60 MIN_SCORE_PER_Q = 1
61 MAX_SCORE_PER_Q = 4
62 MIN_SCORE = MIN_SCORE_PER_Q * N_QUESTIONS
63 MAX_SCORE = MAX_SCORE_PER_Q * N_QUESTIONS
64 N_Q_PER_SUBSCALE = 4 # always
65 MIN_SUBSCALE = MIN_SCORE_PER_Q * N_Q_PER_SUBSCALE
66 MAX_SUBSCALE = MAX_SCORE_PER_Q * N_Q_PER_SUBSCALE
68 @classmethod
69 def extend_columns(cls: Type["Suppsp"], **kwargs: Any) -> None:
71 comment_strings = [
72 "see to end",
73 "careful and purposeful",
74 "problem situations",
75 "unfinished bother",
76 "stop and think",
77 "do things regret",
78 "hate to stop",
79 "can't stop what I'm doing",
80 "enjoy risks",
81 "lose control",
82 "finish",
83 "rational sensible",
84 "act without thinking upset",
85 "new and exciting",
86 "say things regret",
87 "airplane",
88 "others shocked",
89 "skiing",
90 "think carefully",
91 "act without thinking excited",
92 ]
94 reverse_questions = {3, 6, 8, 9, 10, 13, 14, 15, 16, 17, 18, 20}
96 for q_index in range(0, cls.N_QUESTIONS):
97 q_num = q_index + 1
98 q_field = "q{}".format(q_num)
100 score_comment = "(1 strongly agree - 4 strongly disagree)"
102 if q_num in reverse_questions:
103 score_comment = "(1 strongly disagree - 4 strongly agree)"
105 setattr(
106 cls,
107 q_field,
108 camcops_column(
109 q_field,
110 Integer,
111 permitted_value_checker=ONE_TO_FOUR_CHECKER,
112 comment="Q{} ({}) {}".format(
113 q_num, comment_strings[q_index], score_comment
114 ),
115 ),
116 )
118 ALL_QUESTIONS = strseq("q", 1, N_QUESTIONS)
119 NEGATIVE_URGENCY_QUESTIONS = Task.fieldnames_from_list("q", {6, 8, 13, 15})
120 LACK_OF_PERSEVERANCE_QUESTIONS = Task.fieldnames_from_list(
121 "q", {1, 4, 7, 11}
122 )
123 LACK_OF_PREMEDITATION_QUESTIONS = Task.fieldnames_from_list(
124 "q", {2, 5, 12, 19}
125 )
126 SENSATION_SEEKING_QUESTIONS = Task.fieldnames_from_list(
127 "q", {9, 14, 16, 18}
128 )
129 POSITIVE_URGENCY_QUESTIONS = Task.fieldnames_from_list(
130 "q", {3, 10, 17, 20}
131 )
133 @staticmethod
134 def longname(req: "CamcopsRequest") -> str:
135 _ = req.gettext
136 return _("Short UPPS-P Impulsive Behaviour Scale")
138 def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]:
139 subscale_range = f"[{self.MIN_SUBSCALE}–{self.MAX_SUBSCALE}]"
140 return self.standard_task_summary_fields() + [
141 SummaryElement(
142 name="total",
143 coltype=Integer(),
144 value=self.total_score(),
145 comment=f"Total score [{self.MIN_SCORE}–{self.MAX_SCORE}]",
146 ),
147 SummaryElement(
148 name="negative_urgency",
149 coltype=Integer(),
150 value=self.negative_urgency_score(),
151 comment=f"Negative urgency {subscale_range}",
152 ),
153 SummaryElement(
154 name="lack_of_perseverance",
155 coltype=Integer(),
156 value=self.lack_of_perseverance_score(),
157 comment=f"Lack of perseverance {subscale_range}",
158 ),
159 SummaryElement(
160 name="lack_of_premeditation",
161 coltype=Integer(),
162 value=self.lack_of_premeditation_score(),
163 comment=f"Lack of premeditation {subscale_range}",
164 ),
165 SummaryElement(
166 name="sensation_seeking",
167 coltype=Integer(),
168 value=self.sensation_seeking_score(),
169 comment=f"Sensation seeking {subscale_range}",
170 ),
171 SummaryElement(
172 name="positive_urgency",
173 coltype=Integer(),
174 value=self.positive_urgency_score(),
175 comment=f"Positive urgency {subscale_range}",
176 ),
177 ]
179 def is_complete(self) -> bool:
180 if self.any_fields_none(self.ALL_QUESTIONS):
181 return False
182 if not self.field_contents_valid():
183 return False
184 return True
186 def total_score(self) -> int:
187 return cast(int, self.sum_fields(self.ALL_QUESTIONS))
189 def negative_urgency_score(self) -> int:
190 return cast(int, self.sum_fields(self.NEGATIVE_URGENCY_QUESTIONS))
192 def lack_of_perseverance_score(self) -> int:
193 return cast(int, self.sum_fields(self.LACK_OF_PERSEVERANCE_QUESTIONS))
195 def lack_of_premeditation_score(self) -> int:
196 return cast(int, self.sum_fields(self.LACK_OF_PREMEDITATION_QUESTIONS))
198 def sensation_seeking_score(self) -> int:
199 return cast(int, self.sum_fields(self.SENSATION_SEEKING_QUESTIONS))
201 def positive_urgency_score(self) -> int:
202 return cast(int, self.sum_fields(self.POSITIVE_URGENCY_QUESTIONS))
204 def get_task_html(self, req: CamcopsRequest) -> str:
205 normal_score_dict = {
206 None: None,
207 1: "1 — " + self.wxstring(req, "a0"),
208 2: "2 — " + self.wxstring(req, "a1"),
209 3: "3 — " + self.wxstring(req, "a2"),
210 4: "4 — " + self.wxstring(req, "a3"),
211 }
212 reverse_score_dict = {
213 None: None,
214 4: "4 — " + self.wxstring(req, "a0"),
215 3: "3 — " + self.wxstring(req, "a1"),
216 2: "2 — " + self.wxstring(req, "a2"),
217 1: "1 — " + self.wxstring(req, "a3"),
218 }
219 reverse_q_nums = {3, 6, 8, 9, 10, 13, 14, 15, 16, 17, 18, 20}
220 fullscale_range = f"[{self.MIN_SCORE}–{self.MAX_SCORE}]"
221 subscale_range = f"[{self.MIN_SUBSCALE}–{self.MAX_SUBSCALE}]"
223 rows = ""
224 for q_num in range(1, self.N_QUESTIONS + 1):
225 q_field = "q" + str(q_num)
226 question_cell = "{}. {}".format(q_num, self.wxstring(req, q_field))
228 score = getattr(self, q_field)
229 score_dict = normal_score_dict
231 if q_num in reverse_q_nums:
232 score_dict = reverse_score_dict
234 answer_cell = get_from_dict(score_dict, score)
236 rows += tr_qa(question_cell, answer_cell)
238 html = """
239 <div class="{CssClass.SUMMARY}">
240 <table class="{CssClass.SUMMARY}">
241 {tr_is_complete}
242 {total_score}
243 {negative_urgency_score}
244 {lack_of_perseverance_score}
245 {lack_of_premeditation_score}
246 {sensation_seeking_score}
247 {positive_urgency_score}
248 </table>
249 </div>
250 <table class="{CssClass.TASKDETAIL}">
251 <tr>
252 <th width="60%">Question</th>
253 <th width="40%">Score</th>
254 </tr>
255 {rows}
256 </table>
257 <div class="{CssClass.FOOTNOTES}">
258 [1] Sum for questions 1–20.
259 [2] Sum for questions 6, 8, 13, 15.
260 [3] Sum for questions 1, 4, 7, 11.
261 [4] Sum for questions 2, 5, 12, 19.
262 [5] Sum for questions 9, 14, 16, 18.
263 [6] Sum for questions 3, 10, 17, 20.
264 </div>
265 """.format(
266 CssClass=CssClass,
267 tr_is_complete=self.get_is_complete_tr(req),
268 total_score=tr(
269 req.sstring(SS.TOTAL_SCORE) + " <sup>[1]</sup>",
270 f"{answer(self.total_score())} {fullscale_range}",
271 ),
272 negative_urgency_score=tr(
273 self.wxstring(req, "negative_urgency") + " <sup>[2]</sup>",
274 f"{answer(self.negative_urgency_score())} {subscale_range}",
275 ),
276 lack_of_perseverance_score=tr(
277 self.wxstring(req, "lack_of_perseverance") + " <sup>[3]</sup>",
278 f"{answer(self.lack_of_perseverance_score())} {subscale_range}", # noqa: E501
279 ),
280 lack_of_premeditation_score=tr(
281 self.wxstring(req, "lack_of_premeditation")
282 + " <sup>[4]</sup>",
283 f"{answer(self.lack_of_premeditation_score())} {subscale_range}", # noqa: E501
284 ),
285 sensation_seeking_score=tr(
286 self.wxstring(req, "sensation_seeking") + " <sup>[5]</sup>",
287 f"{answer(self.sensation_seeking_score())} {subscale_range}",
288 ),
289 positive_urgency_score=tr(
290 self.wxstring(req, "positive_urgency") + " <sup>[6]</sup>",
291 f"{answer(self.positive_urgency_score())} {subscale_range}",
292 ),
293 rows=rows,
294 )
295 return html