| # Copyright 2016 The Chromium Authors |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| """Fake object classes that are useful for unit tests.""" |
| from __future__ import print_function |
| from __future__ import division |
| from __future__ import absolute_import |
| |
| import collections |
| import itertools |
| import logging |
| import re |
| import sys |
| import time |
| |
| from six import string_types |
| |
| import settings |
| from features import filterrules_helpers |
| from framework import exceptions |
| from framework import framework_bizobj |
| from framework import framework_constants |
| from framework import framework_helpers |
| from framework import monorailrequest |
| from framework import permissions |
| from framework import profiler |
| from framework import validate |
| from mrproto import features_pb2 |
| from mrproto import project_pb2 |
| from mrproto import tracker_pb2 |
| from mrproto import user_pb2 |
| from mrproto import usergroup_pb2 |
| from services import caches |
| from services import config_svc |
| from services import features_svc |
| from services import project_svc |
| from tracker import tracker_bizobj |
| from tracker import tracker_constants |
| |
| # Many fakes return partial or constant values, regardless of their arguments. |
| # pylint: disable=unused-argument |
| |
| BOUNDARY = '-----thisisaboundary' |
| OWNER_ROLE = 'OWNER_ROLE' |
| COMMITTER_ROLE = 'COMMITTER_ROLE' |
| CONTRIBUTOR_ROLE = 'CONTRIBUTOR_ROLE' |
| EDITOR_ROLE = 'EDITOR_ROLE' |
| FOLLOWER_ROLE = 'FOLLOWER_ROLE' |
| |
| def Hotlist( |
| hotlist_name, hotlist_id, hotlist_item_fields=None, |
| is_private=False, owner_ids=None, editor_ids=None, follower_ids=None, |
| default_col_spec=None, summary=None, description=None): |
| hotlist_id = hotlist_id or hash(hotlist_name) |
| return features_pb2.MakeHotlist( |
| hotlist_name, hotlist_item_fields=hotlist_item_fields, |
| hotlist_id=hotlist_id, is_private=is_private, owner_ids=owner_ids or [], |
| editor_ids=editor_ids or [], follower_ids=follower_ids or [], |
| default_col_spec=default_col_spec, summary=summary, |
| description=description) |
| |
| def HotlistItem(issue_id, rank=None, adder_id=None, date_added=None, note=None): |
| return features_pb2.MakeHotlistItem(issue_id=issue_id, rank=rank, |
| adder_id=adder_id, date_added=date_added, |
| note=None) |
| |
| def Project( |
| project_name='proj', project_id=None, state=project_pb2.ProjectState.LIVE, |
| access=project_pb2.ProjectAccess.ANYONE, moved_to=None, |
| cached_content_timestamp=None, |
| owner_ids=None, committer_ids=None, contributor_ids=None): |
| """Returns a project protocol buffer with the given attributes.""" |
| project_id = project_id or hash(project_name) |
| return project_pb2.MakeProject( |
| project_name, project_id=project_id, state=state, access=access, |
| moved_to=moved_to, cached_content_timestamp=cached_content_timestamp, |
| owner_ids=owner_ids, committer_ids=committer_ids, |
| contributor_ids=contributor_ids) |
| |
| |
| def MakeTestFieldDef( |
| field_id, project_id, field_type, field_name='', applic_type=None, |
| applic_pred=None, is_required=False, is_niche=False, is_multivalued=False, |
| min_value=None, max_value=None, regex=None, needs_member=False, |
| needs_perm=None, grants_perm=None, notify_on=None, date_action_str=None, |
| docstring=None, admin_ids=None, editor_ids=None, approval_id=None, |
| is_phase_field=False, is_restricted_field=False): |
| return tracker_bizobj.MakeFieldDef( |
| field_id, project_id, field_name, field_type, applic_type, applic_pred, |
| is_required, is_niche, is_multivalued, min_value, max_value, regex, |
| needs_member, needs_perm, grants_perm, notify_on, date_action_str, |
| docstring, False, |
| approval_id=approval_id, is_phase_field=is_phase_field, |
| is_restricted_field=is_restricted_field, admin_ids=admin_ids, |
| editor_ids=editor_ids) |
| |
| def MakeTestApprovalDef(approval_id, approver_ids=None, survey=None): |
| return tracker_pb2.ApprovalDef( |
| approval_id=approval_id, |
| approver_ids = approver_ids, |
| survey = survey) |
| |
| def MakePhase(phase_id, name='', rank=0): |
| return tracker_pb2.Phase(phase_id=phase_id, name=name, rank=rank) |
| |
| |
| def MakeApprovalValue( |
| approval_id, |
| status=tracker_pb2.ApprovalStatus.NOT_SET, |
| setter_id=None, |
| set_on=None, |
| approver_ids=None, |
| phase_id=None): |
| if approver_ids is None: |
| approver_ids = [] |
| return tracker_pb2.ApprovalValue( |
| approval_id=approval_id, |
| status=status, |
| setter_id=setter_id, |
| set_on=set_on, |
| approver_ids=approver_ids, |
| phase_id=phase_id) |
| |
| |
| def MakeFieldValue( |
| field_id, |
| int_value=None, |
| str_value=None, |
| user_id=None, |
| date_value=None, |
| url_value=None, |
| derived=None, |
| phase_id=None): |
| return tracker_pb2.FieldValue( |
| field_id=field_id, |
| int_value=int_value, |
| str_value=str_value, |
| user_id=user_id, |
| date_value=date_value, |
| url_value=url_value, |
| derived=derived, |
| phase_id=phase_id) |
| |
| |
| def MakeTestIssue( |
| project_id, |
| local_id, |
| summary, |
| status, |
| owner_id, |
| labels=None, |
| derived_labels=None, |
| derived_status=None, |
| merged_into=0, |
| star_count=0, |
| derived_owner_id=0, |
| issue_id=None, |
| reporter_id=None, |
| opened_timestamp=None, |
| closed_timestamp=None, |
| modified_timestamp=None, |
| migration_modified_timestamp=None, |
| is_spam=False, |
| component_ids=None, |
| project_name=None, |
| field_values=None, |
| cc_ids=None, |
| derived_cc_ids=None, |
| assume_stale=True, |
| phases=None, |
| approval_values=None, |
| merged_into_external=None, |
| attachment_count=0, |
| derived_component_ids=None): |
| """Easily make an Issue for testing.""" |
| issue = tracker_pb2.Issue() |
| issue.project_id = project_id |
| issue.project_name = project_name |
| issue.local_id = local_id |
| issue.issue_id = issue_id if issue_id else 100000 + local_id |
| issue.reporter_id = reporter_id if reporter_id else owner_id |
| issue.summary = summary |
| issue.status = status |
| issue.owner_id = owner_id |
| issue.derived_owner_id = derived_owner_id |
| issue.star_count = star_count |
| issue.merged_into = merged_into |
| issue.merged_into_external = merged_into_external |
| issue.is_spam = is_spam |
| issue.attachment_count = attachment_count |
| if cc_ids: |
| issue.cc_ids = cc_ids |
| if derived_cc_ids: |
| issue.derived_cc_ids = derived_cc_ids |
| issue.assume_stale = assume_stale |
| if opened_timestamp: |
| issue.opened_timestamp = opened_timestamp |
| issue.owner_modified_timestamp = opened_timestamp |
| issue.status_modified_timestamp = opened_timestamp |
| issue.component_modified_timestamp = opened_timestamp |
| if modified_timestamp: |
| issue.modified_timestamp = modified_timestamp |
| # By default, make migration_modified_timestamp the same as |
| # modified_timestamp |
| issue.migration_modified_timestamp = modified_timestamp |
| if migration_modified_timestamp: |
| issue.migration_modified_timestamp = migration_modified_timestamp |
| if closed_timestamp: |
| issue.closed_timestamp = closed_timestamp |
| if labels is not None: |
| if isinstance(labels, string_types): |
| labels = labels.split() |
| issue.labels.extend(labels) |
| if derived_labels is not None: |
| if isinstance(derived_labels, string_types): |
| derived_labels = derived_labels.split() |
| issue.derived_labels.extend(derived_labels) |
| if derived_status is not None: |
| issue.derived_status = derived_status |
| if component_ids is not None: |
| issue.component_ids = component_ids |
| if derived_component_ids is not None: |
| issue.derived_component_ids = derived_component_ids |
| if field_values is not None: |
| issue.field_values = field_values |
| if phases is not None: |
| issue.phases = phases |
| if approval_values is not None: |
| issue.approval_values = approval_values |
| return issue |
| |
| |
| def MakeTestComponentDef(project_id, comp_id, path='', cc_ids=None): |
| if cc_ids is None: |
| cc_ids = [] |
| return tracker_bizobj.MakeComponentDef( |
| comp_id, project_id, path, '', False, [], cc_ids, None, None) |
| |
| |
| def MakeTestConfig(project_id, labels, statuses): |
| """Convenient function to make a ProjectIssueConfig object.""" |
| config = tracker_bizobj.MakeDefaultProjectIssueConfig(project_id) |
| if isinstance(labels, string_types): |
| labels = labels.split() |
| if isinstance(statuses, string_types): |
| statuses = statuses.split() |
| config.well_known_labels = [ |
| tracker_pb2.LabelDef(label=lab) for lab in labels] |
| config.well_known_statuses = [ |
| tracker_pb2.StatusDef(status=stat) for stat in statuses] |
| return config |
| |
| |
| class MonorailConnection(object): |
| """Fake connection to databases for use in tests.""" |
| |
| def Commit(self): |
| pass |
| |
| def Close(self): |
| pass |
| |
| |
| class MonorailRequest(monorailrequest.MonorailRequest): |
| """Subclass of MonorailRequest suitable for testing.""" |
| |
| def __init__(self, services, user_info=None, project=None, perms=None, |
| hotlist=None, **kwargs): |
| """Construct a test MonorailRequest. |
| |
| Typically, this is constructed via testing.helpers.GetRequestObjects, |
| which also causes url parsing and optionally initializes the user, |
| project, and permissions info. |
| |
| Args: |
| services: connections to backends. |
| user_info: a dict of user attributes to set on a MonorailRequest object. |
| For example, "user_id: 5" causes self.auth.user_id=5. |
| project: the Project pb for this request. |
| perms: a PermissionSet for this request. |
| """ |
| super(MonorailRequest, self).__init__(services, **kwargs) |
| |
| if user_info is not None: |
| for key in user_info: |
| setattr(self.auth, key, user_info[key]) |
| if 'user_id' in user_info: |
| self.auth.effective_ids = {user_info['user_id']} |
| |
| self.perms = perms or permissions.ADMIN_PERMISSIONSET |
| self.profiler = profiler.Profiler() |
| self.project = project |
| self.hotlist = hotlist |
| if hotlist is not None: |
| self.hotlist_id = hotlist.hotlist_id |
| |
| class UserGroupService(object): |
| """Fake UserGroupService class for testing other code.""" |
| |
| def __init__(self): |
| # Test-only sequence of expunged users. |
| self.expunged_users_in_groups = [] |
| |
| self.group_settings = {} |
| self.group_members = {} |
| self.group_addrs = {} |
| self.role_dict = {} |
| |
| def TestAddGroupSettings( |
| self, |
| group_id, |
| email, |
| who_can_view=None, |
| anyone_can_join=False, |
| who_can_add=None, |
| external_group_type=None, |
| last_sync_time=0, |
| friend_projects=None, |
| notify_members=True, |
| notify_group=False): |
| """Set up a fake group for testing. |
| |
| Args: |
| group_id: int user ID of the new user group. |
| email: string email address to identify the user group. |
| who_can_view: string enum 'owners', 'members', or 'anyone'. |
| anyone_can_join: optional boolean to allow any users to join the group. |
| who_can_add: optional list of int user IDs of users who can add |
| more members to the group. |
| notify_members: optional boolean for if emails to this group should be |
| sent directly to members. |
| notify_group: optional boolean for if emails to this group should be |
| sent directly to the group email. |
| """ |
| friend_projects = friend_projects or [] |
| group_settings = usergroup_pb2.MakeSettings( |
| who_can_view or 'members', external_group_type, last_sync_time, |
| friend_projects, notify_members, notify_group) |
| self.group_settings[group_id] = group_settings |
| self.group_addrs[group_id] = email |
| # TODO(jrobbins): store the other settings. |
| |
| def TestAddMembers(self, group_id, user_ids, role='member'): |
| self.group_members.setdefault(group_id, []).extend(user_ids) |
| for user_id in user_ids: |
| self.role_dict.setdefault(group_id, {})[user_id] = role |
| |
| def LookupAllMemberships(self, _cnxn, user_ids, use_cache=True): |
| return { |
| user_id: self.LookupMemberships(_cnxn, user_id) |
| for user_id in user_ids |
| } |
| |
| def LookupMemberships(self, _cnxn, user_id): |
| memberships = { |
| group_id for group_id, member_ids in self.group_members.items() |
| if user_id in member_ids} |
| return memberships |
| |
| def DetermineWhichUserIDsAreGroups(self, _cnxn, user_ids): |
| return [uid for uid in user_ids |
| if uid in self.group_settings] |
| |
| def GetAllUserGroupsInfo(self, cnxn): |
| infos = [] |
| for group_id in self.group_settings: |
| infos.append( |
| (self.group_addrs[group_id], |
| len(self.group_members.get(group_id, [])), |
| self.group_settings[group_id], group_id)) |
| |
| return infos |
| |
| def GetAllGroupSettings(self, _cnxn, group_ids): |
| return {gid: self.group_settings[gid] |
| for gid in group_ids |
| if gid in self.group_settings} |
| |
| def GetGroupSettings(self, cnxn, group_id): |
| return self.GetAllGroupSettings(cnxn, [group_id]).get(group_id) |
| |
| def CreateGroup(self, cnxn, services, email, who_can_view_members, |
| ext_group_type=None, friend_projects=None): |
| friend_projects = friend_projects or [] |
| group_id = services.user.LookupUserID( |
| cnxn, email, autocreate=True, allowgroups=True) |
| self.group_addrs[group_id] = email |
| group_settings = usergroup_pb2.MakeSettings( |
| who_can_view_members, ext_group_type, 0, friend_projects) |
| self.UpdateSettings(cnxn, group_id, group_settings) |
| return group_id |
| |
| def DeleteGroups(self, cnxn, group_ids): |
| member_ids_dict, owner_ids_dict = self.LookupMembers(cnxn, group_ids) |
| citizens_id_dict = collections.defaultdict(list) |
| for g_id, user_ids in member_ids_dict.items(): |
| citizens_id_dict[g_id].extend(user_ids) |
| for g_id, user_ids in owner_ids_dict.items(): |
| citizens_id_dict[g_id].extend(user_ids) |
| for g_id, citizen_ids in citizens_id_dict.items(): |
| # Remove group members, friend projects and settings |
| self.RemoveMembers(cnxn, g_id, citizen_ids) |
| self.group_settings.pop(g_id, None) |
| |
| def LookupComputedMemberships(self, cnxn, domain, use_cache=True): |
| group_email = 'everyone@%s' % domain |
| group_id = self.LookupUserGroupID(cnxn, group_email, use_cache=use_cache) |
| if group_id: |
| return [group_id] |
| |
| return [] |
| |
| def LookupUserGroupID(self, cnxn, group_email, use_cache=True): |
| for group_id in self.group_settings: |
| if group_email == self.group_addrs.get(group_id): |
| return group_id |
| return None |
| |
| def LookupMembers(self, _cnxn, group_id_list): |
| members_dict = {} |
| owners_dict = {} |
| for gid in group_id_list: |
| members_dict[gid] = [] |
| owners_dict[gid] = [] |
| for mid in self.group_members.get(gid, []): |
| if self.role_dict.get(gid, {}).get(mid) == 'owner': |
| owners_dict[gid].append(mid) |
| elif self.role_dict.get(gid, {}).get(mid) == 'member': |
| members_dict[gid].append(mid) |
| return members_dict, owners_dict |
| |
| def LookupAllMembers(self, _cnxn, group_id_list): |
| direct_members, direct_owners = self.LookupMembers( |
| _cnxn, group_id_list) |
| members_dict = {} |
| owners_dict = {} |
| for gid in group_id_list: |
| members = direct_members[gid] |
| owners = direct_owners[gid] |
| owners_dict[gid] = owners |
| members_dict[gid] = members |
| group_ids = set([uid for uid in members + owners |
| if uid in self.group_settings]) |
| while group_ids: |
| indirect_members, indirect_owners = self.LookupMembers( |
| _cnxn, group_ids) |
| child_members = set() |
| child_owners = set() |
| for _, children in indirect_members.items(): |
| child_members.update(children) |
| for _, children in indirect_owners.items(): |
| child_owners.update(children) |
| members_dict[gid].extend(list(child_members)) |
| owners_dict[gid].extend(list(child_owners)) |
| group_ids = set(self.DetermineWhichUserIDsAreGroups( |
| _cnxn, list(child_members) + list(child_owners))) |
| members_dict[gid] = list(set(members_dict[gid])) |
| return members_dict, owners_dict |
| |
| |
| def RemoveMembers(self, _cnxn, group_id, old_member_ids): |
| current_member_ids = self.group_members.get(group_id, []) |
| revised_member_ids = [mid for mid in current_member_ids |
| if mid not in old_member_ids] |
| self.group_members[group_id] = revised_member_ids |
| |
| def UpdateMembers(self, _cnxn, group_id, member_ids, new_role): |
| self.RemoveMembers(_cnxn, group_id, member_ids) |
| self.TestAddMembers(group_id, member_ids, new_role) |
| |
| def UpdateSettings(self, _cnxn, group_id, group_settings): |
| self.group_settings[group_id] = group_settings |
| |
| def ExpandAnyGroupEmailRecipients(self, cnxn, user_ids): |
| group_ids = set(self.DetermineWhichUserIDsAreGroups(cnxn, user_ids)) |
| group_settings_dict = self.GetAllGroupSettings(cnxn, group_ids) |
| member_ids_dict, owner_ids_dict = self.LookupAllMembers(cnxn, group_ids) |
| indirect_ids = set() |
| direct_ids = {uid for uid in user_ids if uid not in group_ids} |
| for gid, group_settings in group_settings_dict.items(): |
| if group_settings.notify_members: |
| indirect_ids.update(member_ids_dict.get(gid, set())) |
| indirect_ids.update(owner_ids_dict.get(gid, set())) |
| if group_settings.notify_group: |
| direct_ids.add(gid) |
| |
| return list(direct_ids), list(indirect_ids) |
| |
| def LookupVisibleMembers( |
| self, cnxn, group_id_list, perms, effective_ids, services): |
| settings_dict = self.GetAllGroupSettings(cnxn, group_id_list) |
| group_ids = list(settings_dict.keys()) |
| |
| direct_member_ids_dict, direct_owner_ids_dict = self.LookupMembers( |
| cnxn, group_ids) |
| all_member_ids_dict, all_owner_ids_dict = self.LookupAllMembers( |
| cnxn, group_ids) |
| visible_member_ids_dict = {} |
| visible_owner_ids_dict = {} |
| for gid in group_ids: |
| member_ids = all_member_ids_dict[gid] |
| owner_ids = all_owner_ids_dict[gid] |
| if permissions.CanViewGroupMembers( |
| perms, effective_ids, settings_dict[gid], member_ids, owner_ids, []): |
| visible_member_ids_dict[gid] = direct_member_ids_dict[gid] |
| visible_owner_ids_dict[gid] = direct_owner_ids_dict[gid] |
| |
| return visible_member_ids_dict, visible_owner_ids_dict |
| |
| def ValidateFriendProjects(self, cnxn, services, friend_projects): |
| project_names = list(filter(None, re.split('; |, | |;|,', friend_projects))) |
| id_dict = services.project.LookupProjectIDs(cnxn, project_names) |
| missed_projects = [] |
| result = [] |
| for p_name in project_names: |
| if p_name in id_dict: |
| result.append(id_dict[p_name]) |
| else: |
| missed_projects.append(p_name) |
| error_msg = '' |
| if missed_projects: |
| error_msg = 'Project(s) %s do not exist' % ', '.join(missed_projects) |
| return None, error_msg |
| else: |
| return result, None |
| |
| def ExpungeUsersInGroups(self, cnxn, ids): |
| self.expunged_users_in_groups.extend(ids) |
| |
| |
| class CacheManager(object): |
| |
| def __init__(self, invalidate_tbl=None): |
| self.last_call = None |
| self.cache_registry = collections.defaultdict(list) |
| self.processed_invalidations_up_to = 0 |
| |
| def RegisterCache(self, cache, kind): |
| """Register a cache to be notified of future invalidations.""" |
| self.cache_registry[kind].append(cache) |
| |
| def DoDistributedInvalidation(self, cnxn): |
| """Drop any cache entries that were invalidated by other jobs.""" |
| self.last_call = 'DoDistributedInvalidation', cnxn |
| |
| def StoreInvalidateRows(self, cnxn, kind, keys): |
| """Store database rows to let all frontends know to invalidate.""" |
| self.last_call = 'StoreInvalidateRows', cnxn, kind, keys |
| |
| def StoreInvalidateAll(self, cnxn, kind): |
| """Store a database row to let all frontends know to invalidate.""" |
| self.last_call = 'StoreInvalidateAll', cnxn, kind |
| |
| |
| |
| class UserService(object): |
| |
| def __init__(self): |
| """Creates a test-appropriate UserService object.""" |
| self.users_by_email = {} # {email: user_id, ...} |
| self.users_by_id = {} # {user_id: email, ...} |
| self.test_users = {} # {user_id: user_pb, ...} |
| self.visited_hotlists = {} # user_id:[(hotlist_id, viewed), ...] |
| self.invite_rows = [] # (parent_id, child_id) |
| self.linked_account_rows = [] # (parent_id, child_id) |
| self.prefs_dict = {} # {user_id: UserPrefs} |
| |
| def TestAddUser( |
| self, email, user_id, add_user=True, banned=False, obscure_email=True): |
| """Add a user to the fake UserService instance. |
| |
| Args: |
| email: Email of the user. |
| user_id: int user ID. |
| add_user: Flag whether user pb should be created, i.e. whether a |
| Monorail account should be created |
| banned: Boolean to set the user as banned |
| obscure_email: Boolean to determine whether to obscure the user's email. |
| |
| Returns: |
| The User PB that was added, or None. |
| """ |
| self.users_by_email[email] = user_id |
| self.users_by_id[user_id] = email |
| |
| user = None |
| if add_user: |
| user = user_pb2.MakeUser(user_id) |
| user.is_site_admin = False |
| user.email = email |
| user.obscure_email = obscure_email |
| if banned: |
| user.banned = 'is banned' |
| self.test_users[user_id] = user |
| |
| return user |
| |
| def GetUser(self, cnxn, user_id): |
| return self.GetUsersByIDs(cnxn, [user_id])[user_id] |
| |
| def _CreateUser(self, _cnxn, email): |
| if email in self.users_by_email: |
| return |
| user_id = framework_helpers.MurmurHash3_x86_32(email) |
| self.TestAddUser(email, user_id) |
| |
| def _CreateUsers(self, cnxn, emails): |
| for email in emails: |
| self._CreateUser(cnxn, email) |
| |
| def LookupUserID(self, cnxn, email, autocreate=False, allowgroups=False): |
| email_dict = self.LookupUserIDs( |
| cnxn, [email], autocreate=autocreate, allowgroups=allowgroups) |
| if email in email_dict: |
| return email_dict[email] |
| raise exceptions.NoSuchUserException('%r not found' % email) |
| |
| def GetUsersByIDs(self, cnxn, user_ids, use_cache=True, skip_missed=False): |
| user_dict = {} |
| for user_id in user_ids: |
| if user_id and self.test_users.get(user_id): |
| user_dict[user_id] = self.test_users[user_id] |
| elif not skip_missed: |
| user_dict[user_id] = user_pb2.MakeUser(user_id) |
| return user_dict |
| |
| def LookupExistingUserIDs(self, cnxn, emails): |
| email_dict = { |
| email: self.users_by_email[email] |
| for email in emails |
| if email in self.users_by_email} |
| return email_dict |
| |
| def LookupUserIDs(self, cnxn, emails, autocreate=False, |
| allowgroups=False): |
| email_dict = {} |
| needed_emails = [email.lower() for email in emails |
| if email |
| and not framework_constants.NO_VALUE_RE.match(email)] |
| for email in needed_emails: |
| user_id = self.users_by_email.get(email) |
| if not user_id: |
| if autocreate and validate.IsValidEmail(email): |
| self._CreateUser(cnxn, email) |
| user_id = self.users_by_email.get(email) |
| elif not autocreate: |
| raise exceptions.NoSuchUserException('%r' % email) |
| if user_id: |
| email_dict[email] = user_id |
| return email_dict |
| |
| def LookupUserEmail(self, _cnxn, user_id): |
| email = self.users_by_id.get(user_id) |
| if not email: |
| raise exceptions.NoSuchUserException('No user has ID %r' % user_id) |
| return email |
| |
| def LookupUserEmails(self, cnxn, user_ids, ignore_missed=False): |
| if ignore_missed: |
| user_dict = {} |
| for user_id in user_ids: |
| try: |
| user_dict[user_id] = self.LookupUserEmail(cnxn, user_id) |
| except exceptions.NoSuchUserException: |
| continue |
| return user_dict |
| user_dict = { |
| user_id: self.LookupUserEmail(cnxn, user_id) |
| for user_id in user_ids} |
| return user_dict |
| |
| def UpdateUser(self, _cnxn, user_id, user): |
| """Updates the user pb.""" |
| self.test_users[user_id] = user |
| |
| def UpdateUserBan(self, _cnxn, user_id, user, is_banned=None, |
| banned_reason=None): |
| """Updates the user pb.""" |
| self.test_users[user_id] = user |
| user.banned = banned_reason if is_banned else '' |
| |
| def GetPendingLinkedInvites(self, cnxn, user_id): |
| invite_as_parent = [row[1] for row in self.invite_rows |
| if row[0] == user_id] |
| invite_as_child = [row[0] for row in self.invite_rows |
| if row[1] == user_id] |
| return invite_as_parent, invite_as_child |
| |
| def InviteLinkedParent(self, cnxn, parent_id, child_id): |
| self.invite_rows.append((parent_id, child_id)) |
| |
| def AcceptLinkedChild(self, cnxn, parent_id, child_id): |
| if (parent_id, child_id) not in self.invite_rows: |
| raise exceptions.InputException('No such invite') |
| self.linked_account_rows.append((parent_id, child_id)) |
| self.invite_rows = [ |
| (p_id, c_id) for (p_id, c_id) in self.invite_rows |
| if p_id != parent_id and c_id != child_id] |
| self.GetUser(cnxn, parent_id).linked_child_ids.append(child_id) |
| self.GetUser(cnxn, child_id).linked_parent_id = parent_id |
| |
| def UnlinkAccounts(self, _cnxn, parent_id, child_id): |
| """Delete a linked-account relationship.""" |
| if not parent_id: |
| raise exceptions.InputException('Parent account is missing') |
| if not child_id: |
| raise exceptions.InputException('Child account is missing') |
| self.linked_account_rows = [(p, c) for (p, c) in self.linked_account_rows |
| if (p, c) != (parent_id, child_id)] |
| |
| def UpdateUserSettings( |
| self, cnxn, user_id, user, notify=None, notify_starred=None, |
| email_compact_subject=None, email_view_widget=None, |
| notify_starred_ping=None, obscure_email=None, after_issue_update=None, |
| is_site_admin=None, is_banned=None, banned_reason=None, |
| keep_people_perms_open=None, preview_on_hover=None, |
| vacation_message=None): |
| # notifications |
| if notify is not None: |
| user.notify_issue_change = notify |
| if notify_starred is not None: |
| user.notify_starred_issue_change = notify_starred |
| if notify_starred_ping is not None: |
| user.notify_starred_ping = notify_starred_ping |
| if email_compact_subject is not None: |
| user.email_compact_subject = email_compact_subject |
| if email_view_widget is not None: |
| user.email_view_widget = email_view_widget |
| |
| # display options |
| if after_issue_update is not None: |
| user.after_issue_update = user_pb2.IssueUpdateNav(after_issue_update) |
| if preview_on_hover is not None: |
| user.preview_on_hover = preview_on_hover |
| if keep_people_perms_open is not None: |
| user.keep_people_perms_open = keep_people_perms_open |
| |
| # misc |
| if obscure_email is not None: |
| user.obscure_email = obscure_email |
| |
| # admin |
| if is_site_admin is not None: |
| user.is_site_admin = is_site_admin |
| if is_banned is not None: |
| if is_banned: |
| user.banned = banned_reason or 'No reason given' |
| else: |
| user.reset('banned') |
| |
| # user availability |
| if vacation_message is not None: |
| user.vacation_message = vacation_message |
| |
| return self.UpdateUser(cnxn, user_id, user) |
| |
| def GetUsersPrefs(self, cnxn, user_ids, use_cache=True): |
| for user_id in user_ids: |
| if user_id not in self.prefs_dict: |
| self.prefs_dict[user_id] = user_pb2.UserPrefs(user_id=user_id) |
| return self.prefs_dict |
| |
| def GetUserPrefs(self, cnxn, user_id, use_cache=True): |
| """Return a UserPrefs PB for the requested user ID.""" |
| prefs_dict = self.GetUsersPrefs(cnxn, [user_id], use_cache=use_cache) |
| return prefs_dict[user_id] |
| |
| def GetUserPrefsByEmail(self, cnxn, email, use_cache=True): |
| """Return a UserPrefs PB for the requested email, or an empty UserPrefs.""" |
| try: |
| user_id = self.LookupUserID(cnxn, email) |
| user_prefs = self.GetUserPrefs(cnxn, user_id, use_cache=use_cache) |
| except exceptions.NoSuchUserException: |
| user_prefs = user_pb2.UserPrefs() |
| return user_prefs |
| |
| def SetUserPrefs(self, cnxn, user_id, pref_values): |
| userprefs = self.GetUserPrefs(cnxn, user_id) |
| names_to_overwrite = {upv.name for upv in pref_values} |
| userprefs.prefs = [upv for upv in userprefs.prefs |
| if upv.name not in names_to_overwrite] |
| userprefs.prefs.extend(pref_values) |
| |
| def ExpungeUsers(self, cnxn, user_ids): |
| for user_id in user_ids: |
| self.test_users.pop(user_id, None) |
| self.prefs_dict.pop(user_id, None) |
| email = self.users_by_id.pop(user_id, None) |
| if email: |
| self.users_by_email.pop(email, None) |
| |
| self.invite_rows = [row for row in self.invite_rows |
| if row[0] not in user_ids and row[1] not in user_ids] |
| self.linked_account_rows = [ |
| row for row in self.linked_account_rows |
| if row[0] not in user_ids and row[1] not in user_ids] |
| |
| def TotalUsersCount(self, cnxn): |
| return len(self.users_by_id) - 1 if ( |
| framework_constants.DELETED_USER_ID in self.users_by_id |
| ) else len(self.users_by_id) |
| |
| def GetAllUserEmailsBatch(self, cnxn, limit=1000, offset=0): |
| sorted_user_ids = sorted(self.users_by_id.keys()) |
| sorted_user_ids = [ |
| user_id for user_id in sorted_user_ids |
| if user_id != framework_constants.DELETED_USER_ID] |
| emails = [] |
| for i in range(offset, offset + limit): |
| try: |
| user_id = sorted_user_ids[i] |
| if user_id != framework_constants.DELETED_USER_ID: |
| emails.append(self.users_by_id[user_id]) |
| except IndexError: |
| break |
| return emails |
| |
| def GetRecentlyVisitedHotlists(self, _cnxn, user_id): |
| try: |
| return self.visited_hotlists[user_id] |
| except KeyError: |
| return [] |
| |
| def AddVisitedHotlist(self, _cnxn, user_id, hotlist_id, commit=True): |
| try: |
| user_visited_tuples = self.visited_hotlists[user_id] |
| self.visited_hotlists[user_id] = [ |
| hid for hid in user_visited_tuples if hid != hotlist_id] |
| except KeyError: |
| self.visited_hotlists[user_id] = [] |
| self.visited_hotlists[user_id].append(hotlist_id) |
| |
| def ExpungeUsersHotlistsHistory(self, cnxn, user_ids, commit=True): |
| for user_id in user_ids: |
| self.visited_hotlists.pop(user_id, None) |
| |
| |
| class AbstractStarService(object): |
| """Fake StarService.""" |
| |
| def __init__(self): |
| self.stars_by_item_id = {} |
| self.stars_by_starrer_id = {} |
| self.expunged_item_ids = [] |
| |
| def ExpungeStars(self, _cnxn, item_id, commit=True, limit=None): |
| self.expunged_item_ids.append(item_id) |
| old_starrers = self.stars_by_item_id.get(item_id, []) |
| self.stars_by_item_id[item_id] = [] |
| for old_starrer in old_starrers: |
| if self.stars_by_starrer_id.get(old_starrer): |
| self.stars_by_starrer_id[old_starrer] = [ |
| it for it in self.stars_by_starrer_id[old_starrer] |
| if it != item_id] |
| |
| def ExpungeStarsByUsers(self, _cnxn, user_ids, limit=None): |
| for user_id in user_ids: |
| item_ids = self.stars_by_starrer_id.pop(user_id, []) |
| for item_id in item_ids: |
| starrers = self.stars_by_item_id.get(item_id, None) |
| if starrers: |
| self.stars_by_item_id[item_id] = [ |
| starrer for starrer in starrers if starrer != user_id] |
| |
| def LookupItemStarrers(self, _cnxn, item_id): |
| return self.stars_by_item_id.get(item_id, []) |
| |
| def LookupItemsStarrers(self, cnxn, item_ids): |
| return { |
| item_id: self.LookupItemStarrers(cnxn, item_id) for item_id in item_ids} |
| |
| def LookupStarredItemIDs(self, _cnxn, starrer_user_id): |
| return self.stars_by_starrer_id.get(starrer_user_id, []) |
| |
| def IsItemStarredBy(self, cnxn, item_id, starrer_user_id): |
| return item_id in self.LookupStarredItemIDs(cnxn, starrer_user_id) |
| |
| def CountItemStars(self, cnxn, item_id): |
| return len(self.LookupItemStarrers(cnxn, item_id)) |
| |
| def CountItemsStars(self, cnxn, item_ids): |
| return {item_id: self.CountItemStars(cnxn, item_id) |
| for item_id in item_ids} |
| |
| def _SetStar(self, cnxn, item_id, starrer_user_id, starred): |
| if starred and not self.IsItemStarredBy(cnxn, item_id, starrer_user_id): |
| self.stars_by_item_id.setdefault(item_id, []).append(starrer_user_id) |
| self.stars_by_starrer_id.setdefault(starrer_user_id, []).append(item_id) |
| |
| elif not starred and self.IsItemStarredBy(cnxn, item_id, starrer_user_id): |
| self.stars_by_item_id[item_id].remove(starrer_user_id) |
| self.stars_by_starrer_id[starrer_user_id].remove(item_id) |
| |
| def SetStar(self, cnxn, item_id, starrer_user_id, starred): |
| self._SetStar(cnxn, item_id, starrer_user_id, starred) |
| |
| def SetStarsBatch( |
| self, cnxn, item_id, starrer_user_ids, starred, commit=True): |
| for starrer_user_id in starrer_user_ids: |
| self._SetStar(cnxn, item_id, starrer_user_id, starred) |
| |
| |
| class UserStarService(AbstractStarService): |
| pass |
| |
| |
| class ProjectStarService(AbstractStarService): |
| pass |
| |
| |
| class HotlistStarService(AbstractStarService): |
| pass |
| |
| |
| class IssueStarService(AbstractStarService): |
| |
| # pylint: disable=arguments-differ |
| def SetStar( |
| self, cnxn, services, _config, issue_id, starrer_user_id, |
| starred): |
| super(IssueStarService, self).SetStar( |
| cnxn, issue_id, starrer_user_id, starred) |
| try: |
| issue = services.issue.GetIssue(cnxn, issue_id) |
| issue.star_count += (1 if starred else -1) |
| except exceptions.NoSuchIssueException: |
| pass |
| |
| # pylint: disable=arguments-differ |
| def SetStarsBatch( |
| self, cnxn, _service, _config, issue_id, starrer_user_ids, |
| starred): |
| super(IssueStarService, self).SetStarsBatch( |
| cnxn, issue_id, starrer_user_ids, starred) |
| |
| def SetStarsBatch_SkipIssueUpdate( |
| self, cnxn, issue_id, starrer_user_ids, starred, commit=True): |
| super(IssueStarService, self).SetStarsBatch( |
| cnxn, issue_id, starrer_user_ids, starred) |
| |
| |
| class ProjectService(object): |
| """Fake ProjectService object. |
| |
| Provides methods for creating users and projects, which are accessible |
| through parts of the real ProjectService interface. |
| """ |
| |
| def __init__(self): |
| self.test_projects = {} # project_name -> project_pb |
| self.projects_by_id = {} # project_id -> project_pb |
| self.test_star_manager = None |
| self.indexed_projects = {} |
| self.unindexed_projects = set() |
| self.index_counter = 0 |
| self.project_commitments = {} |
| self.ac_exclusion_ids = {} |
| self.no_expand_ids = {} |
| |
| def TestAddProject( |
| self, name, summary='', state=project_pb2.ProjectState.LIVE, |
| owner_ids=None, committer_ids=None, contrib_ids=None, |
| issue_notify_address=None, state_reason='', description=None, |
| project_id=None, process_inbound_email=None, access=None, |
| extra_perms=None): |
| """Add a project to the fake ProjectService object. |
| |
| Args: |
| name: The name of the project. Will replace any existing project under |
| the same name. |
| summary: The summary string of the project. |
| state: Initial state for the project from project_pb2.ProjectState. |
| owner_ids: List of user ids for project owners |
| committer_ids: List of user ids for project committers |
| contrib_ids: List of user ids for project contributors |
| issue_notify_address: email address to send issue change notifications |
| state_reason: string describing the reason the project is in its current |
| state. |
| description: The description string for this project |
| project_id: A unique integer identifier for the created project. |
| process_inbound_email: True to make this project accept inbound email. |
| access: One of the values of enum project_pb2.ProjectAccess. |
| extra_perms: List of ExtraPerms PBs for project members. |
| |
| Returns: |
| A populated project PB. |
| """ |
| proj_pb = project_pb2.Project() |
| proj_pb.project_id = project_id or hash(name) % 100000 |
| proj_pb.project_name = name |
| proj_pb.summary = summary |
| proj_pb.state = state |
| proj_pb.state_reason = state_reason |
| proj_pb.extra_perms = extra_perms or [] |
| if description is not None: |
| proj_pb.description = description |
| |
| self.TestAddProjectMembers(owner_ids, proj_pb, OWNER_ROLE) |
| self.TestAddProjectMembers(committer_ids, proj_pb, COMMITTER_ROLE) |
| self.TestAddProjectMembers(contrib_ids, proj_pb, CONTRIBUTOR_ROLE) |
| |
| if issue_notify_address is not None: |
| proj_pb.issue_notify_address = issue_notify_address |
| if process_inbound_email is not None: |
| proj_pb.process_inbound_email = process_inbound_email |
| if access is not None: |
| proj_pb.access = access |
| |
| self.test_projects[name] = proj_pb |
| self.projects_by_id[proj_pb.project_id] = proj_pb |
| return proj_pb |
| |
| def TestAddProjectMembers(self, user_id_list, proj_pb, role): |
| if user_id_list is not None: |
| for user_id in user_id_list: |
| if role == OWNER_ROLE: |
| proj_pb.owner_ids.append(user_id) |
| elif role == COMMITTER_ROLE: |
| proj_pb.committer_ids.append(user_id) |
| elif role == CONTRIBUTOR_ROLE: |
| proj_pb.contributor_ids.append(user_id) |
| |
| def LookupProjectIDs(self, cnxn, project_names): |
| return { |
| project_name: self.test_projects[project_name].project_id |
| for project_name in project_names |
| if project_name in self.test_projects} |
| |
| def LookupProjectNames(self, cnxn, project_ids): |
| projects_dict = self.GetProjects(cnxn, project_ids) |
| return {p.project_id: p.project_name |
| for p in projects_dict.values()} |
| |
| def CreateProject( |
| self, _cnxn, project_name, owner_ids, committer_ids, |
| contributor_ids, summary, description, |
| state=project_pb2.ProjectState.LIVE, access=None, |
| read_only_reason=None, |
| home_page=None, docs_url=None, source_url=None, |
| logo_gcs_id=None, logo_file_name=None): |
| """Create and store a Project with the given attributes.""" |
| if project_name in self.test_projects: |
| raise exceptions.ProjectAlreadyExists() |
| project = self.TestAddProject( |
| project_name, summary=summary, state=state, |
| owner_ids=owner_ids, committer_ids=committer_ids, |
| contrib_ids=contributor_ids, description=description, |
| access=access) |
| return project.project_id |
| |
| def ExpungeProject(self, _cnxn, project_id): |
| project = self.projects_by_id.get(project_id) |
| if project: |
| self.test_projects.pop(project.project_name, None) |
| |
| def GetProjectsByName(self, _cnxn, project_name_list, use_cache=True): |
| return { |
| pn: self.test_projects[pn] for pn in project_name_list |
| if pn in self.test_projects} |
| |
| def GetProjectByName(self, _cnxn, name, use_cache=True): |
| return self.test_projects.get(name) |
| |
| def GetProjectList(self, cnxn, project_id_list, use_cache=True): |
| project_dict = self.GetProjects(cnxn, project_id_list, use_cache=use_cache) |
| return [project_dict[pid] for pid in project_id_list |
| if pid in project_dict] |
| |
| def GetVisibleProjects( |
| self, _cnxn, logged_in_user, effective_ids, domain=None, use_cache=True): |
| project_ids = sorted(self.projects_by_id.keys()) |
| visible_project_ids = [] |
| for pid in project_ids: |
| can_view = permissions.UserCanViewProject( |
| logged_in_user, effective_ids, self.projects_by_id[pid]) |
| different_domain = framework_helpers.GetNeededDomain( |
| self.projects_by_id[pid].project_name, domain) |
| if can_view and not different_domain: |
| visible_project_ids.append(pid) |
| |
| return visible_project_ids |
| |
| def GetProjects(self, _cnxn, project_ids, use_cache=True): |
| result = {} |
| for project_id in project_ids: |
| project = self.projects_by_id.get(project_id) |
| if project: |
| result[project_id] = project |
| else: |
| raise exceptions.NoSuchProjectException(project_id) |
| return result |
| |
| def GetAllProjects(self, _cnxn, use_cache=True): |
| result = {} |
| for project_id in self.projects_by_id: |
| project = self.projects_by_id.get(project_id) |
| result[project_id] = project |
| return result |
| |
| |
| def GetProject(self, cnxn, project_id, use_cache=True): |
| """Load the specified project from the database.""" |
| project_id_dict = self.GetProjects(cnxn, [project_id], use_cache=use_cache) |
| if project_id not in project_id_dict: |
| raise exceptions.NoSuchProjectException() |
| return project_id_dict[project_id] |
| |
| def GetProjectCommitments(self, _cnxn, project_id): |
| if project_id in self.project_commitments: |
| return self.project_commitments[project_id] |
| |
| project_commitments = project_pb2.ProjectCommitments() |
| project_commitments.project_id = project_id |
| return project_commitments |
| |
| def TestStoreProjectCommitments(self, project_commitments): |
| key = project_commitments.project_id |
| self.project_commitments[key] = project_commitments |
| |
| def GetProjectAutocompleteExclusion(self, cnxn, project_id): |
| return (self.ac_exclusion_ids.get(project_id, []), |
| self.no_expand_ids.get(project_id, [])) |
| |
| def UpdateProject( |
| self, |
| _cnxn, |
| project_id, |
| summary=None, |
| description=None, |
| state=None, |
| state_reason=None, |
| access=None, |
| issue_notify_address=None, |
| attachment_bytes_used=None, |
| attachment_quota=None, |
| moved_to=None, |
| process_inbound_email=None, |
| only_owners_remove_restrictions=None, |
| read_only_reason=None, |
| cached_content_timestamp=None, |
| only_owners_see_contributors=None, |
| delete_time=None, |
| recent_activity=None, |
| revision_url_format=None, |
| home_page=None, |
| docs_url=None, |
| source_url=None, |
| logo_gcs_id=None, |
| logo_file_name=None, |
| issue_notify_always_detailed=None, |
| commit=True): |
| project = self.projects_by_id.get(project_id) |
| if not project: |
| raise exceptions.NoSuchProjectException( |
| 'Project "%s" not found!' % project_id) |
| |
| # TODO(jrobbins): implement all passed arguments - probably as a utility |
| # method shared with the real persistence implementation. |
| if read_only_reason is not None: |
| project.read_only_reason = read_only_reason |
| if attachment_bytes_used is not None: |
| project.attachment_bytes_used = attachment_bytes_used |
| |
| def UpdateProjectRoles( |
| self, _cnxn, project_id, owner_ids, committer_ids, |
| contributor_ids, now=None): |
| project = self.projects_by_id.get(project_id) |
| if not project: |
| raise exceptions.NoSuchProjectException( |
| 'Project "%s" not found!' % project_id) |
| |
| project.owner_ids = owner_ids |
| project.committer_ids = committer_ids |
| project.contributor_ids = contributor_ids |
| |
| def MarkProjectDeletable( |
| self, _cnxn, project_id, _config_service): |
| project = self.projects_by_id[project_id] |
| project.project_name = 'DELETABLE_%d' % project_id |
| project.state = project_pb2.ProjectState.DELETABLE |
| |
| def UpdateRecentActivity(self, _cnxn, _project_id, now=None): |
| pass |
| |
| def GetUserRolesInAllProjects(self, _cnxn, effective_ids): |
| owned_project_ids = set() |
| membered_project_ids = set() |
| contrib_project_ids = set() |
| |
| for project in self.projects_by_id.values(): |
| if not effective_ids.isdisjoint(project.owner_ids): |
| owned_project_ids.add(project.project_id) |
| elif not effective_ids.isdisjoint(project.committer_ids): |
| membered_project_ids.add(project.project_id) |
| elif not effective_ids.isdisjoint(project.contributor_ids): |
| contrib_project_ids.add(project.project_id) |
| |
| return owned_project_ids, membered_project_ids, contrib_project_ids |
| |
| def GetProjectMemberships(self, _cnxn, effective_ids, use_cache=True): |
| # type: MonorailConnection, Collection[int], bool -> |
| # Mapping[int, Collection[int]] |
| projects_by_user_id = collections.defaultdict(set) |
| |
| for project in self.projects_by_id.values(): |
| member_ids = set( |
| itertools.chain( |
| project.owner_ids, project.committer_ids, |
| project.contributor_ids)) |
| for user_id in effective_ids: |
| if user_id in member_ids: |
| projects_by_user_id[user_id].add(project.project_id) |
| return projects_by_user_id |
| |
| def ExpungeUsersInProjects(self, cnxn, user_ids, limit=None): |
| for project in self.projects_by_id.values(): |
| project.owner_ids = [owner_id for owner_id in project.owner_ids |
| if owner_id not in user_ids] |
| project.committer_ids = [com_id for com_id in project.committer_ids |
| if com_id not in user_ids] |
| project.contributor_ids = [con_id for con_id in project.contributor_ids |
| if con_id not in user_ids] |
| |
| |
| class ConfigService(object): |
| """Fake version of ConfigService that just works in-RAM.""" |
| |
| def __init__(self, user_id=None): |
| self.project_configs = {} |
| self.next_field_id = 123 |
| self.next_component_id = 345 |
| self.next_template_id = 23 |
| self.expunged_configs = [] |
| self.expunged_users_in_configs = [] |
| self.component_ids_to_templates = {} |
| self.label_to_id = {} |
| self.id_to_label = {} |
| self.strict = False # Set true to raise more exceptions like real class. |
| |
| def TestAddLabelsDict(self, label_to_id): |
| self.label_to_id = label_to_id |
| self.id_to_label = { |
| label_id: label |
| for label, label_id in list(self.label_to_id.items())} |
| |
| def TestAddFieldDef(self, fd): |
| self.project_configs[fd.project_id].field_defs.append(fd) |
| |
| def TestAddApprovalDef(self, ad, project_id): |
| self.project_configs[project_id].approval_defs.append(ad) |
| |
| def ExpungeConfig(self, _cnxn, project_id): |
| self.expunged_configs.append(project_id) |
| |
| def ExpungeUsersInConfigs(self, _cnxn, user_ids, limit=None): |
| self.expunged_users_in_configs.extend(user_ids) |
| |
| def GetLabelDefRows(self, cnxn, project_id, use_cache=True): |
| """This always returns empty results. Mock it to test other cases.""" |
| return [] |
| |
| def GetLabelDefRowsAnyProject(self, cnxn, where=None): |
| """This always returns empty results. Mock it to test other cases.""" |
| return [] |
| |
| def LookupLabel(self, cnxn, project_id, label_id): |
| if label_id in self.id_to_label: |
| return self.id_to_label[label_id] |
| if label_id == 999: |
| return None |
| return 'label_%d_%d' % (project_id, label_id) |
| |
| def LookupLabelID( |
| self, cnxn, project_id, label, autocreate=True, case_sensitive=False): |
| if label in self.label_to_id: |
| return self.label_to_id[label] |
| # TODO: The condition here is specifically added to return 'None' and |
| # allow testing for label freezing. This can be removed after refactoring |
| # other dependent tests to not fail for returning 'None' instead of '1' |
| # when label is not found in 'label_to_id' dict. |
| if label == 'freeze_new_label': |
| return None |
| return 1 |
| |
| def LookupLabelIDs(self, cnxn, project_id, labels, autocreate=False): |
| ids = [] |
| next_label_id = 0 |
| if self.id_to_label.keys(): |
| existing_ids = sorted(self.id_to_label.keys()) |
| next_label_id = existing_ids[-1] + 1 |
| for label in labels: |
| if self.label_to_id.get(label) is not None: |
| ids.append(self.label_to_id[label]) |
| elif autocreate: |
| self.label_to_id[label] = next_label_id |
| self.id_to_label[next_label_id] = label |
| ids.append(next_label_id) |
| next_label_id += 1 |
| return ids |
| |
| def LookupIDsOfLabelsMatching(self, cnxn, project_id, regex): |
| return [1, 2, 3] |
| |
| def LookupStatus(self, cnxn, project_id, status_id): |
| return 'status_%d_%d' % (project_id, status_id) |
| |
| def LookupStatusID(self, cnxn, project_id, status, autocreate=True): |
| if status: |
| return 1 |
| else: |
| return 0 |
| |
| def LookupStatusIDs(self, cnxn, project_id, statuses): |
| return [idx for idx, _status in enumerate(statuses)] |
| |
| def LookupClosedStatusIDs(self, cnxn, project_id): |
| return [7, 8, 9] |
| |
| def StoreConfig(self, _cnxn, config): |
| self.project_configs[config.project_id] = config |
| |
| def GetProjectConfig(self, _cnxn, project_id, use_cache=True): |
| if project_id in self.project_configs: |
| return self.project_configs[project_id] |
| elif self.strict: |
| raise exceptions.NoSuchProjectException() |
| else: |
| return tracker_bizobj.MakeDefaultProjectIssueConfig(project_id) |
| |
| def GetProjectConfigs(self, _cnxn, project_ids, use_cache=True): |
| config_dict = {} |
| for project_id in project_ids: |
| if project_id in self.project_configs: |
| config_dict[project_id] = self.project_configs[project_id] |
| elif not self.strict: |
| config_dict[project_id] = tracker_bizobj.MakeDefaultProjectIssueConfig( |
| project_id) |
| return config_dict |
| |
| def UpdateConfig( |
| self, cnxn, project, well_known_statuses=None, |
| statuses_offer_merge=None, well_known_labels=None, |
| excl_label_prefixes=None, default_template_for_developers=None, |
| default_template_for_users=None, list_prefs=None, restrict_to_known=None, |
| approval_defs=None): |
| project_id = project.project_id |
| project_config = self.GetProjectConfig(cnxn, project_id, use_cache=False) |
| |
| if well_known_statuses is not None: |
| tracker_bizobj.SetConfigStatuses(project_config, well_known_statuses) |
| |
| if statuses_offer_merge is not None: |
| project_config.statuses_offer_merge = statuses_offer_merge |
| |
| if well_known_labels is not None: |
| tracker_bizobj.SetConfigLabels(project_config, well_known_labels) |
| |
| if excl_label_prefixes is not None: |
| project_config.exclusive_label_prefixes = excl_label_prefixes |
| |
| if approval_defs is not None: |
| tracker_bizobj.SetConfigApprovals(project_config, approval_defs) |
| |
| if default_template_for_developers is not None: |
| project_config.default_template_for_developers = ( |
| default_template_for_developers) |
| if default_template_for_users is not None: |
| project_config.default_template_for_users = default_template_for_users |
| |
| if list_prefs: |
| default_col_spec, default_sort_spec, x_attr, y_attr, m_d_q = list_prefs |
| project_config.default_col_spec = default_col_spec |
| project_config.default_sort_spec = default_sort_spec |
| project_config.default_x_attr = x_attr |
| project_config.default_y_attr = y_attr |
| project_config.member_default_query = m_d_q |
| |
| if restrict_to_known is not None: |
| project_config.restrict_to_known = restrict_to_known |
| |
| self.StoreConfig(cnxn, project_config) |
| return project_config |
| |
| def CreateFieldDef( |
| self, |
| cnxn, |
| project_id, |
| field_name, |
| field_type_str, |
| applic_type, |
| applic_pred, |
| is_required, |
| is_niche, |
| is_multivalued, |
| min_value, |
| max_value, |
| regex, |
| needs_member, |
| needs_perm, |
| grants_perm, |
| notify_on, |
| date_action_str, |
| docstring, |
| admin_ids, |
| editor_ids, |
| approval_id=None, |
| is_phase_field=False, |
| is_restricted_field=False): |
| config = self.GetProjectConfig(cnxn, project_id) |
| field_type = tracker_pb2.FieldTypes(field_type_str) |
| field_id = self.next_field_id |
| self.next_field_id += 1 |
| fd = tracker_bizobj.MakeFieldDef( |
| field_id, project_id, field_name, field_type, applic_type, applic_pred, |
| is_required, is_niche, is_multivalued, min_value, max_value, regex, |
| needs_member, needs_perm, grants_perm, notify_on, date_action_str, |
| docstring, False, approval_id, is_phase_field, is_restricted_field, |
| admin_ids=admin_ids, editor_ids=editor_ids) |
| config.field_defs.append(fd) |
| self.StoreConfig(cnxn, config) |
| return field_id |
| |
| def LookupFieldID(self, cnxn, project_id, field): |
| config = self.GetProjectConfig(cnxn, project_id) |
| for fd in config.field_defs: |
| if fd.field_name == field: |
| return fd.field_id |
| |
| return None |
| |
| def SoftDeleteFieldDefs(self, cnxn, project_id, field_ids): |
| config = self.GetProjectConfig(cnxn, project_id) |
| for fd in config.field_defs: |
| if fd.field_id in field_ids: |
| fd.is_deleted = True |
| self.StoreConfig(cnxn, config) |
| |
| def UpdateFieldDef( |
| self, |
| cnxn, |
| project_id, |
| field_id, |
| field_name=None, |
| applicable_type=None, |
| applicable_predicate=None, |
| is_required=None, |
| is_niche=None, |
| is_multivalued=None, |
| min_value=None, |
| max_value=None, |
| regex=None, |
| needs_member=None, |
| needs_perm=None, |
| grants_perm=None, |
| notify_on=None, |
| date_action=None, |
| docstring=None, |
| admin_ids=None, |
| editor_ids=None, |
| is_restricted_field=None): |
| config = self.GetProjectConfig(cnxn, project_id) |
| fd = tracker_bizobj.FindFieldDefByID(field_id, config) |
| # pylint: disable=multiple-statements |
| if field_name is not None: fd.field_name = field_name |
| if applicable_type is not None: fd.applicable_type = applicable_type |
| if applicable_predicate is not None: |
| fd.applicable_predicate = applicable_predicate |
| if is_required is not None: fd.is_required = is_required |
| if is_niche is not None: fd.is_niche = is_niche |
| if is_multivalued is not None: fd.is_multivalued = is_multivalued |
| if min_value is not None: fd.min_value = min_value |
| if max_value is not None: fd.max_value = max_value |
| if regex is not None: fd.regex = regex |
| if date_action is not None: |
| fd.date_action = config_svc.DATE_ACTION_ENUM.index(date_action) |
| if docstring is not None: fd.docstring = docstring |
| if admin_ids is not None: fd.admin_ids = admin_ids |
| if editor_ids is not None: |
| fd.editor_ids = editor_ids |
| if is_restricted_field is not None: |
| fd.is_restricted_field = is_restricted_field |
| self.StoreConfig(cnxn, config) |
| |
| def CreateComponentDef( |
| self, cnxn, project_id, path, docstring, deprecated, admin_ids, cc_ids, |
| created, creator_id, label_ids): |
| config = self.GetProjectConfig(cnxn, project_id) |
| cd = tracker_bizobj.MakeComponentDef( |
| self.next_component_id, project_id, path, docstring, deprecated, |
| admin_ids, cc_ids, created, creator_id, label_ids=label_ids) |
| config.component_defs.append(cd) |
| self.next_component_id += 1 |
| self.StoreConfig(cnxn, config) |
| return self.next_component_id - 1 |
| |
| def UpdateComponentDef( |
| self, cnxn, project_id, component_id, path=None, docstring=None, |
| deprecated=None, admin_ids=None, cc_ids=None, created=None, |
| creator_id=None, modified=None, modifier_id=None, label_ids=None): |
| config = self.GetProjectConfig(cnxn, project_id) |
| cd = tracker_bizobj.FindComponentDefByID(component_id, config) |
| if path is not None: |
| assert path |
| cd.path = path |
| # pylint: disable=multiple-statements |
| if docstring is not None: cd.docstring = docstring |
| if deprecated is not None: cd.deprecated = deprecated |
| if admin_ids is not None: cd.admin_ids = admin_ids |
| if cc_ids is not None: cd.cc_ids = cc_ids |
| if created is not None: cd.created = created |
| if creator_id is not None: cd.creator_id = creator_id |
| if modified is not None: cd.modified = modified |
| if modifier_id is not None: cd.modifier_id = modifier_id |
| if label_ids is not None: cd.label_ids = label_ids |
| self.StoreConfig(cnxn, config) |
| |
| def DeleteComponentDef(self, cnxn, project_id, component_id): |
| """Delete the specified component definition.""" |
| config = self.GetProjectConfig(cnxn, project_id) |
| config.component_defs = [ |
| cd for cd in config.component_defs |
| if cd.component_id != component_id] |
| self.StoreConfig(cnxn, config) |
| |
| def InvalidateMemcache(self, issues, key_prefix=''): |
| pass |
| |
| def InvalidateMemcacheForEntireProject(self, project_id): |
| pass |
| |
| |
| class IssueService(object): |
| """Fake version of IssueService that just works in-RAM.""" |
| # pylint: disable=unused-argument |
| |
| def __init__(self, user_id=None): |
| self.user_id = user_id |
| # Dictionary {project_id: issue_pb_dict} |
| # where issue_pb_dict is a dictionary of the form |
| # {local_id: issue_pb} |
| self.issues_by_project = {} |
| self.issues_by_iid = {} |
| # Dictionary {project_id: comment_pb_dict} |
| # where comment_pb_dict is a dictionary of the form |
| # {local_id: comment_pb_list} |
| self.comments_by_project = {} |
| self.comments_by_iid = {} |
| self.comments_by_cid = {} |
| self.attachments_by_id = {} |
| |
| # Set of issue IDs for issues that have been indexed by calling |
| # IndexIssues(). |
| self.indexed_issue_iids = set() |
| |
| # Set of issue IDs for issues that have been moved by calling MoveIssue(). |
| self.moved_back_iids = set() |
| |
| # Dict of issue IDs mapped to other issue IDs to represent moved issues. |
| self.moved_issues = {} |
| |
| # Test-only indication that the indexer would have been called |
| # by the real DITPersist. |
| self.indexer_called = False |
| |
| # Test-only sequence of updated and enqueued. |
| self.updated_issues = [] |
| self.enqueued_issues = [] # issue_ids |
| |
| # Test-only sequence of expunged issues and projects. |
| self.expunged_issues = [] |
| self.expunged_former_locations = [] |
| self.expunged_local_ids = [] |
| self.expunged_users_in_issues = [] |
| |
| # Test-only indicators that methods were called. |
| self.get_all_issues_in_project_called = False |
| self.update_issues_called = False |
| self.enqueue_issues_called = False |
| self.get_issue_acitivity_called = False |
| |
| # The next id to return if it is > 0. |
| self.next_id = -1 |
| |
| def UpdateIssue( |
| self, |
| cnxn, |
| issue, |
| update_cols=None, |
| just_derived=False, |
| commit=True, |
| invalidate=True): |
| self.UpdateIssues( |
| cnxn, [issue], update_cols, just_derived, commit, invalidate) |
| |
| def UpdateIssues( |
| self, cnxn, issues, update_cols=None, just_derived=False, |
| commit=True, invalidate=True): |
| self.update_issues_called = True |
| assert all(issue.assume_stale == False for issue in issues) |
| self.updated_issues.extend(issues) |
| |
| def GetIssueActivity( |
| self, cnxn, num=50, before=None, after=None, |
| project_ids=None, user_ids=None, ascending=False): |
| self.get_issue_acitivity_called = True |
| comments_dict = self.comments_by_cid |
| comments = [] |
| for value in comments_dict.values(): |
| if project_ids is not None: |
| if value.issue_id > 0 and value.issue_id in self.issues_by_iid: |
| issue = self.issues_by_iid[value.issue_id] |
| if issue.project_id in project_ids: |
| comments.append(value) |
| elif user_ids is not None: |
| if value.user_id in user_ids: |
| comments.append(value) |
| else: |
| comments.append(value) |
| return comments |
| |
| def EnqueueIssuesForIndexing(self, _cnxn, issue_ids, commit=True): |
| self.enqueue_issues_called = True |
| for i in issue_ids: |
| if i not in self.enqueued_issues: |
| self.enqueued_issues.extend(issues) |
| |
| def ExpungeIssues(self, _cnxn, issue_ids): |
| self.expunged_issues.extend(issue_ids) |
| |
| def ExpungeFormerLocations(self, _cnxn, project_id): |
| self.expunged_former_locations.append(project_id) |
| |
| def ExpungeLocalIDCounters(self, _cnxn, project_id): |
| self.expunged_local_ids.append(project_id) |
| |
| def TestAddIssue(self, issue, importer_id=None): |
| project_id = issue.project_id |
| self.issues_by_project.setdefault(project_id, {}) |
| self.issues_by_project[project_id][issue.local_id] = issue |
| self.issues_by_iid[issue.issue_id] = issue |
| if issue.issue_id not in self.enqueued_issues: |
| self.enqueued_issues.append(issue.issue_id) |
| self.enqueue_issues_called = True |
| |
| # Adding a new issue should add the first comment to the issue |
| comment = tracker_pb2.IssueComment() |
| comment.project_id = issue.project_id |
| comment.issue_id = issue.issue_id |
| comment.content = issue.summary |
| comment.timestamp = issue.opened_timestamp |
| comment.is_description = True |
| if issue.reporter_id: |
| comment.user_id = issue.reporter_id |
| if importer_id: |
| comment.importer_id = importer_id |
| comment.sequence = 0 |
| self.TestAddComment(comment, issue.local_id) |
| |
| def TestAddMovedIssueRef(self, source_project_id, source_local_id, |
| target_project_id, target_local_id): |
| self.moved_issues[(source_project_id, source_local_id)] = ( |
| target_project_id, target_local_id) |
| |
| def TestAddComment(self, comment, local_id): |
| pid = comment.project_id |
| if not comment.id: |
| comment.id = len(self.comments_by_cid) |
| |
| self.comments_by_project.setdefault(pid, {}) |
| self.comments_by_project[pid].setdefault(local_id, []).append(comment) |
| self.comments_by_iid.setdefault(comment.issue_id, []).append(comment) |
| self.comments_by_cid[comment.id] = comment |
| |
| def TestAddAttachment(self, attachment, comment_id, issue_id): |
| if not attachment.attachment_id: |
| attachment.attachment_id = len(self.attachments_by_id) |
| |
| aid = attachment.attachment_id |
| self.attachments_by_id[aid] = attachment, comment_id, issue_id |
| comment = self.comments_by_cid[comment_id] |
| if attachment not in comment.attachments: |
| comment.attachments.extend([attachment]) |
| |
| def SoftDeleteAttachment( |
| self, _cnxn, _issue, comment, attach_id, _user_service, delete=True, |
| index_now=False): |
| attachment = None |
| for attach in comment.attachments: |
| if attach.attachment_id == attach_id: |
| attachment = attach |
| if not attachment: |
| return |
| attachment.deleted = delete |
| |
| def GetAttachmentAndContext(self, _cnxn, attachment_id): |
| if attachment_id in self.attachments_by_id: |
| attach, comment_id, issue_id = self.attachments_by_id[attachment_id] |
| if not attach.deleted: |
| return attach, comment_id, issue_id |
| |
| raise exceptions.NoSuchAttachmentException() |
| |
| def GetComments( |
| self, _cnxn, where=None, order_by=None, content_only=False, **kwargs): |
| # This is a very limited subset of what the real GetComments() can do. |
| cid = kwargs.get('id') |
| |
| comment = self.comments_by_cid.get(cid) |
| if comment: |
| return [comment] |
| else: |
| return [] |
| |
| def GetComment(self, cnxn, comment_id): |
| """Get the requested comment, or raise an exception.""" |
| comments = self.GetComments(cnxn, id=comment_id) |
| if len(comments) == 1: |
| return comments[0] |
| |
| raise exceptions.NoSuchCommentException() |
| |
| def ResolveIssueRefs(self, cnxn, ref_projects, default_project_name, refs): |
| result = [] |
| misses = [] |
| for project_name, local_id in refs: |
| project = ref_projects.get(project_name or default_project_name) |
| if not project or project.state == project_pb2.ProjectState.DELETABLE: |
| continue # ignore any refs to issues in deleted projects |
| try: |
| issue = self.GetIssueByLocalID(cnxn, project.project_id, local_id) |
| result.append(issue.issue_id) |
| except exceptions.NoSuchIssueException: |
| misses.append((project.project_id, local_id)) |
| |
| return result, misses |
| |
| def LookupIssueRefs(self, cnxn, issue_ids): |
| issue_dict, _misses = self.GetIssuesDict(cnxn, issue_ids) |
| return { |
| issue_id: (issue.project_name, issue.local_id) |
| for issue_id, issue in issue_dict.items()} |
| |
| def GetAllIssuesInProject( |
| self, _cnxn, project_id, min_local_id=None, use_cache=True): |
| self.get_all_issues_in_project_called = True |
| if project_id in self.issues_by_project: |
| return list(self.issues_by_project[project_id].values()) |
| else: |
| return [] |
| |
| def GetIssuesByLocalIDs( |
| self, _cnxn, project_id, local_id_list, use_cache=True, shard_id=None): |
| results = [] |
| for local_id in local_id_list: |
| if (project_id in self.issues_by_project |
| and local_id in self.issues_by_project[project_id]): |
| results.append(self.issues_by_project[project_id][local_id]) |
| |
| return results |
| |
| def GetIssueByLocalID(self, _cnxn, project_id, local_id, use_cache=True): |
| try: |
| return self.issues_by_project[project_id][local_id] |
| except KeyError: |
| raise exceptions.NoSuchIssueException() |
| |
| def GetAnyOnHandIssue(self, issue_ids, start=None, end=None): |
| return None # Treat them all like misses. |
| |
| def GetIssue(self, cnxn, issue_id, use_cache=True): |
| issues = self.GetIssues(cnxn, [issue_id], use_cache=use_cache) |
| try: |
| return issues[0] |
| except IndexError: |
| raise exceptions.NoSuchIssueException() |
| |
| def GetCurrentLocationOfMovedIssue(self, cnxn, project_id, local_id): |
| key = (project_id, local_id) |
| if key in self.moved_issues: |
| ref = self.moved_issues[key] |
| return ref[0], ref[1] |
| return None, None |
| |
| def GetPreviousLocations(self, cnxn, issue): |
| return [] |
| |
| def GetCommentsByUser(self, cnxn, user_id): |
| """Get all comments created by a user""" |
| comments = [] |
| for cid in self.comments_by_cid: |
| comment = self.comments_by_cid[cid] |
| if comment.user_id == user_id and not comment.is_description: |
| comments.append(comment) |
| return comments |
| |
| def GetCommentsByID(self, cnxn, comment_ids, _sequences, use_cache=True, |
| shard_id=None): |
| """Return all IssueComment PBs by comment ids.""" |
| comments = [self.comments_by_cid[cid] for cid in comment_ids] |
| return comments |
| |
| def GetIssueIDsReportedByUser(self, cnxn, user_id): |
| """Get all issues created by a user""" |
| ids = [] |
| for iid in self.issues_by_iid: |
| issue = self.issues_by_iid[iid] |
| if issue.reporter_id == user_id: |
| ids.append(iid) |
| return ids |
| |
| def LookupIssueIDs(self, _cnxn, project_local_id_pairs): |
| hits = [] |
| misses = [] |
| for (project_id, local_id) in project_local_id_pairs: |
| try: |
| issue = self.issues_by_project[project_id][local_id] |
| hits.append(issue.issue_id) |
| except KeyError: |
| misses.append((project_id, local_id)) |
| |
| return hits, misses |
| |
| def LookupIssueIDsFollowMoves(self, _cnxn, project_local_id_pairs): |
| hits = [] |
| misses = [] |
| for pair in project_local_id_pairs: |
| project_id, local_id = self.moved_issues.get(pair, pair) |
| try: |
| issue = self.issues_by_project[project_id][local_id] |
| hits.append(issue.issue_id) |
| except KeyError: |
| misses.append((project_id, local_id)) |
| |
| return hits, misses |
| |
| def LookupIssueID(self, _cnxn, project_id, local_id): |
| try: |
| issue = self.issues_by_project[project_id][local_id] |
| except KeyError: |
| raise exceptions.NoSuchIssueException() |
| return issue.issue_id |
| |
| def GetCommentsForIssue(self, _cnxn, issue_id): |
| comments = self.comments_by_iid.get(issue_id, []) |
| for idx, c in enumerate(comments): |
| c.sequence = idx |
| |
| return comments |
| |
| def InsertIssue(self, cnxn, issue): |
| issue.issue_id = issue.project_id * 1000000 + issue.local_id |
| self.issues_by_project.setdefault(issue.project_id, {}) |
| self.issues_by_project[issue.project_id][issue.local_id] = issue |
| self.issues_by_iid[issue.issue_id] = issue |
| return issue.issue_id |
| |
| def CreateIssue( |
| self, |
| cnxn, |
| services, |
| issue, |
| marked_description, |
| attachments=None, |
| index_now=False, |
| importer_id=None): |
| project_id = issue.project_id |
| |
| issue.local_id = self.AllocateNextLocalID(cnxn, project_id) |
| issue.issue_id = project_id * 1000000 + issue.local_id |
| |
| self.TestAddIssue(issue, importer_id=importer_id) |
| comment = self.comments_by_iid[issue.issue_id][0] |
| comment.content = marked_description |
| return issue, comment |
| |
| def GetIssueApproval(self, cnxn, issue_id, approval_id, use_cache=True): |
| issue = self.GetIssue(cnxn, issue_id, use_cache=use_cache) |
| approval = tracker_bizobj.FindApprovalValueByID( |
| approval_id, issue.approval_values) |
| if approval: |
| return issue, approval |
| raise exceptions.NoSuchIssueApprovalException() |
| |
| def UpdateIssueApprovalStatus( |
| self, cnxn, issue_id, approval_id, status, setter_id, set_on, |
| commit=True): |
| issue = self.GetIssue(cnxn, issue_id) |
| for av in issue.approval_values: |
| if av.approval_id == approval_id: |
| av.status = status |
| av.setter_id = setter_id |
| av.set_on = set_on |
| return |
| return |
| |
| def UpdateIssueApprovalApprovers( |
| self, cnxn, issue_id, approval_id, approver_ids, commit=True): |
| issue = self.GetIssue(cnxn, issue_id) |
| for av in issue.approval_values: |
| if av.approval_id == approval_id: |
| av.approver_ids = approver_ids |
| return |
| return |
| |
| def UpdateIssueStructure( |
| self, cnxn, config, issue, template, reporter_id, comment_content, |
| commit=True, invalidate=True): |
| approval_defs_by_id = {ad.approval_id: ad for ad in config.approval_defs} |
| issue_avs_by_id = {av.approval_id: av for av in issue.approval_values} |
| |
| new_issue_approvals = [] |
| |
| for template_av in template.approval_values: |
| existing_issue_av = issue_avs_by_id.get(template_av.approval_id) |
| # Keep approval values as-if fi it exists in issue and template |
| if existing_issue_av: |
| existing_issue_av.phase_id = template_av.phase_id |
| new_issue_approvals.append(existing_issue_av) |
| else: |
| new_issue_approvals.append(template_av) |
| |
| # Update all approval surveys so latest ApprovalDef survey changes |
| # appear in the converted issue's approval values. |
| ad = approval_defs_by_id.get(template_av.approval_id) |
| if ad: |
| self.CreateIssueComment( |
| cnxn, issue, reporter_id, ad.survey, |
| is_description=True, approval_id=ad.approval_id, commit=False) |
| else: |
| logging.info('ApprovalDef not found for approval %r', template_av) |
| |
| template_phase_by_name = { |
| phase.name.lower(): phase for phase in template.phases} |
| issue_phase_by_id = {phase.phase_id: phase for phase in issue.phases} |
| updated_fvs = [] |
| # Trim issue FieldValues or update FieldValue phase_ids |
| for fv in issue.field_values: |
| # If a fv's phase has the same name as a template's phase, update |
| # the fv's phase_id to that of the template phase's. Otherwise, |
| # remove the fv. |
| if fv.phase_id: |
| issue_phase = issue_phase_by_id.get(fv.phase_id) |
| if issue_phase and issue_phase.name: |
| template_phase = template_phase_by_name.get(issue_phase.name.lower()) |
| if template_phase: |
| fv.phase_id = template_phase.phase_id |
| updated_fvs.append(fv) |
| # keep all fvs that do not belong to phases. |
| else: |
| updated_fvs.append(fv) |
| |
| fd_names_by_id = {fd.field_id: fd.field_name for fd in config.field_defs} |
| amendment = tracker_bizobj.MakeApprovalStructureAmendment( |
| [fd_names_by_id.get(av.approval_id) for av in new_issue_approvals], |
| [fd_names_by_id.get(av.approval_id) for av in issue.approval_values]) |
| |
| issue.approval_values = new_issue_approvals |
| issue.phases = template.phases |
| issue.field_values = updated_fvs |
| |
| return self.CreateIssueComment( |
| cnxn, issue, reporter_id, comment_content, |
| amendments=[amendment], commit=False) |
| |
| def SetUsedLocalID(self, cnxn, project_id): |
| self.next_id = self.GetHighestLocalID(cnxn, project_id) + 1 |
| |
| def AllocateNextLocalID(self, cnxn, project_id): |
| return self.GetHighestLocalID(cnxn, project_id) + 1 |
| |
| def GetHighestLocalID(self, _cnxn, project_id): |
| if self.next_id > 0: |
| return self.next_id - 1 |
| else: |
| issue_dict = self.issues_by_project.get(project_id, {}) |
| highest = max([0] + [issue.local_id for issue in issue_dict.values()]) |
| return highest |
| |
| def _MakeIssueComment( |
| self, project_id, user_id, content, inbound_message=None, |
| amendments=None, attachments=None, kept_attachments=None, timestamp=None, |
| is_spam=False, is_description=False, approval_id=None, importer_id=None): |
| comment = tracker_pb2.IssueComment() |
| comment.project_id = project_id |
| comment.user_id = user_id |
| comment.content = content or '' |
| comment.is_spam = is_spam |
| comment.is_description = is_description |
| if not timestamp: |
| timestamp = int(time.time()) |
| comment.timestamp = int(timestamp) |
| if inbound_message: |
| comment.inbound_message = inbound_message |
| if amendments: |
| comment.amendments.extend(amendments) |
| if approval_id: |
| comment.approval_id = approval_id |
| if importer_id: |
| comment.importer_id = importer_id |
| return comment |
| |
| def CopyIssues(self, cnxn, dest_project, issues, user_service, copier_id): |
| created_issues = [] |
| for target_issue in issues: |
| new_issue = tracker_pb2.Issue() |
| new_issue.project_id = dest_project.project_id |
| new_issue.project_name = dest_project.project_name |
| new_issue.summary = target_issue.summary |
| new_issue.labels.extend(target_issue.labels) |
| new_issue.field_values.extend(target_issue.field_values) |
| new_issue.reporter_id = copier_id |
| |
| timestamp = int(time.time()) |
| new_issue.opened_timestamp = timestamp |
| new_issue.modified_timestamp = timestamp |
| new_issue.migration_modified_timestamp = timestamp |
| |
| target_comments = self.GetCommentsForIssue(cnxn, target_issue.issue_id) |
| initial_summary_comment = target_comments[0] |
| |
| # Note that blocking and merge_into are not copied. |
| new_issue.blocked_on_iids = target_issue.blocked_on_iids |
| new_issue.blocked_on_ranks = target_issue.blocked_on_ranks |
| |
| # Create the same summary comment as the target issue. |
| comment = self._MakeIssueComment( |
| dest_project.project_id, copier_id, initial_summary_comment.content, |
| is_description=True) |
| |
| new_issue.local_id = self.AllocateNextLocalID( |
| cnxn, dest_project.project_id) |
| issue_id = self.InsertIssue(cnxn, new_issue) |
| comment.issue_id = issue_id |
| self.InsertComment(cnxn, comment) |
| created_issues.append(new_issue) |
| |
| return created_issues |
| |
| def MoveIssues(self, cnxn, dest_project, issues, user_service): |
| move_to = dest_project.project_id |
| self.issues_by_project.setdefault(move_to, {}) |
| moved_back_iids = set() |
| for issue in issues: |
| if issue.issue_id in self.moved_back_iids: |
| moved_back_iids.add(issue.issue_id) |
| self.moved_back_iids.add(issue.issue_id) |
| project_id = issue.project_id |
| self.issues_by_project[project_id].pop(issue.local_id) |
| issue.local_id = self.AllocateNextLocalID(cnxn, move_to) |
| self.issues_by_project[move_to][issue.local_id] = issue |
| issue.project_id = move_to |
| issue.project_name = dest_project.project_name |
| return moved_back_iids |
| |
| def GetCommentsForIssues(self, _cnxn, issue_ids, content_only=False): |
| comments_dict = {} |
| for issue_id in issue_ids: |
| comments_dict[issue_id] = self.comments_by_iid[issue_id] |
| |
| return comments_dict |
| |
| def InsertComment(self, cnxn, comment, commit=True): |
| issue = self.GetIssue(cnxn, comment.issue_id) |
| self.TestAddComment(comment, issue.local_id) |
| |
| # pylint: disable=unused-argument |
| def DeltaUpdateIssue( |
| self, cnxn, services, reporter_id, project_id, |
| config, issue, delta, index_now=False, comment=None, attachments=None, |
| iids_to_invalidate=None, rules=None, predicate_asts=None, |
| is_description=False, timestamp=None, kept_attachments=None, |
| importer_id=None, inbound_message=None): |
| # Return a bogus amendments list if any of the fields changed |
| amendments, _ = tracker_bizobj.ApplyIssueDelta( |
| cnxn, self, issue, delta, config) |
| |
| if not amendments and (not comment or not comment.strip()): |
| return [], None |
| |
| comment_pb = self.CreateIssueComment( |
| cnxn, issue, reporter_id, comment, attachments=attachments, |
| amendments=amendments, is_description=is_description, |
| kept_attachments=kept_attachments, importer_id=importer_id, |
| inbound_message=inbound_message) |
| |
| self.indexer_called = index_now |
| return amendments, comment_pb |
| |
| def InvalidateIIDs(self, cnxn, iids_to_invalidate): |
| pass |
| |
| # pylint: disable=unused-argument |
| def CreateIssueComment( |
| self, _cnxn, issue, user_id, content, |
| inbound_message=None, amendments=None, attachments=None, |
| kept_attachments=None, timestamp=None, is_spam=False, |
| is_description=False, approval_id=None, commit=True, |
| importer_id=None): |
| # Add a comment to an issue |
| comment = tracker_pb2.IssueComment() |
| comment.id = len(self.comments_by_cid) |
| comment.project_id = issue.project_id |
| comment.issue_id = issue.issue_id |
| comment.content = content |
| comment.user_id = user_id |
| if timestamp is not None: |
| comment.timestamp = timestamp |
| else: |
| comment.timestamp = 1234567890 |
| if amendments: |
| comment.amendments.extend(amendments) |
| if inbound_message: |
| comment.inbound_message = inbound_message |
| comment.is_spam = is_spam |
| comment.is_description = is_description |
| if approval_id: |
| comment.approval_id = approval_id |
| |
| pid = issue.project_id |
| self.comments_by_project.setdefault(pid, {}) |
| self.comments_by_project[pid].setdefault(issue.local_id, []).append(comment) |
| self.comments_by_iid.setdefault(issue.issue_id, []).append(comment) |
| self.comments_by_cid[comment.id] = comment |
| |
| if attachments: |
| for filename, filecontent, mimetype in attachments: |
| aid = len(self.attachments_by_id) |
| attach = tracker_pb2.Attachment( |
| attachment_id=aid, |
| filename=filename, |
| filesize=len(filecontent), |
| mimetype=mimetype, |
| gcs_object_id='gcs_object_id(%s)' % filename) |
| comment.attachments.append(attach) |
| self.attachments_by_id[aid] = attach, pid, comment.id |
| |
| if kept_attachments: |
| comment.attachments.extend([ |
| self.attachments_by_id[aid][0] |
| for aid in kept_attachments]) |
| |
| return comment |
| |
| def GetOpenAndClosedIssues(self, _cnxn, issue_ids): |
| open_issues = [] |
| closed_issues = [] |
| for issue_id in issue_ids: |
| try: |
| issue = self.issues_by_iid[issue_id] |
| if issue.status == 'Fixed': |
| closed_issues.append(issue) |
| else: |
| open_issues.append(issue) |
| except KeyError: |
| continue |
| |
| return open_issues, closed_issues |
| |
| def GetIssuesDict( |
| self, _cnxn, issue_ids, use_cache=True, shard_id=None): |
| missing_ids = [iid for iid in issue_ids if iid not in self.issues_by_iid] |
| issues_by_id = {} |
| for iid in issue_ids: |
| if iid in self.issues_by_iid: |
| issue = self.issues_by_iid[iid] |
| if not use_cache: |
| issue.assume_stale = False |
| issues_by_id[iid] = issue |
| |
| return issues_by_id, missing_ids |
| |
| def GetIssues(self, cnxn, issue_ids, use_cache=True, shard_id=None): |
| issues_by_iid, _misses = self.GetIssuesDict( |
| cnxn, issue_ids, use_cache=use_cache, shard_id=shard_id) |
| results = [ |
| issues_by_iid[issue_id] |
| for issue_id in issue_ids |
| if issue_id in issues_by_iid |
| ] |
| |
| return results |
| |
| def SoftDeleteIssue( |
| self, _cnxn, project_id, local_id, deleted, user_service): |
| issue = self.issues_by_project[project_id][local_id] |
| issue.deleted = deleted |
| |
| def SoftDeleteComment( |
| self, cnxn, issue, comment, deleted_by_user_id, user_service, |
| delete=True, reindex=False, is_spam=False): |
| pid = comment.project_id |
| # Find the original comment by the sequence number. |
| c = None |
| by_iid_idx = -1 |
| for by_iid_idx, c in enumerate(self.comments_by_iid[issue.issue_id]): |
| if c.sequence == comment.sequence: |
| break |
| comment = c |
| by_project_idx = ( |
| self.comments_by_project[pid][issue.local_id].index(comment)) |
| comment.is_spam = is_spam |
| if delete: |
| comment.deleted_by = deleted_by_user_id |
| else: |
| comment.reset('deleted_by') |
| self.comments_by_project[pid][issue.local_id][by_project_idx] = comment |
| self.comments_by_iid[issue.issue_id][by_iid_idx] = comment |
| self.comments_by_cid[comment.id] = comment |
| |
| def DeleteComponentReferences(self, _cnxn, component_id): |
| for _, issue in self.issues_by_iid.items(): |
| issue.component_ids = [ |
| cid for cid in issue.component_ids if cid != component_id] |
| |
| def RunIssueQuery( |
| self, cnxn, left_joins, where, order_by, shard_id=None, limit=None): |
| """This always returns empty results. Mock it to test other cases.""" |
| return [], False |
| |
| def GetIIDsByLabelIDs(self, cnxn, label_ids, project_id, shard_id): |
| """This always returns empty results. Mock it to test other cases.""" |
| return [] |
| |
| def GetIIDsByParticipant(self, cnxn, user_ids, project_ids, shard_id): |
| """This always returns empty results. Mock it to test other cases.""" |
| return [] |
| |
| def SortBlockedOn(self, cnxn, issue, blocked_on_iids): |
| return blocked_on_iids, [0] * len(blocked_on_iids) |
| |
| def ApplyIssueRerank( |
| self, cnxn, parent_id, relations_to_change, commit=True, invalidate=True): |
| issue = self.GetIssue(cnxn, parent_id) |
| relations_dict = dict( |
| list(zip(issue.blocked_on_iids, issue.blocked_on_ranks))) |
| relations_dict.update(relations_to_change) |
| issue.blocked_on_ranks = sorted(issue.blocked_on_ranks, reverse=True) |
| issue.blocked_on_iids = sorted( |
| issue.blocked_on_iids, key=relations_dict.get, reverse=True) |
| |
| def SplitRanks(self, cnxn, parent_id, target_id, open_ids, split_above=False): |
| pass |
| |
| def ExpungeUsersInIssues(self, cnxn, user_ids_by_email, limit=None): |
| user_ids = list(user_ids_by_email.values()) |
| self.expunged_users_in_issues.extend(user_ids) |
| return [] |
| |
| |
| class TemplateService(object): |
| """Fake version of TemplateService that just works in-RAM.""" |
| |
| def __init__(self): |
| self.templates_by_id = {} # template_id: template_pb |
| self.templates_by_project_id = {} # project_id: [template_id] |
| |
| def TestAddIssueTemplateDef( |
| self, template_id, project_id, name, content="", summary="", |
| summary_must_be_edited=False, status='New', members_only=False, |
| owner_defaults_to_member=False, component_required=False, owner_id=None, |
| labels=None, component_ids=None, admin_ids=None, field_values=None, |
| phases=None, approval_values=None): |
| template = tracker_bizobj.MakeIssueTemplate( |
| name, |
| summary, |
| status, |
| owner_id, |
| content, |
| labels, |
| field_values or [], |
| admin_ids or [], |
| component_ids, |
| summary_must_be_edited=summary_must_be_edited, |
| owner_defaults_to_member=owner_defaults_to_member, |
| component_required=component_required, |
| members_only=members_only, |
| phases=phases, |
| approval_values=approval_values) |
| template.template_id = template_id |
| self.templates_by_id[template_id] = template |
| if project_id not in self.templates_by_project_id: |
| self.templates_by_project_id[project_id] = [] |
| self.templates_by_project_id[project_id].append(template_id) |
| return template |
| |
| def GetTemplateByName(self, cnxn, template_name, project_id): |
| if project_id not in self.templates_by_project_id: |
| return None |
| else: |
| project_templates = self.templates_by_project_id[project_id] |
| for template_id in project_templates: |
| template = self.GetTemplateById(cnxn, template_id) |
| if template.name == template_name: |
| return template |
| return None |
| |
| def GetTemplateById(self, cnxn, template_id): |
| return self.templates_by_id.get(template_id) |
| |
| def GetTemplatesById(self, cnxn, template_ids): |
| return filter( |
| lambda template: template.template_id in template_ids, |
| self.templates_by_id.values()) |
| |
| def GetProjectTemplates(self, cnxn, project_id): |
| template_ids = self.templates_by_project_id[project_id] |
| return self.GetTemplatesById(cnxn, template_ids) |
| |
| def ExpungeUsersInTemplates(self, cnxn, user_ids, limit=None): |
| for _, template in self.templates_by_id.items(): |
| template.admin_ids = [user_id for user_id in template.admin_ids |
| if user_id not in user_ids] |
| if template.owner_id in user_ids: |
| template.owner_id = None |
| template.field_values = [fv for fv in template.field_values |
| if fv.user_id in user_ids] |
| |
| class SpamService(object): |
| """Fake version of SpamService that just works in-RAM.""" |
| |
| def __init__(self, user_id=None): |
| self.user_id = user_id |
| self.reports_by_issue_id = collections.defaultdict(list) |
| self.comment_reports_by_issue_id = collections.defaultdict(dict) |
| self.manual_verdicts_by_issue_id = collections.defaultdict(dict) |
| self.manual_verdicts_by_comment_id = collections.defaultdict(dict) |
| self.expunged_users_in_spam = [] |
| |
| def LookupIssuesFlaggers(self, cnxn, issue_ids): |
| return { |
| issue_id: (self.reports_by_issue_id.get(issue_id, []), |
| self.comment_reports_by_issue_id.get(issue_id, {})) |
| for issue_id in issue_ids} |
| |
| def LookupIssueFlaggers(self, cnxn, issue_id): |
| return self.LookupIssuesFlaggers(cnxn, [issue_id])[issue_id] |
| |
| def FlagIssues(self, cnxn, issue_service, issues, user_id, flagged_spam): |
| for issue in issues: |
| if flagged_spam: |
| self.reports_by_issue_id[issue.issue_id].append(user_id) |
| else: |
| self.reports_by_issue_id[issue.issue_id].remove(user_id) |
| |
| def FlagComment( |
| self, cnxn, issue, comment_id, reported_user_id, user_id, flagged_spam): |
| if not comment_id in self.comment_reports_by_issue_id[issue.issue_id]: |
| self.comment_reports_by_issue_id[issue.issue_id][comment_id] = [] |
| if flagged_spam: |
| self.comment_reports_by_issue_id[issue.issue_id][comment_id].append( |
| user_id) |
| else: |
| self.comment_reports_by_issue_id[issue.issue_id][comment_id].remove( |
| user_id) |
| |
| def RecordManualIssueVerdicts( |
| self, cnxn, issue_service, issues, user_id, is_spam): |
| for issue in issues: |
| self.manual_verdicts_by_issue_id[issue.issue_id][user_id] = is_spam |
| |
| def RecordManualCommentVerdict( |
| self, cnxn, issue_service, user_service, comment_id, |
| user_id, is_spam): |
| self.manual_verdicts_by_comment_id[comment_id][user_id] = is_spam |
| comment = issue_service.GetComment(cnxn, comment_id) |
| comment.is_spam = is_spam |
| issue = issue_service.GetIssue(cnxn, comment.issue_id, use_cache=False) |
| issue_service.SoftDeleteComment( |
| cnxn, issue, comment, user_id, user_service, is_spam, True, is_spam) |
| |
| def RecordClassifierIssueVerdict(self, cnxn, issue, is_spam, confidence, |
| failed_open): |
| return |
| |
| def RecordClassifierCommentVerdict(self, cnxn, issue, is_spam, confidence, |
| failed_open): |
| return |
| |
| def ClassifyComment(self, comment, commenter): |
| return {'outputLabel': 'ham', |
| 'outputMulti': [{'label': 'ham', 'score': '1.0'}], |
| 'failed_open': False} |
| |
| def ClassifyIssue(self, issue, firstComment, reporter): |
| return {'outputLabel': 'ham', |
| 'outputMulti': [{'label': 'ham', 'score': '1.0'}], |
| 'failed_open': False} |
| |
| def ExpungeUsersInSpam(self, cnxn, user_ids): |
| self.expunged_users_in_spam.extend(user_ids) |
| |
| |
| class FeaturesService(object): |
| """A fake implementation of FeaturesService.""" |
| def __init__(self): |
| # Test-only sequence of expunged projects and users. |
| self.expunged_saved_queries = [] |
| self.expunged_users_in_saved_queries = [] |
| self.expunged_filter_rules = [] |
| self.expunged_users_in_filter_rules = [] |
| self.expunged_quick_edit = [] |
| self.expunged_users_in_quick_edits = [] |
| self.expunged_hotlist_ids = [] |
| self.expunged_users_in_hotlists = [] |
| |
| # filter rules, project_id => filterrule_pb |
| self.test_rules = collections.defaultdict(list) |
| |
| # TODO(crbug/monorail/7104): Confirm that these are never reassigned |
| # to empty {} and then change these to collections.defaultdicts instead. |
| # hotlists |
| self.test_hotlists = {} # (hotlist_name, owner_id) => hotlist_pb |
| self.hotlists_by_id = {} |
| self.hotlists_id_by_user = {} # user_id => [hotlist_id, hotlist_id, ...] |
| self.hotlists_id_by_issue = {} # issue_id => [hotlist_id, hotlist_id, ...] |
| |
| # saved queries |
| self.saved_queries = [] # [(pid, uid, sq), ...] |
| |
| def TestAddFilterRule( |
| self, project_id, predicate, default_status=None, default_owner_id=None, |
| add_cc_ids=None, add_labels=None, add_notify=None, warning=None, |
| error=None): |
| 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, warning=warning, |
| error=error) |
| self.test_rules[project_id].append(rule) |
| return rule |
| |
| def TestAddHotlist(self, name, summary='', owner_ids=None, editor_ids=None, |
| follower_ids=None, description=None, hotlist_id=None, |
| is_private=False, hotlist_item_fields=None, |
| default_col_spec=None): |
| """Add a hotlist to the fake FeaturesService object. |
| |
| Args: |
| name: the name of the hotlist. Will replace any existing hotlist under |
| the same name. |
| summary: the summary string of the hotlist |
| owner_ids: List of user ids for the hotlist owners |
| editor_ids: List of user ids for the hotlist editors |
| follower_ids: List of user ids for the hotlist followers |
| description: The description string for this hotlist |
| hotlist_id: A unique integer identifier for the created hotlist |
| is_private: A boolean indicating whether the hotlist is private/public |
| hotlist_item_fields: a list of tuples -> |
| [(issue_id, rank, adder_id, date_added, note),...] |
| default_col_spec: string of default columns for the hotlist. |
| |
| Returns: |
| A populated hotlist PB. |
| """ |
| hotlist_pb = features_pb2.Hotlist() |
| hotlist_pb.hotlist_id = hotlist_id or hash(name) % 100000 |
| hotlist_pb.name = name |
| hotlist_pb.summary = summary |
| hotlist_pb.is_private = is_private |
| hotlist_pb.default_col_spec = default_col_spec |
| if description is not None: |
| hotlist_pb.description = description |
| |
| self.TestAddHotlistMembers(owner_ids, hotlist_pb, OWNER_ROLE) |
| self.TestAddHotlistMembers(follower_ids, hotlist_pb, FOLLOWER_ROLE) |
| self.TestAddHotlistMembers(editor_ids, hotlist_pb, EDITOR_ROLE) |
| |
| if hotlist_item_fields is not None: |
| for(issue_id, rank, adder_id, date, note) in hotlist_item_fields: |
| hotlist_pb.items.append( |
| features_pb2.Hotlist.HotlistItem( |
| issue_id=issue_id, rank=rank, adder_id=adder_id, |
| date_added=date, note=note)) |
| try: |
| self.hotlists_id_by_issue[issue_id].append(hotlist_pb.hotlist_id) |
| except KeyError: |
| self.hotlists_id_by_issue[issue_id] = [hotlist_pb.hotlist_id] |
| |
| owner_id = None |
| if hotlist_pb.owner_ids: |
| owner_id = hotlist_pb.owner_ids[0] |
| self.test_hotlists[(name, owner_id)] = hotlist_pb |
| self.hotlists_by_id[hotlist_pb.hotlist_id] = hotlist_pb |
| return hotlist_pb |
| |
| def TestAddHotlistMembers(self, user_id_list, hotlist_pb, role): |
| if user_id_list is not None: |
| for user_id in user_id_list: |
| if role == OWNER_ROLE: |
| hotlist_pb.owner_ids.append(user_id) |
| elif role == EDITOR_ROLE: |
| hotlist_pb.editor_ids.append(user_id) |
| elif role == FOLLOWER_ROLE: |
| hotlist_pb.follower_ids.append(user_id) |
| try: |
| self.hotlists_id_by_user[user_id].append(hotlist_pb.hotlist_id) |
| except KeyError: |
| self.hotlists_id_by_user[user_id] = [hotlist_pb.hotlist_id] |
| |
| def CheckHotlistName(self, cnxn, name, owner_ids): |
| if not framework_bizobj.IsValidHotlistName(name): |
| raise exceptions.InputException( |
| '%s is not a valid name for a Hotlist' % name) |
| if self.LookupHotlistIDs(cnxn, [name], owner_ids): |
| raise features_svc.HotlistAlreadyExists() |
| |
| def CreateHotlist( |
| self, _cnxn, hotlist_name, summary, description, owner_ids, editor_ids, |
| issue_ids=None, is_private=None, default_col_spec=None, ts=None): |
| """Create and store a Hotlist with the given attributes.""" |
| if not framework_bizobj.IsValidHotlistName(hotlist_name): |
| raise exceptions.InputException() |
| if not owner_ids: # Should never happen. |
| raise features_svc.UnownedHotlistException() |
| if (hotlist_name, owner_ids[0]) in self.test_hotlists: |
| raise features_svc.HotlistAlreadyExists() |
| hotlist_item_fields = [ |
| (issue_id, rank*100, owner_ids[0] or None, ts, '') for |
| rank, issue_id in enumerate(issue_ids or [])] |
| return self.TestAddHotlist(hotlist_name, summary=summary, |
| owner_ids=owner_ids, editor_ids=editor_ids, |
| description=description, is_private=is_private, |
| hotlist_item_fields=hotlist_item_fields, |
| default_col_spec=default_col_spec) |
| |
| def UpdateHotlist( |
| self, cnxn, hotlist_id, name=None, summary=None, description=None, |
| is_private=None, default_col_spec=None, owner_id=None, |
| add_editor_ids=None): |
| hotlist = self.hotlists_by_id.get(hotlist_id) |
| if not hotlist: |
| raise features_svc.NoSuchHotlistException( |
| 'Hotlist "%s" not found!' % hotlist_id) |
| |
| if owner_id: |
| old_owner_id = hotlist.owner_ids[0] |
| self.test_hotlists.pop((hotlist.name, old_owner_id), None) |
| self.test_hotlists[(hotlist.name, owner_id)] = hotlist |
| |
| if add_editor_ids: |
| for editor_id in add_editor_ids: |
| self.hotlists_id_by_user.get(editor_id, []).append(hotlist_id) |
| |
| if name is not None: |
| hotlist.name = name |
| if summary is not None: |
| hotlist.summary = summary |
| if description is not None: |
| hotlist.description = description |
| if is_private is not None: |
| hotlist.is_private = is_private |
| if default_col_spec is not None: |
| hotlist.default_col_spec = default_col_spec |
| if owner_id is not None: |
| hotlist.owner_ids = [owner_id] |
| if add_editor_ids: |
| hotlist.editor_ids.extend(add_editor_ids) |
| |
| def RemoveHotlistEditors(self, cnxn, hotlist_id, remove_editor_ids): |
| hotlist = self.hotlists_by_id.get(hotlist_id) |
| if not hotlist: |
| raise features_svc.NoSuchHotlistException( |
| 'Hotlist "%s" not found!' % hotlist_id) |
| for editor_id in remove_editor_ids: |
| hotlist.editor_ids.remove(editor_id) |
| self.hotlists_id_by_user[editor_id].remove(hotlist_id) |
| |
| def AddIssuesToHotlists(self, cnxn, hotlist_ids, added_tuples, issue_svc, |
| chart_svc, commit=True): |
| for hotlist_id in hotlist_ids: |
| self.UpdateHotlistItems(cnxn, hotlist_id, [], added_tuples, commit=commit) |
| |
| def RemoveIssuesFromHotlists(self, cnxn, hotlist_ids, issue_ids, issue_svc, |
| chart_svc, commit=True): |
| for hotlist_id in hotlist_ids: |
| self.UpdateHotlistItems(cnxn, hotlist_id, issue_ids, [], commit=commit) |
| |
| def UpdateHotlistIssues( |
| self, |
| cnxn, |
| hotlist_id, |
| updated_items, |
| remove_issue_ids, |
| issue_svc, |
| chart_svc, |
| commit=True): |
| if not updated_items and not remove_issue_ids: |
| raise exceptions.InputException('No changes to make') |
| |
| hotlist = self.hotlists_by_id.get(hotlist_id) |
| if not hotlist: |
| raise NoSuchHotlistException() |
| |
| updated_ids = [item.issue_id for item in updated_items] |
| items = [ |
| item for item in hotlist.items |
| if item.issue_id not in updated_ids + remove_issue_ids |
| ] |
| hotlist.items = sorted(updated_items + items, key=lambda item: item.rank) |
| |
| # Remove all removed and updated issues. |
| for issue_id in remove_issue_ids + updated_ids: |
| try: |
| self.hotlists_id_by_issue[issue_id].remove(hotlist_id) |
| except (ValueError, KeyError): |
| pass |
| # Add all new or updated issues. |
| for item in updated_items: |
| self.hotlists_id_by_issue.setdefault(item.issue_id, []).append(hotlist_id) |
| |
| def UpdateHotlistItems( |
| self, cnxn, hotlist_id, remove, added_issue_tuples, commit=True): |
| hotlist = self.hotlists_by_id.get(hotlist_id) |
| if not hotlist: |
| raise features_svc.NoSuchHotlistException( |
| 'Hotlist "%s" not found!' % hotlist_id) |
| current_issues_ids = { |
| item.issue_id for item in hotlist.items} |
| items = [ |
| item for item in hotlist.items if |
| item.issue_id not in remove] |
| |
| if hotlist.items: |
| items_sorted = sorted(hotlist.items, key=lambda item: item.rank) |
| rank_base = items_sorted[-1].rank + 10 |
| else: |
| rank_base = 1 |
| |
| new_hotlist_items = [ |
| features_pb2.MakeHotlistItem( |
| issue_id, rank+rank_base*10, adder_id, date, note) |
| for rank, (issue_id, adder_id, date, note) in |
| enumerate(added_issue_tuples) |
| if issue_id not in current_issues_ids] |
| items.extend(new_hotlist_items) |
| hotlist.items = items |
| |
| for issue_id in remove: |
| try: |
| self.hotlists_id_by_issue[issue_id].remove(hotlist_id) |
| except ValueError: |
| pass |
| for item in new_hotlist_items: |
| try: |
| self.hotlists_id_by_issue[item.issue_id].append(hotlist_id) |
| except KeyError: |
| self.hotlists_id_by_issue[item.issue_id] = [hotlist_id] |
| |
| def UpdateHotlistItemsFields( |
| self, cnxn, hotlist_id, new_ranks=None, new_notes=None, commit=True): |
| hotlist = self.hotlists_by_id.get(hotlist_id) |
| if not hotlist: |
| raise features_svc.NoSuchHotlistException( |
| 'Hotlist "%s" not found!' % hotlist_id) |
| if new_ranks is None: |
| new_ranks = {} |
| if new_notes is None: |
| new_notes = {} |
| for hotlist_item in hotlist.items: |
| if hotlist_item.issue_id in new_ranks: |
| hotlist_item.rank = new_ranks[hotlist_item.issue_id] |
| if hotlist_item.issue_id in new_notes: |
| hotlist_item.note = new_notes[hotlist_item.issue_id] |
| |
| hotlist.items.sort(key=lambda item: item.rank) |
| |
| def TransferHotlistOwnership( |
| self, cnxn, hotlist, new_owner_id, remain_editor, commit=True): |
| """Transfers ownership of a hotlist to a new owner.""" |
| new_editor_ids = hotlist.editor_ids |
| if remain_editor: |
| new_editor_ids.extend(hotlist.owner_ids) |
| if new_owner_id in new_editor_ids: |
| new_editor_ids.remove(new_owner_id) |
| new_follower_ids = hotlist.follower_ids |
| if new_owner_id in new_follower_ids: |
| new_follower_ids.remove(new_owner_id) |
| self.UpdateHotlistRoles( |
| cnxn, hotlist.hotlist_id, [new_owner_id], new_editor_ids, |
| new_follower_ids, commit=commit) |
| |
| def LookupUserHotlists(self, cnxn, user_ids): |
| """Return dict of {user_id: [hotlist_id, hotlist_id...]}.""" |
| users_hotlists_dict = { |
| user_id: self.hotlists_id_by_user.get(user_id, []) |
| for user_id in user_ids |
| } |
| return users_hotlists_dict |
| |
| def LookupIssueHotlists(self, cnxn, issue_ids): |
| """Return dict of {issue_id: [hotlist_id, hotlist_id...]}.""" |
| issues_hotlists_dict = { |
| issue_id: self.hotlists_id_by_issue[issue_id] |
| for issue_id in issue_ids |
| if issue_id in self.hotlists_id_by_issue} |
| return issues_hotlists_dict |
| |
| def LookupHotlistIDs(self, cnxn, hotlist_names, owner_ids): |
| id_dict = {} |
| for name in hotlist_names: |
| for owner_id in owner_ids: |
| hotlist = self.test_hotlists.get((name, owner_id)) |
| if hotlist: |
| if not hotlist.owner_ids: # Should never happen. |
| logging.warning( |
| 'Unowned Hotlist: id:%r, name:%r', hotlist.hotlist_id, |
| hotlist.name) |
| continue |
| id_dict[(name.lower(), owner_id)] = hotlist.hotlist_id |
| return id_dict |
| |
| def GetHotlists(self, cnxn, hotlist_ids, use_cache=True): |
| """Returns dict of {hotlist_id: hotlist PB}.""" |
| result = {} |
| for hotlist_id in hotlist_ids: |
| hotlist = self.hotlists_by_id.get(hotlist_id) |
| if hotlist: |
| result[hotlist_id] = hotlist |
| else: |
| raise features_svc.NoSuchHotlistException() |
| return result |
| |
| def GetHotlistsByUserID(self, cnxn, user_id, use_cache=True): |
| """Get a list of hotlist PBs for a given user.""" |
| hotlist_id_dict = self.LookupUserHotlists(cnxn, [user_id]) |
| hotlists = self.GetHotlists(cnxn, hotlist_id_dict.get( |
| user_id, []), use_cache=use_cache) |
| return list(hotlists.values()) |
| |
| def GetHotlistsByIssueID(self, cnxn, issue_id, use_cache=True): |
| """Get a list of hotlist PBs for a given issue.""" |
| hotlist_id_dict = self.LookupIssueHotlists(cnxn, [issue_id]) |
| hotlists = self.GetHotlists(cnxn, hotlist_id_dict.get( |
| issue_id, []), use_cache=use_cache) |
| return list(hotlists.values()) |
| |
| def GetHotlist(self, cnxn, hotlist_id, use_cache=True): |
| """Return hotlist PB.""" |
| hotlist_id_dict = self.GetHotlists(cnxn, [hotlist_id], use_cache=use_cache) |
| return hotlist_id_dict.get(hotlist_id) |
| |
| def GetHotlistsByID(self, cnxn, hotlist_ids, use_cache=True): |
| hotlists_dict = {} |
| missed_ids = [] |
| for hotlist_id in hotlist_ids: |
| hotlist = self.hotlists_by_id.get(hotlist_id) |
| if hotlist: |
| hotlists_dict[hotlist_id] = hotlist |
| else: |
| missed_ids.append(hotlist_id) |
| return hotlists_dict, missed_ids |
| |
| def GetHotlistByID(self, cnxn, hotlist_id, use_cache=True): |
| hotlists_dict, _ = self.GetHotlistsByID( |
| cnxn, [hotlist_id], use_cache=use_cache) |
| return hotlists_dict[hotlist_id] |
| |
| def UpdateHotlistRoles( |
| self, cnxn, hotlist_id, owner_ids, editor_ids, follower_ids, commit=True): |
| hotlist = self.hotlists_by_id.get(hotlist_id) |
| if not hotlist: |
| raise features_svc.NoSuchHotlistException( |
| 'Hotlist "%s" not found!' % hotlist_id) |
| |
| # Remove hotlist_ids to clear old roles |
| for user_id in (hotlist.owner_ids + hotlist.editor_ids + |
| hotlist.follower_ids): |
| if hotlist_id in self.hotlists_id_by_user[user_id]: |
| self.hotlists_id_by_user[user_id].remove(hotlist_id) |
| old_owner_id = None |
| if hotlist.owner_ids: |
| old_owner_id = hotlist.owner_ids[0] |
| self.test_hotlists.pop((hotlist.name, old_owner_id), None) |
| |
| hotlist.owner_ids = owner_ids |
| hotlist.editor_ids = editor_ids |
| hotlist.follower_ids = follower_ids |
| |
| # Add new hotlist roles |
| for user_id in owner_ids+editor_ids+follower_ids: |
| try: |
| if hotlist_id not in self.hotlists_id_by_user[user_id]: |
| self.hotlists_id_by_user[user_id].append(hotlist_id) |
| except KeyError: |
| self.hotlists_id_by_user[user_id] = [hotlist_id] |
| new_owner_id = None |
| if owner_ids: |
| new_owner_id = owner_ids[0] |
| self.test_hotlists[(hotlist.name, new_owner_id)] = hotlist |
| |
| def DeleteHotlist(self, cnxn, hotlist_id, commit=True): |
| hotlist = self.hotlists_by_id.pop(hotlist_id, None) |
| if hotlist is not None: |
| self.test_hotlists.pop((hotlist.name, hotlist.owner_ids[0]), None) |
| user_ids = hotlist.owner_ids+hotlist.editor_ids+hotlist.follower_ids |
| for user_id in user_ids: |
| try: |
| self.hotlists_id_by_user[user_id].remove(hotlist_id) |
| except (ValueError, KeyError): |
| pass |
| for item in hotlist.items: |
| try: |
| self.hotlists_id_by_issue[item.issue_id].remove(hotlist_id) |
| except (ValueError, KeyError): |
| pass |
| for owner_id in hotlist.owner_ids: |
| self.test_hotlists.pop((hotlist.name, owner_id), None) |
| |
| def ExpungeHotlists( |
| self, cnxn, hotlist_ids, star_svc, user_svc, chart_svc, commit=True): |
| self.expunged_hotlist_ids.extend(hotlist_ids) |
| for hotlist_id in hotlist_ids: |
| self.DeleteHotlist(cnxn, hotlist_id) |
| |
| def ExpungeUsersInHotlists( |
| self, cnxn, user_ids, star_svc, user_svc, chart_svc): |
| self.expunged_users_in_hotlists.extend(user_ids) |
| |
| # end of Hotlist functions |
| |
| def GetRecentCommands(self, cnxn, user_id, project_id): |
| return [], [] |
| |
| def ExpungeSavedQueriesExecuteInProject(self, _cnxn, project_id): |
| self.expunged_saved_queries.append(project_id) |
| |
| def ExpungeSavedQueriesByUsers(self, cnxn, user_ids, limit=None): |
| self.expunged_users_in_saved_queries.extend(user_ids) |
| |
| def ExpungeFilterRules(self, _cnxn, project_id): |
| self.expunged_filter_rules.append(project_id) |
| |
| def ExpungeFilterRulesByUser(self, cnxn, user_ids_by_email): |
| emails = user_ids_by_email.keys() |
| user_ids = user_ids_by_email.values() |
| project_rules_dict = collections.defaultdict(list) |
| for project_id, rules in self.test_rules.items(): |
| for rule in rules: |
| if rule.default_owner_id in user_ids: |
| project_rules_dict[project_id].append(rule) |
| continue |
| if any(cc_id in user_ids for cc_id in rule.add_cc_ids): |
| project_rules_dict[project_id].append(rule) |
| continue |
| if any(addr in emails for addr in rule.add_notify_addrs): |
| project_rules_dict[project_id].append(rule) |
| continue |
| if any((email in rule.predicate) for email in emails): |
| project_rules_dict[project_id].append(rule) |
| continue |
| self.test_rules[project_id] = [ |
| rule for rule in rules |
| if rule not in project_rules_dict[project_id]] |
| return project_rules_dict |
| |
| def ExpungeQuickEditHistory(self, _cnxn, project_id): |
| self.expunged_quick_edit.append(project_id) |
| |
| def ExpungeQuickEditsByUsers(self, cnxn, user_ids, limit=None): |
| self.expunged_users_in_quick_edits.extend(user_ids) |
| |
| def GetFilterRules(self, cnxn, project_id): |
| return self.test_rules[project_id] |
| |
| def GetCannedQueriesByProjectID(self, cnxn, project_id): |
| return [sq for (pid, _, sq) in self.saved_queries if pid == project_id] |
| |
| def GetSavedQueriesByUserID(self, cnxn, user_id): |
| return [sq for (_, uid, sq) in self.saved_queries if uid == user_id] |
| |
| def UpdateCannedQueries(self, cnxn, project_id, canned_queries): |
| self.saved_queries.extend( |
| [(project_id, None, cq) for cq in canned_queries]) |
| |
| def UpdateUserSavedQueries(self, cnxn, user_id, saved_queries): |
| self.saved_queries = [ |
| (pid, uid, sq) for (pid, uid, sq) in self.saved_queries |
| if uid != user_id] |
| for sq in saved_queries: |
| if sq.executes_in_project_ids: |
| self.saved_queries.extend( |
| [(eipid, user_id, sq) for eipid in sq.executes_in_project_ids]) |
| else: |
| self.saved_queries.append((None, user_id, sq)) |
| |
| def GetSubscriptionsInProjects(self, cnxn, project_ids): |
| sq_by_uid = {} |
| for pid, uid, sq in self.saved_queries: |
| if pid in project_ids: |
| if uid in sq_by_uid: |
| sq_by_uid[uid].append(sq) |
| else: |
| sq_by_uid[uid] = [sq] |
| |
| return sq_by_uid |
| |
| def GetSavedQuery(self, cnxn, query_id): |
| return tracker_pb2.SavedQuery() |
| |
| |
| class PostData(object): |
| """A dictionary-like object that also implements getall().""" |
| |
| def __init__(self, *args, **kwargs): |
| self.dictionary = dict(*args, **kwargs) |
| |
| def getall(self, key): |
| """Return all values, assume that the value at key is already a list.""" |
| return self.dictionary.get(key, []) |
| |
| def getlist(self, key): |
| """Return all values, assume that the value at key is already a list.""" |
| return self.dictionary.get(key, []) |
| |
| def get(self, key, default=None): |
| """Return first value, assume that the value at key is already a list.""" |
| return self.dictionary.get(key, [default])[0] |
| |
| def __getitem__(self, key): |
| """Return first value, assume that the value at key is already a list.""" |
| return self.dictionary[key][0] |
| |
| def __contains__(self, key): |
| return key in self.dictionary |
| |
| def keys(self): |
| """Return the keys in the POST data.""" |
| return list(self.dictionary.keys()) |
| |
| |
| class FakeFile: |
| def __init__(self, data=None): |
| self.data = data |
| |
| def read(self): |
| return self.data |
| |
| def write(self, content): |
| return |
| |
| def __enter__(self): |
| return self |
| |
| def __exit__(self, __1, __2, __3): |
| return None |
| |
| |
| def gcs_open(filename, mode): |
| return FakeFile(filename) |