Coverage for tasks/cage.py: 59%

61 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-11-08 23:14 +0000

1#!/usr/bin/env python 

2 

3""" 

4camcops_server/tasks/cage.py 

5 

6=============================================================================== 

7 

8 Copyright (C) 2012, University of Cambridge, Department of Psychiatry. 

9 Created by Rudolf Cardinal (rnc1001@cam.ac.uk). 

10 

11 This file is part of CamCOPS. 

12 

13 CamCOPS is free software: you can redistribute it and/or modify 

14 it under the terms of the GNU General Public License as published by 

15 the Free Software Foundation, either version 3 of the License, or 

16 (at your option) any later version. 

17 

18 CamCOPS is distributed in the hope that it will be useful, 

19 but WITHOUT ANY WARRANTY; without even the implied warranty of 

20 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

21 GNU General Public License for more details. 

22 

23 You should have received a copy of the GNU General Public License 

24 along with CamCOPS. If not, see <https://www.gnu.org/licenses/>. 

25 

26=============================================================================== 

27 

28""" 

29 

30from typing import Any, Dict, List, Tuple, Type 

31 

32from cardinal_pythonlib.stringfunc import strseq 

33from sqlalchemy.ext.declarative import DeclarativeMeta 

34from sqlalchemy.sql.sqltypes import Integer 

35 

36from camcops_server.cc_modules.cc_constants import CssClass 

37from camcops_server.cc_modules.cc_db import add_multiple_columns 

38from camcops_server.cc_modules.cc_ctvinfo import CTV_INCOMPLETE, CtvInfo 

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

40from camcops_server.cc_modules.cc_request import CamcopsRequest 

41from camcops_server.cc_modules.cc_snomed import SnomedExpression, SnomedLookup 

42from camcops_server.cc_modules.cc_sqla_coltypes import CharColType 

43from camcops_server.cc_modules.cc_summaryelement import SummaryElement 

44from camcops_server.cc_modules.cc_task import Task, TaskHasPatientMixin 

45from camcops_server.cc_modules.cc_text import SS 

46from camcops_server.cc_modules.cc_trackerhelpers import TrackerInfo 

47 

48 

49# ============================================================================= 

50# CAGE 

51# ============================================================================= 

52 

53 

54class CageMetaclass(DeclarativeMeta): 

55 # noinspection PyInitNewSignature 

56 def __init__( 

57 cls: Type["Cage"], 

58 name: str, 

59 bases: Tuple[Type, ...], 

60 classdict: Dict[str, Any], 

61 ) -> None: 

62 add_multiple_columns( 

63 cls, 

64 "q", 

65 1, 

66 cls.NQUESTIONS, 

67 CharColType, 

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

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

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

71 ) 

72 super().__init__(name, bases, classdict) 

73 

74 

75class Cage(TaskHasPatientMixin, Task, metaclass=CageMetaclass): 

76 """ 

77 Server implementation of the CAGE task. 

78 """ 

79 

80 __tablename__ = "cage" 

81 shortname = "CAGE" 

82 provides_trackers = True 

83 

84 NQUESTIONS = 4 

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

86 

87 @staticmethod 

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

89 _ = req.gettext 

90 return _("CAGE Questionnaire") 

91 

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

93 return [ 

94 TrackerInfo( 

95 value=self.total_score(), 

96 plot_label="CAGE total score", 

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

98 axis_min=-0.5, 

99 axis_max=self.NQUESTIONS + 0.5, 

100 horizontal_lines=[1.5], 

101 ) 

102 ] 

103 

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

105 if not self.is_complete(): 

106 return CTV_INCOMPLETE 

107 return [ 

108 CtvInfo( 

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

110 ) 

111 ] 

112 

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

114 return self.standard_task_summary_fields() + [ 

115 SummaryElement( 

116 name="total", 

117 coltype=Integer(), 

118 value=self.total_score(), 

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

120 ) 

121 ] 

122 

123 def is_complete(self) -> bool: 

124 return ( 

125 self.all_fields_not_none(Cage.TASK_FIELDS) 

126 and self.field_contents_valid() 

127 ) 

128 

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

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

131 

132 def total_score(self) -> int: 

133 total = 0 

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

135 total += self.get_value(i) 

136 return total 

137 

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

139 score = self.total_score() 

140 exceeds_cutoff = score >= 2 

141 q_a = "" 

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

143 q_a += tr_qa( 

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

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

146 ) # answer is itself Y/N/NULL # noqa 

147 total_score = tr( 

148 req.sstring(SS.TOTAL_SCORE), 

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

150 ) 

151 over_threshold = tr_qa( 

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

153 get_yes_no(req, exceeds_cutoff), 

154 ) 

155 return f""" 

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

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

158 {self.get_is_complete_tr(req)} 

159 {total_score} 

160 {over_threshold} 

161 </table> 

162 </div> 

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

164 <tr> 

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

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

167 </tr> 

168 {q_a} 

169 </table> 

170 """ 

171 

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

173 codes = [ 

174 SnomedExpression( 

175 req.snomed(SnomedLookup.CAGE_PROCEDURE_ASSESSMENT) 

176 ) 

177 ] 

178 if self.is_complete(): 

179 codes.append( 

180 SnomedExpression( 

181 req.snomed(SnomedLookup.CAGE_SCALE), 

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

183 ) 

184 ) 

185 return codes