Coverage for tasks/suppsp.py: 60%

1"""

2 camcops_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

30 from typing import Any, cast, List, Type

31

32 from cardinal_pythonlib.stringfunc import strseq

33 from sqlalchemy import Integer

34

35 from camcops_server.cc_modules.cc_constants import CssClass

36 from camcops_server.cc_modules.cc_html import tr_qa, tr, answer

37 from camcops_server.cc_modules.cc_request import CamcopsRequest

38 from camcops_server.cc_modules.cc_sqla_coltypes import (

39 camcops_column,

40 ONE_TO_FOUR_CHECKER,

41)

42

43 from camcops_server.cc_modules.cc_summaryelement import SummaryElement

44 from camcops_server.cc_modules.cc_task import (

45 TaskHasPatientMixin,

46 Task,

47 get_from_dict,

48)

49 from camcops_server.cc_modules.cc_text import SS

50

51

52 class 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