Coverage for tasks/shaps.py: 57%

58 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/shaps.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**Snaith–Hamilton Pleasure Scale (SHAPS) task.** 

29 

30""" 

31 

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

33 

34from cardinal_pythonlib.stringfunc import strseq 

35from sqlalchemy import Integer 

36from sqlalchemy.ext.declarative import DeclarativeMeta 

37 

38from camcops_server.cc_modules.cc_constants import CssClass 

39from camcops_server.cc_modules.cc_db import add_multiple_columns 

40from camcops_server.cc_modules.cc_html import tr_qa, tr, answer 

41from camcops_server.cc_modules.cc_request import CamcopsRequest 

42from camcops_server.cc_modules.cc_summaryelement import SummaryElement 

43from camcops_server.cc_modules.cc_task import ( 

44 get_from_dict, 

45 TaskHasPatientMixin, 

46 Task, 

47) 

48from camcops_server.cc_modules.cc_text import SS 

49 

50 

51class ShapsMetaclass(DeclarativeMeta): 

52 # noinspection PyInitNewSignature 

53 def __init__( 

54 cls: Type["Shaps"], 

55 name: str, 

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

57 classdict: Dict[str, Any], 

58 ) -> None: 

59 

60 add_multiple_columns( 

61 cls, 

62 "q", 

63 1, 

64 cls.N_QUESTIONS, 

65 minimum=0, 

66 maximum=3, 

67 comment_fmt="Q{n} - {s}", 

68 comment_strings=[ 

69 "television", 

70 "family", 

71 "hobbies", 

72 "meal", 

73 "bath", 

74 "flowers", 

75 "smiling", 

76 "smart", 

77 "book", 

78 "tea", 

79 "sunny", 

80 "landscape", 

81 "helping", 

82 "praise", 

83 ], 

84 ) 

85 

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

87 

88 

89class Shaps(TaskHasPatientMixin, Task, metaclass=ShapsMetaclass): 

90 __tablename__ = "shaps" 

91 shortname = "SHAPS" 

92 

93 N_QUESTIONS = 14 

94 MAX_SCORE = 14 

95 ALL_QUESTIONS = strseq("q", 1, N_QUESTIONS) 

96 

97 STRONGLY_DISAGREE = 0 

98 DISAGREE = 1 

99 AGREE = 2 

100 STRONGLY_OR_DEFINITELY_AGREE = 3 

101 

102 # Q11 in British Journal of Psychiatry (1995), 167, 99-103 

103 # actually has two "Strongly disagree" options. Assuming this 

104 # is not intentional! 

105 REVERSE_QUESTIONS = {2, 4, 5, 7, 9, 12, 14} 

106 

107 @staticmethod 

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

109 _ = req.gettext 

110 return _("Snaith–Hamilton Pleasure Scale") 

111 

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

113 return self.standard_task_summary_fields() + [ 

114 SummaryElement( 

115 name="total", 

116 coltype=Integer(), 

117 value=self.total_score(), 

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

119 ) 

120 ] 

121 

122 def is_complete(self) -> bool: 

123 if self.any_fields_none(self.ALL_QUESTIONS): 

124 return False 

125 if not self.field_contents_valid(): 

126 return False 

127 return True 

128 

129 def total_score(self) -> int: 

130 # Consistent with client implementation 

131 return self.count_where( 

132 self.ALL_QUESTIONS, [self.STRONGLY_DISAGREE, self.DISAGREE] 

133 ) 

134 

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

136 strongly_disagree = self.wxstring(req, "strongly_disagree") 

137 disagree = self.wxstring(req, "disagree") 

138 agree = self.wxstring(req, "agree") 

139 

140 # We store the actual answers given but these are scored 1 or 0 

141 forward_answer_dict = { 

142 None: None, 

143 self.STRONGLY_DISAGREE: "1 — " + strongly_disagree, 

144 self.DISAGREE: "1 — " + disagree, 

145 self.AGREE: "0 — " + agree, 

146 self.STRONGLY_OR_DEFINITELY_AGREE: "0 — " 

147 + self.wxstring(req, "strongly_agree"), 

148 } 

149 

150 # Subtle difference in wording when options presented in reverse 

151 reverse_answer_dict = { 

152 None: None, 

153 self.STRONGLY_OR_DEFINITELY_AGREE: "0 — " 

154 + self.wxstring(req, "definitely_agree"), 

155 self.AGREE: "0 — " + agree, 

156 self.DISAGREE: "1 — " + disagree, 

157 self.STRONGLY_DISAGREE: "1 — " + strongly_disagree, 

158 } 

159 

160 rows = "" 

161 for q_num in range(1, self.N_QUESTIONS + 1): 

162 q_field = "q" + str(q_num) 

163 question_cell = "{}. {}".format(q_num, self.wxstring(req, q_field)) 

164 

165 answer_dict = forward_answer_dict 

166 

167 if q_num in self.REVERSE_QUESTIONS: 

168 answer_dict = reverse_answer_dict 

169 

170 answer_cell = get_from_dict(answer_dict, getattr(self, q_field)) 

171 rows += tr_qa(question_cell, answer_cell) 

172 

173 html = """ 

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

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

176 {tr_is_complete} 

177 {total_score} 

178 </table> 

179 </div> 

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

181 <tr> 

182 <th width="60%">Question</th> 

183 <th width="40%">Answer</th> 

184 </tr> 

185 {rows} 

186 </table> 

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

188 [1] Score 1 point for either ‘disagree’ option, 

189 0 points for either ‘agree’ option. 

190 </div> 

191 """.format( 

192 CssClass=CssClass, 

193 tr_is_complete=self.get_is_complete_tr(req), 

194 total_score=tr( 

195 req.sstring(SS.TOTAL_SCORE) + " <sup>[1]</sup>", 

196 "{} / {}".format(answer(self.total_score()), self.MAX_SCORE), 

197 ), 

198 rows=rows, 

199 ) 

200 return html