Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1#!/usr/bin/env python 

2 

3""" 

4camcops_server/tasks/lynall_iam_life.py 

5 

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

7 

8 Copyright (C) 2012-2020 Rudolf Cardinal (rudolf@pobox.com). 

9 

10 This file is part of CamCOPS. 

11 

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

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

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

15 (at your option) any later version. 

16 

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

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

19 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

20 GNU General Public License for more details. 

21 

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

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

24 

25=============================================================================== 

26 

27**Lynall M-E — IAM study — life events.** 

28 

29""" 

30 

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

32 

33from sqlalchemy.ext.declarative import DeclarativeMeta 

34from sqlalchemy.sql.sqltypes import Integer 

35 

36from camcops_server.cc_modules.cc_constants import CssClass 

37from camcops_server.cc_modules.cc_html import answer, get_yes_no_none 

38from camcops_server.cc_modules.cc_request import CamcopsRequest 

39from camcops_server.cc_modules.cc_sqla_coltypes import ( 

40 BoolColumn, 

41 CamcopsColumn, 

42 MIN_ZERO_CHECKER, 

43 ONE_TO_THREE_CHECKER, 

44 ZERO_TO_100_CHECKER, 

45) 

46from camcops_server.cc_modules.cc_task import Task, TaskHasPatientMixin 

47 

48 

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

50# LynallIamLifeEvents 

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

52 

53N_QUESTIONS = 14 

54 

55SPECIAL_SEVERITY_QUESTIONS = [14] 

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

57FREQUENCY_AS_PERCENT_QUESTIONS = [1, 2, 8] 

58 

59QPREFIX = "q" 

60QSUFFIX_MAIN = "_main" 

61QSUFFIX_SEVERITY = "_severity" 

62QSUFFIX_FREQUENCY = "_frequency" 

63 

64SEVERITY_MIN = 1 

65SEVERITY_MAX = 3 

66 

67 

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

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

70 

71 

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

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

74 

75 

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

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

78 

79 

80class LynallIamLifeEventsMetaclass(DeclarativeMeta): 

81 # noinspection PyInitNewSignature 

82 def __init__(cls: Type['LynallIamLifeEvents'], 

83 name: str, 

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

85 classdict: Dict[str, Any]) -> None: 

86 comment_strings = [ 

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

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

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

90 "close family friend/other relative died", 

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

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

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

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

95 "sacked/made redundant", # 9 

96 "major financial crisis", # 10 

97 "problem with police involving court appearance", 

98 "something valued lost/stolen", 

99 "self/partner gave birth", 

100 "other significant negative events", # 14 

101 ] 

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

103 i = q - 1 

104 

105 fn_main = qfieldname_main(q) 

106 cmt_main = f"Q{q}: in last 6 months: {comment_strings[i]} (0 no, 1 yes)" # noqa 

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

108 

109 fn_severity = qfieldname_severity(q) 

110 cmt_severity = ( 

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

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

113 ) 

114 setattr(cls, fn_severity, CamcopsColumn( 

115 fn_severity, 

116 Integer, 

117 comment=cmt_severity, 

118 permitted_value_checker=ONE_TO_THREE_CHECKER 

119 )) 

120 

121 fn_frequency = qfieldname_frequency(q) 

122 if q in FREQUENCY_AS_PERCENT_QUESTIONS: 

123 cmt_frequency = ( 

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

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

126 f"(0-100)" 

127 ) 

128 pv_frequency = ZERO_TO_100_CHECKER 

129 else: 

130 cmt_frequency = ( 

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

132 f"you in total?" 

133 ) 

134 pv_frequency = MIN_ZERO_CHECKER 

135 setattr(cls, fn_frequency, CamcopsColumn( 

136 fn_frequency, 

137 Integer, 

138 comment=cmt_frequency, 

139 permitted_value_checker=pv_frequency 

140 )) 

141 

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

143 

144 

145class LynallIamLifeEvents(TaskHasPatientMixin, Task, 

146 metaclass=LynallIamLifeEventsMetaclass): 

147 """ 

148 Server implementation of the LynallIamLifeEvents task. 

149 """ 

150 __tablename__ = "lynall_iam_life" 

151 shortname = "Lynall_IAM_Life" 

152 

153 prohibits_commercial = True 

154 

155 @staticmethod 

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

157 _ = req.gettext 

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

159 

160 def is_complete(self) -> bool: 

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

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

163 if value_main is None: 

164 return False 

165 if not value_main: 

166 continue 

167 if (getattr(self, qfieldname_severity(q)) is None or 

168 getattr(self, qfieldname_frequency(q)) is None): 

169 return False 

170 return True 

171 

172 def n_endorsed(self) -> int: 

173 """ 

174 The number of main items endorsed. 

175 """ 

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

177 return self.count_booleans(fieldnames) 

178 

179 def severity_score(self) -> int: 

180 """ 

181 The sum of severity scores. 

182 

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

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

185 """ 

186 total = 0 

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

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

189 if v_main: # if endorsed 

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

191 if v_severity is not None: 

192 total += v_severity 

193 return total 

194 

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

196 options_severity = { 

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

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

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

200 } 

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

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

203 fieldname_main = qfieldname_main(q) 

204 q_main = self.wxstring(req, fieldname_main) 

205 v_main = getattr(self, fieldname_main) 

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

207 if v_main: 

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

209 a_severity = answer( 

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

211 if v_severity is not None else None 

212 ) 

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

214 text_frequency = v_frequency 

215 if q in FREQUENCY_AS_PERCENT_QUESTIONS: 

216 note_frequency = "a" 

217 if v_frequency is not None: 

218 text_frequency = f"{v_frequency}%" 

219 else: 

220 note_frequency = "b" 

221 a_frequency = ( 

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

223 if text_frequency is not None else answer(None) 

224 ) 

225 else: 

226 a_severity = "" 

227 a_frequency = "" 

228 q_a.append(f""" 

229 <tr> 

230 <td>{q_main}</td> 

231 <td>{a_main}</td> 

232 <td>{a_severity}</td> 

233 <td>{a_frequency}</td> 

234 </tr> 

235 """) 

236 return f""" 

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

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

239 {self.get_is_complete_tr(req)} 

240 <tr> 

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

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

243 </tr> 

244 <tr> 

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

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

247 {N_QUESTIONS * 3}</td> 

248 </tr> 

249 </table> 

250 </div> 

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

252 <tr> 

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

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

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

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

257 </tr> 

258 {"".join(q_a)} 

259 </table> 

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

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

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

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

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

265 3 = very bad). 

266 </div> 

267 """