Coverage for tasks/sfmpq2.py: 64%

61 statements  

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

1""" 

2camcops_server/tasks/sfmpq2.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**Short-Form McGill Pain Questionnaire (SF-MPQ2) task.** 

27 

28""" 

29 

30from camcops_server.cc_modules.cc_constants import CssClass 

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

32from camcops_server.cc_modules.cc_request import CamcopsRequest 

33from camcops_server.cc_modules.cc_sqla_coltypes import ( 

34 camcops_column, 

35 ZERO_TO_10_CHECKER, 

36) 

37 

38from camcops_server.cc_modules.cc_summaryelement import SummaryElement 

39from camcops_server.cc_modules.cc_task import TaskHasPatientMixin, Task 

40import cardinal_pythonlib.rnc_web as ws 

41from cardinal_pythonlib.stringfunc import strseq 

42from sqlalchemy import Float, Integer 

43from typing import Any, List, Optional, Type 

44 

45 

46class Sfmpq2( # type: ignore[misc] 

47 TaskHasPatientMixin, 

48 Task, 

49): 

50 __tablename__ = "sfmpq2" 

51 shortname = "SF-MPQ2" 

52 

53 N_QUESTIONS = 22 

54 MAX_SCORE_PER_Q = 10 

55 

56 @classmethod 

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

58 

59 # Field descriptions are open access, as per: 

60 # https://www.ncbi.nlm.nih.gov/pmc/articles/PMC5221718/ 

61 # https://www.ncbi.nlm.nih.gov/pmc/articles/PMC3225325/ 

62 comment_strings = [ 

63 "throbbing", 

64 "shooting", 

65 "stabbing", 

66 "sharp", 

67 "cramping", 

68 "gnawing", 

69 "hot-burning", 

70 "aching", 

71 "heavy", 

72 "tender", 

73 "splitting", 

74 "tiring–exhausting", 

75 "sickening", 

76 "fearful", 

77 "punishing–cruel", 

78 "electric-shock", 

79 "cold-freezing", 

80 "piercing", 

81 "light touch", 

82 "itching", 

83 "tingling", 

84 "numbness", 

85 ] 

86 score_comment = "(0 none - 10 worst)" 

87 

88 for q_index in range(0, cls.N_QUESTIONS): 

89 q_num = q_index + 1 

90 q_field = "q{}".format(q_num) 

91 

92 setattr( 

93 cls, 

94 q_field, 

95 camcops_column( 

96 q_field, 

97 Integer, 

98 permitted_value_checker=ZERO_TO_10_CHECKER, 

99 comment="Q{} ({}) {}".format( 

100 q_num, comment_strings[q_index], score_comment 

101 ), 

102 ), 

103 ) 

104 

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

106 

107 CONTINUOUS_PAIN_QUESTIONS = Task.fieldnames_from_list( 

108 "q", {1, 5, 6, 8, 9, 10} 

109 ) 

110 INTERMITTENT_PAIN_QUESTIONS = Task.fieldnames_from_list( 

111 "q", {2, 3, 4, 11, 16, 18} 

112 ) 

113 NEUROPATHIC_PAIN_QUESTIONS = Task.fieldnames_from_list( 

114 "q", {7, 17, 19, 20, 21, 22} 

115 ) 

116 AFFECTIVE_PAIN_QUESTIONS = Task.fieldnames_from_list("q", {12, 13, 14, 15}) 

117 

118 @staticmethod 

119 def longname(req: CamcopsRequest) -> str: 

120 _ = req.gettext 

121 return _("Short-Form McGill Pain Questionnaire 2") 

122 

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

124 return self.standard_task_summary_fields() + [ 

125 SummaryElement( 

126 name="total_pain", 

127 coltype=Float(), 

128 value=self.total_pain(), 

129 comment=f"Total pain (/{self.MAX_SCORE_PER_Q})", 

130 ), 

131 SummaryElement( 

132 name="continuous_pain", 

133 coltype=Float(), 

134 value=self.continuous_pain(), 

135 comment=f"Continuous pain (/{self.MAX_SCORE_PER_Q})", 

136 ), 

137 SummaryElement( 

138 name="intermittent_pain", 

139 coltype=Float(), 

140 value=self.intermittent_pain(), 

141 comment=f"Intermittent pain (/{self.MAX_SCORE_PER_Q})", 

142 ), 

143 SummaryElement( 

144 name="neuropathic_pain", 

145 coltype=Float(), 

146 value=self.neuropathic_pain(), 

147 comment=f"Neuropathic pain (/{self.MAX_SCORE_PER_Q})", 

148 ), 

149 SummaryElement( 

150 name="affective_pain", 

151 coltype=Float(), 

152 value=self.affective_pain(), 

153 comment=f"Affective pain (/{self.MAX_SCORE_PER_Q})", 

154 ), 

155 ] 

156 

157 def is_complete(self) -> bool: 

158 if self.any_fields_none(self.ALL_QUESTIONS): 

159 return False 

160 if not self.field_contents_valid(): 

161 return False 

162 return True 

163 

164 def total_pain(self) -> float: 

165 return self.mean_fields(self.ALL_QUESTIONS) 

166 

167 def continuous_pain(self) -> float: 

168 return self.mean_fields(self.CONTINUOUS_PAIN_QUESTIONS) 

169 

170 def intermittent_pain(self) -> float: 

171 return self.mean_fields(self.INTERMITTENT_PAIN_QUESTIONS) 

172 

173 def neuropathic_pain(self) -> float: 

174 return self.mean_fields(self.NEUROPATHIC_PAIN_QUESTIONS) 

175 

176 def affective_pain(self) -> float: 

177 return self.mean_fields(self.AFFECTIVE_PAIN_QUESTIONS) 

178 

179 def format_average(self, value: Optional[float]) -> str: 

180 return "{} / {}".format( 

181 answer(ws.number_to_dp(value, 3, default="?")), 

182 self.MAX_SCORE_PER_Q, 

183 ) 

184 

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

186 rows = "" 

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

188 q_field = "q" + str(q_num) 

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

190 

191 score = getattr(self, q_field) 

192 

193 rows += tr_qa(question_cell, score) 

194 

195 html = """ 

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

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

198 {tr_is_complete} 

199 {total_pain} 

200 {continuous_pain} 

201 {intermittent_pain} 

202 {neuropathic_pain} 

203 {affective_pain} 

204 </table> 

205 </div> 

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

207 <tr> 

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

209 <th width="40%">Answer <sup>[6]</sup></th> 

210 </tr> 

211 {rows} 

212 </table> 

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

214 [1] Average of items 1–22. 

215 [2] Average of items 1, 5, 6, 8, 9, 10. 

216 [3] Average of items 2, 3, 4, 11, 16, 18. 

217 [4] Average of items 7, 17, 19, 20, 21, 22. 

218 [5] Average of items 12, 13, 14, 15. 

219 [6] All items are rated from “0 – none” to 

220 “10 – worst possible”. 

221 </div> 

222 """.format( 

223 CssClass=CssClass, 

224 tr_is_complete=self.get_is_complete_tr(req), 

225 total_pain=tr( 

226 self.wxstring(req, "total_pain") + " <sup>[1]</sup>", 

227 self.format_average(self.total_pain()), 

228 ), 

229 continuous_pain=tr( 

230 self.wxstring(req, "continuous_pain") + " <sup>[2]</sup>", 

231 self.format_average(self.continuous_pain()), 

232 ), 

233 intermittent_pain=tr( 

234 self.wxstring(req, "intermittent_pain") + " <sup>[3]</sup>", 

235 self.format_average(self.intermittent_pain()), 

236 ), 

237 neuropathic_pain=tr( 

238 self.wxstring(req, "neuropathic_pain") + " <sup>[4]</sup>", 

239 self.format_average(self.neuropathic_pain()), 

240 ), 

241 affective_pain=tr( 

242 self.wxstring(req, "affective_pain") + " <sup>[5]</sup>", 

243 self.format_average(self.affective_pain()), 

244 ), 

245 rows=rows, 

246 ) 

247 return html