Coverage for tasks/lynall_iam_life.py: 54%

1"""

2 camcops_server/tasks/lynall_iam_life.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**Lynall M-E — IAM study — life events.**

27

28"""

29

30 from typing import Any, List, Type

31

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_html import answer, get_yes_no_none

36 from camcops_server.cc_modules.cc_request import CamcopsRequest

37 from camcops_server.cc_modules.cc_sqla_coltypes import (

38 bool_column,

39 camcops_column,

40 MIN_ZERO_CHECKER,

41 ONE_TO_THREE_CHECKER,

42 ZERO_TO_100_CHECKER,

43)

44 from camcops_server.cc_modules.cc_task import Task, TaskHasPatientMixin

45

46

47# =============================================================================

48# LynallIamLifeEvents

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

50

51 N_QUESTIONS = 14

52

53 SPECIAL_SEVERITY_QUESTIONS = [14]

54 SPECIAL_FREQUENCY_QUESTIONS = [1, 2, 3, 8]

55 FREQUENCY_AS_PERCENT_QUESTIONS = [1, 2, 8]

56

57 QPREFIX = "q"

58 QSUFFIX_MAIN = "_main"

59 QSUFFIX_SEVERITY = "_severity"

60 QSUFFIX_FREQUENCY = "_frequency"

61

62 SEVERITY_MIN = 1

63 SEVERITY_MAX = 3

64

65

66 def qfieldname_main(qnum: int) -> str:

67 return f"{QPREFIX}{qnum}{QSUFFIX_MAIN}"

68

69

70 def qfieldname_severity(qnum: int) -> str:

71 return f"{QPREFIX}{qnum}{QSUFFIX_SEVERITY}"

72

73

74 def qfieldname_frequency(qnum: int) -> str:

75 return f"{QPREFIX}{qnum}{QSUFFIX_FREQUENCY}"

76

77

78 class LynallIamLifeEvents( # type: ignore[misc]

79 TaskHasPatientMixin,

80 Task,

81):

82 """

83 Server implementation of the LynallIamLifeEvents task.

84 """

85

86 __tablename__ = "lynall_iam_life"

87 shortname = "Lynall_IAM_Life"

88

89 prohibits_commercial = True

90

91 @classmethod

92 def extend_columns(

93 cls: Type["LynallIamLifeEvents"], **kwargs: Any

94 ) -> None:

95 comment_strings = [

96 "illness/injury/assault (self)", # 1

97 "illness/injury/assault (relative)",

98 "parent/child/spouse/sibling died",

99 "close family friend/other relative died",

100 "marital separation or broke off relationship", # 5

101 "ended long-lasting friendship with close friend/relative",

102 "problems with close friend/neighbour/relative",

103 "unsuccessful job-seeking for >1 month", # 8

104 "sacked/made redundant", # 9

105 "major financial crisis", # 10

106 "problem with police involving court appearance",

107 "something valued lost/stolen",

108 "self/partner gave birth",

109 "other significant negative events", # 14

110 ]

111 for q in range(1, N_QUESTIONS + 1):

112 i = q - 1

113

114 fn_main = qfieldname_main(q)

115 cmt_main = (

116 f"Q{q}: in last 6 months: {comment_strings[i]} (0 no, 1 yes)"

117 )

118 setattr(cls, fn_main, bool_column(fn_main, comment=cmt_main))

119

120 fn_severity = qfieldname_severity(q)

121 cmt_severity = (

122 f"Q{q}: (if yes) how bad was that "

123 f"(1 not too bad, 2 moderately bad, 3 very bad)"

124 )

125 setattr(

126 cls,

127 fn_severity,

128 camcops_column(

129 fn_severity,

130 Integer,

131 comment=cmt_severity,

132 permitted_value_checker=ONE_TO_THREE_CHECKER,

133 ),

134 )

135

136 fn_frequency = qfieldname_frequency(q)

137 if q in FREQUENCY_AS_PERCENT_QUESTIONS:

138 cmt_frequency = (

139 f"Q{q}: For what percentage of your life since aged 18 "

140 f"has [this event: {comment_strings[i]}] been happening? "

141 f"(0-100)"

142 )

143 pv_frequency = ZERO_TO_100_CHECKER

144 else:

145 cmt_frequency = (

146 f"Q{q}: Since age 18, how many times has this happened to "

147 f"you in total?"

148 )

149 pv_frequency = MIN_ZERO_CHECKER

150 setattr(

151 cls,

152 fn_frequency,

153 camcops_column(

154 fn_frequency,

155 Integer,

156 comment=cmt_frequency,

157 permitted_value_checker=pv_frequency,

158 ),

159 )

160

161 @staticmethod

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

163 _ = req.gettext

164 return _("Lynall M-E — IAM — Life events")

165

166 def is_complete(self) -> bool:

167 for q in range(1, N_QUESTIONS + 1):

168 value_main = getattr(self, qfieldname_main(q))

169 if value_main is None:

170 return False

171 if not value_main:

172 continue

173 if (

174 getattr(self, qfieldname_severity(q)) is None

175 or getattr(self, qfieldname_frequency(q)) is None

176 ):

177 return False

178 return True

179

180 def n_endorsed(self) -> int:

181 """

182 The number of main items endorsed.

183 """

184 fieldnames = [qfieldname_main(q) for q in range(1, N_QUESTIONS + 1)]

185 return self.count_booleans(fieldnames)

186

187 def severity_score(self) -> int:

188 """

189 The sum of severity scores.

190

191 These are intrinsically coded 1 = not too bad, 2 = moderately bad, 3 =

192 very bad. In addition, we score 0 for "not experienced".

193 """

194 total = 0

195 for q in range(1, N_QUESTIONS + 1):

196 v_main = getattr(self, qfieldname_main(q))

197 if v_main: # if endorsed

198 v_severity = getattr(self, qfieldname_severity(q))

199 if v_severity is not None:

200 total += v_severity

201 return total

202

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

204 options_severity = {

205 3: self.wxstring(req, "severity_a3"),

206 2: self.wxstring(req, "severity_a2"),

207 1: self.wxstring(req, "severity_a1"),

208 }

209 q_a = [] # type: List[str]

210 for q in range(1, N_QUESTIONS + 1):

211 fieldname_main = qfieldname_main(q)

212 q_main = self.wxstring(req, fieldname_main)

213 v_main = getattr(self, fieldname_main)

214 a_main = answer(get_yes_no_none(req, v_main))

215 if v_main:

216 v_severity = getattr(self, qfieldname_severity(q))

217 a_severity = answer(

218 f"{v_severity}: {options_severity.get(v_severity)}"

219 if v_severity is not None

220 else None

221 )

222 v_frequency = getattr(self, qfieldname_frequency(q))

223 text_frequency = v_frequency

224 if q in FREQUENCY_AS_PERCENT_QUESTIONS:

225 note_frequency = "a"

226 if v_frequency is not None:

227 text_frequency = f"{v_frequency}%"

228 else:

229 note_frequency = "b"

230 a_frequency = (

231 f"{answer(text_frequency)} <sup>[{note_frequency}]</sup>"

232 if text_frequency is not None

233 else answer(None)

234 )

235 else:

236 a_severity = ""

237 a_frequency = ""

238 q_a.append(

239 f"""

240 <tr>

241 <td>{q_main}</td>

242 <td>{a_main}</td>

243 <td>{a_severity}</td>

244 <td>{a_frequency}</td>

245 </tr>

246 """

247 )

248 return f"""

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

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

251 {self.get_is_complete_tr(req)}

252 <tr>

253 <td>Number of categories endorsed</td>

254 <td>{answer(self.n_endorsed())} / {N_QUESTIONS}</td>

255 </tr>

256 <tr>

257 <td>Severity score <sup>[c]</sup></td>

258 <td>{answer(self.severity_score())} /

259 {N_QUESTIONS * 3}</td>

260 </tr>

261 </table>

262 </div>

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

264 <tr>

265 <th width="40%">Question</th>

266 <th width="20%">Experienced</th>

267 <th width="20%">Severity</th>

268 <th width="20%">Frequency</th>

269 </tr>

270 {"".join(q_a)}

271 </table>

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

273 [a] Percentage of life, since age 18, spent experiencing this.

274 [b] Number of times this has happened, since age 18.

275 [c] The severity score is the sum of “severity” ratings

276 (0 = not experienced, 1 = not too bad, 1 = moderately bad,

277 3 = very bad).

278 </div>

279 """