Coverage for tasks/cia.py: 50%

64 statements  

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

1""" 

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

30from typing import Any, List, Optional, Type 

31 

32from cardinal_pythonlib.stringfunc import strnumlist, strseq 

33from sqlalchemy.sql.sqltypes import Integer 

34 

35from camcops_server.cc_modules.cc_constants import CssClass 

36from camcops_server.cc_modules.cc_db import add_multiple_columns 

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

38from camcops_server.cc_modules.cc_request import CamcopsRequest 

39from camcops_server.cc_modules.cc_task import TaskHasPatientMixin, Task 

40from camcops_server.cc_modules.cc_text import SS 

41from camcops_server.cc_modules.cc_trackerhelpers import TrackerInfo 

42 

43 

44class 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}")