Coverage for portality/bll/services/todo.py: 67%
132 statements
« prev ^ index » next coverage.py v6.4.2, created at 2022-09-21 00:49 +0100
« prev ^ index » next coverage.py v6.4.2, created at 2022-09-21 00:49 +0100
1from portality.lib.argvalidate import argvalidate
2from portality import models
3from portality.bll import exceptions
4from portality import constants
6class TodoService(object):
7 """
8 ~~Todo:Service->DOAJ:Service~~
9 """
11 def group_stats(self, group_id):
12 # ~~-> EditorGroup:Model~~
13 eg = models.EditorGroup.pull(group_id)
14 stats = {"editor_group" : eg.data}
16 #~~-> Account:Model ~~
17 stats["editors"] = {}
18 editors = [eg.editor] + eg.associates
19 for editor in editors:
20 acc = models.Account.pull(editor)
21 stats["editors"][editor] = {
22 "email" : acc.email
23 }
25 q = GroupStatsQuery(eg.name)
26 resp = models.Application.query(q=q.query())
28 stats["total"] = {"applications": 0, "update_requests": 0}
30 stats["by_editor"] = {}
31 for bucket in resp.get("aggregations", {}).get("editor", {}).get("buckets", []):
32 stats["by_editor"][bucket["key"]] = {"applications": 0, "update_requests": 0}
34 for b in bucket.get("application_type", {}).get("buckets", []):
35 if b["key"] == constants.APPLICATION_TYPE_NEW_APPLICATION:
36 stats["by_editor"][bucket["key"]]["applications"] = b["doc_count"]
37 stats["total"]["applications"] += b["doc_count"]
38 elif b["key"] == constants.APPLICATION_TYPE_UPDATE_REQUEST:
39 stats["by_editor"][bucket["key"]]["update_requests"] = b["doc_count"]
40 stats["total"]["update_requests"] += b["doc_count"]
42 unassigned_buckets = resp.get("aggregations", {}).get("unassigned", {}).get("application_type", {}).get("buckets", [])
43 stats["unassigned"] = {"applications": 0, "update_requests": 0}
44 for ub in unassigned_buckets:
45 if ub["key"] == constants.APPLICATION_TYPE_NEW_APPLICATION:
46 stats["unassigned"]["applications"] = ub["doc_count"]
47 stats["total"]["applications"] += ub["doc_count"]
48 elif ub["key"] == constants.APPLICATION_TYPE_UPDATE_REQUEST:
49 stats["unassigned"]["update_requests"] = ub["doc_count"]
50 stats["total"]["update_requests"] += ub["doc_count"]
52 stats["by_status"] = {}
53 for bucket in resp.get("aggregations", {}).get("status", {}).get("buckets", []):
54 stats["by_status"][bucket["key"]] = {"applications": 0, "update_requests": 0}
55 for b in bucket.get("application_type", {}).get("buckets", []):
56 if b["key"] == constants.APPLICATION_TYPE_NEW_APPLICATION:
57 stats["by_status"][bucket["key"]]["applications"] = b["doc_count"]
58 elif b["key"] == constants.APPLICATION_TYPE_UPDATE_REQUEST:
59 stats["by_status"][bucket["key"]]["update_requests"] = b["doc_count"]
61 return stats
64 def top_todo(self, account, size=25):
65 """
66 Returns the top number of todo items for a given user
68 :param account:
69 :return:
70 """
71 # first validate the incoming arguments to ensure that we've got the right thing
72 argvalidate("top_todo", [
73 {"arg" : account, "instance" : models.Account, "allow_none" : False, "arg_name" : "account"}
74 ], exceptions.ArgumentException)
77 queries = []
78 if account.has_role("admin"):
79 maned_of = models.EditorGroup.groups_by_maned(account.id)
80 queries.append(TodoRules.maned_stalled(size, maned_of))
81 queries.append(TodoRules.maned_follow_up_old(size, maned_of))
82 queries.append(TodoRules.maned_ready(size, maned_of))
83 queries.append(TodoRules.maned_completed(size, maned_of))
84 queries.append(TodoRules.maned_assign_pending(size, maned_of))
86 todos = []
87 for aid, q, sort, boost in queries:
88 applications = models.Application.object_query(q=q.query())
89 for ap in applications:
90 todos.append({
91 "date": ap.last_manual_update_timestamp if sort == "last_manual_update" else ap.created_timestamp,
92 "date_type": sort,
93 "action_id" : [aid],
94 "title" : ap.bibjson().title,
95 "object_id" : ap.id,
96 "object" : ap,
97 "boost": boost
98 })
100 todos = self._rationalise_todos(todos, size)
102 return todos
104 def _rationalise_todos(self, todos, size):
105 boosted = list(filter(lambda x: x["boost"], todos))
106 unboosted = list(filter(lambda x: not x["boost"], todos))
108 stds = sorted(boosted, key=lambda x: x['date']) + sorted(unboosted, key=lambda x: x['date'])
110 id_map = {}
111 removals = []
112 for i in range(len(stds)):
113 todo = stds[i]
114 oid = todo["object_id"]
115 if oid in id_map:
116 removals.append(i)
117 stds[id_map[oid]]['action_id'] += todo['action_id']
118 else:
119 id_map[oid] = i
121 removals.reverse()
122 for r in removals:
123 del stds[r]
125 return stds[:size]
128class TodoRules(object):
129 @classmethod
130 def maned_stalled(cls, size, maned_of):
131 stalled = TodoQuery(
132 musts=[
133 TodoQuery.lmu_older_than(8),
134 TodoQuery.editor_group(maned_of)
135 ],
136 must_nots=[
137 TodoQuery.status([constants.APPLICATION_STATUS_ACCEPTED, constants.APPLICATION_STATUS_REJECTED])
138 ],
139 sort="last_manual_update",
140 size=size
141 )
142 return constants.TODO_MANED_STALLED, stalled, "last_manual_update", False
144 @classmethod
145 def maned_follow_up_old(cls, size, maned_of):
146 follow_up_old = TodoQuery(
147 musts=[
148 TodoQuery.cd_older_than(10),
149 TodoQuery.editor_group(maned_of)
150 ],
151 must_nots=[
152 TodoQuery.status([constants.APPLICATION_STATUS_ACCEPTED, constants.APPLICATION_STATUS_REJECTED])
153 ],
154 sort="created_date",
155 size=size
156 )
157 return constants.TODO_MANED_FOLLOW_UP_OLD, follow_up_old, "created_date", False
159 @classmethod
160 def maned_ready(cls, size, maned_of):
161 ready = TodoQuery(
162 musts=[
163 TodoQuery.status([constants.APPLICATION_STATUS_READY]),
164 TodoQuery.editor_group(maned_of)
165 ],
166 sort="last_manual_update",
167 size=size
168 )
169 return constants.TODO_MANED_READY, ready, "last_manual_update", True
171 @classmethod
172 def maned_completed(cls, size, maned_of):
173 completed = TodoQuery(
174 musts=[
175 TodoQuery.status([constants.APPLICATION_STATUS_COMPLETED]),
176 TodoQuery.lmu_older_than(2),
177 TodoQuery.editor_group(maned_of)
178 ],
179 sort="last_manual_update",
180 size=size
181 )
182 return constants.TODO_MANED_COMPLETED, completed, "last_manual_update", False
184 @classmethod
185 def maned_assign_pending(cls, size, maned_of):
186 assign_pending = TodoQuery(
187 musts=[
188 TodoQuery.exists("admin.editor_group"),
189 TodoQuery.lmu_older_than(2),
190 TodoQuery.status([constants.APPLICATION_STATUS_PENDING]),
191 TodoQuery.editor_group(maned_of)
192 ],
193 must_nots=[
194 TodoQuery.exists("admin.editor")
195 ],
196 sort="created_date",
197 size=size
198 )
199 return constants.TODO_MANED_ASSIGN_PENDING, assign_pending, "last_manual_update", False
202class TodoQuery(object):
203 """
204 ~~->$Todo:Query~~
205 ~~^->Elasticsearch:Technology~~
206 """
207 lmu_sort = {"last_manual_update" : {"order" : "asc"}}
208 cd_sort = {"created_date" : {"order" : "asc"}}
210 def __init__(self, musts=None, must_nots=None, sort="last_manual_update", size=10):
211 self._musts = [] if musts is None else musts
212 self._must_nots = [] if must_nots is None else must_nots
213 self._sort = sort
214 self._size = size
216 def query(self):
217 sort = self.lmu_sort if self._sort == "last_manual_update" else self.cd_sort
218 q = {
219 "query" : {
220 "bool" : {
221 "must": self._musts,
222 "must_not": self._must_nots
223 }
224 },
225 "sort" : [
226 sort
227 ],
228 "size" : self._size
229 }
230 return q
232 @classmethod
233 def editor_group(cls, groups):
234 return {
235 "terms" : {
236 "admin.editor_group.exact" : [g.name for g in groups]
237 }
238 }
240 @classmethod
241 def lmu_older_than(cls, weeks):
242 return {
243 "range": {
244 "last_manual_update": {
245 "lte": "now-" + str(weeks) + "w"
246 }
247 }
248 }
250 @classmethod
251 def cd_older_than(cls, weeks):
252 return {
253 "range": {
254 "created_date": {
255 "lte": "now-" + str(weeks) + "w"
256 }
257 }
258 }
260 @classmethod
261 def status(cls, statuses):
262 return {
263 "terms" : {
264 "admin.application_status.exact" : statuses
265 }
266 }
268 @classmethod
269 def exists(cls, field):
270 return {
271 "exists" : {
272 "field" : field
273 }
274 }
277class GroupStatsQuery():
278 """
279 ~~->$GroupStats:Query~~
280 ~~^->Elasticsearch:Technology~~
281 """
282 def __init__(self, group_name, editor_count=10):
283 self.group_name = group_name
284 self.editor_count = editor_count
286 def query(self):
287 return {
288 "track_total_hits" : True,
289 "query": {
290 "bool": {
291 "must": [
292 {
293 "term": {
294 "admin.editor_group.exact": self.group_name
295 }
296 }
297 ],
298 "must_not" : [
299 {
300 "terms" : {
301 "admin.application_status.exact" : [
302 constants.APPLICATION_STATUS_ACCEPTED,
303 constants.APPLICATION_STATUS_REJECTED
304 ]
305 }
306 }
307 ]
308 }
309 },
310 "size" : 0,
311 "aggs" : {
312 "editor" : {
313 "terms" : {
314 "field" : "admin.editor.exact",
315 "size" : self.editor_count
316 },
317 "aggs" : {
318 "application_type" : {
319 "terms" : {
320 "field": "admin.application_type.exact",
321 "size": 2
322 }
323 }
324 }
325 },
326 "status" : {
327 "terms" : {
328 "field" : "admin.application_status.exact",
329 "size" : len(constants.APPLICATION_STATUSES_ALL)
330 },
331 "aggs": {
332 "application_type": {
333 "terms": {
334 "field": "admin.application_type.exact",
335 "size": 2
336 }
337 }
338 }
339 },
340 "unassigned" : {
341 "missing" : {
342 "field": "admin.editor.exact"
343 },
344 "aggs" : {
345 "application_type" : {
346 "terms" : {
347 "field": "admin.application_type.exact",
348 "size": 2
349 }
350 }
351 }
352 }
353 }
354 }