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 set of functions that provide persistence for stars. |
| 7 |
| 8 Stars can be on users, projects, or issues. |
| 9 """ |
| 10 |
| 11 import logging |
| 12 |
| 13 import settings |
| 14 from features import filterrules_helpers |
| 15 from framework import sql |
| 16 |
| 17 |
| 18 USERSTAR_TABLE_NAME = 'UserStar' |
| 19 PROJECTSTAR_TABLE_NAME = 'ProjectStar' |
| 20 ISSUESTAR_TABLE_NAME = 'IssueStar' |
| 21 |
| 22 # TODO(jrobbins): Consider adding memcache here if performance testing shows |
| 23 # that stars are a bottleneck. Keep in mind that issue star counts are |
| 24 # already denormalized and stored in the Issue, which is cached in memcache. |
| 25 |
| 26 |
| 27 class AbstractStarService(object): |
| 28 """The persistence layer for any kind of star data.""" |
| 29 |
| 30 def __init__(self, cache_manager, tbl, item_col, user_col, cache_kind): |
| 31 """Constructor. |
| 32 |
| 33 Args: |
| 34 cache_manager: local cache with distributed invalidation. |
| 35 tbl: SQL table that stores star data. |
| 36 item_col: string SQL column name that holds int item IDs. |
| 37 user_col: string SQL column name that holds int user IDs |
| 38 of the user who starred the item. |
| 39 cache_kind: string saying the kind of RAM cache. |
| 40 """ |
| 41 self.tbl = tbl |
| 42 self.item_col = item_col |
| 43 self.user_col = user_col |
| 44 |
| 45 # Items starred by users, keyed by user who did the starring. |
| 46 self.star_cache = cache_manager.MakeCache('user') |
| 47 # Users that starred an item, keyed by item ID. |
| 48 self.starrer_cache = cache_manager.MakeCache(cache_kind) |
| 49 # Counts of the users that starred an item, keyed by item ID. |
| 50 self.star_count_cache = cache_manager.MakeCache(cache_kind) |
| 51 |
| 52 def ExpungeStars(self, cnxn, item_id): |
| 53 """Wipes an item's stars from the system.""" |
| 54 self.tbl.Delete(cnxn, **{self.item_col: item_id}) |
| 55 |
| 56 def LookupItemStarrers(self, cnxn, item_id): |
| 57 """Returns list of users having stars on the specified item.""" |
| 58 starrer_list_dict = self.LookupItemsStarrers(cnxn, [item_id]) |
| 59 return starrer_list_dict[item_id] |
| 60 |
| 61 def LookupItemsStarrers(self, cnxn, items_ids): |
| 62 """Returns {item_id: [uid, ...]} of users who starred these items.""" |
| 63 starrer_list_dict, missed_ids = self.starrer_cache.GetAll(items_ids) |
| 64 |
| 65 if missed_ids: |
| 66 rows = self.tbl.Select( |
| 67 cnxn, cols=[self.item_col, self.user_col], |
| 68 **{self.item_col: missed_ids}) |
| 69 # Ensure that every requested item_id has an entry so that even |
| 70 # zero-star items get cached. |
| 71 retrieved_starrers = {item_id: [] for item_id in missed_ids} |
| 72 for item_id, starrer_id in rows: |
| 73 retrieved_starrers[item_id].append(starrer_id) |
| 74 starrer_list_dict.update(retrieved_starrers) |
| 75 self.starrer_cache.CacheAll(retrieved_starrers) |
| 76 |
| 77 return starrer_list_dict |
| 78 |
| 79 def LookupStarredItemIDs(self, cnxn, starrer_user_id): |
| 80 """Returns list of item IDs that were starred by the specified user.""" |
| 81 if not starrer_user_id: |
| 82 return [] # Anon user cannot star anything. |
| 83 |
| 84 cached_item_ids = self.star_cache.GetItem(starrer_user_id) |
| 85 if cached_item_ids is not None: |
| 86 return cached_item_ids |
| 87 |
| 88 rows = self.tbl.Select(cnxn, cols=[self.item_col], user_id=starrer_user_id) |
| 89 starred_ids = [row[0] for row in rows] |
| 90 self.star_cache.CacheItem(starrer_user_id, starred_ids) |
| 91 return starred_ids |
| 92 |
| 93 def IsItemStarredBy(self, cnxn, item_id, starrer_user_id): |
| 94 """Return True if the given issue is starred by the given user.""" |
| 95 starred_ids = self.LookupStarredItemIDs(cnxn, starrer_user_id) |
| 96 return item_id in starred_ids |
| 97 |
| 98 def CountItemStars(self, cnxn, item_id): |
| 99 """Returns the number of stars on the specified item.""" |
| 100 count_dict = self.CountItemsStars(cnxn, [item_id]) |
| 101 return count_dict.get(item_id, 0) |
| 102 |
| 103 def CountItemsStars(self, cnxn, item_ids): |
| 104 """Get a dict {item_id: count} for the given items.""" |
| 105 item_count_dict, missed_ids = self.star_count_cache.GetAll(item_ids) |
| 106 |
| 107 if missed_ids: |
| 108 rows = self.tbl.Select( |
| 109 cnxn, cols=[self.item_col, 'COUNT(%s)' % self.user_col], |
| 110 group_by=[self.item_col], |
| 111 **{self.item_col: missed_ids}) |
| 112 # Ensure that every requested item_id has an entry so that even |
| 113 # zero-star items get cached. |
| 114 retrieved_counts = {item_id: 0 for item_id in missed_ids} |
| 115 retrieved_counts.update(rows) |
| 116 item_count_dict.update(retrieved_counts) |
| 117 self.star_count_cache.CacheAll(retrieved_counts) |
| 118 |
| 119 return item_count_dict |
| 120 |
| 121 def SetStar(self, cnxn, item_id, starrer_user_id, starred): |
| 122 """Sets or unsets a star for the specified item and user.""" |
| 123 if starred: |
| 124 self.tbl.InsertRow( |
| 125 cnxn, ignore=True, |
| 126 **{self.item_col: item_id, self.user_col: starrer_user_id}) |
| 127 else: |
| 128 self.tbl.Delete( |
| 129 cnxn, **{self.item_col: item_id, self.user_col: starrer_user_id}) |
| 130 |
| 131 self.star_cache.Invalidate(cnxn, starrer_user_id) |
| 132 self.starrer_cache.Invalidate(cnxn, item_id) |
| 133 |
| 134 |
| 135 class UserStarService(AbstractStarService): |
| 136 """Star service for stars on users.""" |
| 137 |
| 138 def __init__(self, cache_manager): |
| 139 tbl = sql.SQLTableManager(USERSTAR_TABLE_NAME) |
| 140 super(UserStarService, self).__init__( |
| 141 cache_manager, tbl, 'starred_user_id', 'user_id', 'user') |
| 142 |
| 143 |
| 144 class ProjectStarService(AbstractStarService): |
| 145 """Star service for stars on projects.""" |
| 146 |
| 147 def __init__(self, cache_manager): |
| 148 tbl = sql.SQLTableManager(PROJECTSTAR_TABLE_NAME) |
| 149 super(ProjectStarService, self).__init__( |
| 150 cache_manager, tbl, 'project_id', 'user_id', 'project') |
| 151 |
| 152 |
| 153 class IssueStarService(AbstractStarService): |
| 154 """Star service for stars on issues.""" |
| 155 |
| 156 def __init__(self, cache_manager): |
| 157 tbl = sql.SQLTableManager(ISSUESTAR_TABLE_NAME) |
| 158 super(IssueStarService, self).__init__( |
| 159 cache_manager, tbl, 'issue_id', 'user_id', 'issue') |
| 160 |
| 161 # pylint: disable=arguments-differ |
| 162 def SetStar( |
| 163 self, cnxn, services, config, issue_id, starrer_user_id, starred): |
| 164 # TODO(agable): The number of arguments required by this function is |
| 165 # crazy. Find a way to simplify it so that it only needs the same |
| 166 # arguments as AbstractSetStar above. |
| 167 """Add or remove a star on the given issue for the given user. |
| 168 |
| 169 Args: |
| 170 cnxn: connection to SQL database. |
| 171 services: connections to persistence layer. |
| 172 config: ProjectIssueConfig PB for the project containing the issue. |
| 173 issue_id: integer global ID of an issue. |
| 174 starrer_user_id: user ID of the user who starred the issue. |
| 175 starred: boolean True for adding a star, False when removing one. |
| 176 """ |
| 177 logging.info( |
| 178 'SetIssueStar:%06d, %s, %s', issue_id, starrer_user_id, starred) |
| 179 super(IssueStarService, self).SetStar( |
| 180 cnxn, issue_id, starrer_user_id, starred) |
| 181 |
| 182 issue = services.issue.GetIssue(cnxn, issue_id) |
| 183 issue.star_count = self.CountItemStars(cnxn, issue_id) |
| 184 filterrules_helpers.ApplyFilterRules(cnxn, services, issue, config) |
| 185 # Note: only star_count could change due to the starring, but any |
| 186 # field could have changed as a result of filter rules. |
| 187 services.issue.UpdateIssue(cnxn, issue) |
| 188 |
| 189 self.star_cache.Invalidate(cnxn, starrer_user_id) |
| 190 self.starrer_cache.Invalidate(cnxn, issue_id) |
OLD | NEW |