Index: appengine/monorail/project/peoplelist.py |
diff --git a/appengine/monorail/project/peoplelist.py b/appengine/monorail/project/peoplelist.py |
new file mode 100644 |
index 0000000000000000000000000000000000000000..bc6dac20158ffd9acbe80d13f073002e106de344 |
--- /dev/null |
+++ b/appengine/monorail/project/peoplelist.py |
@@ -0,0 +1,203 @@ |
+# 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 |
+ |
+"""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. |
+""" |
+ |
+import logging |
+import time |
+ |
+from third_party import ezt |
+ |
+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): |
+ 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 self.profiler.Phase('gathering members on this page'): |
+ users_by_id = framework_views.MakeAllUserViews( |
+ mr.cnxn, self.services.user, all_members) |
+ framework_views.RevealAllEmailsToMembers(mr, users_by_id) |
+ |
+ # TODO(jrobbins): re-implement FindUntrustedGroups() |
+ untrusted_user_group_proxies = [] |
+ |
+ with self.profiler.Phase('gathering commitments (notes)'): |
+ project_commitments = self.services.project.GetProjectCommitments( |
+ mr.cnxn, mr.project_id) |
+ |
+ with self.profiler.Phase('making member views'): |
+ owner_views = self._MakeMemberViews( |
+ mr.auth.user_id, users_by_id, mr.project.owner_ids, mr.project, |
+ project_commitments) |
+ committer_views = self._MakeMemberViews( |
+ mr.auth.user_id, users_by_id, mr.project.committer_ids, mr.project, |
+ project_commitments) |
+ contributor_views = self._MakeMemberViews( |
+ mr.auth.user_id, users_by_id, mr.project.contributor_ids, mr.project, |
+ project_commitments) |
+ all_member_views = owner_views + committer_views + contributor_views |
+ |
+ pagination = paginate.ArtifactPagination( |
+ mr, all_member_views, MEMBERS_PER_PAGE, urls.PEOPLE_LIST) |
+ |
+ offer_membership_editing = mr.perms.HasPerm( |
+ permissions.EDIT_PROJECT, mr.auth.user_id, mr.project) |
+ |
+ check_abandonment = permissions.ShouldCheckForAbandonment(mr) |
+ |
+ 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), |
+ } |
+ |
+ 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. |
+ """ |
+ cue = None |
+ if (mr.auth.user_id and |
+ not framework_bizobj.UserIsInProject( |
+ mr.project, mr.auth.effective_ids) and |
+ 'how_to_join_project' not in mr.auth.user_pb.dismissed_cues): |
+ cue = 'how_to_join_project' |
+ |
+ return {'cue': cue} |
+ |
+ def _MakeMemberViews( |
+ self, logged_in_user_id, users_by_id, member_ids, project, |
+ project_commitments): |
+ """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) |
+ 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.MembersWith( |
+ 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())) |
+ |
+ 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.MembersWithout( |
+ 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())) |