Coverage for portality/bll/services/todo.py: 67%

132 statements  

« prev     ^ index     » next       coverage.py v6.4.2, created at 2022-09-20 23:33 +0100

1from portality.lib.argvalidate import argvalidate 

2from portality import models 

3from portality.bll import exceptions 

4from portality import constants 

5 

6class TodoService(object): 

7 """ 

8 ~~Todo:Service->DOAJ:Service~~ 

9 """ 

10 

11 def group_stats(self, group_id): 

12 # ~~-> EditorGroup:Model~~ 

13 eg = models.EditorGroup.pull(group_id) 

14 stats = {"editor_group" : eg.data} 

15 

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 } 

24 

25 q = GroupStatsQuery(eg.name) 

26 resp = models.Application.query(q=q.query()) 

27 

28 stats["total"] = {"applications": 0, "update_requests": 0} 

29 

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} 

33 

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"] 

41 

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"] 

51 

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"] 

60 

61 return stats 

62 

63 

64 def top_todo(self, account, size=25): 

65 """ 

66 Returns the top number of todo items for a given user 

67 

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) 

75 

76 

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)) 

85 

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 }) 

99 

100 todos = self._rationalise_todos(todos, size) 

101 

102 return todos 

103 

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)) 

107 

108 stds = sorted(boosted, key=lambda x: x['date']) + sorted(unboosted, key=lambda x: x['date']) 

109 

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 

120 

121 removals.reverse() 

122 for r in removals: 

123 del stds[r] 

124 

125 return stds[:size] 

126 

127 

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 

143 

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 

158 

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 

170 

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 

183 

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 

200 

201 

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"}} 

209 

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 

215 

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 

231 

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 } 

239 

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 } 

249 

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 } 

259 

260 @classmethod 

261 def status(cls, statuses): 

262 return { 

263 "terms" : { 

264 "admin.application_status.exact" : statuses 

265 } 

266 } 

267 

268 @classmethod 

269 def exists(cls, field): 

270 return { 

271 "exists" : { 

272 "field" : field 

273 } 

274 } 

275 

276 

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 

285 

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 }