Coverage for tasks/npiq.py: 44%

90 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/npiq.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""" 

29 

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

31 

32from cardinal_pythonlib.stringfunc import strseq 

33from sqlalchemy.ext.declarative import DeclarativeMeta 

34from sqlalchemy.sql.sqltypes import Boolean, Integer 

35 

36from camcops_server.cc_modules.cc_constants import ( 

37 CssClass, 

38 DATA_COLLECTION_UNLESS_UPGRADED_DIV, 

39 PV, 

40) 

41from camcops_server.cc_modules.cc_ctvinfo import CTV_INCOMPLETE, CtvInfo 

42from camcops_server.cc_modules.cc_db import add_multiple_columns 

43from camcops_server.cc_modules.cc_html import answer, get_yes_no_unknown, tr 

44from camcops_server.cc_modules.cc_request import CamcopsRequest 

45from camcops_server.cc_modules.cc_summaryelement import SummaryElement 

46from camcops_server.cc_modules.cc_task import ( 

47 Task, 

48 TaskHasPatientMixin, 

49 TaskHasRespondentMixin, 

50) 

51 

52 

53# ============================================================================= 

54# NPI-Q 

55# ============================================================================= 

56 

57ENDORSED = "endorsed" 

58SEVERITY = "severity" 

59DISTRESS = "distress" 

60 

61 

62class NpiQMetaclass(DeclarativeMeta): 

63 # noinspection PyInitNewSignature 

64 def __init__( 

65 cls: Type["NpiQ"], 

66 name: str, 

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

68 classdict: Dict[str, Any], 

69 ) -> None: 

70 question_snippets = [ 

71 "delusions", # 1 

72 "hallucinations", 

73 "agitation/aggression", 

74 "depression/dysphoria", 

75 "anxiety", # 5 

76 "elation/euphoria", 

77 "apathy/indifference", 

78 "disinhibition", 

79 "irritability/lability", 

80 "motor disturbance", # 10 

81 "night-time behaviour", 

82 "appetite/eating", 

83 ] 

84 add_multiple_columns( 

85 cls, 

86 ENDORSED, 

87 1, 

88 cls.NQUESTIONS, 

89 Boolean, 

90 pv=PV.BIT, 

91 comment_fmt="Q{n}, {s}, endorsed?", 

92 comment_strings=question_snippets, 

93 ) 

94 add_multiple_columns( 

95 cls, 

96 SEVERITY, 

97 1, 

98 cls.NQUESTIONS, 

99 pv=list(range(1, 3 + 1)), 

100 comment_fmt="Q{n}, {s}, severity (1-3), if endorsed", 

101 comment_strings=question_snippets, 

102 ) 

103 add_multiple_columns( 

104 cls, 

105 DISTRESS, 

106 1, 

107 cls.NQUESTIONS, 

108 pv=list(range(0, 5 + 1)), 

109 comment_fmt="Q{n}, {s}, distress (0-5), if endorsed", 

110 comment_strings=question_snippets, 

111 ) 

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

113 

114 

115class NpiQ( 

116 TaskHasPatientMixin, TaskHasRespondentMixin, Task, metaclass=NpiQMetaclass 

117): 

118 """ 

119 Server implementation of the NPI-Q task. 

120 """ 

121 

122 __tablename__ = "npiq" 

123 shortname = "NPI-Q" 

124 

125 NQUESTIONS = 12 

126 ENDORSED_FIELDS = strseq(ENDORSED, 1, NQUESTIONS) 

127 MAX_SEVERITY = 3 * NQUESTIONS 

128 MAX_DISTRESS = 5 * NQUESTIONS 

129 

130 @staticmethod 

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

132 _ = req.gettext 

133 return _("Neuropsychiatric Inventory Questionnaire") 

134 

135 def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]: 

136 return self.standard_task_summary_fields() + [ 

137 SummaryElement( 

138 name="n_endorsed", 

139 coltype=Integer(), 

140 value=self.n_endorsed(), 

141 comment=f"Number endorsed (/ {self.NQUESTIONS})", 

142 ), 

143 SummaryElement( 

144 name="severity_score", 

145 coltype=Integer(), 

146 value=self.severity_score(), 

147 comment=f"Severity score (/ {self.MAX_SEVERITY})", 

148 ), 

149 SummaryElement( 

150 name="distress_score", 

151 coltype=Integer(), 

152 value=self.distress_score(), 

153 comment=f"Distress score (/ {self.MAX_DISTRESS})", 

154 ), 

155 ] 

156 

157 def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]: 

158 if not self.is_complete(): 

159 return CTV_INCOMPLETE 

160 return [ 

161 CtvInfo( 

162 content=( 

163 "Endorsed: {e}/{me}; severity {s}/{ms}; " 

164 "distress {d}/{md}".format( 

165 e=self.n_endorsed(), 

166 me=self.NQUESTIONS, 

167 s=self.severity_score(), 

168 ms=self.MAX_SEVERITY, 

169 d=self.distress_score(), 

170 md=self.MAX_DISTRESS, 

171 ) 

172 ) 

173 ) 

174 ] 

175 

176 def q_endorsed(self, q: int) -> bool: 

177 return bool(getattr(self, ENDORSED + str(q))) 

178 

179 def n_endorsed(self) -> int: 

180 return self.count_booleans(self.ENDORSED_FIELDS) 

181 

182 def severity_score(self) -> int: 

183 total = 0 

184 for q in range(1, self.NQUESTIONS + 1): 

185 if self.q_endorsed(q): 

186 s = getattr(self, SEVERITY + str(q)) 

187 if s is not None: 

188 total += s 

189 return total 

190 

191 def distress_score(self) -> int: 

192 total = 0 

193 for q in range(1, self.NQUESTIONS + 1): 

194 if self.q_endorsed(q): 

195 d = getattr(self, DISTRESS + str(q)) 

196 if d is not None: 

197 total += d 

198 return total 

199 

200 def q_complete(self, q: int) -> bool: 

201 qstr = str(q) 

202 endorsed = getattr(self, ENDORSED + qstr) 

203 if endorsed is None: 

204 return False 

205 if not endorsed: 

206 return True 

207 if getattr(self, SEVERITY + qstr) is None: 

208 return False 

209 if getattr(self, DISTRESS + qstr) is None: 

210 return False 

211 return True 

212 

213 def is_complete(self) -> bool: 

214 return ( 

215 self.is_respondent_complete() 

216 and all(self.q_complete(q) for q in range(1, self.NQUESTIONS + 1)) 

217 and self.field_contents_valid() 

218 ) 

219 

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

221 h = f""" 

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

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

224 {self.get_is_complete_tr(req)} 

225 <tr> 

226 <td>Endorsed</td> 

227 <td>{self.n_endorsed()} / 12</td> 

228 </td> 

229 <tr> 

230 <td>Severity score</td> 

231 <td>{self.severity_score()} / 36</td> 

232 </td> 

233 <tr> 

234 <td>Distress score</td> 

235 <td>{self.distress_score()} / 60</td> 

236 </td> 

237 </table> 

238 </div> 

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

240 <tr> 

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

242 <th width="20%">Endorsed</th> 

243 <th width="20%">Severity (patient)</th> 

244 <th width="20%">Distress (carer)</th> 

245 </tr> 

246 """ 

247 for q in range(1, self.NQUESTIONS + 1): 

248 qstr = str(q) 

249 e = getattr(self, ENDORSED + qstr) 

250 s = getattr(self, SEVERITY + qstr) 

251 d = getattr(self, DISTRESS + qstr) 

252 qtext = "<b>{}:</b> {}".format( 

253 self.wxstring(req, "t" + qstr), self.wxstring(req, "q" + qstr) 

254 ) 

255 etext = get_yes_no_unknown(req, e) 

256 if e: 

257 stext = self.wxstring( 

258 req, f"severity_{s}", s, provide_default_if_none=False 

259 ) 

260 dtext = self.wxstring( 

261 req, f"distress_{d}", d, provide_default_if_none=False 

262 ) 

263 else: 

264 stext = "" 

265 dtext = "" 

266 h += tr(qtext, answer(etext), answer(stext), answer(dtext)) 

267 h += f""" 

268 </table> 

269 {DATA_COLLECTION_UNLESS_UPGRADED_DIV} 

270 """ 

271 return h