| # Copyright 2016 The Chromium Authors |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| """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 paginated 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 = permissions.CanEditProjectConfig( |
| mr, self.services) |
| |
| 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 = permissions.CanEditProjectConfig(mr, self.services) |
| 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.getlist('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())) |
| |
| def GetPeopleListPage(self, **kwargs): |
| return self.handler(**kwargs) |
| |
| def PostPeopleListPage(self, **kwargs): |
| return self.handler(**kwargs) |