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

34 

35from camcops_server.cc_modules.cc_constants import ( 

36 CssClass, 

37 DATA_COLLECTION_ONLY_DIV, 

38) 

39from camcops_server.cc_modules.cc_ctvinfo import CTV_INCOMPLETE, CtvInfo 

40from camcops_server.cc_modules.cc_db import add_multiple_columns 

41from camcops_server.cc_modules.cc_html import tr_qa 

42from camcops_server.cc_modules.cc_request import CamcopsRequest 

43from camcops_server.cc_modules.cc_snomed import SnomedExpression, SnomedLookup 

44from camcops_server.cc_modules.cc_summaryelement import SummaryElement 

45from camcops_server.cc_modules.cc_task import ( 

46 get_from_dict, 

47 Task, 

48 TaskHasClinicianMixin, 

49 TaskHasPatientMixin, 

50) 

51from camcops_server.cc_modules.cc_text import SS 

52from camcops_server.cc_modules.cc_trackerhelpers import TrackerInfo 

53 

54 

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

56# PANSS 

57# ============================================================================= 

58 

59class PanssMetaclass(DeclarativeMeta): 

60 # noinspection PyInitNewSignature 

61 def __init__(cls: Type['Panss'], 

62 name: str, 

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

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

65 add_multiple_columns( 

66 cls, "p", 1, cls.NUM_P, 

67 minimum=1, maximum=7, 

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

69 comment_strings=[ 

70 "delusions", "conceptual disorganisation", 

71 "hallucinatory behaviour", "excitement", 

72 "grandiosity", "suspiciousness/persecution", 

73 "hostility", 

74 ] 

75 ) 

76 add_multiple_columns( 

77 cls, "n", 1, cls.NUM_N, 

78 minimum=1, maximum=7, 

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

80 comment_strings=[ 

81 "blunted affect", "emotional withdrawal", 

82 "poor rapport", "passive/apathetic social withdrawal", 

83 "difficulty in abstract thinking", 

84 "lack of spontaneity/conversation flow", 

85 "stereotyped thinking", 

86 ] 

87 ) 

88 add_multiple_columns( 

89 cls, "g", 1, cls.NUM_G, 

90 minimum=1, maximum=7, 

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

92 comment_strings=[ 

93 "somatic concern", 

94 "anxiety", 

95 "guilt feelings", 

96 "tension", 

97 "mannerisms/posturing", 

98 "depression", 

99 "motor retardation", 

100 "uncooperativeness", 

101 "unusual thought content", 

102 "disorientation", 

103 "poor attention", 

104 "lack of judgement/insight", 

105 "disturbance of volition", 

106 "poor impulse control", 

107 "preoccupation", 

108 "active social avoidance", 

109 ] 

110 ) 

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

112 

113 

114class Panss(TaskHasPatientMixin, TaskHasClinicianMixin, Task, 

115 metaclass=PanssMetaclass): 

116 """ 

117 Server implementation of the PANSS task. 

118 """ 

119 __tablename__ = "panss" 

120 shortname = "PANSS" 

121 provides_trackers = True 

122 

123 NUM_P = 7 

124 NUM_N = 7 

125 NUM_G = 16 

126 

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

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

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

130 TASK_FIELDS = P_FIELDS + N_FIELDS + G_FIELDS 

131 

132 MIN_P = 1 * NUM_P 

133 MAX_P = 7 * NUM_P 

134 MIN_N = 1 * NUM_N 

135 MAX_N = 7 * NUM_N 

136 MIN_G = 1 * NUM_G 

137 MAX_G = 7 * NUM_G 

138 MIN_TOTAL = MIN_P + MIN_N + MIN_G 

139 MAX_TOTAL = MAX_P + MAX_N + MAX_G 

140 MIN_P_MINUS_N = MIN_P - MAX_N 

141 MAX_P_MINUS_N = MAX_P - MIN_N 

142 

143 @staticmethod 

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

145 _ = req.gettext 

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

147 

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

149 return [ 

150 TrackerInfo( 

151 value=self.total_score(), 

152 plot_label="PANSS total score", 

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

154 axis_min=self.MIN_TOTAL - 0.5, 

155 axis_max=self.MAX_TOTAL + 0.5 

156 ), 

157 TrackerInfo( 

158 value=self.score_p(), 

159 plot_label="PANSS P score", 

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

161 axis_min=self.MIN_P - 0.5, 

162 axis_max=self.MAX_P + 0.5 

163 ), 

164 TrackerInfo( 

165 value=self.score_n(), 

166 plot_label="PANSS N score", 

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

168 axis_min=self.MIN_N - 0.5, 

169 axis_max=self.MAX_N + 0.5 

170 ), 

171 TrackerInfo( 

172 value=self.score_g(), 

173 plot_label="PANSS G score", 

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

175 axis_min=self.MIN_G - 0.5, 

176 axis_max=self.MAX_G + 0.5 

177 ), 

178 TrackerInfo( 

179 value=self.composite(), 

180 plot_label=f"PANSS composite score " 

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

182 axis_label="P - N" 

183 ), 

184 ] 

185 

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

187 if not self.is_complete(): 

188 return CTV_INCOMPLETE 

189 return [CtvInfo( 

190 content=( 

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

192 f"(P {self.score_p()}, N {self.score_n()}, G {self.score_g()}, " 

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

194 ) 

195 )] 

196 

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

198 return self.standard_task_summary_fields() + [ 

199 SummaryElement( 

200 name="total", 

201 coltype=Integer(), 

202 value=self.total_score(), 

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

204 ), 

205 SummaryElement( 

206 name="p", 

207 coltype=Integer(), 

208 value=self.score_p(), 

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

210 ), 

211 SummaryElement( 

212 name="n", 

213 coltype=Integer(), 

214 value=self.score_n(), 

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

216 ), 

217 SummaryElement( 

218 name="g", 

219 coltype=Integer(), 

220 value=self.score_g(), 

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

222 ), 

223 SummaryElement( 

224 name="composite", 

225 coltype=Integer(), 

226 value=self.composite(), 

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

228 f"to {self.MAX_P_MINUS_N})" 

229 ), 

230 ] 

231 

232 def is_complete(self) -> bool: 

233 return ( 

234 self.all_fields_not_none(self.TASK_FIELDS) and 

235 self.field_contents_valid() 

236 ) 

237 

238 def total_score(self) -> int: 

239 return self.sum_fields(self.TASK_FIELDS) 

240 

241 def score_p(self) -> int: 

242 return self.sum_fields(self.P_FIELDS) 

243 

244 def score_n(self) -> int: 

245 return self.sum_fields(self.N_FIELDS) 

246 

247 def score_g(self) -> int: 

248 return self.sum_fields(self.G_FIELDS) 

249 

250 def composite(self) -> int: 

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

252 

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

254 p = self.score_p() 

255 n = self.score_n() 

256 g = self.score_g() 

257 composite = self.composite() 

258 total = p + n + g 

259 answers = { 

260 None: None, 

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

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

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

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

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

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

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

268 } 

269 q_a = "" 

270 for q in self.TASK_FIELDS: 

271 q_a += tr_qa( 

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

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

274 ) 

275 h = """ 

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

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

278 {tr_is_complete} 

279 {total_score} 

280 {p} 

281 {n} 

282 {g} 

283 {composite} 

284 </table> 

285 </div> 

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

287 <tr> 

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

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

290 </tr> 

291 {q_a} 

292 </table> 

293 {DATA_COLLECTION_ONLY_DIV} 

294 """.format( 

295 CssClass=CssClass, 

296 tr_is_complete=self.get_is_complete_tr(req), 

297 total_score=tr_qa( 

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

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

300 total 

301 ), 

302 p=tr_qa( 

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

304 p 

305 ), 

306 n=tr_qa( 

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

308 n 

309 ), 

310 g=tr_qa( 

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

312 g 

313 ), 

314 composite=tr_qa( 

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

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

317 composite 

318 ), 

319 q_a=q_a, 

320 DATA_COLLECTION_ONLY_DIV=DATA_COLLECTION_ONLY_DIV, 

321 ) 

322 return h 

323 

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

325 if not self.is_complete(): 

326 return [] 

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