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
« prev ^ index » next coverage.py v6.5.0, created at 2022-11-08 23:14 +0000
1#!/usr/bin/env python
3"""
4camcops_server/cc_modules/cc_taskreports.py
6===============================================================================
8 Copyright (C) 2012, University of Cambridge, Department of Psychiatry.
9 Created by Rudolf Cardinal (rnc1001@cam.ac.uk).
11 This file is part of CamCOPS.
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.
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.
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/>.
26===============================================================================
28**Server reports on CamCOPS tasks.**
30"""
32from collections import Counter, namedtuple
33from operator import attrgetter
34from typing import Any, List, Sequence, Tuple, Type, TYPE_CHECKING, Union
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
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)
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
70if TYPE_CHECKING:
71 from camcops_server.cc_modules.cc_request import CamcopsRequest
74# =============================================================================
75# Parameter schema
76# =============================================================================
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
87# =============================================================================
88# Reports
89# =============================================================================
92class TaskCountReport(Report):
93 """
94 Report to count task instances.
95 """
97 # noinspection PyMethodParameters
98 @classproperty
99 def report_id(cls) -> str:
100 return "taskcount"
102 @classmethod
103 def title(cls, req: "CamcopsRequest") -> str:
104 _ = req.gettext
105 return _("(Server) Count current task instances")
107 # noinspection PyMethodParameters
108 @classproperty
109 def superuser_only(cls) -> bool:
110 return False
112 @staticmethod
113 def get_paramform_schema_class() -> Type["ReportParamSchema"]:
114 return TaskCountReportSchema
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 ]
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
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)
137 label_year = "year"
138 label_month = "month"
139 label_task = "task"
140 label_user = "adding_user_name"
141 label_n = "num_tasks_added"
143 final_rows = [] # type: List[Sequence[Sequence[Any]]]
144 colnames = [] # type: List[str] # for type checker
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))
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)
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))
226 classes = Task.all_subclasses_by_tablename()
227 counter = Counter()
228 for cls in classes:
229 selectors = [] # type: List[FunctionElement]
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))
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)
291 return PlainReportType(rows=final_rows, column_names=colnames)