blob: 0db5ee648af2b8c8b59f77ce055cd3f0fda17b98 [file] [log] [blame]
Copybara854996b2021-09-07 19:36:02 +00001# Copyright 2016 The Chromium Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style
3# license that can be found in the LICENSE file or at
4# https://developers.google.com/open-source/licenses/bsd
5
6"""A class to display a paginated list of project members.
7
8This page lists owners, members, and contribtors. For each
9member, we display their username, permission system role + extra
10perms, and notes on their involvement in the project.
11"""
12from __future__ import print_function
13from __future__ import division
14from __future__ import absolute_import
15
16import logging
17import time
18
19import ezt
20
21from businesslogic import work_env
22from framework import framework_bizobj
23from framework import framework_constants
24from framework import framework_helpers
25from framework import framework_views
26from framework import paginate
27from framework import permissions
28from framework import servlet
29from framework import urls
30from project import project_helpers
31from project import project_views
32
33MEMBERS_PER_PAGE = 50
34
35
36class PeopleList(servlet.Servlet):
37 """People list page shows a paginatied list of project members."""
38
39 _PAGE_TEMPLATE = 'project/people-list-page.ezt'
40 _MAIN_TAB_MODE = servlet.Servlet.MAIN_TAB_PEOPLE
41
42 def AssertBasePermission(self, mr):
43 super(PeopleList, self).AssertBasePermission(mr)
44 # For now, contributors who cannot view other contributors are further
45 # restricted from viewing any part of the member list or detail pages.
46 if not permissions.CanViewContributorList(mr, mr.project):
47 raise permissions.PermissionException(
48 'User is not allowed to view the project people list')
49
50 def GatherPageData(self, mr):
51 """Build up a dictionary of data values to use when rendering the page."""
52 all_members = (mr.project.owner_ids +
53 mr.project.committer_ids +
54 mr.project.contributor_ids)
55
56 with mr.profiler.Phase('gathering members on this page'):
57 users_by_id = framework_views.MakeAllUserViews(
58 mr.cnxn, self.services.user, all_members)
59 framework_views.RevealAllEmailsToMembers(
60 mr.cnxn, self.services, mr.auth, users_by_id, mr.project)
61
62 # TODO(jrobbins): re-implement FindUntrustedGroups()
63 untrusted_user_group_proxies = []
64
65 with mr.profiler.Phase('gathering commitments (notes)'):
66 project_commitments = self.services.project.GetProjectCommitments(
67 mr.cnxn, mr.project_id)
68
69 with mr.profiler.Phase('gathering autocomple exclusion ids'):
70 group_ids = set(self.services.usergroup.DetermineWhichUserIDsAreGroups(
71 mr.cnxn, all_members))
72 (ac_exclusion_ids, no_expand_ids
73 ) = self.services.project.GetProjectAutocompleteExclusion(
74 mr.cnxn, mr.project_id)
75
76 with mr.profiler.Phase('making member views'):
77 owner_views = self._MakeMemberViews(
78 mr.auth.user_id, users_by_id, mr.project.owner_ids, mr.project,
79 project_commitments, ac_exclusion_ids, no_expand_ids, group_ids)
80 committer_views = self._MakeMemberViews(
81 mr.auth.user_id, users_by_id, mr.project.committer_ids, mr.project,
82 project_commitments, ac_exclusion_ids, no_expand_ids, group_ids)
83 contributor_views = self._MakeMemberViews(
84 mr.auth.user_id, users_by_id, mr.project.contributor_ids, mr.project,
85 project_commitments, ac_exclusion_ids, no_expand_ids, group_ids)
86 all_member_views = owner_views + committer_views + contributor_views
87
88 url_params = [(name, mr.GetParam(name)) for name in
89 framework_helpers.RECOGNIZED_PARAMS]
90 pagination = paginate.ArtifactPagination(
91 all_member_views, mr.GetPositiveIntParam('num', MEMBERS_PER_PAGE),
92 mr.GetPositiveIntParam('start'), mr.project_name, urls.PEOPLE_LIST,
93 url_params=url_params)
94
95 offer_membership_editing = mr.perms.HasPerm(
96 permissions.EDIT_PROJECT, mr.auth.user_id, mr.project)
97
98 check_abandonment = permissions.ShouldCheckForAbandonment(mr)
99
100 newly_added_views = [mv for mv in all_member_views
101 if str(mv.user.user_id) in mr.GetParam('new', [])]
102
103 return {
104 'pagination': pagination,
105 'subtab_mode': None,
106 'offer_membership_editing': ezt.boolean(offer_membership_editing),
107 'initial_add_members': '',
108 'initially_expand_form': ezt.boolean(False),
109 'untrusted_user_groups': untrusted_user_group_proxies,
110 'check_abandonment': ezt.boolean(check_abandonment),
111 'total_num_owners': len(mr.project.owner_ids),
112 'newly_added_views': newly_added_views,
113 'is_hotlist': ezt.boolean(False),
114 }
115
116 def GatherHelpData(self, mr, page_data):
117 """Return a dict of values to drive on-page user help.
118
119 Args:
120 mr: common information parsed from the HTTP request.
121 page_data: Dictionary of base and page template data.
122
123 Returns:
124 A dict of values to drive on-page user help, to be added to page_data.
125 """
126 help_data = super(PeopleList, self).GatherHelpData(mr, page_data)
127 with work_env.WorkEnv(mr, self.services) as we:
128 userprefs = we.GetUserPrefs(mr.auth.user_id)
129 dismissed = [
130 pv.name for pv in userprefs.prefs if pv.value == 'true']
131 if (mr.auth.user_id and
132 not framework_bizobj.UserIsInProject(
133 mr.project, mr.auth.effective_ids) and
134 'how_to_join_project' not in dismissed):
135 help_data['cue'] = 'how_to_join_project'
136
137 return help_data
138
139 def _MakeMemberViews(
140 self, logged_in_user_id, users_by_id, member_ids, project,
141 project_commitments, ac_exclusion_ids, no_expand_ids, group_ids):
142 """Return a sorted list of MemberViews for display by EZT."""
143 member_views = [
144 project_views.MemberView(
145 logged_in_user_id, member_id, users_by_id[member_id], project,
146 project_commitments,
147 ac_exclusion=(member_id in ac_exclusion_ids),
148 no_expand=(member_id in no_expand_ids),
149 is_group=(member_id in group_ids))
150 for member_id in member_ids]
151 member_views.sort(key=lambda mv: mv.user.email)
152 return member_views
153
154 def ProcessFormData(self, mr, post_data):
155 """Process the posted form."""
156 permit_edit = mr.perms.HasPerm(
157 permissions.EDIT_PROJECT, mr.auth.user_id, mr.project)
158 if not permit_edit:
159 raise permissions.PermissionException(
160 'User is not permitted to edit project membership')
161
162 if 'addbtn' in post_data:
163 return self.ProcessAddMembers(mr, post_data)
164 elif 'removebtn' in post_data:
165 return self.ProcessRemoveMembers(mr, post_data)
166
167 def ProcessAddMembers(self, mr, post_data):
168 """Process the user's request to add members.
169
170 Args:
171 mr: common information parsed from the HTTP request.
172 post_data: dictionary of form data.
173
174 Returns:
175 String URL to redirect the user to after processing.
176 """
177 # 1. Parse and validate user input.
178 new_member_ids = project_helpers.ParseUsernames(
179 mr.cnxn, self.services.user, post_data.get('addmembers'))
180 role = post_data['role']
181
182 (owner_ids, committer_ids,
183 contributor_ids) = project_helpers.MembersWithGivenIDs(
184 mr.project, new_member_ids, role)
185
186 total_people = len(owner_ids) + len(committer_ids) + len(contributor_ids)
187 if total_people > framework_constants.MAX_PROJECT_PEOPLE:
188 mr.errors.addmembers = (
189 'Too many project members. The combined limit is %d.' %
190 framework_constants.MAX_PROJECT_PEOPLE)
191
192 # 2. Call services layer to save changes.
193 if not mr.errors.AnyErrors():
194 self.services.project.UpdateProjectRoles(
195 mr.cnxn, mr.project.project_id,
196 owner_ids, committer_ids, contributor_ids)
197
198 # 3. Determine the next page in the UI flow.
199 if mr.errors.AnyErrors():
200 add_members_str = post_data.get('addmembers', '')
201 self.PleaseCorrect(
202 mr, initial_add_members=add_members_str, initially_expand_form=True)
203 else:
204 return framework_helpers.FormatAbsoluteURL(
205 mr, urls.PEOPLE_LIST, saved=1, ts=int(time.time()),
206 new=','.join([str(u) for u in new_member_ids]))
207
208 def ProcessRemoveMembers(self, mr, post_data):
209 """Process the user's request to remove members.
210
211 Args:
212 mr: common information parsed from the HTTP request.
213 post_data: dictionary of form data.
214
215 Returns:
216 String URL to redirect the user to after processing.
217 """
218 # 1. Parse and validate user input.
219 remove_strs = post_data.getall('remove')
220 logging.info('remove_strs = %r', remove_strs)
221 remove_ids = set(
222 self.services.user.LookupUserIDs(mr.cnxn, remove_strs).values())
223 (owner_ids, committer_ids,
224 contributor_ids) = project_helpers.MembersWithoutGivenIDs(
225 mr.project, remove_ids)
226
227 # 2. Call services layer to save changes.
228 self.services.project.UpdateProjectRoles(
229 mr.cnxn, mr.project.project_id, owner_ids, committer_ids,
230 contributor_ids)
231
232 # 3. Determine the next page in the UI flow.
233 return framework_helpers.FormatAbsoluteURL(
234 mr, urls.PEOPLE_LIST, saved=1, ts=int(time.time()))