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