Coverage for tasks/lynall_iam_life.py: 54%

94 statements  

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

1""" 

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

30from typing import Any, List, Type 

31 

32from sqlalchemy.sql.sqltypes import Integer 

33 

34from camcops_server.cc_modules.cc_constants import CssClass 

35from camcops_server.cc_modules.cc_html import answer, get_yes_no_none 

36from camcops_server.cc_modules.cc_request import CamcopsRequest 

37from 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) 

44from camcops_server.cc_modules.cc_task import Task, TaskHasPatientMixin 

45 

46 

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

48# LynallIamLifeEvents 

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

50 

51N_QUESTIONS = 14 

52 

53SPECIAL_SEVERITY_QUESTIONS = [14] 

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

55FREQUENCY_AS_PERCENT_QUESTIONS = [1, 2, 8] 

56 

57QPREFIX = "q" 

58QSUFFIX_MAIN = "_main" 

59QSUFFIX_SEVERITY = "_severity" 

60QSUFFIX_FREQUENCY = "_frequency" 

61 

62SEVERITY_MIN = 1 

63SEVERITY_MAX = 3 

64 

65 

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

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

68 

69 

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

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

72 

73 

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

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

76 

77 

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