Coverage for cc_modules/cc_taskreports.py: 28%

135 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/cc_modules/cc_taskreports.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**Server reports on CamCOPS tasks.** 

29 

30""" 

31 

32from collections import Counter, namedtuple 

33from operator import attrgetter 

34from typing import Any, List, Sequence, Tuple, Type, TYPE_CHECKING, Union 

35 

36from cardinal_pythonlib.classes import classproperty 

37from cardinal_pythonlib.sqlalchemy.orm_query import ( 

38 get_rows_fieldnames_from_query, 

39) 

40from cardinal_pythonlib.sqlalchemy.sqlfunc import extract_month, extract_year 

41from sqlalchemy.engine.result import RowProxy 

42from sqlalchemy.sql.elements import UnaryExpression 

43from sqlalchemy.sql.expression import desc, func, literal, select 

44from sqlalchemy.sql.functions import FunctionElement 

45 

46from camcops_server.cc_modules.cc_sqla_coltypes import ( 

47 isotzdatetime_to_utcdatetime, 

48) 

49from camcops_server.cc_modules.cc_forms import ( 

50 ReportParamSchema, 

51 ViaIndexSelector, 

52) 

53from camcops_server.cc_modules.cc_pyramid import ViewParam 

54from camcops_server.cc_modules.cc_report import Report, PlainReportType 

55from camcops_server.cc_modules.cc_reportschema import ( 

56 ByYearSelector, 

57 ByMonthSelector, 

58 ByTaskSelector, 

59 ByUserSelector, 

60 DEFAULT_BY_MONTH, 

61 DEFAULT_BY_TASK, 

62 DEFAULT_BY_USER, 

63 DEFAULT_BY_YEAR, 

64) 

65 

66from camcops_server.cc_modules.cc_task import Task 

67from camcops_server.cc_modules.cc_taskindex import TaskIndexEntry 

68from camcops_server.cc_modules.cc_user import User 

69 

70if TYPE_CHECKING: 

71 from camcops_server.cc_modules.cc_request import CamcopsRequest 

72 

73 

74# ============================================================================= 

75# Parameter schema 

76# ============================================================================= 

77 

78 

79class TaskCountReportSchema(ReportParamSchema): 

80 by_year = ByYearSelector() # must match ViewParam.BY_YEAR 

81 by_month = ByMonthSelector() # must match ViewParam.BY_MONTH 

82 by_task = ByTaskSelector() # must match ViewParam.BY_TASK 

83 by_user = ByUserSelector() # must match ViewParam.BY_USER 

84 via_index = ViaIndexSelector() # must match ViewParam.VIA_INDEX 

85 

86 

87# ============================================================================= 

88# Reports 

89# ============================================================================= 

90 

91 

92class TaskCountReport(Report): 

93 """ 

94 Report to count task instances. 

95 """ 

96 

97 # noinspection PyMethodParameters 

98 @classproperty 

99 def report_id(cls) -> str: 

100 return "taskcount" 

101 

102 @classmethod 

103 def title(cls, req: "CamcopsRequest") -> str: 

104 _ = req.gettext 

105 return _("(Server) Count current task instances") 

106 

107 # noinspection PyMethodParameters 

108 @classproperty 

109 def superuser_only(cls) -> bool: 

110 return False 

111 

112 @staticmethod 

113 def get_paramform_schema_class() -> Type["ReportParamSchema"]: 

114 return TaskCountReportSchema 

115 

116 @classmethod 

117 def get_specific_http_query_keys(cls) -> List[str]: 

118 return [ 

119 ViewParam.BY_YEAR, 

120 ViewParam.BY_MONTH, 

121 ViewParam.BY_TASK, 

122 ViewParam.BY_USER, 

123 ViewParam.VIA_INDEX, 

124 ] 

125 

126 def get_rows_colnames(self, req: "CamcopsRequest") -> PlainReportType: 

127 dbsession = req.dbsession 

128 group_ids = req.user.ids_of_groups_user_may_report_on 

129 superuser = req.user.superuser 

130 

131 by_year = req.get_bool_param(ViewParam.BY_YEAR, DEFAULT_BY_YEAR) 

132 by_month = req.get_bool_param(ViewParam.BY_MONTH, DEFAULT_BY_MONTH) 

133 by_task = req.get_bool_param(ViewParam.BY_TASK, DEFAULT_BY_TASK) 

134 by_user = req.get_bool_param(ViewParam.BY_USER, DEFAULT_BY_USER) 

135 via_index = req.get_bool_param(ViewParam.VIA_INDEX, True) 

136 

137 label_year = "year" 

138 label_month = "month" 

139 label_task = "task" 

140 label_user = "adding_user_name" 

141 label_n = "num_tasks_added" 

142 

143 final_rows = [] # type: List[Sequence[Sequence[Any]]] 

144 colnames = [] # type: List[str] # for type checker 

145 

146 if via_index: 

147 # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 

148 # Indexed method (preferable) 

149 # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 

150 selectors = [] # type: List[FunctionElement] 

151 groupers = [] # type: List[str] 

152 sorters = [] # type: List[Union[str, UnaryExpression]] 

153 if by_year: 

154 selectors.append( 

155 extract_year(TaskIndexEntry.when_created_utc).label( 

156 label_year 

157 ) 

158 ) 

159 groupers.append(label_year) 

160 sorters.append(desc(label_year)) 

161 if by_month: 

162 selectors.append( 

163 extract_month(TaskIndexEntry.when_created_utc).label( 

164 label_month 

165 ) 

166 ) 

167 groupers.append(label_month) 

168 sorters.append(desc(label_month)) 

169 if by_task: 

170 selectors.append( 

171 TaskIndexEntry.task_table_name.label(label_task) 

172 ) 

173 groupers.append(label_task) 

174 sorters.append(label_task) 

175 if by_user: 

176 selectors.append(User.username.label(label_user)) 

177 groupers.append(label_user) 

178 sorters.append(label_user) 

179 # Regardless: 

180 selectors.append(func.count().label(label_n)) 

181 

182 # noinspection PyUnresolvedReferences 

183 query = ( 

184 select(selectors) 

185 .select_from(TaskIndexEntry.__table__) 

186 .group_by(*groupers) 

187 .order_by(*sorters) 

188 # ... https://docs.sqlalchemy.org/en/latest/core/tutorial.html#ordering-or-grouping-by-a-label # noqa 

189 ) 

190 if by_user: 

191 # noinspection PyUnresolvedReferences 

192 query = query.select_from(User.__table__).where( 

193 TaskIndexEntry.adding_user_id == User.id 

194 ) 

195 if not superuser: 

196 # Restrict to accessible groups 

197 # noinspection PyProtectedMember 

198 query = query.where(TaskIndexEntry.group_id.in_(group_ids)) 

199 rows, colnames = get_rows_fieldnames_from_query(dbsession, query) 

200 # noinspection PyTypeChecker 

201 final_rows = rows 

202 else: 

203 # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 

204 # Without using the server method (worse) 

205 # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 

206 groupers = [] # type: List[str] 

207 sorters = [] # type: List[Tuple[str, bool]] 

208 # ... (key, reversed/descending) 

209 

210 if by_year: 

211 groupers.append(label_year) 

212 sorters.append((label_year, True)) 

213 if by_month: 

214 groupers.append(label_month) 

215 sorters.append((label_month, True)) 

216 if by_task: 

217 groupers.append(label_task) 

218 # ... redundant in the SQL, which involves multiple queries 

219 # (one per task type), but useful for the Python 

220 # aggregation. 

221 sorters.append((label_task, False)) 

222 if by_user: 

223 groupers.append(label_user) 

224 sorters.append((label_user, False)) 

225 

226 classes = Task.all_subclasses_by_tablename() 

227 counter = Counter() 

228 for cls in classes: 

229 selectors = [] # type: List[FunctionElement] 

230 

231 if by_year: 

232 selectors.append( 

233 # func.year() is specific to some DBs, e.g. MySQL 

234 # so is func.extract(); 

235 # http://modern-sql.com/feature/extract 

236 extract_year( 

237 isotzdatetime_to_utcdatetime(cls.when_created) 

238 ).label(label_year) 

239 ) 

240 if by_month: 

241 selectors.append( 

242 extract_month( 

243 isotzdatetime_to_utcdatetime(cls.when_created) 

244 ).label(label_month) 

245 ) 

246 if by_task: 

247 selectors.append( 

248 literal(cls.__tablename__).label(label_task) 

249 ) 

250 if by_user: 

251 selectors.append(User.username.label(label_user)) 

252 # Regardless: 

253 selectors.append(func.count().label(label_n)) 

254 

255 # noinspection PyUnresolvedReferences 

256 query = ( 

257 select(selectors) 

258 .select_from(cls.__table__) 

259 .where(cls._current == True) # noqa: E712 

260 .group_by(*groupers) 

261 ) 

262 if by_user: 

263 # noinspection PyUnresolvedReferences 

264 query = query.select_from(User.__table__).where( 

265 cls._adding_user_id == User.id 

266 ) 

267 if not superuser: 

268 # Restrict to accessible groups 

269 # noinspection PyProtectedMember 

270 query = query.where(cls._group_id.in_(group_ids)) 

271 rows, colnames = get_rows_fieldnames_from_query( 

272 dbsession, query 

273 ) 

274 if by_task: 

275 final_rows.extend(rows) 

276 else: 

277 for row in rows: # type: RowProxy 

278 key = tuple(row[keyname] for keyname in groupers) 

279 count = row[label_n] 

280 counter.update({key: count}) 

281 if not by_task: 

282 PseudoRow = namedtuple("PseudoRow", groupers + [label_n]) 

283 for key, total in counter.items(): 

284 values = list(key) + [total] 

285 final_rows.append(PseudoRow(*values)) 

286 # Complex sorting: 

287 # https://docs.python.org/3/howto/sorting.html#sort-stability-and-complex-sorts # noqa 

288 for key, descending in reversed(sorters): 

289 final_rows.sort(key=attrgetter(key), reverse=descending) 

290 

291 return PlainReportType(rows=final_rows, column_names=colnames)