Coverage for tasks/suppsp.py: 60%

81 statements  

« prev     ^ index     » next       coverage.py v7.9.2, created at 2025-07-15 14:23 +0100

1""" 

2camcops_server/tasks/suppsp.py 

3 

4=============================================================================== 

5 

6 Copyright (C) 2012, University of Cambridge, Department of Psychiatry. 

7 Created by Rudolf Cardinal (rnc1001@cam.ac.uk). 

8 

9 This file is part of CamCOPS. 

10 

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. 

15 

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. 

20 

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/>. 

23 

24=============================================================================== 

25 

26**Short UPPS-P Impulsive Behaviour Scale (SUPPS-P) task.** 

27 

28""" 

29 

30from typing import Any, cast, List, Type 

31 

32from cardinal_pythonlib.stringfunc import strseq 

33from sqlalchemy import Integer 

34 

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) 

42 

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 

50 

51 

52class Suppsp( # type: ignore[misc] 

53 TaskHasPatientMixin, 

54 Task, 

55): 

56 __tablename__ = "suppsp" 

57 shortname = "SUPPS-P" 

58 

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 

67 

68 @classmethod 

69 def extend_columns(cls: Type["Suppsp"], **kwargs: Any) -> None: 

70 

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 ] 

93 

94 reverse_questions = {3, 6, 8, 9, 10, 13, 14, 15, 16, 17, 18, 20} 

95 

96 for q_index in range(0, cls.N_QUESTIONS): 

97 q_num = q_index + 1 

98 q_field = "q{}".format(q_num) 

99 

100 score_comment = "(1 strongly agree - 4 strongly disagree)" 

101 

102 if q_num in reverse_questions: 

103 score_comment = "(1 strongly disagree - 4 strongly agree)" 

104 

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 ) 

117 

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 ) 

132 

133 @staticmethod 

134 def longname(req: "CamcopsRequest") -> str: 

135 _ = req.gettext 

136 return _("Short UPPS-P Impulsive Behaviour Scale") 

137 

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 ] 

178 

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 

185 

186 def total_score(self) -> int: 

187 return cast(int, self.sum_fields(self.ALL_QUESTIONS)) 

188 

189 def negative_urgency_score(self) -> int: 

190 return cast(int, self.sum_fields(self.NEGATIVE_URGENCY_QUESTIONS)) 

191 

192 def lack_of_perseverance_score(self) -> int: 

193 return cast(int, self.sum_fields(self.LACK_OF_PERSEVERANCE_QUESTIONS)) 

194 

195 def lack_of_premeditation_score(self) -> int: 

196 return cast(int, self.sum_fields(self.LACK_OF_PREMEDITATION_QUESTIONS)) 

197 

198 def sensation_seeking_score(self) -> int: 

199 return cast(int, self.sum_fields(self.SENSATION_SEEKING_QUESTIONS)) 

200 

201 def positive_urgency_score(self) -> int: 

202 return cast(int, self.sum_fields(self.POSITIVE_URGENCY_QUESTIONS)) 

203 

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}]" 

222 

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)) 

227 

228 score = getattr(self, q_field) 

229 score_dict = normal_score_dict 

230 

231 if q_num in reverse_q_nums: 

232 score_dict = reverse_score_dict 

233 

234 answer_cell = get_from_dict(score_dict, score) 

235 

236 rows += tr_qa(question_cell, answer_cell) 

237 

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