Adrià Vilanova Martínez | f19ea43 | 2024-01-23 20:20:52 +0100 | [diff] [blame] | 1 | # Copyright 2016 The Chromium Authors |
| 2 | # Use of this source code is governed by a BSD-style license that can be |
| 3 | # found in the LICENSE file. |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 4 | |
| 5 | """Helper functions and classes used by the hotlist pages.""" |
| 6 | from __future__ import print_function |
| 7 | from __future__ import division |
| 8 | from __future__ import absolute_import |
| 9 | |
| 10 | import logging |
| 11 | import collections |
| 12 | |
| 13 | from features import features_constants |
| 14 | from framework import framework_views |
| 15 | from framework import framework_helpers |
| 16 | from framework import sorting |
| 17 | from framework import table_view_helpers |
| 18 | from framework import timestr |
| 19 | from framework import paginate |
| 20 | from framework import permissions |
| 21 | from framework import urls |
| 22 | from tracker import tracker_bizobj |
| 23 | from tracker import tracker_constants |
| 24 | from tracker import tracker_helpers |
| 25 | from tracker import tablecell |
| 26 | |
| 27 | |
| 28 | # Type to hold a HotlistRef |
| 29 | HotlistRef = collections.namedtuple('HotlistRef', 'user_id, hotlist_name') |
| 30 | |
| 31 | |
| 32 | def GetSortedHotlistIssues( |
| 33 | cnxn, hotlist_items, issues, auth, can, sort_spec, group_by_spec, |
| 34 | harmonized_config, services, profiler): |
| 35 | # type: (MonorailConnection, List[HotlistItem], List[Issue], AuthData, |
| 36 | # ProjectIssueConfig, Services, Profiler) -> (List[Issue], Dict, Dict) |
| 37 | """Sorts the given HotlistItems and Issues and filters out Issues that |
| 38 | the user cannot view. |
| 39 | |
| 40 | Args: |
| 41 | cnxn: MonorailConnection for connection to the SQL database. |
| 42 | hotlist_items: list of HotlistItems in the Hotlist we want to sort. |
| 43 | issues: list of Issues in the Hotlist we want to sort. |
| 44 | auth: AuthData object that identifies the logged in user. |
| 45 | can: int "canned query" number to scope the visible issues. |
| 46 | sort_spec: string that lists the sort order. |
| 47 | group_by_spec: string that lists the grouping order. |
| 48 | harmonized_config: ProjectIssueConfig created from all configs of projects |
| 49 | with issues in the issues list. |
| 50 | services: Services object for connections to backend services. |
| 51 | profiler: Profiler object to display and record processes. |
| 52 | |
| 53 | Returns: |
| 54 | A tuple of (sorted_issues, hotlist_items_context, issues_users_by_id) where: |
| 55 | |
| 56 | sorted_issues: list of Issues that are sorted and issues the user cannot |
| 57 | view are filtered out. |
| 58 | hotlist_items_context: a dict of dicts providing HotlistItem values that |
| 59 | are associated with each Hotlist Issue. E.g: |
| 60 | {issue.issue_id: {'issue_rank': hotlist item rank, |
| 61 | 'adder_id': hotlist item adder's user_id, |
| 62 | 'date_added': timestamp when this issue was added to the |
| 63 | hotlist, |
| 64 | 'note': note for this issue in the hotlist,}, |
| 65 | issue.issue_id: {...}} |
| 66 | issues_users_by_id: dict of {user_id: UserView, ...} for all users involved |
| 67 | in the hotlist items and issues. |
| 68 | """ |
| 69 | with profiler.Phase('Checking issue permissions and getting ranks'): |
| 70 | |
| 71 | allowed_issues = FilterIssues(cnxn, auth, can, issues, services) |
| 72 | allowed_iids = [issue.issue_id for issue in allowed_issues] |
| 73 | # The values for issues in a hotlist are specific to the hotlist |
| 74 | # (rank, adder, added) without invalidating the keys, an issue will retain |
| 75 | # the rank value it has in one hotlist when navigating to another hotlist. |
| 76 | sorting.InvalidateArtValuesKeys( |
| 77 | cnxn, [issue.issue_id for issue in allowed_issues]) |
| 78 | sorted_ranks = sorted( |
| 79 | [hotlist_item.rank for hotlist_item in hotlist_items if |
| 80 | hotlist_item.issue_id in allowed_iids]) |
| 81 | friendly_ranks = { |
| 82 | rank: friendly for friendly, rank in enumerate(sorted_ranks, 1)} |
| 83 | issue_adders = framework_views.MakeAllUserViews( |
| 84 | cnxn, services.user, [hotlist_item.adder_id for |
| 85 | hotlist_item in hotlist_items]) |
| 86 | hotlist_items_context = { |
| 87 | hotlist_item.issue_id: {'issue_rank': |
| 88 | friendly_ranks[hotlist_item.rank], |
| 89 | 'adder_id': hotlist_item.adder_id, |
| 90 | 'date_added': timestr.FormatAbsoluteDate( |
| 91 | hotlist_item.date_added), |
| 92 | 'note': hotlist_item.note} |
| 93 | for hotlist_item in hotlist_items if |
| 94 | hotlist_item.issue_id in allowed_iids} |
| 95 | |
| 96 | with profiler.Phase('Making user views'): |
| 97 | issues_users_by_id = framework_views.MakeAllUserViews( |
| 98 | cnxn, services.user, |
| 99 | tracker_bizobj.UsersInvolvedInIssues(allowed_issues or [])) |
| 100 | issues_users_by_id.update(issue_adders) |
| 101 | |
| 102 | with profiler.Phase('Sorting issues'): |
| 103 | sortable_fields = tracker_helpers.SORTABLE_FIELDS.copy() |
| 104 | sortable_fields.update( |
| 105 | {'rank': lambda issue: hotlist_items_context[ |
| 106 | issue.issue_id]['issue_rank'], |
| 107 | 'adder': lambda issue: hotlist_items_context[ |
| 108 | issue.issue_id]['adder_id'], |
| 109 | 'added': lambda issue: hotlist_items_context[ |
| 110 | issue.issue_id]['date_added'], |
| 111 | 'note': lambda issue: hotlist_items_context[ |
| 112 | issue.issue_id]['note']}) |
| 113 | sortable_postproc = tracker_helpers.SORTABLE_FIELDS_POSTPROCESSORS.copy() |
| 114 | sortable_postproc.update( |
| 115 | {'adder': lambda user_view: user_view.email, |
| 116 | }) |
| 117 | |
| 118 | sorted_issues = sorting.SortArtifacts( |
| 119 | allowed_issues, harmonized_config, sortable_fields, |
| 120 | sortable_postproc, group_by_spec, sort_spec, |
| 121 | users_by_id=issues_users_by_id, tie_breakers=['rank', 'id']) |
| 122 | return sorted_issues, hotlist_items_context, issues_users_by_id |
| 123 | |
| 124 | |
| 125 | def CreateHotlistTableData(mr, hotlist_issues, services): |
| 126 | """Creates the table data for the hotlistissues table.""" |
| 127 | with mr.profiler.Phase('getting stars'): |
| 128 | starred_iid_set = set(services.issue_star.LookupStarredItemIDs( |
| 129 | mr.cnxn, mr.auth.user_id)) |
| 130 | |
| 131 | with mr.profiler.Phase('Computing col_spec'): |
| 132 | mr.ComputeColSpec(mr.hotlist) |
| 133 | |
| 134 | issues_list = services.issue.GetIssues( |
| 135 | mr.cnxn, |
| 136 | [hotlist_issue.issue_id for hotlist_issue in hotlist_issues]) |
| 137 | with mr.profiler.Phase('Getting config'): |
| 138 | hotlist_issues_project_ids = GetAllProjectsOfIssues( |
| 139 | [issue for issue in issues_list]) |
| 140 | is_cross_project = len(hotlist_issues_project_ids) > 1 |
| 141 | config_list = GetAllConfigsOfProjects( |
| 142 | mr.cnxn, hotlist_issues_project_ids, services) |
| 143 | harmonized_config = tracker_bizobj.HarmonizeConfigs(config_list) |
| 144 | |
| 145 | # With no sort_spec specified, a hotlist should default to be sorted by |
| 146 | # 'rank'. sort_spec needs to be modified because hotlistissues.py |
| 147 | # checks for 'rank' in sort_spec to set 'allow_rerank' which determines if |
| 148 | # drag and drop reranking should be enabled. |
| 149 | if not mr.sort_spec: |
| 150 | mr.sort_spec = 'rank' |
| 151 | (sorted_issues, hotlist_issues_context, |
| 152 | issues_users_by_id) = GetSortedHotlistIssues( |
| 153 | mr.cnxn, hotlist_issues, issues_list, mr.auth, mr.can, mr.sort_spec, |
| 154 | mr.group_by_spec, harmonized_config, services, mr.profiler) |
| 155 | |
| 156 | with mr.profiler.Phase("getting related issues"): |
| 157 | related_iids = set() |
| 158 | results_needing_related = sorted_issues |
| 159 | lower_cols = mr.col_spec.lower().split() |
| 160 | for issue in results_needing_related: |
| 161 | if 'blockedon' in lower_cols: |
| 162 | related_iids.update(issue.blocked_on_iids) |
| 163 | if 'blocking' in lower_cols: |
| 164 | related_iids.update(issue.blocking_iids) |
| 165 | if 'mergedinto' in lower_cols: |
| 166 | related_iids.add(issue.merged_into) |
| 167 | related_issues_list = services.issue.GetIssues( |
| 168 | mr.cnxn, list(related_iids)) |
| 169 | related_issues = {issue.issue_id: issue for issue in related_issues_list} |
| 170 | |
| 171 | with mr.profiler.Phase('filtering unviewable issues'): |
| 172 | viewable_iids_set = {issue.issue_id |
| 173 | for issue in tracker_helpers.GetAllowedIssues( |
| 174 | mr, [related_issues.values()], services)[0]} |
| 175 | |
| 176 | with mr.profiler.Phase('building table'): |
| 177 | context_for_all_issues = { |
| 178 | issue.issue_id: hotlist_issues_context[issue.issue_id] |
| 179 | for issue in sorted_issues} |
| 180 | |
| 181 | column_values = table_view_helpers.ExtractUniqueValues( |
| 182 | mr.col_spec.lower().split(), sorted_issues, issues_users_by_id, |
| 183 | harmonized_config, related_issues, |
| 184 | hotlist_context_dict=context_for_all_issues) |
| 185 | unshown_columns = table_view_helpers.ComputeUnshownColumns( |
| 186 | sorted_issues, mr.col_spec.split(), harmonized_config, |
| 187 | features_constants.OTHER_BUILT_IN_COLS) |
| 188 | url_params = [(name, mr.GetParam(name)) for name in |
| 189 | framework_helpers.RECOGNIZED_PARAMS] |
| 190 | # We are passing in None for the project_name because we are not operating |
| 191 | # under any project. |
| 192 | pagination = paginate.ArtifactPagination( |
| 193 | sorted_issues, mr.num, mr.GetPositiveIntParam('start'), |
| 194 | None, GetURLOfHotlist(mr.cnxn, mr.hotlist, services.user), |
| 195 | total_count=len(sorted_issues), url_params=url_params) |
| 196 | |
| 197 | sort_spec = '%s %s %s' % ( |
| 198 | mr.group_by_spec, mr.sort_spec, harmonized_config.default_sort_spec) |
| 199 | |
| 200 | table_data = _MakeTableData( |
| 201 | pagination.visible_results, starred_iid_set, |
| 202 | mr.col_spec.lower().split(), mr.group_by_spec.lower().split(), |
| 203 | issues_users_by_id, tablecell.CELL_FACTORIES, related_issues, |
| 204 | viewable_iids_set, harmonized_config, context_for_all_issues, |
| 205 | mr.hotlist_id, sort_spec) |
| 206 | |
| 207 | table_related_dict = { |
| 208 | 'column_values': column_values, 'unshown_columns': unshown_columns, |
| 209 | 'pagination': pagination, 'is_cross_project': is_cross_project } |
| 210 | return table_data, table_related_dict |
| 211 | |
| 212 | |
| 213 | def _MakeTableData(issues, starred_iid_set, lower_columns, |
| 214 | lower_group_by, users_by_id, cell_factories, |
| 215 | related_issues, viewable_iids_set, config, |
| 216 | context_for_all_issues, |
| 217 | hotlist_id, sort_spec): |
| 218 | """Returns data from MakeTableData after adding additional information.""" |
| 219 | table_data = table_view_helpers.MakeTableData( |
| 220 | issues, starred_iid_set, lower_columns, lower_group_by, |
| 221 | users_by_id, cell_factories, lambda issue: issue.issue_id, |
| 222 | related_issues, viewable_iids_set, config, context_for_all_issues) |
| 223 | |
| 224 | for row, art in zip(table_data, issues): |
| 225 | row.issue_id = art.issue_id |
| 226 | row.local_id = art.local_id |
| 227 | row.project_name = art.project_name |
| 228 | row.project_url = framework_helpers.FormatURL( |
| 229 | None, '/p/%s' % row.project_name) |
| 230 | row.issue_ref = '%s:%d' % (art.project_name, art.local_id) |
| 231 | row.issue_clean_url = tracker_helpers.FormatRelativeIssueURL( |
| 232 | art.project_name, urls.ISSUE_DETAIL, id=art.local_id) |
| 233 | row.issue_ctx_url = tracker_helpers.FormatRelativeIssueURL( |
| 234 | art.project_name, urls.ISSUE_DETAIL, |
| 235 | id=art.local_id, sort=sort_spec, hotlist_id=hotlist_id) |
| 236 | |
| 237 | return table_data |
| 238 | |
| 239 | |
| 240 | def FilterIssues(cnxn, auth, can, issues, services): |
| 241 | # (MonorailConnection, AuthData, int, List[Issue], Services) -> List[Issue] |
| 242 | """Return a list of issues that the user is allowed to view. |
| 243 | |
| 244 | Args: |
| 245 | cnxn: MonorailConnection for connection to the SQL database. |
| 246 | auth: AuthData object that identifies the logged in user. |
| 247 | can: in "canned_query" number to scope the visible issues. |
| 248 | issues: list of Issues to be filtered. |
| 249 | services: Services object for connections to backend services. |
| 250 | |
| 251 | Returns: |
| 252 | A list of Issues that the user has permissions to view. |
| 253 | """ |
| 254 | allowed_issues = [] |
| 255 | project_ids = GetAllProjectsOfIssues(issues) |
| 256 | issue_projects = services.project.GetProjects(cnxn, project_ids) |
| 257 | configs_by_project_id = services.config.GetProjectConfigs(cnxn, project_ids) |
| 258 | perms_by_project_id = { |
| 259 | pid: permissions.GetPermissions(auth.user_pb, auth.effective_ids, p) |
| 260 | for pid, p in issue_projects.items()} |
| 261 | for issue in issues: |
| 262 | if (can == 1) or not issue.closed_timestamp: |
| 263 | issue_project = issue_projects[issue.project_id] |
| 264 | config = configs_by_project_id[issue.project_id] |
| 265 | perms = perms_by_project_id[issue.project_id] |
| 266 | granted_perms = tracker_bizobj.GetGrantedPerms( |
| 267 | issue, auth.effective_ids, config) |
| 268 | permit_view = permissions.CanViewIssue( |
| 269 | auth.effective_ids, perms, |
| 270 | issue_project, issue, granted_perms=granted_perms) |
| 271 | if permit_view: |
| 272 | allowed_issues.append(issue) |
| 273 | |
| 274 | return allowed_issues |
| 275 | |
| 276 | |
| 277 | def GetAllConfigsOfProjects(cnxn, project_ids, services): |
| 278 | """Returns a list of configs for the given list of projects.""" |
| 279 | config_dict = services.config.GetProjectConfigs(cnxn, project_ids) |
| 280 | config_list = [config_dict[project_id] for project_id in project_ids] |
| 281 | return config_list |
| 282 | |
| 283 | |
| 284 | def GetAllProjectsOfIssues(issues): |
| 285 | """Returns a list of all projects that the given issues are in.""" |
| 286 | project_ids = set() |
| 287 | for issue in issues: |
| 288 | project_ids.add(issue.project_id) |
| 289 | return project_ids |
| 290 | |
| 291 | |
| 292 | def MembersWithoutGivenIDs(hotlist, exclude_ids): |
| 293 | """Return three lists of member user IDs, with exclude_ids not in them.""" |
| 294 | owner_ids = [user_id for user_id in hotlist.owner_ids |
| 295 | if user_id not in exclude_ids] |
| 296 | editor_ids = [user_id for user_id in hotlist.editor_ids |
| 297 | if user_id not in exclude_ids] |
| 298 | follower_ids = [user_id for user_id in hotlist.follower_ids |
| 299 | if user_id not in exclude_ids] |
| 300 | |
| 301 | return owner_ids, editor_ids, follower_ids |
| 302 | |
| 303 | |
| 304 | def MembersWithGivenIDs(hotlist, new_member_ids, role): |
| 305 | """Return three lists of member IDs with the new IDs in the right one. |
| 306 | |
| 307 | Args: |
| 308 | hotlist: Hotlist PB for the project to get current members from. |
| 309 | new_member_ids: set of user IDs for members being added. |
| 310 | role: string name of the role that new_member_ids should be granted. |
| 311 | |
| 312 | Returns: |
| 313 | Three lists of member IDs with new_member_ids added to the appropriate |
| 314 | list and removed from any other role. |
| 315 | |
| 316 | Raises: |
| 317 | ValueError: if the role is not one of owner, committer, or contributor. |
| 318 | """ |
| 319 | owner_ids, editor_ids, follower_ids = MembersWithoutGivenIDs( |
| 320 | hotlist, new_member_ids) |
| 321 | |
| 322 | if role == 'owner': |
| 323 | owner_ids.extend(new_member_ids) |
| 324 | elif role == 'editor': |
| 325 | editor_ids.extend(new_member_ids) |
| 326 | elif role == 'follower': |
| 327 | follower_ids.extend(new_member_ids) |
| 328 | else: |
| 329 | raise ValueError() |
| 330 | |
| 331 | return owner_ids, editor_ids, follower_ids |
| 332 | |
| 333 | |
| 334 | def GetURLOfHotlist(cnxn, hotlist, user_service, url_for_token=False): |
Adrià Vilanova Martínez | f19ea43 | 2024-01-23 20:20:52 +0100 | [diff] [blame] | 335 | """Determines the url to be used to access the given hotlist. |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 336 | |
Adrià Vilanova Martínez | f19ea43 | 2024-01-23 20:20:52 +0100 | [diff] [blame] | 337 | Args: |
| 338 | cnxn: connection to SQL database |
| 339 | hotlist: the hotlist_pb |
| 340 | user_service: interface to user data storage |
| 341 | url_for_token: if true, url returned will use user's id |
| 342 | regardless of their user settings, for tokenization. |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 343 | |
Adrià Vilanova Martínez | f19ea43 | 2024-01-23 20:20:52 +0100 | [diff] [blame] | 344 | Returns: |
| 345 | The string url to be used when accessing this hotlist. |
| 346 | """ |
| 347 | if not hotlist.owner_ids: # Should never happen. |
| 348 | logging.error( |
| 349 | 'Unowned Hotlist: id:%r, name:%r', hotlist.hotlist_id, hotlist.name) |
| 350 | return '' |
| 351 | owner_id = hotlist.owner_ids[0] # only one owner allowed |
| 352 | owner = user_service.GetUser(cnxn, owner_id) |
| 353 | if owner.obscure_email or url_for_token: |
| 354 | return '/u/%d/hotlists/%s' % (owner_id, hotlist.name) |
| 355 | return ('/u/%s/hotlists/%s' % (owner.email, hotlist.name)) |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 356 | |
| 357 | |
| 358 | def RemoveHotlist(cnxn, hotlist_id, services): |
| 359 | """Removes the given hotlist from the database. |
Adrià Vilanova Martínez | f19ea43 | 2024-01-23 20:20:52 +0100 | [diff] [blame] | 360 | |
| 361 | Args: |
| 362 | hotlist_id: the id of the hotlist to be removed. |
| 363 | services: interfaces to data storage. |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 364 | """ |
| 365 | services.hotlist_star.ExpungeStars(cnxn, hotlist_id) |
| 366 | services.user.ExpungeHotlistsFromHistory(cnxn, [hotlist_id]) |
| 367 | services.features.DeleteHotlist(cnxn, hotlist_id) |
| 368 | |
| 369 | |
| 370 | # The following are used by issueentry. |
| 371 | |
| 372 | def InvalidParsedHotlistRefsNames(parsed_hotlist_refs, user_hotlist_pbs): |
| 373 | """Find and return all names without a corresponding hotlist so named. |
| 374 | |
| 375 | Args: |
| 376 | parsed_hotlist_refs: a list of ParsedHotlistRef objects |
| 377 | user_hotlist_pbs: the hotlist protobuf objects of all hotlists |
| 378 | belonging to the user |
| 379 | |
| 380 | Returns: |
| 381 | a list of invalid names; if none are found, the empty list |
| 382 | """ |
| 383 | user_hotlist_names = {hotlist.name for hotlist in user_hotlist_pbs} |
| 384 | invalid_names = list() |
| 385 | for parsed_ref in parsed_hotlist_refs: |
| 386 | if parsed_ref.hotlist_name not in user_hotlist_names: |
| 387 | invalid_names.append(parsed_ref.hotlist_name) |
| 388 | return invalid_names |
| 389 | |
| 390 | |
| 391 | def AmbiguousShortrefHotlistNames(short_refs, user_hotlist_pbs): |
| 392 | """Find and return ambiguous hotlist shortrefs' hotlist names. |
| 393 | |
| 394 | A hotlist shortref is ambiguous iff there exists more than |
| 395 | hotlist with that name in the user's hotlists. |
| 396 | |
| 397 | Args: |
| 398 | short_refs: a list of ParsedHotlistRef object specifying only |
| 399 | a hotlist name (user_email being none) |
| 400 | user_hotlist_pbs: the hotlist protobuf objects of all hotlists |
| 401 | belonging to the user |
| 402 | |
| 403 | Returns: |
| 404 | a list of ambiguous hotlist names; if none are found, the empty list |
| 405 | """ |
| 406 | ambiguous_names = set() |
| 407 | seen = set() |
| 408 | for hotlist in user_hotlist_pbs: |
| 409 | if hotlist.name in seen: |
| 410 | ambiguous_names.add(hotlist.name) |
| 411 | seen.add(hotlist.name) |
| 412 | ambiguous_from_refs = list() |
| 413 | for ref in short_refs: |
| 414 | if ref.hotlist_name in ambiguous_names: |
| 415 | ambiguous_from_refs.append(ref.hotlist_name) |
| 416 | return ambiguous_from_refs |
| 417 | |
| 418 | |
| 419 | def InvalidParsedHotlistRefsEmails(full_refs, user_hotlist_emails_to_owners): |
| 420 | """Find and return invalid e-mails in hotlist full refs. |
| 421 | |
| 422 | Args: |
| 423 | full_refs: a list of ParsedHotlistRef object specifying both |
| 424 | user_email and hotlist_name |
| 425 | user_hotlist_emails_to_owners: a dictionary having for its keys only |
| 426 | the e-mails of the owners of the hotlists the user had edit permission |
| 427 | over. (Could also be a set containing these e-mails.) |
| 428 | |
| 429 | Returns: |
| 430 | A list of invalid e-mails; if none are found, the empty list. |
| 431 | """ |
| 432 | parsed_emails = [pref.user_email for pref in full_refs] |
| 433 | invalid_emails = list() |
| 434 | for email in parsed_emails: |
| 435 | if email not in user_hotlist_emails_to_owners: |
| 436 | invalid_emails.append(email) |
| 437 | return invalid_emails |
| 438 | |
| 439 | |
| 440 | def GetHotlistsOfParsedHotlistFullRefs( |
| 441 | full_refs, user_hotlist_emails_to_owners, user_hotlist_refs_to_pbs): |
| 442 | """Check that all full refs are valid. |
| 443 | |
| 444 | A ref is 'invalid' if it doesn't specify one of the user's hotlists. |
| 445 | |
| 446 | Args: |
| 447 | full_refs: a list of ParsedHotlistRef object specifying both |
| 448 | user_email and hotlist_name |
| 449 | user_hotlist_emails_to_owners: a dictionary having for its keys only |
| 450 | the e-mails of the owners of the hotlists the user had edit permission |
| 451 | over. |
| 452 | user_hotlist_refs_to_pbs: a dictionary mapping HotlistRefs |
| 453 | (owner_id, hotlist_name) to the corresponding hotlist protobuf object for |
| 454 | the user's hotlists |
| 455 | |
| 456 | Returns: |
| 457 | A two-tuple: (list of valid refs' corresponding hotlist protobuf objects, |
| 458 | list of invalid refs) |
| 459 | |
| 460 | """ |
| 461 | invalid_refs = list() |
| 462 | valid_pbs = list() |
| 463 | for parsed_ref in full_refs: |
| 464 | hotlist_ref = HotlistRef( |
| 465 | user_hotlist_emails_to_owners[parsed_ref.user_email], |
| 466 | parsed_ref.hotlist_name) |
| 467 | if hotlist_ref not in user_hotlist_refs_to_pbs: |
| 468 | invalid_refs.append(parsed_ref) |
| 469 | else: |
| 470 | valid_pbs.append(user_hotlist_refs_to_pbs[hotlist_ref]) |
| 471 | return valid_pbs, invalid_refs |