Coverage for tasks/maas.py: 50%
113 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/maas.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, List, Optional, Type
30from cardinal_pythonlib.classes import classproperty
31from cardinal_pythonlib.stringfunc import strnumlist, strseq
32from sqlalchemy.sql.sqltypes import Integer
34from camcops_server.cc_modules.cc_constants import CssClass
35from camcops_server.cc_modules.cc_db import add_multiple_columns
36from camcops_server.cc_modules.cc_html import tr_qa
37from camcops_server.cc_modules.cc_report import (
38 AverageScoreReport,
39 ScoreDetails,
40)
41from camcops_server.cc_modules.cc_request import CamcopsRequest
42from camcops_server.cc_modules.cc_summaryelement import SummaryElement
43from camcops_server.cc_modules.cc_task import Task, TaskHasPatientMixin
46# =============================================================================
47# MAAS
48# =============================================================================
50QUESTION_SNIPPETS = [
51 # 1-5
52 "thinking about baby",
53 "strength of emotional feelings",
54 "feelings about baby, negative to positive",
55 "desire for info",
56 "picturing baby",
57 # 6-10
58 "baby's personhood",
59 "baby depends on me",
60 "talking to baby",
61 "thoughts, irritation to tender/loving",
62 "clarity of mental picture",
63 # 11-15
64 "emotions about baby, sad to happy",
65 "thoughts of punishing baby",
66 "emotionally distant or close",
67 "good diet",
68 "expectation of feelings after birth",
69 # 16-19
70 "would like to hold baby when",
71 "dreams about baby",
72 "rubbing over baby",
73 "feelings if pregnancy lost",
74]
77class MaasScore(object):
78 def __init__(self) -> None:
79 self.quality_min = 0
80 self.quality_score = 0
81 self.quality_max = 0
82 self.time_min = 0
83 self.time_score = 0
84 self.time_max = 0
85 self.global_min = 0
86 self.global_score = 0
87 self.global_max = 0
89 def add_question(self, qnum: int, score: Optional[int]) -> None:
90 if score is None:
91 return
92 if qnum in Maas.QUALITY_OF_ATTACHMENT_Q:
93 self.quality_min += Maas.MIN_SCORE_PER_Q
94 self.quality_score += score
95 self.quality_max += Maas.MAX_SCORE_PER_Q
96 if qnum in Maas.TIME_IN_ATTACHMENT_MODE_Q:
97 self.time_min += Maas.MIN_SCORE_PER_Q
98 self.time_score += score
99 self.time_max += Maas.MAX_SCORE_PER_Q
100 self.global_min += Maas.MIN_SCORE_PER_Q
101 self.global_score += score
102 self.global_max += Maas.MAX_SCORE_PER_Q
105class Maas(TaskHasPatientMixin, Task): # type: ignore[misc]
106 """
107 Server implementation of the MAAS task.
108 """
110 __tablename__ = "maas"
111 shortname = "MAAS"
113 FN_QPREFIX = "q"
114 N_QUESTIONS = 19
115 MIN_SCORE_PER_Q = 1
116 MAX_SCORE_PER_Q = 5
117 MIN_GLOBAL = N_QUESTIONS * MIN_SCORE_PER_Q
118 MAX_GLOBAL = N_QUESTIONS * MAX_SCORE_PER_Q
120 TASK_FIELDS = strseq(FN_QPREFIX, 1, N_QUESTIONS)
122 # Questions whose options are presented from 5 to 1, not from 1 to 5:
123 # REVERSED_Q = [1, 3, 5, 6, 7, 9, 10, 12, 15, 16, 18]
125 # Questions that contribute to the "quality of attachment" score:
126 QUALITY_OF_ATTACHMENT_Q = [3, 6, 9, 10, 11, 12, 13, 15, 16, 19]
127 QUALITY_OF_ATTACHMENT_FIELDS = strnumlist(
128 FN_QPREFIX, QUALITY_OF_ATTACHMENT_Q
129 )
130 N_QUALITY = len(QUALITY_OF_ATTACHMENT_Q)
131 MIN_QUALITY = N_QUALITY * MIN_SCORE_PER_Q
132 MAX_QUALITY = N_QUALITY * MAX_SCORE_PER_Q
134 # Questions that contribute to the "time spent in attachment mode" score:
135 TIME_IN_ATTACHMENT_MODE_Q = [1, 2, 4, 5, 8, 14, 17, 18]
136 TIME_IN_ATTACHMENT_FIELDS = strnumlist(
137 FN_QPREFIX, TIME_IN_ATTACHMENT_MODE_Q
138 )
139 N_TIME = len(TIME_IN_ATTACHMENT_MODE_Q)
140 MIN_TIME = N_TIME * MIN_SCORE_PER_Q
141 MAX_TIME = N_TIME * MAX_SCORE_PER_Q
143 @classmethod
144 def extend_columns(cls: Type["Maas"], **kwargs: Any) -> None:
145 add_multiple_columns(
146 cls,
147 cls.FN_QPREFIX,
148 1,
149 cls.N_QUESTIONS,
150 minimum=cls.MIN_SCORE_PER_Q,
151 maximum=cls.MAX_SCORE_PER_Q,
152 comment_fmt="Q{n} ({s}; 1 least attachment - 5 most attachment)",
153 comment_strings=QUESTION_SNIPPETS,
154 )
156 @staticmethod
157 def longname(req: "CamcopsRequest") -> str:
158 _ = req.gettext
159 return _("Maternal Antenatal Attachment Scale")
161 def is_complete(self) -> bool:
162 return (
163 self.all_fields_not_none(self.TASK_FIELDS)
164 and self.field_contents_valid()
165 )
167 def get_score(self) -> MaasScore:
168 scorer = MaasScore()
169 for q in range(1, self.N_QUESTIONS + 1):
170 scorer.add_question(q, getattr(self, self.FN_QPREFIX + str(q)))
171 return scorer
173 def get_quality_score(self) -> int:
174 scorer = self.get_score()
175 return scorer.quality_score
177 def get_time_score(self) -> int:
178 scorer = self.get_score()
179 return scorer.time_score
181 def get_global_score(self) -> int:
182 scorer = self.get_score()
183 return scorer.global_score
185 def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]:
186 scorer = self.get_score()
187 return self.standard_task_summary_fields() + [
188 SummaryElement(
189 name="quality_of_attachment_score",
190 coltype=Integer(),
191 value=scorer.quality_score,
192 comment=f"Quality of attachment score (for complete tasks, "
193 f"range "
194 f"{self.MIN_QUALITY}-"
195 f"{self.MAX_QUALITY})",
196 ),
197 SummaryElement(
198 name="time_in_attachment_mode_score",
199 coltype=Integer(),
200 value=scorer.time_score,
201 comment=f"Time spent in attachment mode (or intensity of "
202 f"preoccupation) score (for complete tasks, range "
203 f"{self.MIN_TIME}-"
204 f"{self.MAX_TIME})",
205 ),
206 SummaryElement(
207 name="global_attachment_score",
208 coltype=Integer(),
209 value=scorer.global_score,
210 comment=f"Global attachment score (for complete tasks, range "
211 f"{self.MIN_GLOBAL}-"
212 f"{self.MAX_GLOBAL})",
213 ),
214 ]
216 def get_task_html(self, req: CamcopsRequest) -> str:
217 scorer = self.get_score()
218 quality = tr_qa(
219 self.wxstring(req, "quality_of_attachment_score")
220 + f" [{scorer.quality_min}–{scorer.quality_max}]",
221 scorer.quality_score,
222 )
223 time = tr_qa(
224 self.wxstring(req, "time_in_attachment_mode_score")
225 + f" [{scorer.time_min}–{scorer.time_max}]",
226 scorer.time_score,
227 )
228 globalscore = tr_qa(
229 self.wxstring(req, "global_attachment_score")
230 + f" [{scorer.global_min}–{scorer.global_max}]",
231 scorer.global_score,
232 )
233 lines = [] # type: List[str]
234 for q in range(1, self.N_QUESTIONS + 1):
235 question = f"{q}. " + self.wxstring(req, f"q{q}_q")
236 value = getattr(self, self.FN_QPREFIX + str(q))
237 answer = None
238 if (
239 value is not None
240 and self.MIN_SCORE_PER_Q <= value <= self.MAX_SCORE_PER_Q
241 ):
242 answer = f"{value}: " + self.wxstring(req, f"q{q}_a{value}")
243 lines.append(tr_qa(question, answer))
244 q_a = "".join(lines)
245 return f"""
246 <div class="{CssClass.SUMMARY}">
247 <table class="{CssClass.SUMMARY}">
248 {self.get_is_complete_tr(req)}
249 {quality}
250 {time}
251 {globalscore}
252 </table>
253 </div>
254 <table class="{CssClass.TASKDETAIL}">
255 <tr>
256 <th width="60%">Question</th>
257 <th width="40%">Answer</th>
258 </tr>
259 {q_a}
260 </table>
261 <div class="{CssClass.EXPLANATION}">
262 Ratings for each question are from {self.MIN_SCORE_PER_Q} (lowest
263 attachment) to {self.MAX_SCORE_PER_Q} (highest attachment). The
264 quality of attachment score is the sum of questions
265 {self.QUALITY_OF_ATTACHMENT_Q}. The “time spent in attachment mode”
266 score is the sum of questions {self.TIME_IN_ATTACHMENT_MODE_Q}. The
267 global score is the sum of all questions.
268 </div>
269 <div class="{CssClass.FOOTNOTES}">
270 Condon, J. (2015). Maternal Antenatal Attachment Scale
271 [Measurement instrument]. Retrieved from <a
272 href="https://hdl.handle.net/2328/35292">https://hdl.handle.net/2328/35292</a>.
274 Copyright © John T Condon 2015. This is an Open Access article
275 distributed under the terms of the Creative Commons Attribution
276 License 3.0 AU (<a
277 href="https://creativecommons.org/licenses/by/3.0">https://creativecommons.org/licenses/by/3.0</a>),
278 which permits unrestricted use, distribution, and reproduction in
279 any medium, provided the original work is properly cited.
280 </div>
281 """
284class MaasReport(AverageScoreReport):
285 # noinspection PyMethodParameters
286 @classproperty
287 def report_id(cls) -> str:
288 return "MAAS"
290 @classmethod
291 def title(cls, req: "CamcopsRequest") -> str:
292 _ = req.gettext
293 return _("MAAS — Average scores")
295 # noinspection PyMethodParameters
296 @classproperty
297 def task_class(cls) -> Type[Task]:
298 return Maas
300 @classmethod
301 def scoretypes(cls, req: "CamcopsRequest") -> List[ScoreDetails]:
302 _ = req.gettext
303 return [
304 ScoreDetails(
305 name=_("Global attachment score"),
306 scorefunc=Maas.get_global_score, # type: ignore[arg-type]
307 minimum=Maas.MIN_GLOBAL,
308 maximum=Maas.MAX_GLOBAL,
309 higher_score_is_better=True,
310 ),
311 ScoreDetails(
312 name=_("Quality of attachment score"),
313 scorefunc=Maas.get_quality_score, # type: ignore[arg-type]
314 minimum=Maas.MIN_QUALITY,
315 maximum=Maas.MAX_QUALITY,
316 higher_score_is_better=True,
317 ),
318 ScoreDetails(
319 name=_("Time spent in attachment mode"),
320 scorefunc=Maas.get_time_score, # type: ignore[arg-type]
321 minimum=Maas.MIN_TIME,
322 maximum=Maas.MAX_TIME,
323 higher_score_is_better=True,
324 ),
325 ]