Coverage for tasks/pdss.py: 63%

63 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/pdss.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 

32import cardinal_pythonlib.rnc_web as ws 

33from cardinal_pythonlib.stringfunc import strseq 

34from sqlalchemy.ext.declarative import DeclarativeMeta 

35from sqlalchemy.sql.sqltypes import Float, Integer 

36 

37from camcops_server.cc_modules.cc_constants import ( 

38 CssClass, 

39 DATA_COLLECTION_UNLESS_UPGRADED_DIV, 

40) 

41from camcops_server.cc_modules.cc_ctvinfo import CTV_INCOMPLETE, CtvInfo 

42from camcops_server.cc_modules.cc_db import add_multiple_columns 

43from camcops_server.cc_modules.cc_html import answer, tr 

44from camcops_server.cc_modules.cc_request import CamcopsRequest 

45from camcops_server.cc_modules.cc_snomed import SnomedExpression, SnomedLookup 

46from camcops_server.cc_modules.cc_summaryelement import SummaryElement 

47from camcops_server.cc_modules.cc_task import Task, TaskHasPatientMixin 

48from camcops_server.cc_modules.cc_trackerhelpers import TrackerInfo 

49 

50 

51# ============================================================================= 

52# PDSS 

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

54 

55DP = 3 

56 

57 

58class PdssMetaclass(DeclarativeMeta): 

59 # noinspection PyInitNewSignature 

60 def __init__( 

61 cls: Type["Pdss"], 

62 name: str, 

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

64 classdict: Dict[str, Any], 

65 ) -> None: 

66 add_multiple_columns( 

67 cls, 

68 "q", 

69 1, 

70 cls.NQUESTIONS, 

71 minimum=cls.MIN_PER_Q, 

72 maximum=cls.MAX_PER_Q, 

73 comment_fmt="Q{n}, {s} (0-4, higher worse)", 

74 comment_strings=[ 

75 "frequency", 

76 "distressing during", 

77 "anxiety about panic", 

78 "places or situations avoided", 

79 "activities avoided", 

80 "interference with responsibilities", 

81 "interference with social life", 

82 ], 

83 ) 

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

85 

86 

87class Pdss(TaskHasPatientMixin, Task, metaclass=PdssMetaclass): 

88 """ 

89 Server implementation of the PDSS task. 

90 """ 

91 

92 __tablename__ = "pdss" 

93 shortname = "PDSS" 

94 provides_trackers = True 

95 

96 MIN_PER_Q = 0 

97 MAX_PER_Q = 4 

98 NQUESTIONS = 7 

99 QUESTION_FIELDS = strseq("q", 1, NQUESTIONS) 

100 MAX_TOTAL = MAX_PER_Q * NQUESTIONS 

101 MAX_COMPOSITE = 4 

102 

103 @staticmethod 

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

105 _ = req.gettext 

106 return _("Panic Disorder Severity Scale") 

107 

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

109 return [ 

110 TrackerInfo( 

111 value=self.total_score(), 

112 plot_label="PDSS total score (lower is better)", 

113 axis_label=f"Total score (out of {self.MAX_TOTAL})", 

114 axis_min=-0.5, 

115 axis_max=self.MAX_TOTAL + 0.5, 

116 ) 

117 ] 

118 

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

120 return self.standard_task_summary_fields() + [ 

121 SummaryElement( 

122 name="total_score", 

123 coltype=Integer(), 

124 value=self.total_score(), 

125 comment=f"Total score (/ {self.MAX_TOTAL})", 

126 ), 

127 SummaryElement( 

128 name="composite_score", 

129 coltype=Float(), 

130 value=self.composite_score(), 

131 comment=f"Composite score (/ {self.MAX_COMPOSITE})", 

132 ), 

133 ] 

134 

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

136 if not self.is_complete(): 

137 return CTV_INCOMPLETE 

138 t = self.total_score() 

139 c = ws.number_to_dp(self.composite_score(), DP, default="?") 

140 return [ 

141 CtvInfo( 

142 content=f"PDSS total score {t}/{self.MAX_TOTAL} " 

143 f"(composite {c}/{self.MAX_COMPOSITE})" 

144 ) 

145 ] 

146 

147 def total_score(self) -> int: 

148 return self.sum_fields(self.QUESTION_FIELDS) 

149 

150 def composite_score(self) -> int: 

151 return self.mean_fields(self.QUESTION_FIELDS) 

152 

153 def is_complete(self) -> bool: 

154 return self.field_contents_valid() and self.all_fields_not_none( 

155 self.QUESTION_FIELDS 

156 ) 

157 

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

159 h = """ 

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

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

162 {complete_tr} 

163 <tr> 

164 <td>Total score</td> 

165 <td>{total} / {tmax}</td> 

166 </td> 

167 <tr> 

168 <td>Composite (mean) score</td> 

169 <td>{composite} / {cmax}</td> 

170 </td> 

171 </table> 

172 </div> 

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

174 <tr> 

175 <th width="60%">Question</th> 

176 <th width="40%">Answer ({qmin}–{qmax})</th> 

177 </tr> 

178 """.format( 

179 CssClass=CssClass, 

180 complete_tr=self.get_is_complete_tr(req), 

181 total=answer(self.total_score()), 

182 tmax=self.MAX_TOTAL, 

183 composite=answer( 

184 ws.number_to_dp(self.composite_score(), DP, default="?") 

185 ), 

186 cmax=self.MAX_COMPOSITE, 

187 qmin=self.MIN_PER_Q, 

188 qmax=self.MAX_PER_Q, 

189 ) 

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

191 qtext = self.wxstring(req, "q" + str(q)) 

192 a = getattr(self, "q" + str(q)) 

193 atext = ( 

194 self.wxstring(req, f"q{q}_option{a}", str(a)) 

195 if a is not None 

196 else None 

197 ) 

198 h += tr(qtext, answer(atext)) 

199 h += ( 

200 """ 

201 </table> 

202 """ 

203 + DATA_COLLECTION_UNLESS_UPGRADED_DIV 

204 ) 

205 return h 

206 

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

208 if not self.is_complete(): 

209 return [] 

210 return [ 

211 SnomedExpression( 

212 req.snomed(SnomedLookup.PDSS_SCALE), 

213 {req.snomed(SnomedLookup.PDSS_SCORE): self.total_score()}, 

214 ) 

215 ]