Coverage for tasks/lynall_iam_life.py: 56%

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

29 

30""" 

31 

32from typing import Any, Dict, List, Tuple, Type 

33 

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

39from camcops_server.cc_modules.cc_request import CamcopsRequest 

40from camcops_server.cc_modules.cc_sqla_coltypes import ( 

41 BoolColumn, 

42 CamcopsColumn, 

43 MIN_ZERO_CHECKER, 

44 ONE_TO_THREE_CHECKER, 

45 ZERO_TO_100_CHECKER, 

46) 

47from camcops_server.cc_modules.cc_task import Task, TaskHasPatientMixin 

48 

49 

50# ============================================================================= 

51# LynallIamLifeEvents 

52# ============================================================================= 

53 

54N_QUESTIONS = 14 

55 

56SPECIAL_SEVERITY_QUESTIONS = [14] 

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

58FREQUENCY_AS_PERCENT_QUESTIONS = [1, 2, 8] 

59 

60QPREFIX = "q" 

61QSUFFIX_MAIN = "_main" 

62QSUFFIX_SEVERITY = "_severity" 

63QSUFFIX_FREQUENCY = "_frequency" 

64 

65SEVERITY_MIN = 1 

66SEVERITY_MAX = 3 

67 

68 

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

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

71 

72 

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

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

75 

76 

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

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

79 

80 

81class LynallIamLifeEventsMetaclass(DeclarativeMeta): 

82 # noinspection PyInitNewSignature 

83 def __init__( 

84 cls: Type["LynallIamLifeEvents"], 

85 name: str, 

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

87 classdict: Dict[str, Any], 

88 ) -> None: 

89 comment_strings = [ 

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

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

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

93 "close family friend/other relative died", 

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

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

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

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

98 "sacked/made redundant", # 9 

99 "major financial crisis", # 10 

100 "problem with police involving court appearance", 

101 "something valued lost/stolen", 

102 "self/partner gave birth", 

103 "other significant negative events", # 14 

104 ] 

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

106 i = q - 1 

107 

108 fn_main = qfieldname_main(q) 

109 cmt_main = ( 

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

111 ) 

112 setattr(cls, fn_main, BoolColumn(fn_main, comment=cmt_main)) 

113 

114 fn_severity = qfieldname_severity(q) 

115 cmt_severity = ( 

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

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

118 ) 

119 setattr( 

120 cls, 

121 fn_severity, 

122 CamcopsColumn( 

123 fn_severity, 

124 Integer, 

125 comment=cmt_severity, 

126 permitted_value_checker=ONE_TO_THREE_CHECKER, 

127 ), 

128 ) 

129 

130 fn_frequency = qfieldname_frequency(q) 

131 if q in FREQUENCY_AS_PERCENT_QUESTIONS: 

132 cmt_frequency = ( 

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

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

135 f"(0-100)" 

136 ) 

137 pv_frequency = ZERO_TO_100_CHECKER 

138 else: 

139 cmt_frequency = ( 

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

141 f"you in total?" 

142 ) 

143 pv_frequency = MIN_ZERO_CHECKER 

144 setattr( 

145 cls, 

146 fn_frequency, 

147 CamcopsColumn( 

148 fn_frequency, 

149 Integer, 

150 comment=cmt_frequency, 

151 permitted_value_checker=pv_frequency, 

152 ), 

153 ) 

154 

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

156 

157 

158class LynallIamLifeEvents( 

159 TaskHasPatientMixin, Task, metaclass=LynallIamLifeEventsMetaclass 

160): 

161 """ 

162 Server implementation of the LynallIamLifeEvents task. 

163 """ 

164 

165 __tablename__ = "lynall_iam_life" 

166 shortname = "Lynall_IAM_Life" 

167 

168 prohibits_commercial = True 

169 

170 @staticmethod 

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

172 _ = req.gettext 

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

174 

175 def is_complete(self) -> bool: 

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

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

178 if value_main is None: 

179 return False 

180 if not value_main: 

181 continue 

182 if ( 

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

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

185 ): 

186 return False 

187 return True 

188 

189 def n_endorsed(self) -> int: 

190 """ 

191 The number of main items endorsed. 

192 """ 

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

194 return self.count_booleans(fieldnames) 

195 

196 def severity_score(self) -> int: 

197 """ 

198 The sum of severity scores. 

199 

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

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

202 """ 

203 total = 0 

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

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

206 if v_main: # if endorsed 

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

208 if v_severity is not None: 

209 total += v_severity 

210 return total 

211 

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

213 options_severity = { 

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

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

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

217 } 

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

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

220 fieldname_main = qfieldname_main(q) 

221 q_main = self.wxstring(req, fieldname_main) 

222 v_main = getattr(self, fieldname_main) 

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

224 if v_main: 

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

226 a_severity = answer( 

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

228 if v_severity is not None 

229 else None 

230 ) 

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

232 text_frequency = v_frequency 

233 if q in FREQUENCY_AS_PERCENT_QUESTIONS: 

234 note_frequency = "a" 

235 if v_frequency is not None: 

236 text_frequency = f"{v_frequency}%" 

237 else: 

238 note_frequency = "b" 

239 a_frequency = ( 

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

241 if text_frequency is not None 

242 else answer(None) 

243 ) 

244 else: 

245 a_severity = "" 

246 a_frequency = "" 

247 q_a.append( 

248 f""" 

249 <tr> 

250 <td>{q_main}</td> 

251 <td>{a_main}</td> 

252 <td>{a_severity}</td> 

253 <td>{a_frequency}</td> 

254 </tr> 

255 """ 

256 ) 

257 return f""" 

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

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

260 {self.get_is_complete_tr(req)} 

261 <tr> 

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

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

264 </tr> 

265 <tr> 

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

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

268 {N_QUESTIONS * 3}</td> 

269 </tr> 

270 </table> 

271 </div> 

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

273 <tr> 

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

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

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

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

278 </tr> 

279 {"".join(q_a)} 

280 </table> 

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

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

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

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

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

286 3 = very bad). 

287 </div> 

288 """