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