Coverage for tasks/suppsp.py: 62%

84 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-11-08 23:14 +0000

1#!/usr/bin/env python 

2 

3""" 

4camcops_server/tasks/suppsp.py 

5 

6=============================================================================== 

7 

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

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

10 

11 This file is part of CamCOPS. 

12 

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. 

17 

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. 

22 

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

25 

26=============================================================================== 

27 

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

29 

30""" 

31 

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) 

39 

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 

51 

52 

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: 

61 

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 ] 

84 

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

86 

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

88 q_num = q_index + 1 

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

90 

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

92 

93 if q_num in reverse_questions: 

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

95 

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 ) 

108 

109 super().__init__(name, bases, classdict) 

110 

111 

112class Suppsp(TaskHasPatientMixin, Task, metaclass=SuppspMetaclass): 

113 __tablename__ = "suppsp" 

114 shortname = "SUPPS-P" 

115 

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 ) 

138 

139 @staticmethod 

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

141 _ = req.gettext 

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

143 

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 ] 

184 

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 

191 

192 def total_score(self) -> int: 

193 return self.sum_fields(self.ALL_QUESTIONS) 

194 

195 def negative_urgency_score(self) -> int: 

196 return self.sum_fields(self.NEGATIVE_URGENCY_QUESTIONS) 

197 

198 def lack_of_perseverance_score(self) -> int: 

199 return self.sum_fields(self.LACK_OF_PERSEVERANCE_QUESTIONS) 

200 

201 def lack_of_premeditation_score(self) -> int: 

202 return self.sum_fields(self.LACK_OF_PREMEDITATION_QUESTIONS) 

203 

204 def sensation_seeking_score(self) -> int: 

205 return self.sum_fields(self.SENSATION_SEEKING_QUESTIONS) 

206 

207 def positive_urgency_score(self) -> int: 

208 return self.sum_fields(self.POSITIVE_URGENCY_QUESTIONS) 

209 

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

228 

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

233 

234 score = getattr(self, q_field) 

235 score_dict = normal_score_dict 

236 

237 if q_num in reverse_q_nums: 

238 score_dict = reverse_score_dict 

239 

240 answer_cell = get_from_dict(score_dict, score) 

241 

242 rows += tr_qa(question_cell, answer_cell) 

243 

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