Coverage for tasks/cope.py: 58%

81 statements  

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

1""" 

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

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

29 

30from sqlalchemy.orm import Mapped, mapped_column 

31from sqlalchemy.sql.sqltypes import Integer, UnicodeText 

32 

33from camcops_server.cc_modules.cc_constants import CssClass 

34from camcops_server.cc_modules.cc_db import add_multiple_columns 

35from camcops_server.cc_modules.cc_html import tr_qa 

36from camcops_server.cc_modules.cc_request import CamcopsRequest 

37from camcops_server.cc_modules.cc_sqla_coltypes import ( 

38 mapped_camcops_column, 

39 BIT_CHECKER, 

40 PermittedValueChecker, 

41) 

42from camcops_server.cc_modules.cc_summaryelement import SummaryElement 

43from 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 

55class 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 """