Coverage for tasks/suppsp.py : 62%

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