Project import generated by Copybara.

GitOrigin-RevId: d9e9e3fb4e31372ec1fb43b178994ca78fa8fe70
diff --git a/project/peoplelist.py b/project/peoplelist.py
new file mode 100644
index 0000000..0db5ee6
--- /dev/null
+++ b/project/peoplelist.py
@@ -0,0 +1,234 @@
+# 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 a paginated list of project members.
+
+This page lists owners, members, and contribtors.  For each
+member, we display their username, permission system role + extra
+perms, and notes on their involvement in the project.
+"""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import logging
+import time
+
+import ezt
+
+from businesslogic import work_env
+from framework import framework_bizobj
+from framework import framework_constants
+from framework import framework_helpers
+from framework import framework_views
+from framework import paginate
+from framework import permissions
+from framework import servlet
+from framework import urls
+from project import project_helpers
+from project import project_views
+
+MEMBERS_PER_PAGE = 50
+
+
+class PeopleList(servlet.Servlet):
+  """People list page shows a paginatied list of project members."""
+
+  _PAGE_TEMPLATE = 'project/people-list-page.ezt'
+  _MAIN_TAB_MODE = servlet.Servlet.MAIN_TAB_PEOPLE
+
+  def AssertBasePermission(self, mr):
+    super(PeopleList, self).AssertBasePermission(mr)
+    # 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):
+      raise permissions.PermissionException(
+          'User is not allowed to view the project people list')
+
+  def GatherPageData(self, mr):
+    """Build up a dictionary of data values to use when rendering the page."""
+    all_members = (mr.project.owner_ids +
+                   mr.project.committer_ids +
+                   mr.project.contributor_ids)
+
+    with mr.profiler.Phase('gathering members on this page'):
+      users_by_id = framework_views.MakeAllUserViews(
+          mr.cnxn, self.services.user, all_members)
+      framework_views.RevealAllEmailsToMembers(
+          mr.cnxn, self.services, mr.auth, users_by_id, mr.project)
+
+    # TODO(jrobbins): re-implement FindUntrustedGroups()
+    untrusted_user_group_proxies = []
+
+    with mr.profiler.Phase('gathering commitments (notes)'):
+      project_commitments = self.services.project.GetProjectCommitments(
+          mr.cnxn, mr.project_id)
+
+    with mr.profiler.Phase('gathering autocomple exclusion ids'):
+      group_ids = set(self.services.usergroup.DetermineWhichUserIDsAreGroups(
+        mr.cnxn, all_members))
+      (ac_exclusion_ids, no_expand_ids
+       ) = self.services.project.GetProjectAutocompleteExclusion(
+          mr.cnxn, mr.project_id)
+
+    with mr.profiler.Phase('making member views'):
+      owner_views = self._MakeMemberViews(
+          mr.auth.user_id, users_by_id, mr.project.owner_ids, mr.project,
+          project_commitments, ac_exclusion_ids, no_expand_ids, group_ids)
+      committer_views = self._MakeMemberViews(
+          mr.auth.user_id, users_by_id, mr.project.committer_ids, mr.project,
+          project_commitments, ac_exclusion_ids, no_expand_ids, group_ids)
+      contributor_views = self._MakeMemberViews(
+          mr.auth.user_id, users_by_id, mr.project.contributor_ids, mr.project,
+          project_commitments, ac_exclusion_ids, no_expand_ids, group_ids)
+      all_member_views = owner_views + committer_views + contributor_views
+
+    url_params = [(name, mr.GetParam(name)) for name in
+                  framework_helpers.RECOGNIZED_PARAMS]
+    pagination = paginate.ArtifactPagination(
+        all_member_views, mr.GetPositiveIntParam('num', MEMBERS_PER_PAGE),
+        mr.GetPositiveIntParam('start'), mr.project_name, urls.PEOPLE_LIST,
+        url_params=url_params)
+
+    offer_membership_editing = mr.perms.HasPerm(
+        permissions.EDIT_PROJECT, mr.auth.user_id, mr.project)
+
+    check_abandonment = permissions.ShouldCheckForAbandonment(mr)
+
+    newly_added_views = [mv for mv in all_member_views
+                         if str(mv.user.user_id) in mr.GetParam('new', [])]
+
+    return {
+        'pagination': pagination,
+        'subtab_mode': None,
+        'offer_membership_editing': ezt.boolean(offer_membership_editing),
+        'initial_add_members': '',
+        'initially_expand_form': ezt.boolean(False),
+        'untrusted_user_groups': untrusted_user_group_proxies,
+        'check_abandonment': ezt.boolean(check_abandonment),
+        'total_num_owners': len(mr.project.owner_ids),
+        'newly_added_views': newly_added_views,
+        'is_hotlist': ezt.boolean(False),
+        }
+
+  def GatherHelpData(self, mr, page_data):
+    """Return a dict of values to drive on-page user help.
+
+    Args:
+      mr: common information parsed from the HTTP request.
+      page_data: Dictionary of base and page template data.
+
+    Returns:
+      A dict of values to drive on-page user help, to be added to page_data.
+    """
+    help_data = super(PeopleList, self).GatherHelpData(mr, page_data)
+    with work_env.WorkEnv(mr, self.services) as we:
+      userprefs = we.GetUserPrefs(mr.auth.user_id)
+    dismissed = [
+        pv.name for pv in userprefs.prefs if pv.value == 'true']
+    if (mr.auth.user_id and
+        not framework_bizobj.UserIsInProject(
+            mr.project, mr.auth.effective_ids) and
+        'how_to_join_project' not in dismissed):
+      help_data['cue'] = 'how_to_join_project'
+
+    return help_data
+
+  def _MakeMemberViews(
+      self, logged_in_user_id, users_by_id, member_ids, project,
+      project_commitments, ac_exclusion_ids, no_expand_ids, group_ids):
+    """Return a sorted list of MemberViews for display by EZT."""
+    member_views = [
+        project_views.MemberView(
+            logged_in_user_id, member_id, users_by_id[member_id], 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))
+        for member_id in member_ids]
+    member_views.sort(key=lambda mv: mv.user.email)
+    return member_views
+
+  def ProcessFormData(self, mr, post_data):
+    """Process the posted form."""
+    permit_edit = mr.perms.HasPerm(
+        permissions.EDIT_PROJECT, mr.auth.user_id, mr.project)
+    if not permit_edit:
+      raise permissions.PermissionException(
+          'User is not permitted to edit project membership')
+
+    if 'addbtn' in post_data:
+      return self.ProcessAddMembers(mr, post_data)
+    elif 'removebtn' in post_data:
+      return self.ProcessRemoveMembers(mr, post_data)
+
+  def ProcessAddMembers(self, mr, post_data):
+    """Process the user's request to add members.
+
+    Args:
+      mr: common information parsed from the HTTP request.
+      post_data: dictionary of form data.
+
+    Returns:
+      String URL to redirect the user to after processing.
+    """
+    # 1. Parse and validate user input.
+    new_member_ids = project_helpers.ParseUsernames(
+        mr.cnxn, self.services.user, post_data.get('addmembers'))
+    role = post_data['role']
+
+    (owner_ids, committer_ids,
+     contributor_ids) = project_helpers.MembersWithGivenIDs(
+        mr.project, new_member_ids, role)
+
+    total_people = len(owner_ids) + len(committer_ids) + len(contributor_ids)
+    if total_people > framework_constants.MAX_PROJECT_PEOPLE:
+      mr.errors.addmembers = (
+          'Too many project members.  The combined limit is %d.' %
+          framework_constants.MAX_PROJECT_PEOPLE)
+
+    # 2. Call services layer to save changes.
+    if not mr.errors.AnyErrors():
+      self.services.project.UpdateProjectRoles(
+          mr.cnxn, mr.project.project_id,
+          owner_ids, committer_ids, contributor_ids)
+
+    # 3. Determine the next page in the UI flow.
+    if mr.errors.AnyErrors():
+      add_members_str = post_data.get('addmembers', '')
+      self.PleaseCorrect(
+          mr, initial_add_members=add_members_str, initially_expand_form=True)
+    else:
+      return framework_helpers.FormatAbsoluteURL(
+          mr, urls.PEOPLE_LIST, saved=1, ts=int(time.time()),
+          new=','.join([str(u) for u in new_member_ids]))
+
+  def ProcessRemoveMembers(self, mr, post_data):
+    """Process the user's request to remove members.
+
+    Args:
+      mr: common information parsed from the HTTP request.
+      post_data: dictionary of form data.
+
+    Returns:
+      String URL to redirect the user to after processing.
+    """
+    # 1. Parse and validate user input.
+    remove_strs = post_data.getall('remove')
+    logging.info('remove_strs = %r', remove_strs)
+    remove_ids = set(
+        self.services.user.LookupUserIDs(mr.cnxn, remove_strs).values())
+    (owner_ids, committer_ids,
+     contributor_ids) = project_helpers.MembersWithoutGivenIDs(
+        mr.project, remove_ids)
+
+    # 2. Call services layer to save changes.
+    self.services.project.UpdateProjectRoles(
+        mr.cnxn, mr.project.project_id, owner_ids, committer_ids,
+        contributor_ids)
+
+    # 3. Determine the next page in the UI flow.
+    return framework_helpers.FormatAbsoluteURL(
+        mr, urls.PEOPLE_LIST, saved=1, ts=int(time.time()))