Coverage for tasks/panss.py: 66%

1"""

2 camcops_server/tasks/panss.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, cast, List, Type

29

30 from cardinal_pythonlib.stringfunc import strseq

31 from sqlalchemy.sql.sqltypes import Integer

32

33 from camcops_server.cc_modules.cc_constants import (

34 CssClass,

35 DATA_COLLECTION_ONLY_DIV,

36)

37 from camcops_server.cc_modules.cc_ctvinfo import CTV_INCOMPLETE, CtvInfo

38 from camcops_server.cc_modules.cc_db import add_multiple_columns

39 from camcops_server.cc_modules.cc_html import tr_qa

40 from camcops_server.cc_modules.cc_request import CamcopsRequest

41 from camcops_server.cc_modules.cc_snomed import SnomedExpression, SnomedLookup

42 from camcops_server.cc_modules.cc_summaryelement import SummaryElement

43 from camcops_server.cc_modules.cc_task import (

44 get_from_dict,

45 Task,

46 TaskHasClinicianMixin,

47 TaskHasPatientMixin,

48)

49 from camcops_server.cc_modules.cc_text import SS

50 from camcops_server.cc_modules.cc_trackerhelpers import TrackerInfo

51

52

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

54# PANSS

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

56

57

58 class Panss( # type: ignore[misc]

59 TaskHasPatientMixin,

60 TaskHasClinicianMixin,

61 Task,

62):

63 """

64 Server implementation of the PANSS task.

65 """

66

67 __tablename__ = "panss"

68 shortname = "PANSS"

69 provides_trackers = True

70

71 NUM_P = 7

72 NUM_N = 7

73 NUM_G = 16

74

75 @classmethod

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

77 add_multiple_columns(

78 cls,

79 "p",

80 1,

81 cls.NUM_P,

82 minimum=1,

83 maximum=7,

84 comment_fmt="P{n}: {s} (1 absent - 7 extreme)",

85 comment_strings=[

86 "delusions",

87 "conceptual disorganisation",

88 "hallucinatory behaviour",

89 "excitement",

90 "grandiosity",

91 "suspiciousness/persecution",

92 "hostility",

93 ],

94 )

95 add_multiple_columns(

96 cls,

97 "n",

98 1,

99 cls.NUM_N,

100 minimum=1,

101 maximum=7,

102 comment_fmt="N{n}: {s} (1 absent - 7 extreme)",

103 comment_strings=[

104 "blunted affect",

105 "emotional withdrawal",

106 "poor rapport",

107 "passive/apathetic social withdrawal",

108 "difficulty in abstract thinking",

109 "lack of spontaneity/conversation flow",

110 "stereotyped thinking",

111 ],

112 )

113 add_multiple_columns(

114 cls,

115 "g",

116 1,

117 cls.NUM_G,

118 minimum=1,

119 maximum=7,

120 comment_fmt="G{n}: {s} (1 absent - 7 extreme)",

121 comment_strings=[

122 "somatic concern",

123 "anxiety",

124 "guilt feelings",

125 "tension",

126 "mannerisms/posturing",

127 "depression",

128 "motor retardation",

129 "uncooperativeness",

130 "unusual thought content",

131 "disorientation",

132 "poor attention",

133 "lack of judgement/insight",

134 "disturbance of volition",

135 "poor impulse control",

136 "preoccupation",

137 "active social avoidance",

138 ],

139 )

140

141 P_FIELDS = strseq("p", 1, NUM_P)

142 N_FIELDS = strseq("n", 1, NUM_N)

143 G_FIELDS = strseq("g", 1, NUM_G)

144 TASK_FIELDS = P_FIELDS + N_FIELDS + G_FIELDS

145

146 MIN_P = 1 * NUM_P

147 MAX_P = 7 * NUM_P

148 MIN_N = 1 * NUM_N

149 MAX_N = 7 * NUM_N

150 MIN_G = 1 * NUM_G

151 MAX_G = 7 * NUM_G

152 MIN_TOTAL = MIN_P + MIN_N + MIN_G

153 MAX_TOTAL = MAX_P + MAX_N + MAX_G

154 MIN_P_MINUS_N = MIN_P - MAX_N

155 MAX_P_MINUS_N = MAX_P - MIN_N

156

157 @staticmethod

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

159 _ = req.gettext

160 return _("Positive and Negative Syndrome Scale")

161

162 def get_trackers(self, req: CamcopsRequest) -> List[TrackerInfo]:

163 return [

164 TrackerInfo(

165 value=self.total_score(),

166 plot_label="PANSS total score",

167 axis_label=f"Total score ({self.MIN_TOTAL}-{self.MAX_TOTAL})",

168 axis_min=self.MIN_TOTAL - 0.5,

169 axis_max=self.MAX_TOTAL + 0.5,

170 ),

171 TrackerInfo(

172 value=self.score_p(),

173 plot_label="PANSS P score",

174 axis_label=f"P score ({self.MIN_P}-{self.MAX_P})",

175 axis_min=self.MIN_P - 0.5,

176 axis_max=self.MAX_P + 0.5,

177 ),

178 TrackerInfo(

179 value=self.score_n(),

180 plot_label="PANSS N score",

181 axis_label=f"N score ({self.MIN_N}-{self.MAX_N})",

182 axis_min=self.MIN_N - 0.5,

183 axis_max=self.MAX_N + 0.5,

184 ),

185 TrackerInfo(

186 value=self.score_g(),

187 plot_label="PANSS G score",

188 axis_label=f"G score ({self.MIN_G}-{self.MAX_G})",

189 axis_min=self.MIN_G - 0.5,

190 axis_max=self.MAX_G + 0.5,

191 ),

192 TrackerInfo(

193 value=self.composite(),

194 plot_label=f"PANSS composite score "

195 f"({self.MIN_P_MINUS_N} to {self.MAX_P_MINUS_N})",

196 axis_label="P - N",

197 ),

198 ]

199

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

201 if not self.is_complete():

202 return CTV_INCOMPLETE

203 return [

204 CtvInfo(

205 content=(

206 f"PANSS total score {self.total_score()} "

207 f"(P {self.score_p()}, "

208 f"N {self.score_n()}, "

209 f"G {self.score_g()}, "

210 f"composite P–N {self.composite()})"

211 )

212 )

213 ]

214

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

216 return self.standard_task_summary_fields() + [

217 SummaryElement(

218 name="total",

219 coltype=Integer(),

220 value=self.total_score(),

221 comment=f"Total score ({self.MIN_TOTAL}-{self.MAX_TOTAL})",

222 ),

223 SummaryElement(

224 name="p",

225 coltype=Integer(),

226 value=self.score_p(),

227 comment=f"Positive symptom (P) score ({self.MIN_P}-{self.MAX_P})", # noqa

228 ),

229 SummaryElement(

230 name="n",

231 coltype=Integer(),

232 value=self.score_n(),

233 comment=f"Negative symptom (N) score ({self.MIN_N}-{self.MAX_N})", # noqa

234 ),

235 SummaryElement(

236 name="g",

237 coltype=Integer(),

238 value=self.score_g(),

239 comment=f"General symptom (G) score ({self.MIN_G}-{self.MAX_G})", # noqa

240 ),

241 SummaryElement(

242 name="composite",

243 coltype=Integer(),

244 value=self.composite(),

245 comment=f"Composite score (P - N) ({self.MIN_P_MINUS_N} "

246 f"to {self.MAX_P_MINUS_N})",

247 ),

248 ]

249

250 def is_complete(self) -> bool:

251 return (

252 self.all_fields_not_none(self.TASK_FIELDS)

253 and self.field_contents_valid()

254 )

255

256 def total_score(self) -> int:

257 return cast(int, self.sum_fields(self.TASK_FIELDS))

258

259 def score_p(self) -> int:

260 return cast(int, self.sum_fields(self.P_FIELDS))

261

262 def score_n(self) -> int:

263 return cast(int, self.sum_fields(self.N_FIELDS))

264

265 def score_g(self) -> int:

266 return cast(int, self.sum_fields(self.G_FIELDS))

267

268 def composite(self) -> int:

269 return cast(int, self.score_p() - self.score_n())

270

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

272 p = self.score_p()

273 n = self.score_n()

274 g = self.score_g()

275 composite = self.composite()

276 total = p + n + g

277 answers = {

278 None: None,

279 1: self.wxstring(req, "option1"),

280 2: self.wxstring(req, "option2"),

281 3: self.wxstring(req, "option3"),

282 4: self.wxstring(req, "option4"),

283 5: self.wxstring(req, "option5"),

284 6: self.wxstring(req, "option6"),

285 7: self.wxstring(req, "option7"),

286 }

287 q_a = ""

288 for q in self.TASK_FIELDS:

289 q_a += tr_qa(

290 self.wxstring(req, "" + q + "_s"),

291 get_from_dict(answers, getattr(self, q)),

292 )

293 h = """

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

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

296 {tr_is_complete}

297 {total_score}

298 {p}

299 {n}

300 {g}

301 {composite}

302 </table>

303 </div>

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

305 <tr>

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

307 <th width="60%">Answer</th>

308 </tr>

309 {q_a}

310 </table>

311 {DATA_COLLECTION_ONLY_DIV}

312 """.format(

313 CssClass=CssClass,

314 tr_is_complete=self.get_is_complete_tr(req),

315 total_score=tr_qa(

316 f"{req.sstring(SS.TOTAL_SCORE)} "

317 f"({self.MIN_TOTAL}–{self.MAX_TOTAL})",

318 total,

319 ),

320 p=tr_qa(

321 f"{self.wxstring(req, 'p')} ({self.MIN_P}–{self.MAX_P})", p

322 ),

323 n=tr_qa(

324 f"{self.wxstring(req, 'n')} ({self.MIN_N}–{self.MAX_N})", n

325 ),

326 g=tr_qa(

327 f"{self.wxstring(req, 'g')} ({self.MIN_G}–{self.MAX_G})", g

328 ),

329 composite=tr_qa(

330 f"{self.wxstring(req, 'composite')} "

331 f"({self.MIN_P_MINUS_N}–{self.MAX_P_MINUS_N})",

332 composite,

333 ),

334 q_a=q_a,

335 DATA_COLLECTION_ONLY_DIV=DATA_COLLECTION_ONLY_DIV,

336 )

337 return h

338

339 def get_snomed_codes(self, req: CamcopsRequest) -> List[SnomedExpression]:

340 if not self.is_complete():

341 return []

342 return [SnomedExpression(req.snomed(SnomedLookup.PANSS_SCALE))]