Coverage for tasks/maas.py: 51%

116 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-11-08 23:14 +0000

1#!/usr/bin/env python 

2 

3""" 

4camcops_server/tasks/maas.py 

5 

6=============================================================================== 

7 

8 Copyright (C) 2012, University of Cambridge, Department of Psychiatry. 

9 Created by Rudolf Cardinal (rnc1001@cam.ac.uk). 

10 

11 This file is part of CamCOPS. 

12 

13 CamCOPS is free software: you can redistribute it and/or modify 

14 it under the terms of the GNU General Public License as published by 

15 the Free Software Foundation, either version 3 of the License, or 

16 (at your option) any later version. 

17 

18 CamCOPS is distributed in the hope that it will be useful, 

19 but WITHOUT ANY WARRANTY; without even the implied warranty of 

20 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

21 GNU General Public License for more details. 

22 

23 You should have received a copy of the GNU General Public License 

24 along with CamCOPS. If not, see <https://www.gnu.org/licenses/>. 

25 

26=============================================================================== 

27 

28""" 

29 

30from typing import Any, Dict, List, Optional, Tuple, Type 

31 

32from cardinal_pythonlib.classes import classproperty 

33from cardinal_pythonlib.stringfunc import strnumlist, strseq 

34from sqlalchemy.ext.declarative import DeclarativeMeta 

35from sqlalchemy.sql.sqltypes import Integer 

36 

37from camcops_server.cc_modules.cc_constants import CssClass 

38from camcops_server.cc_modules.cc_db import add_multiple_columns 

39from camcops_server.cc_modules.cc_html import tr_qa 

40from camcops_server.cc_modules.cc_report import ( 

41 AverageScoreReport, 

42 ScoreDetails, 

43) 

44from camcops_server.cc_modules.cc_request import CamcopsRequest 

45from camcops_server.cc_modules.cc_summaryelement import SummaryElement 

46from camcops_server.cc_modules.cc_task import Task, TaskHasPatientMixin 

47 

48 

49# ============================================================================= 

50# MAAS 

51# ============================================================================= 

52 

53QUESTION_SNIPPETS = [ 

54 # 1-5 

55 "thinking about baby", 

56 "strength of emotional feelings", 

57 "feelings about baby, negative to positive", 

58 "desire for info", 

59 "picturing baby", 

60 # 6-10 

61 "baby's personhood", 

62 "baby depends on me", 

63 "talking to baby", 

64 "thoughts, irritation to tender/loving", 

65 "clarity of mental picture", 

66 # 11-15 

67 "emotions about baby, sad to happy", 

68 "thoughts of punishing baby", 

69 "emotionally distant or close", 

70 "good diet", 

71 "expectation of feelings after birth", 

72 # 16-19 

73 "would like to hold baby when", 

74 "dreams about baby", 

75 "rubbing over baby", 

76 "feelings if pregnancy lost", 

77] 

78 

79 

80class MaasMetaclass(DeclarativeMeta): 

81 # noinspection PyInitNewSignature 

82 def __init__( 

83 cls: Type["Maas"], 

84 name: str, 

85 bases: Tuple[Type, ...], 

86 classdict: Dict[str, Any], 

87 ) -> None: 

88 add_multiple_columns( 

89 cls, 

90 cls.FN_QPREFIX, 

91 1, 

92 cls.N_QUESTIONS, 

93 minimum=cls.MIN_SCORE_PER_Q, 

94 maximum=cls.MAX_SCORE_PER_Q, 

95 comment_fmt="Q{n} ({s}; 1 least attachment - 5 most attachment)", 

96 comment_strings=QUESTION_SNIPPETS, 

97 ) 

98 super().__init__(name, bases, classdict) 

99 

100 

101class MaasScore(object): 

102 def __init__(self) -> None: 

103 self.quality_min = 0 

104 self.quality_score = 0 

105 self.quality_max = 0 

106 self.time_min = 0 

107 self.time_score = 0 

108 self.time_max = 0 

109 self.global_min = 0 

110 self.global_score = 0 

111 self.global_max = 0 

112 

113 def add_question(self, qnum: int, score: Optional[int]) -> None: 

114 if score is None: 

115 return 

116 if qnum in Maas.QUALITY_OF_ATTACHMENT_Q: 

117 self.quality_min += Maas.MIN_SCORE_PER_Q 

118 self.quality_score += score 

119 self.quality_max += Maas.MAX_SCORE_PER_Q 

120 if qnum in Maas.TIME_IN_ATTACHMENT_MODE_Q: 

121 self.time_min += Maas.MIN_SCORE_PER_Q 

122 self.time_score += score 

123 self.time_max += Maas.MAX_SCORE_PER_Q 

124 self.global_min += Maas.MIN_SCORE_PER_Q 

125 self.global_score += score 

126 self.global_max += Maas.MAX_SCORE_PER_Q 

127 

128 

129class Maas(TaskHasPatientMixin, Task, metaclass=MaasMetaclass): 

130 """ 

131 Server implementation of the MAAS task. 

132 """ 

133 

134 __tablename__ = "maas" 

135 shortname = "MAAS" 

136 

137 FN_QPREFIX = "q" 

138 N_QUESTIONS = 19 

139 MIN_SCORE_PER_Q = 1 

140 MAX_SCORE_PER_Q = 5 

141 MIN_GLOBAL = N_QUESTIONS * MIN_SCORE_PER_Q 

142 MAX_GLOBAL = N_QUESTIONS * MAX_SCORE_PER_Q 

143 

144 TASK_FIELDS = strseq(FN_QPREFIX, 1, N_QUESTIONS) 

145 

146 # Questions whose options are presented from 5 to 1, not from 1 to 5: 

147 # REVERSED_Q = [1, 3, 5, 6, 7, 9, 10, 12, 15, 16, 18] 

148 

149 # Questions that contribute to the "quality of attachment" score: 

150 QUALITY_OF_ATTACHMENT_Q = [3, 6, 9, 10, 11, 12, 13, 15, 16, 19] 

151 QUALITY_OF_ATTACHMENT_FIELDS = strnumlist( 

152 FN_QPREFIX, QUALITY_OF_ATTACHMENT_Q 

153 ) 

154 N_QUALITY = len(QUALITY_OF_ATTACHMENT_Q) 

155 MIN_QUALITY = N_QUALITY * MIN_SCORE_PER_Q 

156 MAX_QUALITY = N_QUALITY * MAX_SCORE_PER_Q 

157 

158 # Questions that contribute to the "time spent in attachment mode" score: 

159 TIME_IN_ATTACHMENT_MODE_Q = [1, 2, 4, 5, 8, 14, 17, 18] 

160 TIME_IN_ATTACHMENT_FIELDS = strnumlist( 

161 FN_QPREFIX, TIME_IN_ATTACHMENT_MODE_Q 

162 ) 

163 N_TIME = len(TIME_IN_ATTACHMENT_MODE_Q) 

164 MIN_TIME = N_TIME * MIN_SCORE_PER_Q 

165 MAX_TIME = N_TIME * MAX_SCORE_PER_Q 

166 

167 @staticmethod 

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

169 _ = req.gettext 

170 return _("Maternal Antenatal Attachment Scale") 

171 

172 def is_complete(self) -> bool: 

173 return ( 

174 self.all_fields_not_none(self.TASK_FIELDS) 

175 and self.field_contents_valid() 

176 ) 

177 

178 def get_score(self) -> MaasScore: 

179 scorer = MaasScore() 

180 for q in range(1, self.N_QUESTIONS + 1): 

181 scorer.add_question(q, getattr(self, self.FN_QPREFIX + str(q))) 

182 return scorer 

183 

184 def get_quality_score(self) -> int: 

185 scorer = self.get_score() 

186 return scorer.quality_score 

187 

188 def get_time_score(self) -> int: 

189 scorer = self.get_score() 

190 return scorer.time_score 

191 

192 def get_global_score(self) -> int: 

193 scorer = self.get_score() 

194 return scorer.global_score 

195 

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

197 scorer = self.get_score() 

198 return self.standard_task_summary_fields() + [ 

199 SummaryElement( 

200 name="quality_of_attachment_score", 

201 coltype=Integer(), 

202 value=scorer.quality_score, 

203 comment=f"Quality of attachment score (for complete tasks, " 

204 f"range " 

205 f"{self.MIN_QUALITY}-" 

206 f"{self.MAX_QUALITY})", 

207 ), 

208 SummaryElement( 

209 name="time_in_attachment_mode_score", 

210 coltype=Integer(), 

211 value=scorer.time_score, 

212 comment=f"Time spent in attachment mode (or intensity of " 

213 f"preoccupation) score (for complete tasks, range " 

214 f"{self.MIN_TIME}-" 

215 f"{self.MAX_TIME})", 

216 ), 

217 SummaryElement( 

218 name="global_attachment_score", 

219 coltype=Integer(), 

220 value=scorer.global_score, 

221 comment=f"Global attachment score (for complete tasks, range " 

222 f"{self.MIN_GLOBAL}-" 

223 f"{self.MAX_GLOBAL})", 

224 ), 

225 ] 

226 

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

228 scorer = self.get_score() 

229 quality = tr_qa( 

230 self.wxstring(req, "quality_of_attachment_score") 

231 + f" [{scorer.quality_min}–{scorer.quality_max}]", 

232 scorer.quality_score, 

233 ) 

234 time = tr_qa( 

235 self.wxstring(req, "time_in_attachment_mode_score") 

236 + f" [{scorer.time_min}–{scorer.time_max}]", 

237 scorer.time_score, 

238 ) 

239 globalscore = tr_qa( 

240 self.wxstring(req, "global_attachment_score") 

241 + f" [{scorer.global_min}–{scorer.global_max}]", 

242 scorer.global_score, 

243 ) 

244 lines = [] # type: List[str] 

245 for q in range(1, self.N_QUESTIONS + 1): 

246 question = f"{q}. " + self.wxstring(req, f"q{q}_q") 

247 value = getattr(self, self.FN_QPREFIX + str(q)) 

248 answer = None 

249 if ( 

250 value is not None 

251 and self.MIN_SCORE_PER_Q <= value <= self.MAX_SCORE_PER_Q 

252 ): 

253 answer = f"{value}: " + self.wxstring(req, f"q{q}_a{value}") 

254 lines.append(tr_qa(question, answer)) 

255 q_a = "".join(lines) 

256 return f""" 

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

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

259 {self.get_is_complete_tr(req)} 

260 {quality} 

261 {time} 

262 {globalscore} 

263 </table> 

264 </div> 

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

266 <tr> 

267 <th width="60%">Question</th> 

268 <th width="40%">Answer</th> 

269 </tr> 

270 {q_a} 

271 </table> 

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

273 Ratings for each question are from {self.MIN_SCORE_PER_Q} (lowest 

274 attachment) to {self.MAX_SCORE_PER_Q} (highest attachment). The 

275 quality of attachment score is the sum of questions 

276 {self.QUALITY_OF_ATTACHMENT_Q}. The “time spent in attachment mode” 

277 score is the sum of questions {self.TIME_IN_ATTACHMENT_MODE_Q}. The 

278 global score is the sum of all questions. 

279 </div> 

280 <div class="{CssClass.FOOTNOTES}"> 

281 Condon, J. (2015). Maternal Antenatal Attachment Scale 

282 [Measurement instrument]. Retrieved from <a 

283 href="https://hdl.handle.net/2328/35292">https://hdl.handle.net/2328/35292</a>. 

284 

285 Copyright © John T Condon 2015. This is an Open Access article 

286 distributed under the terms of the Creative Commons Attribution 

287 License 3.0 AU (<a 

288 href="https://creativecommons.org/licenses/by/3.0">https://creativecommons.org/licenses/by/3.0</a>), 

289 which permits unrestricted use, distribution, and reproduction in 

290 any medium, provided the original work is properly cited. 

291 </div> 

292 """ 

293 

294 

295class MaasReport(AverageScoreReport): 

296 # noinspection PyMethodParameters 

297 @classproperty 

298 def report_id(cls) -> str: 

299 return "MAAS" 

300 

301 @classmethod 

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

303 _ = req.gettext 

304 return _("MAAS — Average scores") 

305 

306 # noinspection PyMethodParameters 

307 @classproperty 

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

309 return Maas 

310 

311 @classmethod 

312 def scoretypes(cls, req: "CamcopsRequest") -> List[ScoreDetails]: 

313 _ = req.gettext 

314 return [ 

315 ScoreDetails( 

316 name=_("Global attachment score"), 

317 scorefunc=Maas.get_global_score, 

318 minimum=Maas.MIN_GLOBAL, 

319 maximum=Maas.MAX_GLOBAL, 

320 higher_score_is_better=True, 

321 ), 

322 ScoreDetails( 

323 name=_("Quality of attachment score"), 

324 scorefunc=Maas.get_quality_score, 

325 minimum=Maas.MIN_QUALITY, 

326 maximum=Maas.MAX_QUALITY, 

327 higher_score_is_better=True, 

328 ), 

329 ScoreDetails( 

330 name=_("Time spent in attachment mode"), 

331 scorefunc=Maas.get_time_score, 

332 minimum=Maas.MIN_TIME, 

333 maximum=Maas.MAX_TIME, 

334 higher_score_is_better=True, 

335 ), 

336 ]