blob: 3c4846b1980596121579234f2fcfb2cfb075f10a [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 details about each project member."""
7from __future__ import print_function
8from __future__ import division
9from __future__ import absolute_import
10
11import logging
12import time
13
14import ezt
15
16from framework import exceptions
17from framework import framework_bizobj
18from framework import framework_helpers
19from framework import framework_views
20from framework import jsonfeed
21from framework import permissions
22from framework import servlet
23from framework import template_helpers
24from framework import urls
25from project import project_helpers
26from project import project_views
27
28CHECKBOX_PERMS = [
29 permissions.VIEW,
30 permissions.COMMIT,
31 permissions.CREATE_ISSUE,
32 permissions.ADD_ISSUE_COMMENT,
33 permissions.EDIT_ISSUE,
34 permissions.EDIT_ISSUE_OWNER,
35 permissions.EDIT_ISSUE_SUMMARY,
36 permissions.EDIT_ISSUE_STATUS,
37 permissions.EDIT_ISSUE_CC,
38 permissions.DELETE_ISSUE,
39 permissions.DELETE_OWN,
40 permissions.DELETE_ANY,
41 permissions.EDIT_ANY_MEMBER_NOTES,
42 permissions.MODERATE_SPAM,
43 ]
44
45
46class PeopleDetail(servlet.Servlet):
47 """People detail page documents one partipant's involvement in a project."""
48
49 _PAGE_TEMPLATE = 'project/people-detail-page.ezt'
50 _MAIN_TAB_MODE = servlet.Servlet.MAIN_TAB_PEOPLE
51
52 def AssertBasePermission(self, mr):
53 """Check that the user is allowed to access this servlet."""
54 super(PeopleDetail, self).AssertBasePermission(mr)
55 member_id = self.ValidateMemberID(mr.cnxn, mr.specified_user_id, mr.project)
56 # For now, contributors who cannot view other contributors are further
57 # restricted from viewing any part of the member list or detail pages.
58 if (not permissions.CanViewContributorList(mr, mr.project) and
59 member_id != mr.auth.user_id):
60 raise permissions.PermissionException(
61 'User is not allowed to view other people\'s details')
62
63 def GatherPageData(self, mr):
64 """Build up a dictionary of data values to use when rendering the page."""
65
66 member_id = self.ValidateMemberID(mr.cnxn, mr.specified_user_id, mr.project)
67 group_ids = self.services.usergroup.DetermineWhichUserIDsAreGroups(
68 mr.cnxn, [member_id])
69 users_by_id = framework_views.MakeAllUserViews(
70 mr.cnxn, self.services.user, [member_id])
71 framework_views.RevealAllEmailsToMembers(
72 mr.cnxn, self.services, mr.auth, users_by_id, mr.project)
73
74 project_commitments = self.services.project.GetProjectCommitments(
75 mr.cnxn, mr.project_id)
76 (ac_exclusion_ids, no_expand_ids
77 ) = self.services.project.GetProjectAutocompleteExclusion(
78 mr.cnxn, mr.project_id)
79 member_view = project_views.MemberView(
80 mr.auth.user_id, member_id, users_by_id[member_id], mr.project,
81 project_commitments,
82 ac_exclusion=(member_id in ac_exclusion_ids),
83 no_expand=(member_id in no_expand_ids),
84 is_group=(member_id in group_ids))
85
86 member_user = self.services.user.GetUser(mr.cnxn, member_id)
87 # This ignores indirect memberships, which is ok because we are viewing
88 # the page for a member directly involved in the project
89 role_perms = permissions.GetPermissions(
90 member_user, {member_id}, mr.project)
91
92 # TODO(jrobbins): clarify in the UI which permissions are built-in to
93 # the user's direct role, vs. which are granted via a group membership,
94 # vs. which ones are extra_perms that have been added specifically for
95 # this user.
96 member_perms = template_helpers.EZTItem()
97 for perm in CHECKBOX_PERMS:
98 setattr(member_perms, perm,
99 ezt.boolean(role_perms.HasPerm(perm, member_id, mr.project)))
100
101 displayed_extra_perms = [perm for perm in member_view.extra_perms
102 if perm not in CHECKBOX_PERMS]
103
104 viewing_self = mr.auth.user_id == member_id
105 warn_abandonment = (viewing_self and
106 permissions.ShouldCheckForAbandonment(mr))
107
108 return {
109 'subtab_mode': None,
110 'member': member_view,
111 'role_perms': role_perms,
112 'member_perms': member_perms,
113 'displayed_extra_perms': displayed_extra_perms,
114 'offer_edit_perms': ezt.boolean(self.CanEditPerms(mr)),
115 'offer_edit_member_notes': ezt.boolean(
116 self.CanEditMemberNotes(mr, member_id)),
117 'offer_remove_role': ezt.boolean(self.CanRemoveRole(mr, member_id)),
118 'expand_perms': ezt.boolean(mr.auth.user_pb.keep_people_perms_open),
119 'warn_abandonment': ezt.boolean(warn_abandonment),
120 'total_num_owners': len(mr.project.owner_ids),
121 }
122
123 def ValidateMemberID(self, cnxn, member_id, project):
124 """Lookup a project member by user_id.
125
126 Args:
127 cnxn: connection to SQL database.
128 member_id: int user_id, same format as user profile page.
129 project: the current Project PB.
130
131 Returns:
132 The user ID of the project member. Raises an exception if the username
133 cannot be looked up, or if that user is not in the project.
134 """
135 if not member_id:
136 self.abort(404, 'project member not specified')
137
138 member_username = None
139 try:
140 member_username = self.services.user.LookupUserEmail(cnxn, member_id)
141 except exceptions.NoSuchUserException:
142 logging.info('user_id %s not found', member_id)
143
144 if not member_username:
145 logging.info('There is no such user id %r', member_id)
146 self.abort(404, 'project member not found')
147
148 if not framework_bizobj.UserIsInProject(project, {member_id}):
149 logging.info('User %r is not a member of %r',
150 member_username, project.project_name)
151 self.abort(404, 'project member not found')
152
153 return member_id
154
155 def ProcessFormData(self, mr, post_data):
156 """Process the posted form."""
157 # 1. Parse and validate user input.
158 user_id, role, extra_perms, notes, ac_exclusion, no_expand = (
159 self.ParsePersonData(mr, post_data))
160 member_id = self.ValidateMemberID(mr.cnxn, user_id, mr.project)
161
162 # 2. Call services layer to save changes.
163 if 'remove' in post_data:
164 self.ProcessRemove(mr, member_id)
165 else:
166 self.ProcessSave(
167 mr, role, extra_perms, notes, member_id, ac_exclusion, no_expand)
168
169 # 3. Determine the next page in the UI flow.
170 if 'remove' in post_data:
171 return framework_helpers.FormatAbsoluteURL(
172 mr, urls.PEOPLE_LIST, saved=1, ts=int(time.time()))
173 else:
174 return framework_helpers.FormatAbsoluteURL(
175 mr, urls.PEOPLE_DETAIL, u=user_id, saved=1, ts=int(time.time()))
176
177 def ProcessRemove(self, mr, member_id):
178 """Process the posted form when the user pressed 'Remove'."""
179 if not self.CanRemoveRole(mr, member_id):
180 raise permissions.PermissionException(
181 'User is not allowed to remove this member from the project')
182
183 self.RemoveRole(mr.cnxn, mr.project, member_id)
184
185 def ProcessSave(
186 self, mr, role, extra_perms, notes, member_id, ac_exclusion,
187 no_expand):
188 """Process the posted form when the user pressed 'Save'."""
189 if (not self.CanEditPerms(mr) and
190 not self.CanEditMemberNotes(mr, member_id)):
191 raise permissions.PermissionException(
192 'User is not allowed to edit people in this project')
193
194 if self.CanEditPerms(mr):
195 self.services.project.UpdateExtraPerms(
196 mr.cnxn, mr.project_id, member_id, extra_perms)
197 self.UpdateRole(mr.cnxn, mr.project, role, member_id)
198
199 if self.CanEditMemberNotes(mr, member_id):
200 self.services.project.UpdateCommitments(
201 mr.cnxn, mr.project_id, member_id, notes)
202
203 if self.CanEditPerms(mr):
204 self.services.project.UpdateProjectAutocompleteExclusion(
205 mr.cnxn, mr.project_id, member_id, ac_exclusion, no_expand)
206
207 def CanEditMemberNotes(self, mr, member_id):
208 """Return true if the logged in user can edit the current user's notes."""
209 return (self.CheckPerm(mr, permissions.EDIT_ANY_MEMBER_NOTES) or
210 member_id == mr.auth.user_id)
211
212 def CanEditPerms(self, mr):
213 """Return true if the logged in user can edit the current user's perms."""
214 return self.CheckPerm(mr, permissions.EDIT_PROJECT)
215
216 def CanRemoveRole(self, mr, member_id):
217 """Return true if the logged in user can remove the current user's role."""
218 return (self.CheckPerm(mr, permissions.EDIT_PROJECT) or
219 member_id == mr.auth.user_id)
220
221 def ParsePersonData(self, mr, post_data):
222 """Parse the POST data for a project member.
223
224 Args:
225 mr: common information parsed from the user's request.
226 post_data: dictionary of lists of values for each HTML
227 form field.
228
229 Returns:
230 A tuple with user_id, role, extra_perms, and notes.
231 """
232 if not mr.specified_user_id:
233 raise exceptions.InputException('Field user_id is missing')
234
235 role = post_data.get('role', '').lower()
236 extra_perms = []
237 for ep in post_data.getall('extra_perms'):
238 perm = framework_bizobj.CanonicalizeLabel(ep)
239 # Perms with leading underscores are reserved.
240 perm = perm.strip('_')
241 if perm:
242 extra_perms.append(perm)
243
244 notes = post_data.get('notes', '').strip()
245 ac_exclusion = not post_data.get('ac_include', False)
246 no_expand = not post_data.get('ac_expand', False)
247 return (mr.specified_user_id, role, extra_perms, notes, ac_exclusion,
248 no_expand)
249
250 def RemoveRole(self, cnxn, project, member_id):
251 """Remove the given member from the project."""
252 (owner_ids, committer_ids,
253 contributor_ids) = project_helpers.MembersWithoutGivenIDs(
254 project, {member_id})
255 self.services.project.UpdateProjectRoles(
256 cnxn, project.project_id, owner_ids, committer_ids, contributor_ids)
257
258 def UpdateRole(self, cnxn, project, role, member_id):
259 """If the user's role was changed, update that in the Project."""
260 if not role:
261 return # Role was not in the form data
262
263 if role == framework_helpers.GetRoleName({member_id}, project).lower():
264 return # No change needed
265
266 (owner_ids, committer_ids,
267 contributor_ids) = project_helpers.MembersWithGivenIDs(
268 project, {member_id}, role)
269
270 self.services.project.UpdateProjectRoles(
271 cnxn, project.project_id, owner_ids, committer_ids, contributor_ids)