Coverage for tasks/sfmpq2.py: 64%

1"""

2 camcops_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

30 from camcops_server.cc_modules.cc_constants import CssClass

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

32 from camcops_server.cc_modules.cc_request import CamcopsRequest

33 from camcops_server.cc_modules.cc_sqla_coltypes import (

34 camcops_column,

35 ZERO_TO_10_CHECKER,

36)

37

38 from camcops_server.cc_modules.cc_summaryelement import SummaryElement

39 from camcops_server.cc_modules.cc_task import TaskHasPatientMixin, Task

40 import cardinal_pythonlib.rnc_web as ws

41 from cardinal_pythonlib.stringfunc import strseq

42 from sqlalchemy import Float, Integer

43 from typing import Any, List, Optional, Type

44

45

46 class 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