Project import generated by Copybara.
GitOrigin-RevId: d9e9e3fb4e31372ec1fb43b178994ca78fa8fe70
diff --git a/project/peopledetail.py b/project/peopledetail.py
new file mode 100644
index 0000000..3c4846b
--- /dev/null
+++ b/project/peopledetail.py
@@ -0,0 +1,271 @@
+# 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)