Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1#!/usr/bin/env python 

2 

3""" 

4camcops_server/tasks/shaps.py 

5 

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

7 

8 Copyright (C) 2012-2020 Rudolf Cardinal (rudolf@pobox.com). 

9 

10 This file is part of CamCOPS. 

11 

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

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

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

15 (at your option) any later version. 

16 

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

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

19 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

20 GNU General Public License for more details. 

21 

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

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

24 

25=============================================================================== 

26 

27**Snaith–Hamilton Pleasure Scale (SHAPS) task.** 

28 

29""" 

30 

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

32 

33from cardinal_pythonlib.stringfunc import strseq 

34from sqlalchemy import Integer 

35from sqlalchemy.ext.declarative import DeclarativeMeta 

36 

37from camcops_server.cc_modules.cc_constants import CssClass 

38from camcops_server.cc_modules.cc_db import add_multiple_columns 

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

40from camcops_server.cc_modules.cc_request import CamcopsRequest 

41from camcops_server.cc_modules.cc_summaryelement import SummaryElement 

42from camcops_server.cc_modules.cc_task import ( 

43 get_from_dict, 

44 TaskHasPatientMixin, 

45 Task, 

46) 

47from camcops_server.cc_modules.cc_text import SS 

48 

49 

50class ShapsMetaclass(DeclarativeMeta): 

51 # noinspection PyInitNewSignature 

52 def __init__(cls: Type['Shaps'], 

53 name: str, 

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

55 classdict: Dict[str, Any]) -> None: 

56 

57 add_multiple_columns( 

58 cls, "q", 1, cls.N_QUESTIONS, 

59 minimum=0, maximum=3, 

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

61 comment_strings=[ 

62 "television", 

63 "family", 

64 "hobbies", 

65 "meal", 

66 "bath", 

67 "flowers", 

68 "smiling", 

69 "smart", 

70 "book", 

71 "tea", 

72 "sunny", 

73 "landscape", 

74 "helping", 

75 "praise", 

76 ] 

77 ) 

78 

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

80 

81 

82class Shaps(TaskHasPatientMixin, 

83 Task, 

84 metaclass=ShapsMetaclass): 

85 __tablename__ = "shaps" 

86 shortname = "SHAPS" 

87 

88 N_QUESTIONS = 14 

89 MAX_SCORE = 14 

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

91 

92 STRONGLY_DISAGREE = 0 

93 DISAGREE = 1 

94 AGREE = 2 

95 STRONGLY_OR_DEFINITELY_AGREE = 3 

96 

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

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

99 # is not intentional! 

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

101 

102 @staticmethod 

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

104 _ = req.gettext 

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

106 

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

108 return self.standard_task_summary_fields() + [ 

109 SummaryElement( 

110 name="total", coltype=Integer(), 

111 value=self.total_score(), 

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

113 ] 

114 

115 def is_complete(self) -> bool: 

116 if self.any_fields_none(self.ALL_QUESTIONS): 

117 return False 

118 if not self.field_contents_valid(): 

119 return False 

120 return True 

121 

122 def total_score(self) -> int: 

123 # Consistent with client implementation 

124 return self.count_where(self.ALL_QUESTIONS, 

125 [self.STRONGLY_DISAGREE, self.DISAGREE]) 

126 

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

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

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

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

131 

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

133 forward_answer_dict = { 

134 None: None, 

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

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

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

138 self.STRONGLY_OR_DEFINITELY_AGREE: "0 — " + self.wxstring( 

139 req, "strongly_agree") 

140 } 

141 

142 # Subtle difference in wording when options presented in reverse 

143 reverse_answer_dict = { 

144 None: None, 

145 self.STRONGLY_OR_DEFINITELY_AGREE: "0 — " + self.wxstring( 

146 req, "definitely_agree"), 

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

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

149 self.STRONGLY_DISAGREE: "1 — " + strongly_disagree 

150 } 

151 

152 rows = "" 

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

154 q_field = "q" + str(q_num) 

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

156 

157 answer_dict = forward_answer_dict 

158 

159 if q_num in self.REVERSE_QUESTIONS: 

160 answer_dict = reverse_answer_dict 

161 

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

163 rows += tr_qa(question_cell, answer_cell) 

164 

165 html = """ 

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

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

168 {tr_is_complete} 

169 {total_score} 

170 </table> 

171 </div> 

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

173 <tr> 

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

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

176 </tr> 

177 {rows} 

178 </table> 

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

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

181 0 points for either ‘agree’ option. 

182 </div> 

183 """.format( 

184 CssClass=CssClass, 

185 tr_is_complete=self.get_is_complete_tr(req), 

186 total_score=tr( 

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

188 "{} / {}".format( 

189 answer(self.total_score()), 

190 self.MAX_SCORE 

191 ) 

192 ), 

193 rows=rows, 

194 ) 

195 return html