Coverage for tasks/pswq.py: 57%

1"""

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

28 from typing import Any, List, Optional, Type

29

30 from cardinal_pythonlib.stringfunc import strseq

31 from sqlalchemy.sql.sqltypes import Integer

32

33 from camcops_server.cc_modules.cc_constants import CssClass

34 from camcops_server.cc_modules.cc_ctvinfo import CTV_INCOMPLETE, CtvInfo

35 from camcops_server.cc_modules.cc_db import add_multiple_columns

36 from camcops_server.cc_modules.cc_html import answer, tr

37 from camcops_server.cc_modules.cc_request import CamcopsRequest

38 from camcops_server.cc_modules.cc_snomed import SnomedExpression, SnomedLookup

39 from camcops_server.cc_modules.cc_summaryelement import SummaryElement

40 from camcops_server.cc_modules.cc_task import Task, TaskHasPatientMixin

41 from camcops_server.cc_modules.cc_trackerhelpers import TrackerInfo

42

43

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

45# PSWQ

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

47

48

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