Hide keyboard shortcuts

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 

2 

3""" 

4camcops_server/tasks/suppsp.py 

5 

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

7 

8 Copyright (C) 2012-2020 Rudolf Cardinal (rudolf@pobox.com). 

9 

10 This file is part of CamCOPS. 

11 

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. 

16 

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. 

21 

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

24 

25=============================================================================== 

26 

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

28 

29""" 

30 

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) 

38 

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 

50 

51 

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: 

58 

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 ] 

81 

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

83 

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

85 q_num = q_index + 1 

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

87 

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

89 

90 if q_num in reverse_questions: 

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

92 

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

99 

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

101 

102 

103class Suppsp(TaskHasPatientMixin, 

104 Task, 

105 metaclass=SuppspMetaclass): 

106 __tablename__ = "suppsp" 

107 shortname = "SUPPS-P" 

108 

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

128 

129 @staticmethod 

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

131 _ = req.gettext 

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

133 

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 ] 

162 

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 

169 

170 def total_score(self) -> int: 

171 return self.sum_fields(self.ALL_QUESTIONS) 

172 

173 def negative_urgency_score(self) -> int: 

174 return self.sum_fields(self.NEGATIVE_URGENCY_QUESTIONS) 

175 

176 def lack_of_perseverance_score(self) -> int: 

177 return self.sum_fields(self.LACK_OF_PERSEVERANCE_QUESTIONS) 

178 

179 def lack_of_premeditation_score(self) -> int: 

180 return self.sum_fields(self.LACK_OF_PREMEDITATION_QUESTIONS) 

181 

182 def sensation_seeking_score(self) -> int: 

183 return self.sum_fields(self.SENSATION_SEEKING_QUESTIONS) 

184 

185 def positive_urgency_score(self) -> int: 

186 return self.sum_fields(self.POSITIVE_URGENCY_QUESTIONS) 

187 

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

206 

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

211 

212 score = getattr(self, q_field) 

213 score_dict = normal_score_dict 

214 

215 if q_num in reverse_q_nums: 

216 score_dict = reverse_score_dict 

217 

218 answer_cell = get_from_dict(score_dict, score) 

219 

220 rows += tr_qa(question_cell, answer_cell) 

221 

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