Coverage for tasks/cage.py: 57%

1"""

2 camcops_server/tasks/cage.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, 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_db import add_multiple_columns

35 from camcops_server.cc_modules.cc_ctvinfo import CTV_INCOMPLETE, CtvInfo

36 from camcops_server.cc_modules.cc_html import answer, get_yes_no, tr, tr_qa

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_sqla_coltypes import CharColType

40 from camcops_server.cc_modules.cc_summaryelement import SummaryElement

41 from camcops_server.cc_modules.cc_task import Task, TaskHasPatientMixin

42 from camcops_server.cc_modules.cc_text import SS

43 from camcops_server.cc_modules.cc_trackerhelpers import TrackerInfo

44

45

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

47# CAGE

48# =============================================================================

49

50

51 class Cage( # type: ignore[misc]

52 TaskHasPatientMixin,

53 Task,

54):

55 """

56 Server implementation of the CAGE task.

57 """

58

59 __tablename__ = "cage"

60 shortname = "CAGE"

61 provides_trackers = True

62

63 NQUESTIONS = 4

64

65 @classmethod

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

67 add_multiple_columns(

68 cls,

69 "q",

70 1,

71 cls.NQUESTIONS,

72 CharColType,

73 pv=["Y", "N"],

74 comment_fmt="Q{n}, {s} (Y, N)",

75 comment_strings=["C", "A", "G", "E"],

76 )

77

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

79

80 @staticmethod

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

82 _ = req.gettext

83 return _("CAGE Questionnaire")

84

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

86 return [

87 TrackerInfo(

88 value=self.total_score(),

89 plot_label="CAGE total score",

90 axis_label=f"Total score (out of {self.NQUESTIONS})",

91 axis_min=-0.5,

92 axis_max=self.NQUESTIONS + 0.5,

93 horizontal_lines=[1.5],

94 )

95 ]

96

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

98 if not self.is_complete():

99 return CTV_INCOMPLETE

100 return [

101 CtvInfo(

102 content=f"CAGE score {self.total_score()}/{self.NQUESTIONS}"

103 )

104 ]

105

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

107 return self.standard_task_summary_fields() + [

108 SummaryElement(

109 name="total",

110 coltype=Integer(),

111 value=self.total_score(),

112 comment=f"Total score (/{self.NQUESTIONS})",

113 )

114 ]

115

116 def is_complete(self) -> bool:

117 return (

118 self.all_fields_not_none(Cage.TASK_FIELDS)

119 and self.field_contents_valid()

120 )

121

122 def get_value(self, q: int) -> int:

123 return 1 if getattr(self, "q" + str(q)) == "Y" else 0

124

125 def total_score(self) -> int:

126 total = 0

127 for i in range(1, Cage.NQUESTIONS + 1):

128 total += self.get_value(i)

129 return total

130

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

132 score = self.total_score()

133 exceeds_cutoff = score >= 2

134 q_a = ""

135 for q in range(1, Cage.NQUESTIONS + 1):

136 q_a += tr_qa(

137 str(q) + " — " + self.wxstring(req, "q" + str(q)),

138 getattr(self, "q" + str(q)),

139 ) # answer is itself Y/N/NULL

140 total_score = tr(

141 req.sstring(SS.TOTAL_SCORE),

142 answer(score) + f" / {self.NQUESTIONS}",

143 )

144 over_threshold = tr_qa(

145 self.wxstring(req, "over_threshold"),

146 get_yes_no(req, exceeds_cutoff),

147 )

148 return f"""

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

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

151 {self.get_is_complete_tr(req)}

152 {total_score}

153 {over_threshold}

154 </table>

155 </div>

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

157 <tr>

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

159 <th width="30%">Answer</th>

160 </tr>

161 {q_a}

162 </table>

163 """

164

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

166 codes = [

167 SnomedExpression(

168 req.snomed(SnomedLookup.CAGE_PROCEDURE_ASSESSMENT)

169 )

170 ]

171 if self.is_complete():

172 codes.append(

173 SnomedExpression(

174 req.snomed(SnomedLookup.CAGE_SCALE),

175 {req.snomed(SnomedLookup.CAGE_SCORE): self.total_score()},

176 )

177 )

178 return codes