Coverage for tasks/cope.py: 58%

1"""

2 camcops_server/tasks/cope.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"""

27

28 from typing import Any, cast, List, Optional, Type

29

30 from sqlalchemy.orm import Mapped, mapped_column

31 from sqlalchemy.sql.sqltypes import Integer, UnicodeText

32

33 from camcops_server.cc_modules.cc_constants import CssClass

34 from camcops_server.cc_modules.cc_db import add_multiple_columns

35 from camcops_server.cc_modules.cc_html import tr_qa

36 from camcops_server.cc_modules.cc_request import CamcopsRequest

37 from camcops_server.cc_modules.cc_sqla_coltypes import (

38 mapped_camcops_column,

39 BIT_CHECKER,

40 PermittedValueChecker,

41)

42 from camcops_server.cc_modules.cc_summaryelement import SummaryElement

43 from camcops_server.cc_modules.cc_task import (

44 get_from_dict,

45 Task,

46 TaskHasPatientMixin,

47)

48

49

50# =============================================================================

51# COPE_Brief

52# =============================================================================

53

54

55 class CopeBrief( # type: ignore[misc]

56 TaskHasPatientMixin,

57 Task,

58):

59 """

60 Server implementation of the COPE-Brief task.

61 """

62

63 __tablename__ = "cope_brief"

64 shortname = "COPE-Brief"

65 extrastring_taskname = "cope"

66 info_filename_stem = "cope"

67

68 NQUESTIONS = 28

69 RELATIONSHIP_OTHER_CODE = 0

70 RELATIONSHIPS_FIRST = 0

71 RELATIONSHIPS_FIRST_NON_OTHER = 1

72 RELATIONSHIPS_LAST = 9

73

74 @classmethod

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

76 add_multiple_columns(

77 cls,

78 "q",

79 1,

80 cls.NQUESTIONS,

81 minimum=0,

82 maximum=3,

83 comment_fmt="Q{n}, {s} (0 not at all - 3 a lot)",

84 comment_strings=[

85 "work/activities to take mind off", # 1

86 "concentrating efforts on doing something about it",

87 "saying it's unreal",

88 "alcohol/drugs to feel better",

89 "emotional support from others", # 5

90 "given up trying to deal with it",

91 "taking action to make situation better",

92 "refusing to believe it's happened",

93 "saying things to let unpleasant feelings escape",

94 "getting help/advice from others", # 10

95 "alcohol/drugs to get through it",

96 "trying to see it in a more positive light",

97 "criticizing myself",

98 "trying to come up with a strategy",

99 "getting comfort/understanding from someone", # 15

100 "giving up the attempt to cope",

101 "looking for something good in what's happening",

102 "making jokes about it",

103 "doing something to think about it less",

104 "accepting reality of the fact it's happened", # 20

105 "expressing negative feelings",

106 "seeking comfort in religion/spirituality",

107 "trying to get help/advice from others about what to do",

108 "learning to live with it",

109 "thinking hard about what steps to take", # 25

110 "blaming myself",

111 "praying/meditating",

112 "making fun of the situation", # 28

113 ],

114 )

115

116 completed_by_patient: Mapped[Optional[int]] = mapped_camcops_column(

117 permitted_value_checker=BIT_CHECKER,

118 comment="Task completed by patient? (0 no, 1 yes)",

119 )

120 completed_by: Mapped[Optional[str]] = mapped_column(

121 UnicodeText,

122 comment="Name of person task completed by (if not by patient)",

123 )

124 relationship_to_patient: Mapped[Optional[int]] = mapped_camcops_column(

125 permitted_value_checker=PermittedValueChecker(minimum=0, maximum=9),

126 comment="Relationship of responder to patient (0 other, 1 wife, "

127 "2 husband, 3 daughter, 4 son, 5 sister, 6 brother, "

128 "7 mother, 8 father, 9 friend)",

129 )

130 relationship_to_patient_other: Mapped[Optional[str]] = mapped_column(

131 UnicodeText,

132 comment="Relationship of responder to patient (if OTHER chosen)",

133 )

134

135 @staticmethod

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

137 _ = req.gettext

138 return _("Brief COPE Inventory")

139

140 def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]:

141 return self.standard_task_summary_fields() + [

142 SummaryElement(

143 name="self_distraction",

144 coltype=Integer(),

145 value=self.self_distraction(),

146 comment="Self-distraction (2-8)",

147 ),

148 SummaryElement(

149 name="active_coping",

150 coltype=Integer(),

151 value=self.active_coping(),

152 comment="Active coping (2-8)",

153 ),

154 SummaryElement(

155 name="denial",

156 coltype=Integer(),

157 value=self.denial(),

158 comment="Denial (2-8)",

159 ),

160 SummaryElement(

161 name="substance_use",

162 coltype=Integer(),

163 value=self.substance_use(),

164 comment="Substance use (2-8)",

165 ),

166 SummaryElement(

167 name="emotional_support",

168 coltype=Integer(),

169 value=self.emotional_support(),

170 comment="Use of emotional support (2-8)",

171 ),

172 SummaryElement(

173 name="instrumental_support",

174 coltype=Integer(),

175 value=self.instrumental_support(),

176 comment="Use of instrumental support (2-8)",

177 ),

178 SummaryElement(

179 name="behavioural_disengagement",

180 coltype=Integer(),

181 value=self.behavioural_disengagement(),

182 comment="Behavioural disengagement (2-8)",

183 ),

184 SummaryElement(

185 name="venting",

186 coltype=Integer(),

187 value=self.venting(),

188 comment="Venting (2-8)",

189 ),

190 SummaryElement(

191 name="positive_reframing",

192 coltype=Integer(),

193 value=self.positive_reframing(),

194 comment="Positive reframing (2-8)",

195 ),

196 SummaryElement(

197 name="planning",

198 coltype=Integer(),

199 value=self.planning(),

200 comment="Planning (2-8)",

201 ),

202 SummaryElement(

203 name="humour",

204 coltype=Integer(),

205 value=self.humour(),

206 comment="Humour (2-8)",

207 ),

208 SummaryElement(

209 name="acceptance",

210 coltype=Integer(),

211 value=self.acceptance(),

212 comment="Acceptance (2-8)",

213 ),

214 SummaryElement(

215 name="religion",

216 coltype=Integer(),

217 value=self.religion(),

218 comment="Religion (2-8)",

219 ),

220 SummaryElement(

221 name="self_blame",

222 coltype=Integer(),

223 value=self.self_blame(),

224 comment="Self-blame (2-8)",

225 ),

226 ]

227

228 def is_complete_responder(self) -> bool:

229 if self.completed_by_patient is None:

230 return False

231 if self.completed_by_patient:

232 return True

233 if not self.completed_by or self.relationship_to_patient is None:

234 return False

235 if (

236 self.relationship_to_patient == self.RELATIONSHIP_OTHER_CODE

237 and not self.relationship_to_patient_other

238 ):

239 return False

240 return True

241

242 def is_complete(self) -> bool:

243 return (

244 self.is_complete_responder()

245 and self.all_fields_not_none(

246 [f"q{n}" for n in range(1, self.NQUESTIONS + 1)]

247 )

248 and self.field_contents_valid()

249 )

250

251 def self_distraction(self) -> int:

252 return cast(int, self.sum_fields(["q1", "q19"]))

253

254 def active_coping(self) -> int:

255 return cast(int, self.sum_fields(["q2", "q7"]))

256

257 def denial(self) -> int:

258 return cast(int, self.sum_fields(["q3", "q8"]))

259

260 def substance_use(self) -> int:

261 return cast(int, self.sum_fields(["q4", "q11"]))

262

263 def emotional_support(self) -> int:

264 return cast(int, self.sum_fields(["q5", "q15"]))

265

266 def instrumental_support(self) -> int:

267 return cast(int, self.sum_fields(["q10", "q23"]))

268

269 def behavioural_disengagement(self) -> int:

270 return cast(int, self.sum_fields(["q6", "q16"]))

271

272 def venting(self) -> int:

273 return cast(int, self.sum_fields(["q9", "q21"]))

274

275 def positive_reframing(self) -> int:

276 return cast(int, self.sum_fields(["q12", "q17"]))

277

278 def planning(self) -> int:

279 return cast(int, self.sum_fields(["q14", "q25"]))

280

281 def humour(self) -> int:

282 return cast(int, self.sum_fields(["q18", "q28"]))

283

284 def acceptance(self) -> int:

285 return cast(int, self.sum_fields(["q20", "q24"]))

286

287 def religion(self) -> int:

288 return cast(int, self.sum_fields(["q22", "q27"]))

289

290 def self_blame(self) -> int:

291 return cast(int, self.sum_fields(["q13", "q26"]))

292

293 def get_task_html(self, req: CamcopsRequest) -> str:

294 answer_dict: dict[Optional[int], Optional[str]] = {None: None}

295 for option in range(0, 3 + 1):

296 answer_dict[option] = (

297 str(option) + " — " + self.wxstring(req, "a" + str(option))

298 )

299 q_a = ""

300 for q in range(1, self.NQUESTIONS + 1):

301 q_a += tr_qa(

302 f"Q{q}. {self.wxstring(req, 'q' + str(q))}",

303 get_from_dict(answer_dict, getattr(self, "q" + str(q))),

304 )

305 return f"""

306 <div class="{CssClass.SUMMARY}">

307 <table class="{CssClass.SUMMARY}">

308 {self.get_is_complete_tr(req)}

309 {tr_qa("Self-distraction (Q1, Q19)",

310 self.self_distraction())}

311 {tr_qa("Active coping (Q2, Q7)", self.active_coping())}

312 {tr_qa("Denial (Q3, Q8)", self.denial())}

313 {tr_qa("Substance use (Q4, Q11)", self.substance_use())}

314 {tr_qa("Use of emotional support (Q5, Q15)",

315 self.emotional_support())}

316 {tr_qa("Use of instrumental support (Q10, Q23)",

317 self.instrumental_support())}

318 {tr_qa("Behavioural disengagement (Q6, Q16)",

319 self.behavioural_disengagement())}

320 {tr_qa("Venting (Q9, Q21)", self.venting())}

321 {tr_qa("Positive reframing (Q12, Q17)",

322 self.positive_reframing())}

323 {tr_qa("Planning (Q14, Q25)", self.planning())}

324 {tr_qa("Humour (Q18, Q28)", self.humour())}

325 {tr_qa("Acceptance (Q20, Q24)", self.acceptance())}

326 {tr_qa("Religion (Q22, Q27)", self.religion())}

327 {tr_qa("Self-blame (Q13, Q26)", self.self_blame())}

328 </table>

329 </div>

330 <div class="{CssClass.EXPLANATION}">

331 Individual items are scored 0–3 (as in Carver 1997 PMID

332 16250744), not 1–4 (as in

333 http://www.psy.miami.edu/faculty/ccarver/sclBrCOPE.html).

334 Summaries, which are all

335 based on two items, are therefore scored 0–6.

336 </div>

337 <table class="{CssClass.TASKDETAIL}">

338 <tr>

339 <th width="50%">Question</th>

340 <th width="50%">Answer</th>

341 </tr>

342 {q_a}

343 </table>

344 """