blob: f88820822084e68c9c16f5c8680cd6aea0753539 [file] [log] [blame]
# 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)