Coverage for tasks/wsas.py: 61%

1"""

2 camcops_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

28 from typing import Any, cast, List, Optional, Type

29

30 from cardinal_pythonlib.stringfunc import strseq

31 from sqlalchemy.orm import Mapped, mapped_column

32 from sqlalchemy.sql.sqltypes import Integer

33

34 from camcops_server.cc_modules.cc_constants import (

35 CssClass,

36 DATA_COLLECTION_UNLESS_UPGRADED_DIV,

37)

38 from camcops_server.cc_modules.cc_ctvinfo import CTV_INCOMPLETE, CtvInfo

39 from camcops_server.cc_modules.cc_db import add_multiple_columns

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

41 from camcops_server.cc_modules.cc_request import CamcopsRequest

42 from camcops_server.cc_modules.cc_snomed import SnomedExpression, SnomedLookup

43 from camcops_server.cc_modules.cc_string import AS

44 from camcops_server.cc_modules.cc_summaryelement import SummaryElement

45 from camcops_server.cc_modules.cc_task import (

46 get_from_dict,

47 Task,

48 TaskHasPatientMixin,

49)

50 from camcops_server.cc_modules.cc_trackerhelpers import TrackerInfo

51

52

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

54# WSAS

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

56

57

58 class 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