Coverage for tasks/cia.py: 52%

62 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-11-08 23:14 +0000

1#!/usr/bin/env python 

2 

3""" 

4camcops_server/tasks/cia.py 

5 

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

7 

8 Copyright (C) 2012, University of Cambridge, Department of Psychiatry. 

9 Created by Rudolf Cardinal (rnc1001@cam.ac.uk). 

10 

11 This file is part of CamCOPS. 

12 

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

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

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

16 (at your option) any later version. 

17 

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

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

20 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

21 GNU General Public License for more details. 

22 

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

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

25 

26=============================================================================== 

27 

28**The Clinical Impairment Assessment questionnaire (CIA) task.** 

29 

30""" 

31 

32from typing import Any, Dict, Optional, Type, Tuple 

33 

34from cardinal_pythonlib.stringfunc import strnumlist, strseq 

35from sqlalchemy.ext.declarative import DeclarativeMeta 

36from sqlalchemy.sql.sqltypes import Integer 

37 

38from camcops_server.cc_modules.cc_constants import CssClass 

39from camcops_server.cc_modules.cc_db import add_multiple_columns 

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

41from camcops_server.cc_modules.cc_request import CamcopsRequest 

42from camcops_server.cc_modules.cc_task import TaskHasPatientMixin, Task 

43from camcops_server.cc_modules.cc_text import SS 

44 

45 

46class CiaMetaclass(DeclarativeMeta): 

47 def __init__( 

48 cls: Type["Cia"], 

49 name: str, 

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

51 classdict: Dict[str, Any], 

52 ) -> None: 

53 

54 add_multiple_columns( 

55 cls, 

56 cls.Q_PREFIX, 

57 cls.FIRST_Q, 

58 cls.LAST_Q, 

59 coltype=Integer, 

60 minimum=0, 

61 maximum=3, 

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

63 comment_strings=[ 

64 "difficult to concentrate", 

65 "critical of self", 

66 "going out", 

67 "affected work performance", 

68 "forgetful", 

69 "everyday decisions", 

70 "meals with family", 

71 "upset", 

72 "ashamed", 

73 "difficult to eat out", 

74 "guilty", 

75 "things used to enjoy", 

76 "absent-minded", 

77 "failure", 

78 "relationships", 

79 "worry", 

80 ], 

81 ) 

82 

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

84 

85 

86class Cia(TaskHasPatientMixin, Task, metaclass=CiaMetaclass): 

87 __tablename__ = "cia" 

88 shortname = "CIA" 

89 

90 Q_PREFIX = "q" 

91 FIRST_Q = 1 

92 LAST_Q = 16 

93 MAX_SCORE = 48 

94 

95 ALL_FIELD_NAMES = strseq(Q_PREFIX, FIRST_Q, LAST_Q) 

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

97 MANDATORY_FIELD_NAMES = strnumlist(Q_PREFIX, MANDATORY_QUESTIONS) 

98 

99 @staticmethod 

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

101 _ = req.gettext 

102 return _("") 

103 

104 def is_complete(self) -> bool: 

105 if self.any_fields_none(self.MANDATORY_FIELD_NAMES): 

106 return False 

107 

108 return True 

109 

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

111 """ 

112 The original paper states: 

113 

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

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

116 12 of the 16 items have been rated." 

117 

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

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

120 questionnaire. 

121 """ 

122 if not self.is_complete(): 

123 return None 

124 

125 num_answered = self.n_fields_not_none(self.ALL_FIELD_NAMES) 

126 scale_factor = self.LAST_Q / num_answered 

127 

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

129 

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

131 rows = "" 

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

133 field = self.Q_PREFIX + str(q_num) 

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

135 

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

137 

138 global_score = self.global_score() 

139 if global_score is None: 

140 global_score_display = "?" 

141 else: 

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

143 global_score, self.MAX_SCORE 

144 ) 

145 

146 html = """ 

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

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

149 {tr_is_complete} 

150 {global_score} 

151 </table> 

152 </div> 

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

154 <tr> 

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

156 <th width="40%">Score</th> 

157 </tr> 

158 {rows} 

159 </table> 

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

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

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

163 </div> 

164 """.format( 

165 CssClass=CssClass, 

166 tr_is_complete=self.get_is_complete_tr(req), 

167 global_score=tr( 

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

169 answer(global_score_display), 

170 ), 

171 rows=rows, 

172 ) 

173 return html 

174 

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

176 q_field = self.Q_PREFIX + str(q_num) 

177 

178 score = getattr(self, q_field) 

179 if score is None: 

180 if q_num in self.MANDATORY_QUESTIONS: 

181 return "?" 

182 

183 return req.sstring(SS.NA) 

184 

185 meaning = self.get_score_meaning(req, score) 

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

187 

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

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