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