Coverage for tasks/cope.py: 58%
81 statements
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-15 14:23 +0100
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-15 14:23 +0100
1"""
2camcops_server/tasks/cope.py
4===============================================================================
6 Copyright (C) 2012, University of Cambridge, Department of Psychiatry.
7 Created by Rudolf Cardinal (rnc1001@cam.ac.uk).
9 This file is part of CamCOPS.
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.
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.
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/>.
24===============================================================================
26"""
28from typing import Any, cast, List, Optional, Type
30from sqlalchemy.orm import Mapped, mapped_column
31from sqlalchemy.sql.sqltypes import Integer, UnicodeText
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)
50# =============================================================================
51# COPE_Brief
52# =============================================================================
55class CopeBrief( # type: ignore[misc]
56 TaskHasPatientMixin,
57 Task,
58):
59 """
60 Server implementation of the COPE-Brief task.
61 """
63 __tablename__ = "cope_brief"
64 shortname = "COPE-Brief"
65 extrastring_taskname = "cope"
66 info_filename_stem = "cope"
68 NQUESTIONS = 28
69 RELATIONSHIP_OTHER_CODE = 0
70 RELATIONSHIPS_FIRST = 0
71 RELATIONSHIPS_FIRST_NON_OTHER = 1
72 RELATIONSHIPS_LAST = 9
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 )
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 )
135 @staticmethod
136 def longname(req: "CamcopsRequest") -> str:
137 _ = req.gettext
138 return _("Brief COPE Inventory")
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 ]
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
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 )
251 def self_distraction(self) -> int:
252 return cast(int, self.sum_fields(["q1", "q19"]))
254 def active_coping(self) -> int:
255 return cast(int, self.sum_fields(["q2", "q7"]))
257 def denial(self) -> int:
258 return cast(int, self.sum_fields(["q3", "q8"]))
260 def substance_use(self) -> int:
261 return cast(int, self.sum_fields(["q4", "q11"]))
263 def emotional_support(self) -> int:
264 return cast(int, self.sum_fields(["q5", "q15"]))
266 def instrumental_support(self) -> int:
267 return cast(int, self.sum_fields(["q10", "q23"]))
269 def behavioural_disengagement(self) -> int:
270 return cast(int, self.sum_fields(["q6", "q16"]))
272 def venting(self) -> int:
273 return cast(int, self.sum_fields(["q9", "q21"]))
275 def positive_reframing(self) -> int:
276 return cast(int, self.sum_fields(["q12", "q17"]))
278 def planning(self) -> int:
279 return cast(int, self.sum_fields(["q14", "q25"]))
281 def humour(self) -> int:
282 return cast(int, self.sum_fields(["q18", "q28"]))
284 def acceptance(self) -> int:
285 return cast(int, self.sum_fields(["q20", "q24"]))
287 def religion(self) -> int:
288 return cast(int, self.sum_fields(["q22", "q27"]))
290 def self_blame(self) -> int:
291 return cast(int, self.sum_fields(["q13", "q26"]))
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 """