Coverage for tasks/maas.py: 50%

1"""

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

28 from typing import Any, List, Optional, Type

29

30 from cardinal_pythonlib.classes import classproperty

31 from cardinal_pythonlib.stringfunc import strnumlist, strseq

32 from sqlalchemy.sql.sqltypes import Integer

33

34 from camcops_server.cc_modules.cc_constants import CssClass

35 from camcops_server.cc_modules.cc_db import add_multiple_columns

36 from camcops_server.cc_modules.cc_html import tr_qa

37 from camcops_server.cc_modules.cc_report import (

38 AverageScoreReport,

39 ScoreDetails,

40)

41 from camcops_server.cc_modules.cc_request import CamcopsRequest

42 from camcops_server.cc_modules.cc_summaryelement import SummaryElement

43 from camcops_server.cc_modules.cc_task import Task, TaskHasPatientMixin

44

45

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

47# MAAS

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

49

50 QUESTION_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

77 class 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

105 class 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

284 class 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 ]