Coverage for tasks/npiq.py: 43%

1"""

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

27

28 from typing import Any, List, Type

29

30 from cardinal_pythonlib.stringfunc import strseq

31 from sqlalchemy.sql.sqltypes import Boolean, Integer

32

33 from camcops_server.cc_modules.cc_constants import (

34 CssClass,

35 DATA_COLLECTION_UNLESS_UPGRADED_DIV,

36 PV,

37)

38 from camcops_server.cc_modules.cc_ctvinfo import CTV_INCOMPLETE, CtvInfo

39 from camcops_server.cc_modules.cc_db import add_multiple_columns

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

41 from camcops_server.cc_modules.cc_request import CamcopsRequest

42 from camcops_server.cc_modules.cc_summaryelement import SummaryElement

43 from camcops_server.cc_modules.cc_task import (

44 Task,

45 TaskHasPatientMixin,

46 TaskHasRespondentMixin,

47)

48

49

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

51# NPI-Q

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

53

54 ENDORSED = "endorsed"

55 SEVERITY = "severity"

56 DISTRESS = "distress"

57

58

59 class NpiQ( # type: ignore[misc]

60 TaskHasPatientMixin,

61 TaskHasRespondentMixin,

62 Task,

63):

64 """

65 Server implementation of the NPI-Q task.

66 """

67

68 __tablename__ = "npiq"

69 shortname = "NPI-Q"

70

71 NQUESTIONS = 12

72

73 @classmethod

74 def extend_columns(cls: Type["NpiQ"], **kwargs: Any) -> None:

75 question_snippets = [

76 "delusions", # 1

77 "hallucinations",

78 "agitation/aggression",

79 "depression/dysphoria",

80 "anxiety", # 5

81 "elation/euphoria",

82 "apathy/indifference",

83 "disinhibition",

84 "irritability/lability",

85 "motor disturbance", # 10

86 "night-time behaviour",

87 "appetite/eating",

88 ]

89 add_multiple_columns(

90 cls,

91 ENDORSED,

92 1,

93 cls.NQUESTIONS,

94 Boolean,

95 pv=PV.BIT,

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

97 comment_strings=question_snippets,

98 )

99 add_multiple_columns(

100 cls,

101 SEVERITY,

102 1,

103 cls.NQUESTIONS,

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

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

106 comment_strings=question_snippets,

107 )

108 add_multiple_columns(

109 cls,

110 DISTRESS,

111 1,

112 cls.NQUESTIONS,

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

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

115 comment_strings=question_snippets,

116 )

117

118 ENDORSED_FIELDS = strseq(ENDORSED, 1, NQUESTIONS)

119 MAX_SEVERITY = 3 * NQUESTIONS

120 MAX_DISTRESS = 5 * NQUESTIONS

121

122 @staticmethod

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

124 _ = req.gettext

125 return _("Neuropsychiatric Inventory Questionnaire")

126

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

128 return self.standard_task_summary_fields() + [

129 SummaryElement(

130 name="n_endorsed",

131 coltype=Integer(),

132 value=self.n_endorsed(),

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

134 ),

135 SummaryElement(

136 name="severity_score",

137 coltype=Integer(),

138 value=self.severity_score(),

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

140 ),

141 SummaryElement(

142 name="distress_score",

143 coltype=Integer(),

144 value=self.distress_score(),

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

146 ),

147 ]

148

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

150 if not self.is_complete():

151 return CTV_INCOMPLETE

152 return [

153 CtvInfo(

154 content=(

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

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

157 e=self.n_endorsed(),

158 me=self.NQUESTIONS,

159 s=self.severity_score(),

160 ms=self.MAX_SEVERITY,

161 d=self.distress_score(),

162 md=self.MAX_DISTRESS,

163 )

164 )

165 )

166 ]

167

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

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

170

171 def n_endorsed(self) -> int:

172 return self.count_booleans(self.ENDORSED_FIELDS)

173

174 def severity_score(self) -> int:

175 total = 0

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

177 if self.q_endorsed(q):

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

179 if s is not None:

180 total += s

181 return total

182

183 def distress_score(self) -> int:

184 total = 0

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

186 if self.q_endorsed(q):

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

188 if d is not None:

189 total += d

190 return total

191

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

193 qstr = str(q)

194 endorsed = getattr(self, ENDORSED + qstr)

195 if endorsed is None:

196 return False

197 if not endorsed:

198 return True

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

200 return False

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

202 return False

203 return True

204

205 def is_complete(self) -> bool:

206 return (

207 self.is_respondent_complete()

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

209 and self.field_contents_valid()

210 )

211

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

213 h = f"""

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

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

216 {self.get_is_complete_tr(req)}

217 <tr>

218 <td>Endorsed</td>

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

220 </td>

221 <tr>

222 <td>Severity score</td>

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

224 </td>

225 <tr>

226 <td>Distress score</td>

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

228 </td>

229 </table>

230 </div>

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

232 <tr>

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

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

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

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

237 </tr>

238 """

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

240 qstr = str(q)

241 e = getattr(self, ENDORSED + qstr)

242 s = getattr(self, SEVERITY + qstr)

243 d = getattr(self, DISTRESS + qstr)

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

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

246 )

247 etext = get_yes_no_unknown(req, e)

248 if e:

249 stext = self.wxstring(

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

251 )

252 dtext = self.wxstring(

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

254 )

255 else:

256 stext = ""

257 dtext = ""

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

259 h += f"""

260 </table>

261 {DATA_COLLECTION_UNLESS_UPGRADED_DIV}

262 """

263 return h