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