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

28 

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

30 

31from cardinal_pythonlib.stringfunc import strseq 

32from sqlalchemy.ext.declarative import DeclarativeMeta 

33from sqlalchemy.sql.sqltypes import Boolean, Integer 

34 

35from camcops_server.cc_modules.cc_constants import ( 

36 CssClass, 

37 DATA_COLLECTION_UNLESS_UPGRADED_DIV, 

38 PV, 

39) 

40from camcops_server.cc_modules.cc_ctvinfo import CTV_INCOMPLETE, CtvInfo 

41from camcops_server.cc_modules.cc_db import add_multiple_columns 

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

43from camcops_server.cc_modules.cc_request import CamcopsRequest 

44from camcops_server.cc_modules.cc_summaryelement import SummaryElement 

45from camcops_server.cc_modules.cc_task import ( 

46 Task, 

47 TaskHasPatientMixin, 

48 TaskHasRespondentMixin, 

49) 

50 

51 

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

53# NPI-Q 

54# ============================================================================= 

55 

56ENDORSED = "endorsed" 

57SEVERITY = "severity" 

58DISTRESS = "distress" 

59 

60 

61class NpiQMetaclass(DeclarativeMeta): 

62 # noinspection PyInitNewSignature 

63 def __init__(cls: Type['NpiQ'], 

64 name: str, 

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

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

67 question_snippets = [ 

68 "delusions", # 1 

69 "hallucinations", 

70 "agitation/aggression", 

71 "depression/dysphoria", 

72 "anxiety", # 5 

73 "elation/euphoria", 

74 "apathy/indifference", 

75 "disinhibition", 

76 "irritability/lability", 

77 "motor disturbance", # 10 

78 "night-time behaviour", 

79 "appetite/eating", 

80 ] 

81 add_multiple_columns( 

82 cls, ENDORSED, 1, cls.NQUESTIONS, Boolean, 

83 pv=PV.BIT, 

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

85 comment_strings=question_snippets 

86 ) 

87 add_multiple_columns( 

88 cls, SEVERITY, 1, cls.NQUESTIONS, 

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

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

91 comment_strings=question_snippets 

92 ) 

93 add_multiple_columns( 

94 cls, DISTRESS, 1, cls.NQUESTIONS, 

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

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

97 comment_strings=question_snippets 

98 ) 

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

100 

101 

102class NpiQ(TaskHasPatientMixin, TaskHasRespondentMixin, Task, 

103 metaclass=NpiQMetaclass): 

104 """ 

105 Server implementation of the NPI-Q task. 

106 """ 

107 __tablename__ = "npiq" 

108 shortname = "NPI-Q" 

109 

110 NQUESTIONS = 12 

111 ENDORSED_FIELDS = strseq(ENDORSED, 1, NQUESTIONS) 

112 MAX_SEVERITY = 3 * NQUESTIONS 

113 MAX_DISTRESS = 5 * NQUESTIONS 

114 

115 @staticmethod 

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

117 _ = req.gettext 

118 return _("Neuropsychiatric Inventory Questionnaire") 

119 

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

121 return self.standard_task_summary_fields() + [ 

122 SummaryElement( 

123 name="n_endorsed", coltype=Integer(), 

124 value=self.n_endorsed(), 

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

126 SummaryElement( 

127 name="severity_score", coltype=Integer(), 

128 value=self.severity_score(), 

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

130 SummaryElement( 

131 name="distress_score", coltype=Integer(), 

132 value=self.distress_score(), 

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

134 ] 

135 

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

137 if not self.is_complete(): 

138 return CTV_INCOMPLETE 

139 return [CtvInfo( 

140 content=( 

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

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

143 e=self.n_endorsed(), 

144 me=self.NQUESTIONS, 

145 s=self.severity_score(), 

146 ms=self.MAX_SEVERITY, 

147 d=self.distress_score(), 

148 md=self.MAX_DISTRESS, 

149 ) 

150 ) 

151 )] 

152 

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

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

155 

156 def n_endorsed(self) -> int: 

157 return self.count_booleans(self.ENDORSED_FIELDS) 

158 

159 def severity_score(self) -> int: 

160 total = 0 

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

162 if self.q_endorsed(q): 

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

164 if s is not None: 

165 total += s 

166 return total 

167 

168 def distress_score(self) -> int: 

169 total = 0 

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

171 if self.q_endorsed(q): 

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

173 if d is not None: 

174 total += d 

175 return total 

176 

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

178 qstr = str(q) 

179 endorsed = getattr(self, ENDORSED + qstr) 

180 if endorsed is None: 

181 return False 

182 if not endorsed: 

183 return True 

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

185 return False 

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

187 return False 

188 return True 

189 

190 def is_complete(self) -> bool: 

191 return ( 

192 self.is_respondent_complete() and 

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

194 self.field_contents_valid() 

195 ) 

196 

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

198 h = f""" 

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

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

201 {self.get_is_complete_tr(req)} 

202 <tr> 

203 <td>Endorsed</td> 

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

205 </td> 

206 <tr> 

207 <td>Severity score</td> 

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

209 </td> 

210 <tr> 

211 <td>Distress score</td> 

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

213 </td> 

214 </table> 

215 </div> 

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

217 <tr> 

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

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

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

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

222 </tr> 

223 """ 

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

225 qstr = str(q) 

226 e = getattr(self, ENDORSED + qstr) 

227 s = getattr(self, SEVERITY + qstr) 

228 d = getattr(self, DISTRESS + qstr) 

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

230 self.wxstring(req, "t" + qstr), 

231 self.wxstring(req, "q" + qstr), 

232 ) 

233 etext = get_yes_no_unknown(req, e) 

234 if e: 

235 stext = self.wxstring(req, f"severity_{s}", s, 

236 provide_default_if_none=False) 

237 dtext = self.wxstring(req, f"distress_{d}", d, 

238 provide_default_if_none=False) 

239 else: 

240 stext = "" 

241 dtext = "" 

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

243 h += f""" 

244 </table> 

245 {DATA_COLLECTION_UNLESS_UPGRADED_DIV} 

246 """ 

247 return h