blob: 23a2d4644f6dcadd9d341760a71d94ac3ec56791 [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"""Helper functions and classes used by the project pages."""
7from __future__ import print_function
8from __future__ import division
9from __future__ import absolute_import
10
11import logging
12import re
13
14import settings
15
16from google.appengine.api import app_identity
17from framework import framework_bizobj
18from framework import framework_views
19from framework import gcs_helpers
20from framework import permissions
21from project import project_constants
22from project import project_views
23from proto import project_pb2
24
25
26_RE_EMAIL_SEPARATORS = re.compile(r'\s|,|;')
27
28
29def BuildProjectMembers(cnxn, project, user_service):
30 """Gather data for the members section of a project page.
31
32 Args:
33 cnxn: connection to SQL database.
34 project: Project PB of current project.
35 user_service: an instance of UserService for user persistence.
36
37 Returns:
38 A dictionary suitable for use with EZT.
39 """
40 # First, get all needed info on all users in one batch of requests.
41 users_by_id = framework_views.MakeAllUserViews(
42 cnxn, user_service, AllProjectMembers(project))
43
44 # Second, group the user proxies by role for display.
45 owner_proxies = [users_by_id[owner_id]
46 for owner_id in project.owner_ids]
47 committer_proxies = [users_by_id[committer_id]
48 for committer_id in project.committer_ids]
49 contributor_proxies = [users_by_id[contrib_id]
50 for contrib_id in project.contributor_ids]
51
52 return {
53 'owners': owner_proxies,
54 'committers': committer_proxies,
55 'contributors': contributor_proxies,
56 'all_members': list(users_by_id.values()),
57 }
58
59
60def BuildProjectAccessOptions(project):
61 """Return a list of project access values for use in an HTML menu.
62
63 Args:
64 project: current Project PB, or None when creating a new project.
65
66 Returns:
67 A list of ProjectAccessView objects that can be used in EZT.
68 """
69 access_levels = [project_pb2.ProjectAccess.ANYONE,
70 project_pb2.ProjectAccess.MEMBERS_ONLY]
71 access_views = []
72 for access in access_levels:
73 # Offer the allowed access levels. When editing an existing project,
74 # its current access level may always be kept, even if it is no longer
75 # in the list of allowed access levels for new projects.
76 if (access in settings.allowed_access_levels or
77 (project and access == project.access)):
78 access_views.append(project_views.ProjectAccessView(access))
79
80 return access_views
81
82
83def ParseUsernames(cnxn, user_service, usernames_text):
84 """Parse all usernames from a text field and return a list of user IDs.
85
86 Args:
87 cnxn: connection to SQL database.
88 user_service: an instance of UserService for user persistence.
89 usernames_text: string that the user entered into a form field for a list
90 of email addresses. Or, None if the browser did not send that value.
91
92 Returns:
93 A set of user IDs for the users named. Or, an empty set if the
94 usernames_field was not in post_data.
95 """
96 if not usernames_text: # The user did not enter any addresses.
97 return set()
98
99 email_list = _RE_EMAIL_SEPARATORS.split(usernames_text)
100 # skip empty strings between consecutive separators
101 email_list = [email for email in email_list if email]
102
103 id_dict = user_service.LookupUserIDs(cnxn, email_list, autocreate=True)
104 return set(id_dict.values())
105
106
107def ParseProjectAccess(project, access_num_str):
108 """Parse and validate the "access" field out of post_data.
109
110 Args:
111 project: Project PB for the project that was edited, or None if the
112 user is creating a new project.
113 access_num_str: string of digits from the users POST that identifies
114 the desired project access level. Or, None if that widget was not
115 offered to the user.
116
117 Returns:
118 An enum project access level, or None if the user did not specify
119 any value or if the value specified was invalid.
120 """
121 access = None
122 if access_num_str:
123 access_number = int(access_num_str)
124 available_access_levels = BuildProjectAccessOptions(project)
125 allowed_access_choices = [access_view.key for access_view
126 in available_access_levels]
127 if access_number in allowed_access_choices:
128 access = project_pb2.ProjectAccess(access_number)
129
130 return access
131
132
133def MembersWithoutGivenIDs(project, exclude_ids):
134 """Return three lists of member user IDs, with member_ids not in them."""
135 owner_ids = [user_id for user_id in project.owner_ids
136 if user_id not in exclude_ids]
137 committer_ids = [user_id for user_id in project.committer_ids
138 if user_id not in exclude_ids]
139 contributor_ids = [user_id for user_id in project.contributor_ids
140 if user_id not in exclude_ids]
141
142 return owner_ids, committer_ids, contributor_ids
143
144
145def MembersWithGivenIDs(project, new_member_ids, role):
146 """Return three lists of member IDs with the new IDs in the right one.
147
148 Args:
149 project: Project PB for the project to get current members from.
150 new_member_ids: set of user IDs for members being added.
151 role: string name of the role that new_member_ids should be granted.
152
153 Returns:
154 Three lists of member IDs with new_member_ids added to the appropriate
155 list and removed from any other role.
156
157 Raises:
158 ValueError: if the role is not one of owner, committer, or contributor.
159 """
160 owner_ids, committer_ids, contributor_ids = MembersWithoutGivenIDs(
161 project, new_member_ids)
162
163 if role == 'owner':
164 owner_ids.extend(new_member_ids)
165 elif role == 'committer':
166 committer_ids.extend(new_member_ids)
167 elif role == 'contributor':
168 contributor_ids.extend(new_member_ids)
169 else:
170 raise ValueError()
171
172 return owner_ids, committer_ids, contributor_ids
173
174
175def UsersInvolvedInProject(project):
176 """Return a set of all user IDs referenced in the Project."""
177 result = set()
178 result.update(project.owner_ids)
179 result.update(project.committer_ids)
180 result.update(project.contributor_ids)
181 result.update([perm.member_id for perm in project.extra_perms])
182 return result
183
184
185def UsersWithPermsInProject(project, perms_needed, users_by_id,
186 effective_ids_by_user):
187 # Users that have the given permission are stored in direct_users_for_perm,
188 # users whose effective ids have the given permission are stored in
189 # indirect_users_for_perm.
190 direct_users_for_perm = {perm: set() for perm in perms_needed}
191 indirect_users_for_perm = {perm: set() for perm in perms_needed}
192
193 # Iterate only over users that have extra permissions, so we don't
194 # have to search the extra perms more than once for each user.
195 for extra_perm_pb in project.extra_perms:
196 extra_perms = set(perm.lower() for perm in extra_perm_pb.perms)
197 for perm, users in direct_users_for_perm.items():
198 if perm.lower() in extra_perms:
199 users.add(extra_perm_pb.member_id)
200
201 # Then, iterate over all users, but don't compute extra permissions.
202 for user_id, user_view in users_by_id.items():
203 effective_ids = effective_ids_by_user[user_id].union([user_id])
204 user_perms = permissions.GetPermissions(
205 user_view.user, effective_ids, project)
206 for perm, users in direct_users_for_perm.items():
207 if not effective_ids.isdisjoint(users):
208 indirect_users_for_perm[perm].add(user_id)
209 if user_perms.HasPerm(perm, None, None, []):
210 users.add(user_id)
211
212 for perm, users in direct_users_for_perm.items():
213 users.update(indirect_users_for_perm[perm])
214
215 return direct_users_for_perm
216
217
218def GetThumbnailUrl(gcs_id):
219 # type: (str) -> str
220 """Derive the thumbnail url for a given GCS object ID."""
221 bucket_name = app_identity.get_default_gcs_bucket_name()
222 return gcs_helpers.SignUrl(bucket_name, gcs_id + '-thumbnail')
223
224
225def IsValidProjectName(s):
226 # type: (string) -> bool
227 """Return true if the given string is a valid project name."""
228 return (
229 project_constants.RE_PROJECT_NAME.match(s) and
230 len(s) <= project_constants.MAX_PROJECT_NAME_LENGTH)
231
232
233def AllProjectMembers(project):
234 # type: (proto.project_pb2.Project) -> Sequence[int]
235 """Return a list of user IDs of all members in the given project."""
236 return project.owner_ids + project.committer_ids + project.contributor_ids