blob: 3c4846b1980596121579234f2fcfb2cfb075f10a [file] [log] [blame]
# Copyright 2016 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style
# license that can be found in the LICENSE file or at
# https://developers.google.com/open-source/licenses/bsd
"""A class to display details about each project member."""
from __future__ import print_function
from __future__ import division
from __future__ import absolute_import
import logging
import time
import ezt
from framework import exceptions
from framework import framework_bizobj
from framework import framework_helpers
from framework import framework_views
from framework import jsonfeed
from framework import permissions
from framework import servlet
from framework import template_helpers
from framework import urls
from project import project_helpers
from project import project_views
CHECKBOX_PERMS = [
permissions.VIEW,
permissions.COMMIT,
permissions.CREATE_ISSUE,
permissions.ADD_ISSUE_COMMENT,
permissions.EDIT_ISSUE,
permissions.EDIT_ISSUE_OWNER,
permissions.EDIT_ISSUE_SUMMARY,
permissions.EDIT_ISSUE_STATUS,
permissions.EDIT_ISSUE_CC,
permissions.DELETE_ISSUE,
permissions.DELETE_OWN,
permissions.DELETE_ANY,
permissions.EDIT_ANY_MEMBER_NOTES,
permissions.MODERATE_SPAM,
]
class PeopleDetail(servlet.Servlet):
"""People detail page documents one partipant's involvement in a project."""
_PAGE_TEMPLATE = 'project/people-detail-page.ezt'
_MAIN_TAB_MODE = servlet.Servlet.MAIN_TAB_PEOPLE
def AssertBasePermission(self, mr):
"""Check that the user is allowed to access this servlet."""
super(PeopleDetail, self).AssertBasePermission(mr)
member_id = self.ValidateMemberID(mr.cnxn, mr.specified_user_id, mr.project)
# For now, contributors who cannot view other contributors are further
# restricted from viewing any part of the member list or detail pages.
if (not permissions.CanViewContributorList(mr, mr.project) and
member_id != mr.auth.user_id):
raise permissions.PermissionException(
'User is not allowed to view other people\'s details')
def GatherPageData(self, mr):
"""Build up a dictionary of data values to use when rendering the page."""
member_id = self.ValidateMemberID(mr.cnxn, mr.specified_user_id, mr.project)
group_ids = self.services.usergroup.DetermineWhichUserIDsAreGroups(
mr.cnxn, [member_id])
users_by_id = framework_views.MakeAllUserViews(
mr.cnxn, self.services.user, [member_id])
framework_views.RevealAllEmailsToMembers(
mr.cnxn, self.services, mr.auth, users_by_id, mr.project)
project_commitments = self.services.project.GetProjectCommitments(
mr.cnxn, mr.project_id)
(ac_exclusion_ids, no_expand_ids
) = self.services.project.GetProjectAutocompleteExclusion(
mr.cnxn, mr.project_id)
member_view = project_views.MemberView(
mr.auth.user_id, member_id, users_by_id[member_id], mr.project,
project_commitments,
ac_exclusion=(member_id in ac_exclusion_ids),
no_expand=(member_id in no_expand_ids),
is_group=(member_id in group_ids))
member_user = self.services.user.GetUser(mr.cnxn, member_id)
# This ignores indirect memberships, which is ok because we are viewing
# the page for a member directly involved in the project
role_perms = permissions.GetPermissions(
member_user, {member_id}, mr.project)
# TODO(jrobbins): clarify in the UI which permissions are built-in to
# the user's direct role, vs. which are granted via a group membership,
# vs. which ones are extra_perms that have been added specifically for
# this user.
member_perms = template_helpers.EZTItem()
for perm in CHECKBOX_PERMS:
setattr(member_perms, perm,
ezt.boolean(role_perms.HasPerm(perm, member_id, mr.project)))
displayed_extra_perms = [perm for perm in member_view.extra_perms
if perm not in CHECKBOX_PERMS]
viewing_self = mr.auth.user_id == member_id
warn_abandonment = (viewing_self and
permissions.ShouldCheckForAbandonment(mr))
return {
'subtab_mode': None,
'member': member_view,
'role_perms': role_perms,
'member_perms': member_perms,
'displayed_extra_perms': displayed_extra_perms,
'offer_edit_perms': ezt.boolean(self.CanEditPerms(mr)),
'offer_edit_member_notes': ezt.boolean(
self.CanEditMemberNotes(mr, member_id)),
'offer_remove_role': ezt.boolean(self.CanRemoveRole(mr, member_id)),
'expand_perms': ezt.boolean(mr.auth.user_pb.keep_people_perms_open),
'warn_abandonment': ezt.boolean(warn_abandonment),
'total_num_owners': len(mr.project.owner_ids),
}
def ValidateMemberID(self, cnxn, member_id, project):
"""Lookup a project member by user_id.
Args:
cnxn: connection to SQL database.
member_id: int user_id, same format as user profile page.
project: the current Project PB.
Returns:
The user ID of the project member. Raises an exception if the username
cannot be looked up, or if that user is not in the project.
"""
if not member_id:
self.abort(404, 'project member not specified')
member_username = None
try:
member_username = self.services.user.LookupUserEmail(cnxn, member_id)
except exceptions.NoSuchUserException:
logging.info('user_id %s not found', member_id)
if not member_username:
logging.info('There is no such user id %r', member_id)
self.abort(404, 'project member not found')
if not framework_bizobj.UserIsInProject(project, {member_id}):
logging.info('User %r is not a member of %r',
member_username, project.project_name)
self.abort(404, 'project member not found')
return member_id
def ProcessFormData(self, mr, post_data):
"""Process the posted form."""
# 1. Parse and validate user input.
user_id, role, extra_perms, notes, ac_exclusion, no_expand = (
self.ParsePersonData(mr, post_data))
member_id = self.ValidateMemberID(mr.cnxn, user_id, mr.project)
# 2. Call services layer to save changes.
if 'remove' in post_data:
self.ProcessRemove(mr, member_id)
else:
self.ProcessSave(
mr, role, extra_perms, notes, member_id, ac_exclusion, no_expand)
# 3. Determine the next page in the UI flow.
if 'remove' in post_data:
return framework_helpers.FormatAbsoluteURL(
mr, urls.PEOPLE_LIST, saved=1, ts=int(time.time()))
else:
return framework_helpers.FormatAbsoluteURL(
mr, urls.PEOPLE_DETAIL, u=user_id, saved=1, ts=int(time.time()))
def ProcessRemove(self, mr, member_id):
"""Process the posted form when the user pressed 'Remove'."""
if not self.CanRemoveRole(mr, member_id):
raise permissions.PermissionException(
'User is not allowed to remove this member from the project')
self.RemoveRole(mr.cnxn, mr.project, member_id)
def ProcessSave(
self, mr, role, extra_perms, notes, member_id, ac_exclusion,
no_expand):
"""Process the posted form when the user pressed 'Save'."""
if (not self.CanEditPerms(mr) and
not self.CanEditMemberNotes(mr, member_id)):
raise permissions.PermissionException(
'User is not allowed to edit people in this project')
if self.CanEditPerms(mr):
self.services.project.UpdateExtraPerms(
mr.cnxn, mr.project_id, member_id, extra_perms)
self.UpdateRole(mr.cnxn, mr.project, role, member_id)
if self.CanEditMemberNotes(mr, member_id):
self.services.project.UpdateCommitments(
mr.cnxn, mr.project_id, member_id, notes)
if self.CanEditPerms(mr):
self.services.project.UpdateProjectAutocompleteExclusion(
mr.cnxn, mr.project_id, member_id, ac_exclusion, no_expand)
def CanEditMemberNotes(self, mr, member_id):
"""Return true if the logged in user can edit the current user's notes."""
return (self.CheckPerm(mr, permissions.EDIT_ANY_MEMBER_NOTES) or
member_id == mr.auth.user_id)
def CanEditPerms(self, mr):
"""Return true if the logged in user can edit the current user's perms."""
return self.CheckPerm(mr, permissions.EDIT_PROJECT)
def CanRemoveRole(self, mr, member_id):
"""Return true if the logged in user can remove the current user's role."""
return (self.CheckPerm(mr, permissions.EDIT_PROJECT) or
member_id == mr.auth.user_id)
def ParsePersonData(self, mr, post_data):
"""Parse the POST data for a project member.
Args:
mr: common information parsed from the user's request.
post_data: dictionary of lists of values for each HTML
form field.
Returns:
A tuple with user_id, role, extra_perms, and notes.
"""
if not mr.specified_user_id:
raise exceptions.InputException('Field user_id is missing')
role = post_data.get('role', '').lower()
extra_perms = []
for ep in post_data.getall('extra_perms'):
perm = framework_bizobj.CanonicalizeLabel(ep)
# Perms with leading underscores are reserved.
perm = perm.strip('_')
if perm:
extra_perms.append(perm)
notes = post_data.get('notes', '').strip()
ac_exclusion = not post_data.get('ac_include', False)
no_expand = not post_data.get('ac_expand', False)
return (mr.specified_user_id, role, extra_perms, notes, ac_exclusion,
no_expand)
def RemoveRole(self, cnxn, project, member_id):
"""Remove the given member from the project."""
(owner_ids, committer_ids,
contributor_ids) = project_helpers.MembersWithoutGivenIDs(
project, {member_id})
self.services.project.UpdateProjectRoles(
cnxn, project.project_id, owner_ids, committer_ids, contributor_ids)
def UpdateRole(self, cnxn, project, role, member_id):
"""If the user's role was changed, update that in the Project."""
if not role:
return # Role was not in the form data
if role == framework_helpers.GetRoleName({member_id}, project).lower():
return # No change needed
(owner_ids, committer_ids,
contributor_ids) = project_helpers.MembersWithGivenIDs(
project, {member_id}, role)
self.services.project.UpdateProjectRoles(
cnxn, project.project_id, owner_ids, committer_ids, contributor_ids)