OLD | NEW |
(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) |
OLD | NEW |