blob: b28baa967480b932cf50010817b680337963fc6a [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 user group, including a paginated list of members."""
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_helpers
18from framework import framework_views
19from framework import paginate
20from framework import permissions
21from framework import servlet
22from project import project_helpers
23from proto import usergroup_pb2
24from sitewide import group_helpers
25from sitewide import sitewide_views
26
27MEMBERS_PER_PAGE = 50
28
29
30class GroupDetail(servlet.Servlet):
31 """The group detail page presents information about one user group."""
32
33 _PAGE_TEMPLATE = 'sitewide/group-detail-page.ezt'
34
35 def AssertBasePermission(self, mr):
36 """Assert that the user has the permissions needed to view this page."""
37 super(GroupDetail, self).AssertBasePermission(mr)
38
39 group_id = mr.viewed_user_auth.user_id
40 group_settings = self.services.usergroup.GetGroupSettings(
41 mr.cnxn, group_id)
42 if not group_settings:
43 return
44
45 member_ids, owner_ids = self.services.usergroup.LookupAllMembers(
46 mr.cnxn, [group_id])
47 (owned_project_ids, membered_project_ids,
48 contrib_project_ids) = self.services.project.GetUserRolesInAllProjects(
49 mr.cnxn, mr.auth.effective_ids)
50 project_ids = owned_project_ids.union(
51 membered_project_ids).union(contrib_project_ids)
52 if not permissions.CanViewGroupMembers(
53 mr.perms, mr.auth.effective_ids, group_settings, member_ids[group_id],
54 owner_ids[group_id], project_ids):
55 raise permissions.PermissionException(
56 'User is not allowed to view a user group')
57
58 def GatherPageData(self, mr):
59 """Build up a dictionary of data values to use when rendering the page."""
60 group_id = mr.viewed_user_auth.user_id
61 group_settings = self.services.usergroup.GetGroupSettings(
62 mr.cnxn, group_id)
63 if not group_settings:
64 raise exceptions.NoSuchGroupException()
65
66 member_ids_dict, owner_ids_dict = (
67 self.services.usergroup.LookupVisibleMembers(
68 mr.cnxn, [group_id], mr.perms, mr.auth.effective_ids,
69 self.services))
70 member_ids = member_ids_dict[group_id]
71 owner_ids = owner_ids_dict[group_id]
72 member_pbs_dict = self.services.user.GetUsersByIDs(
73 mr.cnxn, member_ids)
74 owner_pbs_dict = self.services.user.GetUsersByIDs(
75 mr.cnxn, owner_ids)
76 member_dict = {}
77 for user_id, user_pb in member_pbs_dict.items():
78 member_view = group_helpers.GroupMemberView(user_pb, group_id, 'member')
79 member_dict[user_id] = member_view
80 owner_dict = {}
81 for user_id, user_pb in owner_pbs_dict.items():
82 member_view = group_helpers.GroupMemberView(user_pb, group_id, 'owner')
83 owner_dict[user_id] = member_view
84
85 member_user_views = []
86 member_user_views.extend(
87 sorted(list(owner_dict.values()), key=lambda u: u.email))
88 member_user_views.extend(
89 sorted(list(member_dict.values()), key=lambda u: u.email))
90
91 group_view = sitewide_views.GroupView(
92 mr.viewed_user_auth.email, len(member_ids), group_settings,
93 mr.viewed_user_auth.user_id)
94 url_params = [(name, mr.GetParam(name)) for name in
95 framework_helpers.RECOGNIZED_PARAMS]
96 pagination = paginate.ArtifactPagination(
97 member_user_views, mr.GetPositiveIntParam('num', MEMBERS_PER_PAGE),
98 mr.GetPositiveIntParam('start'), mr.project_name, group_view.detail_url,
99 url_params=url_params)
100
101 is_imported_group = bool(group_settings.ext_group_type)
102
103 offer_membership_editing = permissions.CanEditGroup(
104 mr.perms, mr.auth.effective_ids, owner_ids) and not is_imported_group
105
106 group_type = 'Monorail user group'
107 if group_settings.ext_group_type:
108 group_type = str(group_settings.ext_group_type).capitalize()
109
110 return {
111 'admin_tab_mode': self.ADMIN_TAB_META,
112 'offer_membership_editing': ezt.boolean(offer_membership_editing),
113 'initial_add_members': '',
114 'initially_expand_form': ezt.boolean(False),
115 'groupid': group_id,
116 'groupname': mr.viewed_username,
117 'settings': group_settings,
118 'group_type': group_type,
119 'pagination': pagination,
120 }
121
122 def ProcessFormData(self, mr, post_data):
123 """Process the posted form."""
124 _, owner_ids_dict = self.services.usergroup.LookupMembers(
125 mr.cnxn, [mr.viewed_user_auth.user_id])
126 owner_ids = owner_ids_dict[mr.viewed_user_auth.user_id]
127 permit_edit = permissions.CanEditGroup(
128 mr.perms, mr.auth.effective_ids, owner_ids)
129 if not permit_edit:
130 raise permissions.PermissionException(
131 'User is not permitted to edit group membership')
132
133 group_settings = self.services.usergroup.GetGroupSettings(
134 mr.cnxn, mr.viewed_user_auth.user_id)
135 if bool(group_settings.ext_group_type):
136 raise permissions.PermissionException(
137 'Imported groups are read-only')
138
139 if 'addbtn' in post_data:
140 return self.ProcessAddMembers(mr, post_data)
141 elif 'removebtn' in post_data:
142 return self.ProcessRemoveMembers(mr, post_data)
143
144 def ProcessAddMembers(self, mr, post_data):
145 """Process the user's request to add members.
146
147 Args:
148 mr: common information parsed from the HTTP request.
149 post_data: dictionary of form data.
150
151 Returns:
152 String URL to redirect the user to after processing.
153 """
154 # 1. Gather data from the request.
155 group_id = mr.viewed_user_auth.user_id
156 add_members_str = post_data.get('addmembers')
157 new_member_ids = project_helpers.ParseUsernames(
158 mr.cnxn, self.services.user, add_members_str)
159 role = post_data['role']
160
161 # 2. Call services layer to save changes.
162 if not mr.errors.AnyErrors():
163 try:
164 self.services.usergroup.UpdateMembers(
165 mr.cnxn, group_id, new_member_ids, role)
166 except exceptions.CircularGroupException:
167 mr.errors.addmembers = (
168 'The members are already ancestors of current group.')
169
170 # 3. Determine the next page in the UI flow.
171 if mr.errors.AnyErrors():
172 self.PleaseCorrect(
173 mr, initial_add_members=add_members_str,
174 initially_expand_form=ezt.boolean(True))
175 else:
176 return framework_helpers.FormatAbsoluteURL(
177 mr, '/g/%s/' % mr.viewed_username, include_project=False,
178 saved=1, ts=int(time.time()))
179
180 def ProcessRemoveMembers(self, mr, post_data):
181 """Process the user's request to remove members.
182
183 Args:
184 mr: common information parsed from the HTTP request.
185 post_data: dictionary of form data.
186
187 Returns:
188 String URL to redirect the user to after processing.
189 """
190 # 1. Gather data from the request.
191 remove_strs = post_data.getall('remove')
192 logging.info('remove_strs = %r', remove_strs)
193
194 if not remove_strs:
195 mr.errors.remove = 'No users specified'
196
197 # 2. Call services layer to save changes.
198 if not mr.errors.AnyErrors():
199 remove_ids = set(
200 self.services.user.LookupUserIDs(mr.cnxn, remove_strs).values())
201 self.services.usergroup.RemoveMembers(
202 mr.cnxn, mr.viewed_user_auth.user_id, remove_ids)
203
204 # 3. Determine the next page in the UI flow.
205 if mr.errors.AnyErrors():
206 self.PleaseCorrect(mr)
207 else:
208 return framework_helpers.FormatAbsoluteURL(
209 mr, '/g/%s/' % mr.viewed_username, include_project=False,
210 saved=1, ts=int(time.time()))