Coverage for tasks/maas.py: 50%

113 statements  

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

1""" 

2camcops_server/tasks/maas.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, List, Optional, Type 

29 

30from cardinal_pythonlib.classes import classproperty 

31from cardinal_pythonlib.stringfunc import strnumlist, strseq 

32from sqlalchemy.sql.sqltypes import Integer 

33 

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 

44 

45 

46# ============================================================================= 

47# MAAS 

48# ============================================================================= 

49 

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] 

75 

76 

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 

88 

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 

103 

104 

105class Maas(TaskHasPatientMixin, Task): # type: ignore[misc] 

106 """ 

107 Server implementation of the MAAS task. 

108 """ 

109 

110 __tablename__ = "maas" 

111 shortname = "MAAS" 

112 

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 

119 

120 TASK_FIELDS = strseq(FN_QPREFIX, 1, N_QUESTIONS) 

121 

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] 

124 

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 

133 

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 

142 

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 ) 

155 

156 @staticmethod 

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

158 _ = req.gettext 

159 return _("Maternal Antenatal Attachment Scale") 

160 

161 def is_complete(self) -> bool: 

162 return ( 

163 self.all_fields_not_none(self.TASK_FIELDS) 

164 and self.field_contents_valid() 

165 ) 

166 

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 

172 

173 def get_quality_score(self) -> int: 

174 scorer = self.get_score() 

175 return scorer.quality_score 

176 

177 def get_time_score(self) -> int: 

178 scorer = self.get_score() 

179 return scorer.time_score 

180 

181 def get_global_score(self) -> int: 

182 scorer = self.get_score() 

183 return scorer.global_score 

184 

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 ] 

215 

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>. 

273 

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

282 

283 

284class MaasReport(AverageScoreReport): 

285 # noinspection PyMethodParameters 

286 @classproperty 

287 def report_id(cls) -> str: 

288 return "MAAS" 

289 

290 @classmethod 

291 def title(cls, req: "CamcopsRequest") -> str: 

292 _ = req.gettext 

293 return _("MAAS — Average scores") 

294 

295 # noinspection PyMethodParameters 

296 @classproperty 

297 def task_class(cls) -> Type[Task]: 

298 return Maas 

299 

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 ]