Coverage for tasks/cia.py: 50%

1"""

2 camcops_server/tasks/cia.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**The Clinical Impairment Assessment questionnaire (CIA) task.**

27

28"""

29

30 from typing import Any, List, Optional, Type

31

32 from cardinal_pythonlib.stringfunc import strnumlist, strseq

33 from sqlalchemy.sql.sqltypes import Integer

34

35 from camcops_server.cc_modules.cc_constants import CssClass

36 from camcops_server.cc_modules.cc_db import add_multiple_columns

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

38 from camcops_server.cc_modules.cc_request import CamcopsRequest

39 from camcops_server.cc_modules.cc_task import TaskHasPatientMixin, Task

40 from camcops_server.cc_modules.cc_text import SS

41 from camcops_server.cc_modules.cc_trackerhelpers import TrackerInfo

42

43

44 class Cia( # type: ignore[misc]

45 TaskHasPatientMixin,

46 Task,

47):

48 __tablename__ = "cia"

49 shortname = "CIA"

50 provides_trackers = True

51

52 Q_PREFIX = "q"

53 FIRST_Q = 1

54 LAST_Q = 16

55 MAX_SCORE = 48

56

57 @classmethod

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

59

60 add_multiple_columns(

61 cls,

62 cls.Q_PREFIX,

63 cls.FIRST_Q,

64 cls.LAST_Q,

65 coltype=Integer,

66 minimum=0,

67 maximum=3,

68 comment_fmt=cls.Q_PREFIX + "{n} - {s}",

69 comment_strings=[

70 "difficult to concentrate",

71 "critical of self",

72 "going out",

73 "affected work performance",

74 "forgetful",

75 "everyday decisions",

76 "meals with family",

77 "upset",

78 "ashamed",

79 "difficult to eat out",

80 "guilty",

81 "things used to enjoy",

82 "absent-minded",

83 "failure",

84 "relationships",

85 "worry",

86 ],

87 )

88

89 ALL_FIELD_NAMES = strseq(Q_PREFIX, FIRST_Q, LAST_Q)

90 MANDATORY_QUESTIONS = [1, 2, 5, 6, 8, 9, 11, 12, 13, 14, 15, 16]

91 MANDATORY_FIELD_NAMES = strnumlist(Q_PREFIX, MANDATORY_QUESTIONS)

92

93 @staticmethod

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

95 _ = req.gettext

96 return _("Clinical Impairment Assessment questionnaire")

97

98 def is_complete(self) -> bool:

99 if self.any_fields_none(self.MANDATORY_FIELD_NAMES):

100 return False

101

102 return True

103

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

105 return [

106 TrackerInfo(

107 value=self.global_score(),

108 plot_label="CIA global impairment score",

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

110 axis_min=-0.5,

111 axis_max=self.MAX_SCORE + 0.5,

112 ),

113 ]

114

115 def global_score(self) -> Optional[float]:

116 """

117 The original paper states:

118

119 "To obtain the global CIA impairment score the ratings on all items are

120 added together with prorating of missing ratings, so long as at least

121 12 of the 16 items have been rated."

122

123 In our implementation all questions are mandatory except for 3, 4, 7

124 and 10. So there won't be fewer than 12 items rated for a complete

125 questionnaire.

126 """

127 if not self.is_complete():

128 return None

129

130 num_answered = self.n_fields_not_none(self.ALL_FIELD_NAMES)

131 scale_factor = self.LAST_Q / num_answered

132

133 return scale_factor * self.sum_fields(self.ALL_FIELD_NAMES)

134

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

136 question = self.xstring(req, "grid_title")

137

138 rows = ""

139 for q_num in range(self.FIRST_Q, self.LAST_Q + 1):

140 field = self.Q_PREFIX + str(q_num)

141 question_cell = "{}. {}".format(q_num, self.wxstring(req, field))

142

143 rows += tr_qa(question_cell, self.get_answer_cell(req, q_num))

144

145 global_score = self.global_score()

146 if global_score is None:

147 global_score_display = "?"

148 else:

149 global_score_display = "{:.2f} / {}".format(

150 global_score, self.MAX_SCORE

151 )

152

153 html = """

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

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

156 {tr_is_complete}

157 {global_score}

158 </table>

159 </div>

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

161 <tr>

162 <th width="60%">{question}</th>

163 <th width="40%">Response</th>

164 </tr>

165 {rows}

166 </table>

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

168 [1] Sum for all questions with prorating of missing ratings,

169 so long as at least 12 of the 16 items have been rated.

170 </div>

171 """.format(

172 CssClass=CssClass,

173 tr_is_complete=self.get_is_complete_tr(req),

174 global_score=tr(

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

176 answer(global_score_display),

177 ),

178 question=question,

179 rows=rows,

180 )

181 return html

182

183 def get_answer_cell(self, req: CamcopsRequest, q_num: int) -> str:

184 q_field = self.Q_PREFIX + str(q_num)

185

186 score = getattr(self, q_field)

187 if score is None:

188 if q_num in self.MANDATORY_QUESTIONS:

189 return "?"

190

191 return req.sstring(SS.NA)

192

193 meaning = self.get_score_meaning(req, score)

194 return f"{score} [{meaning}]"

195

196 def get_score_meaning(self, req: CamcopsRequest, score: int) -> str:

197 return self.wxstring(req, f"option_{score}")