| 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
|
|
|