Coverage for tasks/panss.py: 67%

82 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/panss.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 Integer 

35 

36from camcops_server.cc_modules.cc_constants import ( 

37 CssClass, 

38 DATA_COLLECTION_ONLY_DIV, 

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 tr_qa 

43from camcops_server.cc_modules.cc_request import CamcopsRequest 

44from camcops_server.cc_modules.cc_snomed import SnomedExpression, SnomedLookup 

45from camcops_server.cc_modules.cc_summaryelement import SummaryElement 

46from camcops_server.cc_modules.cc_task import ( 

47 get_from_dict, 

48 Task, 

49 TaskHasClinicianMixin, 

50 TaskHasPatientMixin, 

51) 

52from camcops_server.cc_modules.cc_text import SS 

53from camcops_server.cc_modules.cc_trackerhelpers import TrackerInfo 

54 

55 

56# ============================================================================= 

57# PANSS 

58# ============================================================================= 

59 

60 

61class PanssMetaclass(DeclarativeMeta): 

62 # noinspection PyInitNewSignature 

63 def __init__( 

64 cls: Type["Panss"], 

65 name: str, 

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

67 classdict: Dict[str, Any], 

68 ) -> None: 

69 add_multiple_columns( 

70 cls, 

71 "p", 

72 1, 

73 cls.NUM_P, 

74 minimum=1, 

75 maximum=7, 

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

77 comment_strings=[ 

78 "delusions", 

79 "conceptual disorganisation", 

80 "hallucinatory behaviour", 

81 "excitement", 

82 "grandiosity", 

83 "suspiciousness/persecution", 

84 "hostility", 

85 ], 

86 ) 

87 add_multiple_columns( 

88 cls, 

89 "n", 

90 1, 

91 cls.NUM_N, 

92 minimum=1, 

93 maximum=7, 

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

95 comment_strings=[ 

96 "blunted affect", 

97 "emotional withdrawal", 

98 "poor rapport", 

99 "passive/apathetic social withdrawal", 

100 "difficulty in abstract thinking", 

101 "lack of spontaneity/conversation flow", 

102 "stereotyped thinking", 

103 ], 

104 ) 

105 add_multiple_columns( 

106 cls, 

107 "g", 

108 1, 

109 cls.NUM_G, 

110 minimum=1, 

111 maximum=7, 

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

113 comment_strings=[ 

114 "somatic concern", 

115 "anxiety", 

116 "guilt feelings", 

117 "tension", 

118 "mannerisms/posturing", 

119 "depression", 

120 "motor retardation", 

121 "uncooperativeness", 

122 "unusual thought content", 

123 "disorientation", 

124 "poor attention", 

125 "lack of judgement/insight", 

126 "disturbance of volition", 

127 "poor impulse control", 

128 "preoccupation", 

129 "active social avoidance", 

130 ], 

131 ) 

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

133 

134 

135class Panss( 

136 TaskHasPatientMixin, TaskHasClinicianMixin, Task, metaclass=PanssMetaclass 

137): 

138 """ 

139 Server implementation of the PANSS task. 

140 """ 

141 

142 __tablename__ = "panss" 

143 shortname = "PANSS" 

144 provides_trackers = True 

145 

146 NUM_P = 7 

147 NUM_N = 7 

148 NUM_G = 16 

149 

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

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

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

153 TASK_FIELDS = P_FIELDS + N_FIELDS + G_FIELDS 

154 

155 MIN_P = 1 * NUM_P 

156 MAX_P = 7 * NUM_P 

157 MIN_N = 1 * NUM_N 

158 MAX_N = 7 * NUM_N 

159 MIN_G = 1 * NUM_G 

160 MAX_G = 7 * NUM_G 

161 MIN_TOTAL = MIN_P + MIN_N + MIN_G 

162 MAX_TOTAL = MAX_P + MAX_N + MAX_G 

163 MIN_P_MINUS_N = MIN_P - MAX_N 

164 MAX_P_MINUS_N = MAX_P - MIN_N 

165 

166 @staticmethod 

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

168 _ = req.gettext 

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

170 

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

172 return [ 

173 TrackerInfo( 

174 value=self.total_score(), 

175 plot_label="PANSS total score", 

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

177 axis_min=self.MIN_TOTAL - 0.5, 

178 axis_max=self.MAX_TOTAL + 0.5, 

179 ), 

180 TrackerInfo( 

181 value=self.score_p(), 

182 plot_label="PANSS P score", 

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

184 axis_min=self.MIN_P - 0.5, 

185 axis_max=self.MAX_P + 0.5, 

186 ), 

187 TrackerInfo( 

188 value=self.score_n(), 

189 plot_label="PANSS N score", 

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

191 axis_min=self.MIN_N - 0.5, 

192 axis_max=self.MAX_N + 0.5, 

193 ), 

194 TrackerInfo( 

195 value=self.score_g(), 

196 plot_label="PANSS G score", 

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

198 axis_min=self.MIN_G - 0.5, 

199 axis_max=self.MAX_G + 0.5, 

200 ), 

201 TrackerInfo( 

202 value=self.composite(), 

203 plot_label=f"PANSS composite score " 

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

205 axis_label="P - N", 

206 ), 

207 ] 

208 

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

210 if not self.is_complete(): 

211 return CTV_INCOMPLETE 

212 return [ 

213 CtvInfo( 

214 content=( 

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

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

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

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

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

220 ) 

221 ) 

222 ] 

223 

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

225 return self.standard_task_summary_fields() + [ 

226 SummaryElement( 

227 name="total", 

228 coltype=Integer(), 

229 value=self.total_score(), 

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

231 ), 

232 SummaryElement( 

233 name="p", 

234 coltype=Integer(), 

235 value=self.score_p(), 

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

237 ), 

238 SummaryElement( 

239 name="n", 

240 coltype=Integer(), 

241 value=self.score_n(), 

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

243 ), 

244 SummaryElement( 

245 name="g", 

246 coltype=Integer(), 

247 value=self.score_g(), 

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

249 ), 

250 SummaryElement( 

251 name="composite", 

252 coltype=Integer(), 

253 value=self.composite(), 

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

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

256 ), 

257 ] 

258 

259 def is_complete(self) -> bool: 

260 return ( 

261 self.all_fields_not_none(self.TASK_FIELDS) 

262 and self.field_contents_valid() 

263 ) 

264 

265 def total_score(self) -> int: 

266 return self.sum_fields(self.TASK_FIELDS) 

267 

268 def score_p(self) -> int: 

269 return self.sum_fields(self.P_FIELDS) 

270 

271 def score_n(self) -> int: 

272 return self.sum_fields(self.N_FIELDS) 

273 

274 def score_g(self) -> int: 

275 return self.sum_fields(self.G_FIELDS) 

276 

277 def composite(self) -> int: 

278 return self.score_p() - self.score_n() 

279 

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

281 p = self.score_p() 

282 n = self.score_n() 

283 g = self.score_g() 

284 composite = self.composite() 

285 total = p + n + g 

286 answers = { 

287 None: None, 

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

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

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

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

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

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

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

295 } 

296 q_a = "" 

297 for q in self.TASK_FIELDS: 

298 q_a += tr_qa( 

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

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

301 ) 

302 h = """ 

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

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

305 {tr_is_complete} 

306 {total_score} 

307 {p} 

308 {n} 

309 {g} 

310 {composite} 

311 </table> 

312 </div> 

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

314 <tr> 

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

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

317 </tr> 

318 {q_a} 

319 </table> 

320 {DATA_COLLECTION_ONLY_DIV} 

321 """.format( 

322 CssClass=CssClass, 

323 tr_is_complete=self.get_is_complete_tr(req), 

324 total_score=tr_qa( 

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

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

327 total, 

328 ), 

329 p=tr_qa( 

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

331 ), 

332 n=tr_qa( 

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

334 ), 

335 g=tr_qa( 

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

337 ), 

338 composite=tr_qa( 

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

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

341 composite, 

342 ), 

343 q_a=q_a, 

344 DATA_COLLECTION_ONLY_DIV=DATA_COLLECTION_ONLY_DIV, 

345 ) 

346 return h 

347 

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

349 if not self.is_complete(): 

350 return [] 

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