Coverage for tasks/cage.py: 57%

58 statements  

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

1""" 

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

28from typing import Any, List, 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_db import add_multiple_columns 

35from camcops_server.cc_modules.cc_ctvinfo import CTV_INCOMPLETE, CtvInfo 

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

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

40from camcops_server.cc_modules.cc_summaryelement import SummaryElement 

41from camcops_server.cc_modules.cc_task import Task, TaskHasPatientMixin 

42from camcops_server.cc_modules.cc_text import SS 

43from camcops_server.cc_modules.cc_trackerhelpers import TrackerInfo 

44 

45 

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

47# CAGE 

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

49 

50 

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