blob: 2fc2c655f43e655f45124f0451a3446f6a1fa80c [file] [log] [blame]
# Copyright 2016 The Chromium Authors. All rights reserved.
# Use of this source code is govered by a BSD-style
# license that can be found in the LICENSE file or at
"""A class that provides persistence for Monorail's additional features.
Business objects are described in and
import collections
import logging
from features import filterrules_helpers
from framework import sql
from tracker import tracker_bizobj
from tracker import tracker_constants
FILTERRULE_COLS = ['project_id', 'rank', 'predicate', 'consequence']
'user_id', 'project_id', 'slot_num', 'command', 'comment']
QUICKEDITMOSTRECENT_COLS = ['user_id', 'project_id', 'slot_num']
SAVEDQUERY_COLS = ['id', 'name', 'base_query_id', 'query']
PROJECT2SAVEDQUERY_COLS = ['project_id', 'rank', 'query_id']
USER2SAVEDQUERY_COLS = ['user_id', 'rank', 'query_id', 'subscription_mode']
class FeaturesService(object):
"""The persistence layer for servlets in the features directory."""
def __init__(self, cache_manager):
"""Initialize this object so that it is ready to use.
cache_manager: local cache with distributed invalidation.
self.quickedithistory_tbl = sql.SQLTableManager(QUICKEDITHISTORY_TABLE_NAME)
self.quickeditmostrecent_tbl = sql.SQLTableManager(
self.savedquery_tbl = sql.SQLTableManager(SAVEDQUERY_TABLE_NAME)
self.project2savedquery_tbl = sql.SQLTableManager(
self.savedqueryexecutesinproject_tbl = sql.SQLTableManager(
self.user2savedquery_tbl = sql.SQLTableManager(USER2SAVEDQUERY_TABLE_NAME)
self.filterrule_tbl = sql.SQLTableManager(FILTERRULE_TABLE_NAME)
self.saved_query_cache = cache_manager.MakeCache('user', max_size=1000)
### QuickEdit command history
def GetRecentCommands(self, cnxn, user_id, project_id):
"""Return recent command items for the "Redo" menu.
cnxn: Connection to SQL database.
user_id: int ID of the current user.
project_id: int ID of the current project.
A pair (cmd_slots, recent_slot_num). cmd_slots is a list of
3-tuples that can be used to populate the "Redo" menu of the
quick-edit dialog. recent_slot_num indicates which of those
slots should initially populate the command and comment fields.
# Always start with the standard 5 commands.
history = tracker_constants.DEFAULT_RECENT_COMMANDS[:]
# If the user has modified any, then overwrite some standard ones.
history_rows = self.quickedithistory_tbl.Select(
cnxn, cols=['slot_num', 'command', 'comment'],
user_id=user_id, project_id=project_id)
for slot_num, command, comment in history_rows:
if slot_num < len(history):
history[slot_num - 1] = (command, comment)
slots = []
for idx, (command, comment) in enumerate(history):
slots.append((idx + 1, command, comment))
recent_slot_num = self.quickeditmostrecent_tbl.SelectValue(
cnxn, 'slot_num', default=1, user_id=user_id, project_id=project_id)
return slots, recent_slot_num
def StoreRecentCommand(
self, cnxn, user_id, project_id, slot_num, command, comment):
"""Store the given command and comment in the user's command history."""
cnxn, replace=True, user_id=user_id, project_id=project_id,
slot_num=slot_num, command=command, comment=comment)
cnxn, replace=True, user_id=user_id, project_id=project_id,
def ExpungeQuickEditHistory(self, cnxn, project_id):
"""Completely delete every users' quick edit history for this project."""
self.quickeditmostrecent_tbl.Delete(cnxn, project_id=project_id)
self.quickedithistory_tbl.Delete(cnxn, project_id=project_id)
### Saved User and Project Queries
def GetSavedQueries(self, cnxn, query_ids):
"""Retrieve the specified SaveQuery PBs."""
# TODO(jrobbins): RAM cache
saved_queries = {}
savedquery_rows = self.savedquery_tbl.Select(
cnxn, cols=SAVEDQUERY_COLS, id=query_ids)
for saved_query_tuple in savedquery_rows:
qid, name, base_id, query = saved_query_tuple
saved_queries[qid] = tracker_bizobj.MakeSavedQuery(
qid, name, base_id, query)
sqeip_rows = self.savedqueryexecutesinproject_tbl.Select(
for query_id, project_id in sqeip_rows:
return saved_queries
def GetSavedQuery(self, cnxn, query_id):
"""Retrieve the specified SaveQuery PB."""
saved_queries = self.GetSavedQueries(cnxn, [query_id])
return saved_queries[query_id]
def _GetUsersSavedQueriesDict(self, cnxn, user_ids):
"""Return a dict of all SavedQuery PBs for the specified users."""
results_dict, missed_uids = self.saved_query_cache.GetAll(user_ids)
if missed_uids:
savedquery_rows = self.user2savedquery_tbl.Select(
cnxn, cols=SAVEDQUERY_COLS + ['user_id', 'subscription_mode'],
left_joins=[('SavedQuery ON query_id = id', [])],
order_by=[('rank', [])], user_id=missed_uids)
sqeip_rows = self.savedqueryexecutesinproject_tbl.Select(
query_id={row[0] for row in savedquery_rows})
sqeip_dict = {}
for qid, pid in sqeip_rows:
sqeip_dict.setdefault(qid, []).append(pid)
for saved_query_tuple in savedquery_rows:
query_id, name, base_id, query, uid, sub_mode = saved_query_tuple
sq = tracker_bizobj.MakeSavedQuery(
query_id, name, base_id, query, subscription_mode=sub_mode,
executes_in_project_ids=sqeip_dict.get(query_id, []))
results_dict.setdefault(uid, []).append(sq)
return results_dict
# TODO(jrobbins): change this termonology to "canned query" rather than
# "saved" throughout the application.
def GetSavedQueriesByUserID(self, cnxn, user_id):
"""Return a list of SavedQuery PBs for the specified user."""
saved_queries_dict = self._GetUsersSavedQueriesDict(cnxn, [user_id])
saved_queries = saved_queries_dict.get(user_id, [])
return saved_queries[:]
def GetCannedQueriesForProjects(self, cnxn, project_ids):
"""Return a dict {project_id: [saved_query]} for the specified projects."""
# TODO(jrobbins): caching
cannedquery_rows = self.project2savedquery_tbl.Select(
cnxn, cols=['project_id'] + SAVEDQUERY_COLS,
left_joins=[('SavedQuery ON query_id = id', [])],
order_by=[('rank', [])], project_id=project_ids)
result_dict = collections.defaultdict(list)
for cq_row in cannedquery_rows:
project_id = cq_row[0]
canned_query_tuple = cq_row[1:]
return result_dict
def GetCannedQueriesByProjectID(self, cnxn, project_id):
"""Return the list of SavedQueries for the specified project."""
project_ids_to_canned_queries = self.GetCannedQueriesForProjects(
cnxn, [project_id])
return project_ids_to_canned_queries.get(project_id, [])
def _UpdateSavedQueries(self, cnxn, saved_queries, commit=True):
"""Store the given SavedQueries to the DB."""
savedquery_rows = [
(sq.query_id or None,, sq.base_query_id, sq.query)
for sq in saved_queries]
existing_query_ids = [sq.query_id for sq in saved_queries if sq.query_id]
if existing_query_ids:
self.savedquery_tbl.Delete(cnxn, id=existing_query_ids, commit=commit)
generated_ids = self.savedquery_tbl.InsertRows(
cnxn, SAVEDQUERY_COLS, savedquery_rows, commit=commit,
if generated_ids:'generated_ids are %r', generated_ids)
for sq in saved_queries:
generated_id = generated_ids.pop(0)
if not sq.query_id:
sq.query_id = generated_id
def UpdateCannedQueries(self, cnxn, project_id, canned_queries):
"""Update the canned queries for a project.
cnxn: connection to SQL database.
project_id: int project ID of the project that contains these queries.
canned_queries: list of SavedQuery PBs to update.
cnxn, project_id=project_id, commit=False)
self._UpdateSavedQueries(cnxn, canned_queries, commit=False)
project2savedquery_rows = [
(project_id, rank, sq.query_id)
for rank, sq in enumerate(canned_queries)]
cnxn, PROJECT2SAVEDQUERY_COLS, project2savedquery_rows,
def UpdateUserSavedQueries(self, cnxn, user_id, saved_queries):
"""Store the given saved_queries for the given user."""
saved_query_ids = [sq.query_id for sq in saved_queries if sq.query_id]
cnxn, query_id=saved_query_ids, commit=False)
self.user2savedquery_tbl.Delete(cnxn, user_id=user_id, commit=False)
self._UpdateSavedQueries(cnxn, saved_queries, commit=False)
user2savedquery_rows = []
for rank, sq in enumerate(saved_queries):
(user_id, rank, sq.query_id, sq.subscription_mode or 'noemail'))
cnxn, USER2SAVEDQUERY_COLS, user2savedquery_rows, commit=False)
sqeip_rows = []
for sq in saved_queries:
for pid in sq.executes_in_project_ids:
sqeip_rows.append((sq.query_id, pid))
cnxn, SAVEDQUERYEXECUTESINPROJECT_COLS, sqeip_rows, commit=False)
self.saved_query_cache.Invalidate(cnxn, user_id)
### Subscriptions
def GetSubscriptionsInProjects(self, cnxn, project_ids):
"""Return all saved queries for users that have any subscription there.
cnxn: Connection to SQL database.
project_ids: list of int project IDs that contain the modified issues.
A dict {user_id: all_saved_queries, ...} for all users that have any
subscription in any of the specified projects.
join_str = (
'SavedQueryExecutesInProject ON '
'SavedQueryExecutesInProject.query_id = User2SavedQuery.query_id')
# TODO(jrobbins): cache this since it rarely changes.
subscriber_rows = self.user2savedquery_tbl.Select(
cnxn, cols=['user_id'], distinct=True,
joins=[(join_str, [])],
subscription_mode='immediate', project_id=project_ids)
subscriber_ids = [row[0] for row in subscriber_rows]'subscribers relevant to projects %r are %r',
project_ids, subscriber_ids)
user_ids_to_saved_queries = self._GetUsersSavedQueriesDict(
cnxn, subscriber_ids)
return user_ids_to_saved_queries
def ExpungeSavedQueriesExecuteInProject(self, cnxn, project_id):
"""Remove any references from saved queries to projects in the database."""
self.savedqueryexecutesinproject_tbl.Delete(cnxn, project_id=project_id)
savedquery_rows = self.project2savedquery_tbl.Select(
cnxn, cols=['query_id'], project_id=project_id)
savedquery_ids = [row[0] for row in savedquery_rows]
self.project2savedquery_tbl.Delete(cnxn, project_id=project_id)
self.savedquery_tbl.Delete(cnxn, id=savedquery_ids)
### Filter rules
def _DeserializeFilterRules(self, filterrule_rows):
"""Convert the given DB row tuples into PBs."""
result_dict = collections.defaultdict(list)
for filterrule_row in sorted(filterrule_rows):
project_id, _rank, predicate, consequence = filterrule_row
(default_status, default_owner_id, add_cc_ids, add_labels,
add_notify) = self._DeserializeRuleConsequence(consequence)
rule = filterrules_helpers.MakeRule(
predicate, default_status=default_status,
default_owner_id=default_owner_id, add_cc_ids=add_cc_ids,
add_labels=add_labels, add_notify=add_notify)
return result_dict
def _DeserializeRuleConsequence(self, consequence):
"""Decode the THEN-part of a filter rule."""
(default_status, default_owner_id, add_cc_ids, add_labels,
add_notify) = None, None, [], [], []
for action in consequence.split():
verb, noun = action.split(':')
if verb == 'default_status':
default_status = noun
elif verb == 'default_owner_id':
default_owner_id = int(noun)
elif verb == 'add_cc_id':
elif verb == 'add_label':
elif verb == 'add_notify':
return (default_status, default_owner_id, add_cc_ids, add_labels,
def _GetFilterRulesByProjectIDs(self, cnxn, project_ids):
"""Return {project_id: [FilterRule, ...]} for the specified projects."""
# TODO(jrobbins): caching
filterrule_rows = self.filterrule_tbl.Select(
cnxn, cols=FILTERRULE_COLS, project_id=project_ids)
return self._DeserializeFilterRules(filterrule_rows)
def GetFilterRules(self, cnxn, project_id):
"""Return a list of FilterRule PBs for the specified project."""
rules_by_project_id = self._GetFilterRulesByProjectIDs(cnxn, [project_id])
return rules_by_project_id[project_id]
def _SerializeRuleConsequence(self, rule):
"""Put all actions of a filter rule into one string."""
assignments = []
for add_lab in rule.add_labels:
assignments.append('add_label:%s' % add_lab)
if rule.default_status:
assignments.append('default_status:%s' % rule.default_status)
if rule.default_owner_id:
assignments.append('default_owner_id:%d' % rule.default_owner_id)
for add_cc_id in rule.add_cc_ids:
assignments.append('add_cc_id:%d' % add_cc_id)
for add_notify in rule.add_notify_addrs:
assignments.append('add_notify:%s' % add_notify)
return ' '.join(assignments)
def UpdateFilterRules(self, cnxn, project_id, rules):
"""Update the filter rules part of a project's issue configuration.
cnxn: connection to SQL database.
project_id: int ID of the current project.
rules: a list of FilterRule PBs.
rows = []
for rank, rule in enumerate(rules):
predicate = rule.predicate
consequence = self._SerializeRuleConsequence(rule)
if predicate and consequence:
rows.append((project_id, rank, predicate, consequence))
self.filterrule_tbl.Delete(cnxn, project_id=project_id)
self.filterrule_tbl.InsertRows(cnxn, FILTERRULE_COLS, rows)
def ExpungeFilterRules(self, cnxn, project_id):
"""Completely destroy filter rule info for the specified project."""
self.filterrule_tbl.Delete(cnxn, project_id=project_id)