Coverage for tasks/wsas.py: 61%

66 statements  

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

1""" 

2camcops_server/tasks/wsas.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, cast, List, Optional, Type 

29 

30from cardinal_pythonlib.stringfunc import strseq 

31from sqlalchemy.orm import Mapped, mapped_column 

32from sqlalchemy.sql.sqltypes import Integer 

33 

34from camcops_server.cc_modules.cc_constants import ( 

35 CssClass, 

36 DATA_COLLECTION_UNLESS_UPGRADED_DIV, 

37) 

38from camcops_server.cc_modules.cc_ctvinfo import CTV_INCOMPLETE, CtvInfo 

39from camcops_server.cc_modules.cc_db import add_multiple_columns 

40from camcops_server.cc_modules.cc_html import answer, get_true_false, tr, tr_qa 

41from camcops_server.cc_modules.cc_request import CamcopsRequest 

42from camcops_server.cc_modules.cc_snomed import SnomedExpression, SnomedLookup 

43from camcops_server.cc_modules.cc_string import AS 

44from camcops_server.cc_modules.cc_summaryelement import SummaryElement 

45from camcops_server.cc_modules.cc_task import ( 

46 get_from_dict, 

47 Task, 

48 TaskHasPatientMixin, 

49) 

50from camcops_server.cc_modules.cc_trackerhelpers import TrackerInfo 

51 

52 

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

54# WSAS 

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

56 

57 

58class Wsas( # type: ignore[misc] 

59 TaskHasPatientMixin, 

60 Task, 

61): 

62 """ 

63 Server implementation of the WSAS task. 

64 """ 

65 

66 __tablename__ = "wsas" 

67 shortname = "WSAS" 

68 provides_trackers = True 

69 

70 @classmethod 

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

72 add_multiple_columns( 

73 cls, 

74 "q", 

75 1, 

76 cls.NQUESTIONS, 

77 minimum=cls.MIN_PER_Q, 

78 maximum=cls.MAX_PER_Q, 

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

80 comment_strings=[ 

81 "work", 

82 "home management", 

83 "social leisure", 

84 "private leisure", 

85 "relationships", 

86 ], 

87 ) 

88 

89 retired_etc: Mapped[Optional[bool]] = mapped_column( 

90 comment="Retired or choose not to have job for reason unrelated " 

91 "to problem", 

92 ) 

93 

94 MIN_PER_Q = 0 

95 MAX_PER_Q = 8 

96 NQUESTIONS = 5 

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

98 Q2_TO_END = strseq("q", 2, NQUESTIONS) 

99 TASK_FIELDS = QUESTION_FIELDS + ["retired_etc"] 

100 MAX_IF_WORKING = MAX_PER_Q * NQUESTIONS 

101 MAX_IF_RETIRED = MAX_PER_Q * (NQUESTIONS - 1) 

102 

103 @staticmethod 

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

105 _ = req.gettext 

106 return _("Work and Social Adjustment Scale") 

107 

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

109 return [ 

110 TrackerInfo( 

111 value=self.total_score(), 

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

113 axis_label=f"Total score (out of " 

114 f"{self.MAX_IF_RETIRED}–{self.MAX_IF_WORKING})", 

115 axis_min=-0.5, 

116 axis_max=self.MAX_IF_WORKING + 0.5, 

117 ) 

118 ] 

119 

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

121 return self.standard_task_summary_fields() + [ 

122 SummaryElement( 

123 name="total_score", 

124 coltype=Integer(), 

125 value=self.total_score(), 

126 comment=f"Total score (/ {self.max_score()})", 

127 ) 

128 ] 

129 

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

131 if not self.is_complete(): 

132 return CTV_INCOMPLETE 

133 return [ 

134 CtvInfo( 

135 content=f"WSAS total score " 

136 f"{self.total_score()}/{self.max_score()}" 

137 ) 

138 ] 

139 

140 def total_score(self) -> int: 

141 return cast( 

142 int, 

143 self.sum_fields( 

144 self.Q2_TO_END if self.retired_etc else self.QUESTION_FIELDS 

145 ), 

146 ) 

147 

148 def max_score(self) -> int: 

149 return self.MAX_IF_RETIRED if self.retired_etc else self.MAX_IF_WORKING 

150 

151 def is_complete(self) -> bool: 

152 return ( 

153 self.all_fields_not_none( 

154 self.Q2_TO_END if self.retired_etc else self.QUESTION_FIELDS 

155 ) 

156 and self.field_contents_valid() 

157 ) 

158 

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

160 option_dict: dict[Optional[int], Optional[str]] = {None: None} 

161 for a in range(self.MIN_PER_Q, self.MAX_PER_Q + 1): 

162 option_dict[a] = req.wappstring(AS.WSAS_A_PREFIX + str(a)) 

163 q_a = "" 

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

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

166 fa = get_from_dict(option_dict, a) if a is not None else None 

167 q_a += tr(self.wxstring(req, "q" + str(q)), answer(fa)) 

168 return f""" 

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

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

171 {self.get_is_complete_tr(req)} 

172 <tr> 

173 <td>Total score</td> 

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

175 </td> 

176 </table> 

177 </div> 

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

179 <tr> 

180 <th width="75%">Question</th> 

181 <th width="25%">Answer</th> 

182 </tr> 

183 {tr_qa(self.wxstring(req, "q_retired_etc"), 

184 get_true_false(req, self.retired_etc))} 

185 </table> 

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

187 <tr> 

188 <th width="75%">Question</th> 

189 <th width="25%">Answer (0–8)</th> 

190 </tr> 

191 {q_a} 

192 </table> 

193 {DATA_COLLECTION_UNLESS_UPGRADED_DIV} 

194 """ 

195 

196 # noinspection PyUnresolvedReferences 

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

198 codes = [ 

199 SnomedExpression( 

200 req.snomed(SnomedLookup.WSAS_PROCEDURE_ASSESSMENT) 

201 ) 

202 ] 

203 if self.is_complete(): 

204 d = { 

205 req.snomed(SnomedLookup.WSAS_SCORE): self.total_score(), 

206 req.snomed(SnomedLookup.WSAS_HOME_MANAGEMENT_SCORE): self.q2, # type: ignore[attr-defined] # noqa: E501 

207 req.snomed(SnomedLookup.WSAS_SOCIAL_LEISURE_SCORE): self.q3, # type: ignore[attr-defined] # noqa: E501 

208 req.snomed(SnomedLookup.WSAS_PRIVATE_LEISURE_SCORE): self.q4, # type: ignore[attr-defined] # noqa: E501 

209 req.snomed(SnomedLookup.WSAS_RELATIONSHIPS_SCORE): self.q5, # type: ignore[attr-defined] # noqa: E501 

210 } 

211 if not self.retired_etc: 

212 d[req.snomed(SnomedLookup.WSAS_WORK_SCORE)] = self.q1 # type: ignore[attr-defined] # noqa: E501 

213 codes.append( 

214 SnomedExpression(req.snomed(SnomedLookup.WSAS_SCALE), d) # type: ignore[arg-type] # noqa: E501 

215 ) 

216 return codes