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

31import cardinal_pythonlib.rnc_web as ws 

32from cardinal_pythonlib.stringfunc import strseq 

33from sqlalchemy.ext.declarative import DeclarativeMeta 

34from sqlalchemy.sql.sqltypes import Float, Integer 

35 

36from camcops_server.cc_modules.cc_constants import ( 

37 CssClass, 

38 DATA_COLLECTION_UNLESS_UPGRADED_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 answer, tr 

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 Task, TaskHasPatientMixin 

47from camcops_server.cc_modules.cc_trackerhelpers import TrackerInfo 

48 

49 

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

51# PDSS 

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

53 

54DP = 3 

55 

56 

57class PdssMetaclass(DeclarativeMeta): 

58 # noinspection PyInitNewSignature 

59 def __init__(cls: Type['Pdss'], 

60 name: str, 

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

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

63 add_multiple_columns( 

64 cls, "q", 1, cls.NQUESTIONS, 

65 minimum=cls.MIN_PER_Q, maximum=cls.MAX_PER_Q, 

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

67 comment_strings=[ 

68 "frequency", 

69 "distressing during", 

70 "anxiety about panic", 

71 "places or situations avoided", 

72 "activities avoided", 

73 "interference with responsibilities", 

74 "interference with social life", 

75 ] 

76 ) 

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

78 

79 

80class Pdss(TaskHasPatientMixin, Task, 

81 metaclass=PdssMetaclass): 

82 """ 

83 Server implementation of the PDSS task. 

84 """ 

85 __tablename__ = "pdss" 

86 shortname = "PDSS" 

87 provides_trackers = True 

88 

89 MIN_PER_Q = 0 

90 MAX_PER_Q = 4 

91 NQUESTIONS = 7 

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

93 MAX_TOTAL = MAX_PER_Q * NQUESTIONS 

94 MAX_COMPOSITE = 4 

95 

96 @staticmethod 

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

98 _ = req.gettext 

99 return _("Panic Disorder Severity Scale") 

100 

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

102 return [TrackerInfo( 

103 value=self.total_score(), 

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

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

106 axis_min=-0.5, 

107 axis_max=self.MAX_TOTAL + 0.5 

108 )] 

109 

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

111 return self.standard_task_summary_fields() + [ 

112 SummaryElement( 

113 name="total_score", coltype=Integer(), 

114 value=self.total_score(), 

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

116 ), 

117 SummaryElement( 

118 name="composite_score", coltype=Float(), 

119 value=self.composite_score(), 

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

121 ), 

122 ] 

123 

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

125 if not self.is_complete(): 

126 return CTV_INCOMPLETE 

127 t = self.total_score() 

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

129 return [CtvInfo( 

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

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

132 )] 

133 

134 def total_score(self) -> int: 

135 return self.sum_fields(self.QUESTION_FIELDS) 

136 

137 def composite_score(self) -> int: 

138 return self.mean_fields(self.QUESTION_FIELDS) 

139 

140 def is_complete(self) -> bool: 

141 return ( 

142 self.field_contents_valid() and 

143 self.all_fields_not_none(self.QUESTION_FIELDS) 

144 ) 

145 

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

147 h = """ 

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

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

150 {complete_tr} 

151 <tr> 

152 <td>Total score</td> 

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

154 </td> 

155 <tr> 

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

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

158 </td> 

159 </table> 

160 </div> 

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

162 <tr> 

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

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

165 </tr> 

166 """.format( 

167 CssClass=CssClass, 

168 complete_tr=self.get_is_complete_tr(req), 

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

170 tmax=self.MAX_TOTAL, 

171 composite=answer(ws.number_to_dp(self.composite_score(), DP, 

172 default="?")), 

173 cmax=self.MAX_COMPOSITE, 

174 qmin=self.MIN_PER_Q, 

175 qmax=self.MAX_PER_Q, 

176 ) 

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

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

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

180 atext = (self.wxstring(req, f"q{q}_option{a}", str(a)) 

181 if a is not None else None) 

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

183 h += """ 

184 </table> 

185 """ + DATA_COLLECTION_UNLESS_UPGRADED_DIV 

186 return h 

187 

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

189 if not self.is_complete(): 

190 return [] 

191 return [SnomedExpression( 

192 req.snomed(SnomedLookup.PDSS_SCALE), 

193 { 

194 req.snomed(SnomedLookup.PDSS_SCORE): self.total_score(), 

195 } 

196 )]