Coverage for tasks/gds.py: 57%

61 statements  

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

1""" 

2camcops_server/tasks/gds.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, String 

32 

33from camcops_server.cc_modules.cc_constants import CssClass, NO_CHAR, YES_CHAR 

34from camcops_server.cc_modules.cc_ctvinfo import CTV_INCOMPLETE, CtvInfo 

35from camcops_server.cc_modules.cc_db import add_multiple_columns 

36from camcops_server.cc_modules.cc_html import answer, 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_summaryelement import SummaryElement 

40from camcops_server.cc_modules.cc_task import Task, TaskHasPatientMixin 

41from camcops_server.cc_modules.cc_text import SS 

42from camcops_server.cc_modules.cc_trackerhelpers import TrackerInfo 

43 

44 

45# ============================================================================= 

46# GDS-15 

47# ============================================================================= 

48 

49 

50class Gds15( # type: ignore[misc] 

51 TaskHasPatientMixin, 

52 Task, 

53): 

54 """ 

55 Server implementation of the GDS-15 task. 

56 """ 

57 

58 __tablename__ = "gds15" 

59 shortname = "GDS-15" 

60 info_filename_stem = "gds" 

61 provides_trackers = True 

62 

63 NQUESTIONS = 15 

64 

65 @classmethod 

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

67 add_multiple_columns( 

68 cls, 

69 "q", 

70 1, 

71 cls.NQUESTIONS, 

72 String(length=1), 

73 pv=[NO_CHAR, YES_CHAR], 

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

75 comment_strings=[ 

76 "satisfied", 

77 "dropped activities", 

78 "life empty", 

79 "bored", 

80 "good spirits", # 5 

81 "afraid", 

82 "happy", 

83 "helpless", 

84 "stay at home", 

85 "memory problems", # 10 

86 "wonderful to be alive", 

87 "worthless", 

88 "full of energy", 

89 "hopeless", 

90 "others better off", # 15 

91 ], 

92 ) 

93 

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

95 SCORE_IF_YES = [2, 3, 4, 6, 8, 9, 10, 12, 14, 15] 

96 SCORE_IF_NO = [1, 5, 7, 11, 13] 

97 MAX_SCORE = 15 

98 

99 @staticmethod 

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

101 _ = req.gettext 

102 return _("Geriatric Depression Scale, 15-item version") 

103 

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

105 return [ 

106 TrackerInfo( 

107 value=self.total_score(), 

108 plot_label="GDS-15 total score", 

109 axis_label=f"Total score (out of {self.MAX_SCORE})", 

110 axis_min=-0.5, 

111 axis_max=self.MAX_SCORE + 0.5, 

112 ) 

113 ] 

114 

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

116 if not self.is_complete(): 

117 return CTV_INCOMPLETE 

118 return [ 

119 CtvInfo( 

120 content=f"GDS-15 total score " 

121 f"{self.total_score()}/{self.MAX_SCORE}" 

122 ) 

123 ] 

124 

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

126 return self.standard_task_summary_fields() + [ 

127 SummaryElement( 

128 name="total", 

129 coltype=Integer(), 

130 value=self.total_score(), 

131 comment=f"Total score (/{self.MAX_SCORE})", 

132 ) 

133 ] 

134 

135 def is_complete(self) -> bool: 

136 return ( 

137 self.all_fields_not_none(self.TASK_FIELDS) 

138 and self.field_contents_valid() 

139 ) 

140 

141 def total_score(self) -> int: 

142 score = 0 

143 for q in self.SCORE_IF_YES: 

144 if getattr(self, "q" + str(q)) == YES_CHAR: 

145 score += 1 

146 for q in self.SCORE_IF_NO: 

147 if getattr(self, "q" + str(q)) == NO_CHAR: 

148 score += 1 

149 return score 

150 

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

152 score = self.total_score() 

153 

154 q_a = "" 

155 for q in range(1, self.NQUESTIONS + 1): 

156 suffix = " †" if q in self.SCORE_IF_YES else " *" 

157 q_a += tr_qa( 

158 str(q) + ". " + self.wxstring(req, "q" + str(q)) + suffix, 

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

160 ) 

161 

162 return f""" 

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

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

165 {self.get_is_complete_tr(req)} 

166 {tr(req.sstring(SS.TOTAL_SCORE), 

167 answer(score) + f" / {self.MAX_SCORE}")} 

168 </table> 

169 </div> 

170 <div class="{CssClass.EXPLANATION}"> 

171 Ratings are over the last 1 week. 

172 </div> 

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

174 <tr> 

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

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

177 </tr> 

178 {q_a} 

179 </table> 

180 <div class="{CssClass.FOOTNOTES}"> 

181 (†) ‘Y’ scores 1; ‘N’ scores 0. 

182 (*) ‘Y’ scores 0; ‘N’ scores 1. 

183 </div> 

184 """ 

185 

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

187 codes = [ 

188 SnomedExpression( 

189 req.snomed(SnomedLookup.GDS15_PROCEDURE_ASSESSMENT) 

190 ) 

191 ] 

192 if self.is_complete(): 

193 codes.append( 

194 SnomedExpression( 

195 req.snomed(SnomedLookup.GDS15_SCALE), 

196 {req.snomed(SnomedLookup.GDS15_SCORE): self.total_score()}, 

197 ) 

198 ) 

199 return codes