Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(704)

Side by Side Diff: appengine/monorail/services/features_svc.py

Issue 1868553004: Open Source Monorail (Closed) Base URL: https://chromium.googlesource.com/infra/infra.git@master
Patch Set: Rebase Created 4 years, 8 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
1 # Copyright 2016 The Chromium Authors. All rights reserved.
2 # Use of this source code is govered by a BSD-style
3 # license that can be found in the LICENSE file or at
4 # https://developers.google.com/open-source/licenses/bsd
5
6 """A class that provides persistence for Monorail's additional features.
7
8 Business objects are described in tracker_pb2.py and tracker_bizobj.py.
9 """
10
11 import collections
12 import logging
13
14 from features import filterrules_helpers
15 from framework import sql
16 from tracker import tracker_bizobj
17 from tracker import tracker_constants
18
19 QUICKEDITHISTORY_TABLE_NAME = 'QuickEditHistory'
20 QUICKEDITMOSTRECENT_TABLE_NAME = 'QuickEditMostRecent'
21 SAVEDQUERY_TABLE_NAME = 'SavedQuery'
22 PROJECT2SAVEDQUERY_TABLE_NAME = 'Project2SavedQuery'
23 SAVEDQUERYEXECUTESINPROJECT_TABLE_NAME = 'SavedQueryExecutesInProject'
24 USER2SAVEDQUERY_TABLE_NAME = 'User2SavedQuery'
25 FILTERRULE_TABLE_NAME = 'FilterRule'
26 FILTERRULE_COLS = ['project_id', 'rank', 'predicate', 'consequence']
27
28
29 QUICKEDITHISTORY_COLS = [
30 'user_id', 'project_id', 'slot_num', 'command', 'comment']
31 QUICKEDITMOSTRECENT_COLS = ['user_id', 'project_id', 'slot_num']
32 SAVEDQUERY_COLS = ['id', 'name', 'base_query_id', 'query']
33 PROJECT2SAVEDQUERY_COLS = ['project_id', 'rank', 'query_id']
34 SAVEDQUERYEXECUTESINPROJECT_COLS = ['query_id', 'project_id']
35 USER2SAVEDQUERY_COLS = ['user_id', 'rank', 'query_id', 'subscription_mode']
36
37
38 class FeaturesService(object):
39 """The persistence layer for servlets in the features directory."""
40
41 def __init__(self, cache_manager):
42 """Initialize this object so that it is ready to use.
43
44 Args:
45 cache_manager: local cache with distributed invalidation.
46 """
47 self.quickedithistory_tbl = sql.SQLTableManager(QUICKEDITHISTORY_TABLE_NAME)
48 self.quickeditmostrecent_tbl = sql.SQLTableManager(
49 QUICKEDITMOSTRECENT_TABLE_NAME)
50
51 self.savedquery_tbl = sql.SQLTableManager(SAVEDQUERY_TABLE_NAME)
52 self.project2savedquery_tbl = sql.SQLTableManager(
53 PROJECT2SAVEDQUERY_TABLE_NAME)
54 self.savedqueryexecutesinproject_tbl = sql.SQLTableManager(
55 SAVEDQUERYEXECUTESINPROJECT_TABLE_NAME)
56 self.user2savedquery_tbl = sql.SQLTableManager(USER2SAVEDQUERY_TABLE_NAME)
57
58 self.filterrule_tbl = sql.SQLTableManager(FILTERRULE_TABLE_NAME)
59
60 self.saved_query_cache = cache_manager.MakeCache('user', max_size=1000)
61
62 ### QuickEdit command history
63
64 def GetRecentCommands(self, cnxn, user_id, project_id):
65 """Return recent command items for the "Redo" menu.
66
67 Args:
68 cnxn: Connection to SQL database.
69 user_id: int ID of the current user.
70 project_id: int ID of the current project.
71
72 Returns:
73 A pair (cmd_slots, recent_slot_num). cmd_slots is a list of
74 3-tuples that can be used to populate the "Redo" menu of the
75 quick-edit dialog. recent_slot_num indicates which of those
76 slots should initially populate the command and comment fields.
77 """
78 # Always start with the standard 5 commands.
79 history = tracker_constants.DEFAULT_RECENT_COMMANDS[:]
80 # If the user has modified any, then overwrite some standard ones.
81 history_rows = self.quickedithistory_tbl.Select(
82 cnxn, cols=['slot_num', 'command', 'comment'],
83 user_id=user_id, project_id=project_id)
84 for slot_num, command, comment in history_rows:
85 if slot_num < len(history):
86 history[slot_num - 1] = (command, comment)
87
88 slots = []
89 for idx, (command, comment) in enumerate(history):
90 slots.append((idx + 1, command, comment))
91
92 recent_slot_num = self.quickeditmostrecent_tbl.SelectValue(
93 cnxn, 'slot_num', default=1, user_id=user_id, project_id=project_id)
94
95 return slots, recent_slot_num
96
97 def StoreRecentCommand(
98 self, cnxn, user_id, project_id, slot_num, command, comment):
99 """Store the given command and comment in the user's command history."""
100 self.quickedithistory_tbl.InsertRow(
101 cnxn, replace=True, user_id=user_id, project_id=project_id,
102 slot_num=slot_num, command=command, comment=comment)
103 self.quickeditmostrecent_tbl.InsertRow(
104 cnxn, replace=True, user_id=user_id, project_id=project_id,
105 slot_num=slot_num)
106
107 def ExpungeQuickEditHistory(self, cnxn, project_id):
108 """Completely delete every users' quick edit history for this project."""
109 self.quickeditmostrecent_tbl.Delete(cnxn, project_id=project_id)
110 self.quickedithistory_tbl.Delete(cnxn, project_id=project_id)
111
112 ### Saved User and Project Queries
113
114 def GetSavedQueries(self, cnxn, query_ids):
115 """Retrieve the specified SaveQuery PBs."""
116 # TODO(jrobbins): RAM cache
117 saved_queries = {}
118 savedquery_rows = self.savedquery_tbl.Select(
119 cnxn, cols=SAVEDQUERY_COLS, id=query_ids)
120 for saved_query_tuple in savedquery_rows:
121 qid, name, base_id, query = saved_query_tuple
122 saved_queries[qid] = tracker_bizobj.MakeSavedQuery(
123 qid, name, base_id, query)
124
125 sqeip_rows = self.savedqueryexecutesinproject_tbl.Select(
126 cnxn, cols=SAVEDQUERYEXECUTESINPROJECT_COLS,
127 query_id=query_ids)
128 for query_id, project_id in sqeip_rows:
129 saved_queries[query_id].executes_in_project_ids.append(project_id)
130
131 return saved_queries
132
133 def GetSavedQuery(self, cnxn, query_id):
134 """Retrieve the specified SaveQuery PB."""
135 saved_queries = self.GetSavedQueries(cnxn, [query_id])
136 return saved_queries[query_id]
137
138 def _GetUsersSavedQueriesDict(self, cnxn, user_ids):
139 """Return a dict of all SavedQuery PBs for the specified users."""
140 results_dict, missed_uids = self.saved_query_cache.GetAll(user_ids)
141
142 if missed_uids:
143 savedquery_rows = self.user2savedquery_tbl.Select(
144 cnxn, cols=SAVEDQUERY_COLS + ['user_id', 'subscription_mode'],
145 left_joins=[('SavedQuery ON query_id = id', [])],
146 order_by=[('rank', [])], user_id=missed_uids)
147 sqeip_rows = self.savedqueryexecutesinproject_tbl.Select(
148 cnxn, cols=SAVEDQUERYEXECUTESINPROJECT_COLS,
149 query_id={row[0] for row in savedquery_rows})
150 sqeip_dict = {}
151 for qid, pid in sqeip_rows:
152 sqeip_dict.setdefault(qid, []).append(pid)
153
154 for saved_query_tuple in savedquery_rows:
155 query_id, name, base_id, query, uid, sub_mode = saved_query_tuple
156 sq = tracker_bizobj.MakeSavedQuery(
157 query_id, name, base_id, query, subscription_mode=sub_mode,
158 executes_in_project_ids=sqeip_dict.get(query_id, []))
159 results_dict.setdefault(uid, []).append(sq)
160
161 self.saved_query_cache.CacheAll(results_dict)
162 return results_dict
163
164 # TODO(jrobbins): change this termonology to "canned query" rather than
165 # "saved" throughout the application.
166 def GetSavedQueriesByUserID(self, cnxn, user_id):
167 """Return a list of SavedQuery PBs for the specified user."""
168 saved_queries_dict = self._GetUsersSavedQueriesDict(cnxn, [user_id])
169 saved_queries = saved_queries_dict.get(user_id, [])
170 return saved_queries[:]
171
172 def GetCannedQueriesForProjects(self, cnxn, project_ids):
173 """Return a dict {project_id: [saved_query]} for the specified projects."""
174 # TODO(jrobbins): caching
175 cannedquery_rows = self.project2savedquery_tbl.Select(
176 cnxn, cols=['project_id'] + SAVEDQUERY_COLS,
177 left_joins=[('SavedQuery ON query_id = id', [])],
178 order_by=[('rank', [])], project_id=project_ids)
179
180 result_dict = collections.defaultdict(list)
181 for cq_row in cannedquery_rows:
182 project_id = cq_row[0]
183 canned_query_tuple = cq_row[1:]
184 result_dict[project_id].append(
185 tracker_bizobj.MakeSavedQuery(*canned_query_tuple))
186
187 return result_dict
188
189 def GetCannedQueriesByProjectID(self, cnxn, project_id):
190 """Return the list of SavedQueries for the specified project."""
191 project_ids_to_canned_queries = self.GetCannedQueriesForProjects(
192 cnxn, [project_id])
193 return project_ids_to_canned_queries.get(project_id, [])
194
195 def _UpdateSavedQueries(self, cnxn, saved_queries, commit=True):
196 """Store the given SavedQueries to the DB."""
197 savedquery_rows = [
198 (sq.query_id or None, sq.name, sq.base_query_id, sq.query)
199 for sq in saved_queries]
200 existing_query_ids = [sq.query_id for sq in saved_queries if sq.query_id]
201 if existing_query_ids:
202 self.savedquery_tbl.Delete(cnxn, id=existing_query_ids, commit=commit)
203
204 generated_ids = self.savedquery_tbl.InsertRows(
205 cnxn, SAVEDQUERY_COLS, savedquery_rows, commit=commit,
206 return_generated_ids=True)
207 if generated_ids:
208 logging.info('generated_ids are %r', generated_ids)
209 for sq in saved_queries:
210 generated_id = generated_ids.pop(0)
211 if not sq.query_id:
212 sq.query_id = generated_id
213
214 def UpdateCannedQueries(self, cnxn, project_id, canned_queries):
215 """Update the canned queries for a project.
216
217 Args:
218 cnxn: connection to SQL database.
219 project_id: int project ID of the project that contains these queries.
220 canned_queries: list of SavedQuery PBs to update.
221 """
222 self.project2savedquery_tbl.Delete(
223 cnxn, project_id=project_id, commit=False)
224 self._UpdateSavedQueries(cnxn, canned_queries, commit=False)
225 project2savedquery_rows = [
226 (project_id, rank, sq.query_id)
227 for rank, sq in enumerate(canned_queries)]
228 self.project2savedquery_tbl.InsertRows(
229 cnxn, PROJECT2SAVEDQUERY_COLS, project2savedquery_rows,
230 commit=False)
231 cnxn.Commit()
232
233 def UpdateUserSavedQueries(self, cnxn, user_id, saved_queries):
234 """Store the given saved_queries for the given user."""
235 saved_query_ids = [sq.query_id for sq in saved_queries if sq.query_id]
236 self.savedqueryexecutesinproject_tbl.Delete(
237 cnxn, query_id=saved_query_ids, commit=False)
238 self.user2savedquery_tbl.Delete(cnxn, user_id=user_id, commit=False)
239
240 self._UpdateSavedQueries(cnxn, saved_queries, commit=False)
241 user2savedquery_rows = []
242 for rank, sq in enumerate(saved_queries):
243 user2savedquery_rows.append(
244 (user_id, rank, sq.query_id, sq.subscription_mode or 'noemail'))
245
246 self.user2savedquery_tbl.InsertRows(
247 cnxn, USER2SAVEDQUERY_COLS, user2savedquery_rows, commit=False)
248
249 sqeip_rows = []
250 for sq in saved_queries:
251 for pid in sq.executes_in_project_ids:
252 sqeip_rows.append((sq.query_id, pid))
253
254 self.savedqueryexecutesinproject_tbl.InsertRows(
255 cnxn, SAVEDQUERYEXECUTESINPROJECT_COLS, sqeip_rows, commit=False)
256 cnxn.Commit()
257
258 self.saved_query_cache.Invalidate(cnxn, user_id)
259
260 ### Subscriptions
261
262 def GetSubscriptionsInProjects(self, cnxn, project_ids):
263 """Return all saved queries for users that have any subscription there.
264
265 Args:
266 cnxn: Connection to SQL database.
267 project_ids: list of int project IDs that contain the modified issues.
268
269 Returns:
270 A dict {user_id: all_saved_queries, ...} for all users that have any
271 subscription in any of the specified projects.
272 """
273 join_str = (
274 'SavedQueryExecutesInProject ON '
275 'SavedQueryExecutesInProject.query_id = User2SavedQuery.query_id')
276 # TODO(jrobbins): cache this since it rarely changes.
277 subscriber_rows = self.user2savedquery_tbl.Select(
278 cnxn, cols=['user_id'], distinct=True,
279 joins=[(join_str, [])],
280 subscription_mode='immediate', project_id=project_ids)
281 subscriber_ids = [row[0] for row in subscriber_rows]
282 logging.info('subscribers relevant to projects %r are %r',
283 project_ids, subscriber_ids)
284 user_ids_to_saved_queries = self._GetUsersSavedQueriesDict(
285 cnxn, subscriber_ids)
286 return user_ids_to_saved_queries
287
288 def ExpungeSavedQueriesExecuteInProject(self, cnxn, project_id):
289 """Remove any references from saved queries to projects in the database."""
290 self.savedqueryexecutesinproject_tbl.Delete(cnxn, project_id=project_id)
291
292 savedquery_rows = self.project2savedquery_tbl.Select(
293 cnxn, cols=['query_id'], project_id=project_id)
294 savedquery_ids = [row[0] for row in savedquery_rows]
295 self.project2savedquery_tbl.Delete(cnxn, project_id=project_id)
296 self.savedquery_tbl.Delete(cnxn, id=savedquery_ids)
297
298 ### Filter rules
299
300 def _DeserializeFilterRules(self, filterrule_rows):
301 """Convert the given DB row tuples into PBs."""
302 result_dict = collections.defaultdict(list)
303
304 for filterrule_row in sorted(filterrule_rows):
305 project_id, _rank, predicate, consequence = filterrule_row
306 (default_status, default_owner_id, add_cc_ids, add_labels,
307 add_notify) = self._DeserializeRuleConsequence(consequence)
308 rule = filterrules_helpers.MakeRule(
309 predicate, default_status=default_status,
310 default_owner_id=default_owner_id, add_cc_ids=add_cc_ids,
311 add_labels=add_labels, add_notify=add_notify)
312 result_dict[project_id].append(rule)
313
314 return result_dict
315
316 def _DeserializeRuleConsequence(self, consequence):
317 """Decode the THEN-part of a filter rule."""
318 (default_status, default_owner_id, add_cc_ids, add_labels,
319 add_notify) = None, None, [], [], []
320 for action in consequence.split():
321 verb, noun = action.split(':')
322 if verb == 'default_status':
323 default_status = noun
324 elif verb == 'default_owner_id':
325 default_owner_id = int(noun)
326 elif verb == 'add_cc_id':
327 add_cc_ids.append(int(noun))
328 elif verb == 'add_label':
329 add_labels.append(noun)
330 elif verb == 'add_notify':
331 add_notify.append(noun)
332
333 return (default_status, default_owner_id, add_cc_ids, add_labels,
334 add_notify)
335
336 def _GetFilterRulesByProjectIDs(self, cnxn, project_ids):
337 """Return {project_id: [FilterRule, ...]} for the specified projects."""
338 # TODO(jrobbins): caching
339 filterrule_rows = self.filterrule_tbl.Select(
340 cnxn, cols=FILTERRULE_COLS, project_id=project_ids)
341 return self._DeserializeFilterRules(filterrule_rows)
342
343 def GetFilterRules(self, cnxn, project_id):
344 """Return a list of FilterRule PBs for the specified project."""
345 rules_by_project_id = self._GetFilterRulesByProjectIDs(cnxn, [project_id])
346 return rules_by_project_id[project_id]
347
348 def _SerializeRuleConsequence(self, rule):
349 """Put all actions of a filter rule into one string."""
350 assignments = []
351 for add_lab in rule.add_labels:
352 assignments.append('add_label:%s' % add_lab)
353 if rule.default_status:
354 assignments.append('default_status:%s' % rule.default_status)
355 if rule.default_owner_id:
356 assignments.append('default_owner_id:%d' % rule.default_owner_id)
357 for add_cc_id in rule.add_cc_ids:
358 assignments.append('add_cc_id:%d' % add_cc_id)
359 for add_notify in rule.add_notify_addrs:
360 assignments.append('add_notify:%s' % add_notify)
361
362 return ' '.join(assignments)
363
364 def UpdateFilterRules(self, cnxn, project_id, rules):
365 """Update the filter rules part of a project's issue configuration.
366
367 Args:
368 cnxn: connection to SQL database.
369 project_id: int ID of the current project.
370 rules: a list of FilterRule PBs.
371 """
372 rows = []
373 for rank, rule in enumerate(rules):
374 predicate = rule.predicate
375 consequence = self._SerializeRuleConsequence(rule)
376 if predicate and consequence:
377 rows.append((project_id, rank, predicate, consequence))
378
379 self.filterrule_tbl.Delete(cnxn, project_id=project_id)
380 self.filterrule_tbl.InsertRows(cnxn, FILTERRULE_COLS, rows)
381
382 def ExpungeFilterRules(self, cnxn, project_id):
383 """Completely destroy filter rule info for the specified project."""
384 self.filterrule_tbl.Delete(cnxn, project_id=project_id)
OLDNEW
« no previous file with comments | « appengine/monorail/services/config_svc.py ('k') | appengine/monorail/services/fulltext_helpers.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698