| Index: appengine/monorail/tracker/issueoptions.py | 
| diff --git a/appengine/monorail/tracker/issueoptions.py b/appengine/monorail/tracker/issueoptions.py | 
| new file mode 100644 | 
| index 0000000000000000000000000000000000000000..83019000cb6fac37b15e418554228371fef3a201 | 
| --- /dev/null | 
| +++ b/appengine/monorail/tracker/issueoptions.py | 
| @@ -0,0 +1,263 @@ | 
| +# Copyright 2016 The Chromium Authors. All rights reserved. | 
| +# Use of this source code is govered by a BSD-style | 
| +# license that can be found in the LICENSE file or at | 
| +# https://developers.google.com/open-source/licenses/bsd | 
| + | 
| +"""JSON feed for issue autocomplete options.""" | 
| + | 
| +import logging | 
| +from third_party import ezt | 
| + | 
| +from framework import framework_helpers | 
| +from framework import framework_views | 
| +from framework import jsonfeed | 
| +from framework import monorailrequest | 
| +from framework import permissions | 
| +from project import project_helpers | 
| +from tracker import tracker_helpers | 
| +from tracker import tracker_views | 
| + | 
| + | 
| +# Here are some restriction labels to help people do the most common things | 
| +# that they might want to do with restrictions. | 
| +_FREQUENT_ISSUE_RESTRICTIONS = [ | 
| +    (permissions.VIEW, permissions.EDIT_ISSUE, | 
| +     'Only users who can edit the issue may access it'), | 
| +    (permissions.ADD_ISSUE_COMMENT, permissions.EDIT_ISSUE, | 
| +     'Only users who can edit the issue may add comments'), | 
| +    ] | 
| + | 
| + | 
| +# These issue restrictions should be offered as examples whenever the project | 
| +# does not have any custom permissions in use already. | 
| +_EXAMPLE_ISSUE_RESTRICTIONS = [ | 
| +    (permissions.VIEW, 'CoreTeam', | 
| +     'Custom permission CoreTeam is needed to access'), | 
| +    ] | 
| + | 
| + | 
| +class IssueOptionsJSON(jsonfeed.JsonFeed): | 
| +  """JSON data describing all issue statuses, labels, and members.""" | 
| + | 
| +  def HandleRequest(self, mr): | 
| +    """Provide the UI with info used in auto-completion. | 
| + | 
| +    Args: | 
| +      mr: common information parsed from the HTTP request. | 
| + | 
| +    Returns: | 
| +      Results dictionary in JSON format | 
| +    """ | 
| +    # Issue options data can be cached separately in each user's browser.  When | 
| +    # the project changes, a new cached_content_timestamp is set and it will | 
| +    # cause new requests to use a new URL. | 
| +    self.SetCacheHeaders(self.response) | 
| + | 
| +    member_data = project_helpers.BuildProjectMembers( | 
| +        mr.cnxn, mr.project, self.services.user) | 
| +    owner_views = member_data['owners'] | 
| +    committer_views = member_data['committers'] | 
| +    contributor_views = member_data['contributors'] | 
| + | 
| +    config = self.services.config.GetProjectConfig(mr.cnxn, mr.project_id) | 
| + | 
| +    open_statuses = [] | 
| +    closed_statuses = [] | 
| +    for wks in config.well_known_statuses: | 
| +      if not wks.deprecated: | 
| +        item = dict(name=wks.status, doc=wks.status_docstring) | 
| +        if wks.means_open: | 
| +          open_statuses.append(item) | 
| +        else: | 
| +          closed_statuses.append(item) | 
| + | 
| +    # TODO(jrobbins): restrictions on component definitions? | 
| +    components = [{'name': cd.path, 'doc': cd.docstring} | 
| +                  for cd in config.component_defs if not cd.deprecated] | 
| + | 
| +    labels = [] | 
| +    field_names = [ | 
| +        fd.field_name for fd in config.field_defs if not fd.is_deleted] | 
| +    non_masked_labels = tracker_helpers.LabelsNotMaskedByFields( | 
| +        config, field_names) | 
| +    for wkl in non_masked_labels: | 
| +      if not wkl.commented: | 
| +        item = dict(name=wkl.name, doc=wkl.docstring) | 
| +        labels.append(item) | 
| + | 
| +    # TODO(jrobbins): omit fields that they don't have permission to view. | 
| +    field_def_views = [ | 
| +        tracker_views.FieldDefView(fd, config) | 
| +        for fd in config.field_defs | 
| +        if not fd.is_deleted] | 
| +    fields = [ | 
| +        dict(field_name=fdv.field_name, field_type=fdv.field_type, | 
| +             field_id=fdv.field_id, needs_perm=fdv.needs_perm, | 
| +             is_required=fdv.is_required, is_multivalued=fdv.is_multivalued, | 
| +             choices=[dict(name=c.name, doc=c.docstring) for c in fdv.choices], | 
| +             docstring=fdv.docstring) | 
| +        for fdv in field_def_views] | 
| + | 
| +    frequent_restrictions = _FREQUENT_ISSUE_RESTRICTIONS[:] | 
| +    custom_permissions = permissions.GetCustomPermissions(mr.project) | 
| +    if not custom_permissions: | 
| +      frequent_restrictions.extend( | 
| +          _EXAMPLE_ISSUE_RESTRICTIONS) | 
| + | 
| +    labels.extend(_BuildRestrictionChoices( | 
| +        mr.project, frequent_restrictions, | 
| +        permissions.STANDARD_ISSUE_PERMISSIONS)) | 
| + | 
| +    group_ids = self.services.usergroup.DetermineWhichUserIDsAreGroups( | 
| +        mr.cnxn, [mem.user_id for mem in member_data['all_members']]) | 
| +    logging.info('group_ids is %r', group_ids) | 
| + | 
| +    # TODO(jrobbins): Normally, users will be allowed view the members | 
| +    # of any user group if the project From: email address is listed | 
| +    # as a group member, as well as any group that they are personally | 
| +    # members of. | 
| +    member_ids, owner_ids = self.services.usergroup.LookupVisibleMembers( | 
| +        mr.cnxn, group_ids, mr.perms, mr.auth.effective_ids, self.services) | 
| +    indirect_ids = set() | 
| +    for gid in group_ids: | 
| +      indirect_ids.update(member_ids.get(gid, [])) | 
| +      indirect_ids.update(owner_ids.get(gid, [])) | 
| +    indirect_user_ids = list(indirect_ids) | 
| +    indirect_member_views = framework_views.MakeAllUserViews( | 
| +        mr.cnxn, self.services.user, indirect_user_ids).values() | 
| + | 
| +    visible_member_views = _FilterMemberData( | 
| +        mr, owner_views, committer_views, contributor_views, | 
| +        indirect_member_views) | 
| +    # Filter out servbice accounts | 
| +    visible_member_views = [m for m in visible_member_views | 
| +                            if not framework_helpers.IsServiceAccount(m.email)] | 
| +    visible_member_email_list = list({ | 
| +        uv.email for uv in visible_member_views}) | 
| +    user_indexes = {email: idx | 
| +                    for idx, email in enumerate(visible_member_email_list)} | 
| +    visible_members_dict = {} | 
| +    for uv in visible_member_views: | 
| +      visible_members_dict[uv.email] = uv.user_id | 
| +    group_ids = self.services.usergroup.DetermineWhichUserIDsAreGroups( | 
| +        mr.cnxn, visible_members_dict.values()) | 
| + | 
| +    for field_dict in fields: | 
| +      needed_perm = field_dict['needs_perm'] | 
| +      if needed_perm: | 
| +        qualified_user_indexes = [] | 
| +        for uv in visible_member_views: | 
| +          # TODO(jrobbins): Similar code occurs in field_helpers.py. | 
| +          user = self.services.user.GetUser(mr.cnxn, uv.user_id) | 
| +          auth = monorailrequest.AuthData.FromUserID( | 
| +              mr.cnxn, uv.user_id, self.services) | 
| +          user_perms = permissions.GetPermissions( | 
| +              user, auth.effective_ids, mr.project) | 
| +          has_perm = user_perms.CanUsePerm( | 
| +              needed_perm, auth.effective_ids, mr.project, []) | 
| +          if has_perm: | 
| +            qualified_user_indexes.append(user_indexes[uv.email]) | 
| + | 
| +        field_dict['user_indexes'] = sorted(set(qualified_user_indexes)) | 
| + | 
| +    excl_prefixes = [prefix.lower() for prefix in | 
| +                     config.exclusive_label_prefixes] | 
| +    members_def_list = [dict(name=email, doc='') | 
| +                        for email in visible_member_email_list] | 
| +    members_def_list = sorted( | 
| +        members_def_list, key=lambda md: md['name']) | 
| +    for md in members_def_list: | 
| +      md_id = visible_members_dict[md['name']] | 
| +      if md_id in group_ids: | 
| +        md['is_group'] = True | 
| + | 
| +    return { | 
| +        'open': open_statuses, | 
| +        'closed': closed_statuses, | 
| +        'statuses_offer_merge': config.statuses_offer_merge, | 
| +        'components': components, | 
| +        'labels': labels, | 
| +        'fields': fields, | 
| +        'excl_prefixes': excl_prefixes, | 
| +        'strict': ezt.boolean(config.restrict_to_known), | 
| +        'members': members_def_list, | 
| +        'custom_permissions': custom_permissions, | 
| +        } | 
| + | 
| + | 
| +def _FilterMemberData( | 
| +    mr, owner_views, committer_views, contributor_views, | 
| +    indirect_member_views): | 
| +  """Return a filtered list of members that the user can view. | 
| + | 
| +  In most projects, everyone can view the entire member list.  But, | 
| +  some projects are configured to only allow project owners to see | 
| +  all members. In those projects, committers and contributors do not | 
| +  see any contributors.  Regardless of how the project is configured | 
| +  or the role that the user plays in the current project, we include | 
| +  any indirect members through user groups that the user has access | 
| +  to view. | 
| + | 
| +  Args: | 
| +    mr: Commonly used info parsed from the HTTP request. | 
| +    owner_views: list of UserViews for project owners. | 
| +    committer_views: list of UserViews for project committers. | 
| +    contributor_views: list of UserViews for project contributors. | 
| +    indirect_member_views: list of UserViews for users who have | 
| +        an indirect role in the project via a user group, and that the | 
| +        logged in user is allowed to see. | 
| + | 
| +  Returns: | 
| +    A list of owners, committer and visible indirect members if the user is not | 
| +    signed in.  If the project is set to display contributors to non-owners or | 
| +    the signed in user has necessary permissions then additionally a list of | 
| +    contributors. | 
| +  """ | 
| +  visible_members = [] | 
| + | 
| +  # Everyone can view owners and committers | 
| +  visible_members.extend(owner_views) | 
| +  visible_members.extend(committer_views) | 
| + | 
| +  # The list of indirect members is already limited to ones that the user | 
| +  # is allowed to see according to user group settings. | 
| +  visible_members.extend(indirect_member_views) | 
| + | 
| +  # If the user is allowed to view the list of contributors, add those too. | 
| +  if permissions.CanViewContributorList(mr): | 
| +    visible_members.extend(contributor_views) | 
| + | 
| +  return visible_members | 
| + | 
| + | 
| +def _BuildRestrictionChoices(project, freq_restrictions, actions): | 
| +  """Return a list of autocompletion choices for restriction labels. | 
| + | 
| +  Args: | 
| +    project: Project PB for the current project. | 
| +    freq_restrictions: list of (action, perm, doc) tuples for restrictions | 
| +        that are frequently used. | 
| +    actions: list of strings for actions that are relevant to the current | 
| +      artifact. | 
| + | 
| +  Returns: | 
| +    A list of dictionaries [{'name': 'perm name', 'doc': 'docstring'}, ...] | 
| +    suitable for use in a JSON feed to our JS autocompletion functions. | 
| +  """ | 
| +  custom_permissions = permissions.GetCustomPermissions(project) | 
| +  choices = [] | 
| + | 
| +  for action, perm, doc in freq_restrictions: | 
| +    choices.append({ | 
| +        'name': 'Restrict-%s-%s' % (action, perm), | 
| +        'doc': doc, | 
| +        }) | 
| + | 
| +  for action in actions: | 
| +    for perm in custom_permissions: | 
| +      choices.append({ | 
| +          'name': 'Restrict-%s-%s' % (action, perm), | 
| +          'doc': 'Permission %s needed to use %s' % (perm, action), | 
| +          }) | 
| + | 
| +  return choices | 
|  |