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