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)