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