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