Coverage for tasks/pswq.py: 57%

63 statements  

« prev     ^ index     » next       coverage.py v7.9.2, created at 2025-07-15 14:23 +0100

1""" 

2camcops_server/tasks/pswq.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 

28from typing import Any, List, Optional, Type 

29 

30from cardinal_pythonlib.stringfunc import strseq 

31from sqlalchemy.sql.sqltypes import Integer 

32 

33from camcops_server.cc_modules.cc_constants import CssClass 

34from camcops_server.cc_modules.cc_ctvinfo import CTV_INCOMPLETE, CtvInfo 

35from camcops_server.cc_modules.cc_db import add_multiple_columns 

36from camcops_server.cc_modules.cc_html import answer, tr 

37from camcops_server.cc_modules.cc_request import CamcopsRequest 

38from camcops_server.cc_modules.cc_snomed import SnomedExpression, SnomedLookup 

39from camcops_server.cc_modules.cc_summaryelement import SummaryElement 

40from camcops_server.cc_modules.cc_task import Task, TaskHasPatientMixin 

41from camcops_server.cc_modules.cc_trackerhelpers import TrackerInfo 

42 

43 

44# ============================================================================= 

45# PSWQ 

46# ============================================================================= 

47 

48 

49class Pswq( # type: ignore[misc] 

50 TaskHasPatientMixin, 

51 Task, 

52): 

53 """ 

54 Server implementation of the PSWQ task. 

55 """ 

56 

57 __tablename__ = "pswq" 

58 shortname = "PSWQ" 

59 provides_trackers = True 

60 

61 MIN_PER_Q = 1 

62 MAX_PER_Q = 5 

63 NQUESTIONS = 16 

64 REVERSE_SCORE = [1, 3, 8, 10, 11] 

65 

66 @classmethod 

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

68 add_multiple_columns( 

69 cls, 

70 "q", 

71 1, 

72 cls.NQUESTIONS, 

73 minimum=cls.MIN_PER_Q, 

74 maximum=cls.MAX_PER_Q, 

75 comment_fmt="Q{n}, {s} (1-5)", 

76 comment_strings=[ 

77 "OK if not enough time [REVERSE SCORE]", # 1 

78 "worries overwhelm", 

79 "do not tend to worry [REVERSE SCORE]", 

80 "many situations make me worry", 

81 "cannot help worrying", # 5 

82 "worry under pressure", 

83 "always worrying", 

84 "easily dismiss worries [REVERSE SCORE]", 

85 "finish then worry about next thing", 

86 "never worry [REVERSE SCORE]", # 10 

87 "if nothing more to do, I do not worry [REVERSE SCORE]", 

88 "lifelong worrier", 

89 "have been worrying", 

90 "when start worrying cannot stop", 

91 "worry all the time", # 15 

92 "worry about projects until done", 

93 ], 

94 ) 

95 

96 TASK_FIELDS = strseq("q", 1, NQUESTIONS) 

97 MIN_TOTAL = MIN_PER_Q * NQUESTIONS 

98 MAX_TOTAL = MAX_PER_Q * NQUESTIONS 

99 

100 @staticmethod 

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

102 _ = req.gettext 

103 return _("Penn State Worry Questionnaire") 

104 

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

106 return [ 

107 TrackerInfo( 

108 value=self.total_score(), 

109 plot_label="PSWQ total score (lower is better)", 

110 axis_label=f"Total score ({self.MIN_TOTAL}–{self.MAX_TOTAL})", 

111 axis_min=self.MIN_TOTAL - 0.5, 

112 axis_max=self.MAX_TOTAL + 0.5, 

113 ) 

114 ] 

115 

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

117 return self.standard_task_summary_fields() + [ 

118 SummaryElement( 

119 name="total_score", 

120 coltype=Integer(), 

121 value=self.total_score(), 

122 comment="Total score (16-80)", 

123 ) 

124 ] 

125 

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

127 if not self.is_complete(): 

128 return CTV_INCOMPLETE 

129 return [ 

130 CtvInfo( 

131 content=f"PSWQ total score {self.total_score()} " 

132 f"(range {self.MIN_TOTAL}–{self.MAX_TOTAL})" 

133 ) 

134 ] 

135 

136 def score(self, q: int) -> Optional[int]: 

137 value = getattr(self, "q" + str(q)) 

138 if value is None: 

139 return None 

140 if q in self.REVERSE_SCORE: 

141 return self.MAX_PER_Q + 1 - value 

142 else: 

143 return value 

144 

145 def total_score(self) -> int: 

146 values = [self.score(q) for q in range(1, self.NQUESTIONS + 1)] 

147 return sum(v for v in values if v is not None) 

148 

149 def is_complete(self) -> bool: 

150 return ( 

151 self.all_fields_not_none(self.TASK_FIELDS) 

152 and self.field_contents_valid() 

153 ) 

154 

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

156 h = f""" 

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

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

159 {self.get_is_complete_tr(req)} 

160 <tr> 

161 <td>Total score (16–80)</td> 

162 <td>{answer(self.total_score())}</td> 

163 </td> 

164 </table> 

165 </div> 

166 <div class="{CssClass.EXPLANATION}"> 

167 Anchor points are 1 = {self.wxstring(req, "anchor1")}, 

168 5 = {self.wxstring(req, "anchor5")}. 

169 Questions {", ".join(str(x) for x in self.REVERSE_SCORE)} 

170 are reverse-scored. 

171 </div> 

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

173 <tr> 

174 <th width="70%">Question</th> 

175 <th width="15%">Answer (1–5)</th> 

176 <th width="15%">Score (1–5)</th> 

177 </tr> 

178 """ 

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

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

181 score = self.score(q) 

182 h += tr(self.wxstring(req, "q" + str(q)), answer(a), score) 

183 h += """ 

184 </table> 

185 """ 

186 return h 

187 

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

189 codes = [ 

190 SnomedExpression( 

191 req.snomed(SnomedLookup.PSWQ_PROCEDURE_ASSESSMENT) 

192 ) 

193 ] 

194 if self.is_complete(): 

195 codes.append( 

196 SnomedExpression( 

197 req.snomed(SnomedLookup.PSWQ_SCALE), 

198 {req.snomed(SnomedLookup.PSWQ_SCORE): self.total_score()}, 

199 ) 

200 ) 

201 return codes