Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(34)

Side by Side Diff: appengine/monorail/services/api_svc_v1.py

Issue 1868553004: Open Source Monorail (Closed) Base URL: https://chromium.googlesource.com/infra/infra.git@master
Patch Set: Rebase Created 4 years, 8 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
1 # Copyright 2016 The Chromium Authors. All rights reserved.
2 # Use of this source code is govered 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 """API service"""
7
8 import datetime
9 import endpoints
10 import functools
11 import logging
12 import re
13 import time
14 from google.appengine.api import oauth
15 from protorpc import message_types
16 from protorpc import protojson
17 from protorpc import remote
18
19 import settings
20 from features import filterrules_helpers
21 from features import notify
22 from framework import actionlimit
23 from framework import framework_constants
24 from framework import framework_helpers
25 from framework import framework_views
26 from framework import monorailrequest
27 from framework import permissions
28 from framework import profiler
29 from framework import sql
30 from project import project_helpers
31 from proto import api_pb2_v1
32 from proto import project_pb2
33 from search import frontendsearchpipeline
34 from services import api_pb2_v1_helpers
35 from services import client_config_svc
36 from services import config_svc
37 from services import issue_svc
38 from services import project_svc
39 from services import service_manager
40 from services import tracker_fulltext
41 from services import user_svc
42 from services import usergroup_svc
43 from sitewide import sitewide_helpers
44 from tracker import field_helpers
45 from tracker import issuedetail
46 from tracker import tracker_constants
47 from tracker import tracker_bizobj
48
49 from infra_libs.ts_mon.common import http_metrics
50
51
52 ENDPOINTS_API_NAME = 'monorail'
53 DOC_URL = ('https://chromium.googlesource.com/infra/infra/+/master/'
54 'appengine/monorail/doc/api.md')
55
56
57 def monorail_api_method(
58 request_message, response_message, **kwargs):
59 """Extends endpoints.method by performing base checks."""
60 time_fn = kwargs.pop('time_fn', time.time)
61 method_name = kwargs.get('name', '')
62 method_path = kwargs.get('path', '')
63 def new_decorator(func):
64 @endpoints.method(request_message, response_message, **kwargs)
65 @functools.wraps(func)
66 def wrapper(self, *args, **kwargs):
67 method_identifier = (ENDPOINTS_API_NAME + '.' +
68 (method_name or func.__name__)
69 + '/' + (method_path or func.__name__))
70 start_time = time_fn()
71 approximate_http_status = 200
72 request = args[0]
73 ret = None
74 try:
75 requester = endpoints.get_current_user()
76 auth_client_ids, auth_emails = (
77 client_config_svc.GetClientConfigSvc().GetClientIDEmails())
78 auth_client_ids.append(endpoints.API_EXPLORER_CLIENT_ID)
79 logging.info('Whitelist ID %r email %r', auth_client_ids, auth_emails)
80 if self._services is None:
81 self._set_services(service_manager.set_up_services())
82 api_base_checks(
83 request, requester,
84 self._services, sql.MonorailConnection(),
85 auth_client_ids, auth_emails)
86 self.increment_request_limit(request)
87 ret = func(self, *args, **kwargs)
88 except user_svc.NoSuchUserException as e:
89 approximate_http_status = 404
90 raise endpoints.NotFoundException(
91 'The user does not exist: %s' % str(e))
92 except (project_svc.NoSuchProjectException,
93 issue_svc.NoSuchIssueException,
94 config_svc.NoSuchComponentException) as e:
95 approximate_http_status = 404
96 raise endpoints.NotFoundException(str(e))
97 except (permissions.BannedUserException,
98 permissions.PermissionException) as e:
99 approximate_http_status = 403
100 raise endpoints.ForbiddenException(str(e))
101 except endpoints.BadRequestException:
102 approximate_http_status = 400
103 raise
104 except endpoints.UnauthorizedException:
105 approximate_http_status = 401
106 raise
107 except actionlimit.ExcessiveActivityException as e:
108 approximate_http_status = 403
109 raise endpoints.ForbiddenException(
110 'The requester has exceeded API quotas limit')
111 except (usergroup_svc.GroupExistsException,
112 config_svc.InvalidComponentNameException) as e:
113 approximate_http_status = 400
114 raise endpoints.BadRequestException(str(e))
115 except Exception as e:
116 approximate_http_status = 500
117 logging.exception('Unexpected error in monorail API')
118 raise
119 finally:
120 elapsed_ms = int((time_fn() - start_time) * 1000)
121
122 fields = {
123 # Endpoints APIs don't return the full set of http status values.
124 'status': approximate_http_status,
125 # Use the api name, not the request path, to prevent an
126 # explosion in possible field values.
127 'name': method_identifier,
128 'is_robot': False,
129 }
130
131 http_metrics.server_durations.add(elapsed_ms, fields=fields)
132 http_metrics.server_response_status.increment(fields=fields)
133 http_metrics.server_request_bytes.add(len(protojson.encode_message(
134 request)), fields=fields)
135 response_size = 0
136 if ret:
137 response_size = len(protojson.encode_message(ret))
138 http_metrics.server_response_bytes.add(response_size, fields=fields)
139
140 return ret
141
142 return wrapper
143 return new_decorator
144
145
146 def api_base_checks(request, requester, services, cnxn,
147 auth_client_ids, auth_emails):
148 """Base checks for API users.
149
150 Args:
151 request: The HTTP request from Cloud Endpoints.
152 requester: The user who sends the request.
153 services: Services object.
154 cnxn: connection to the SQL database.
155 auth_client_ids: authorized client ids.
156 auth_emails: authorized emails when client is anonymous.
157
158 Returns:
159 Nothing
160
161 Raises:
162 endpoints.UnauthorizedException: If the requester is anonymous.
163 user_svc.NoSuchUserException: If the requester does not exist in Monorail.
164 project_svc.NoSuchProjectException: If the project does not exist in
165 Monorail.
166 permissions.BannedUserException: If the requester is banned.
167 permissions.PermissionException: If the requester does not have
168 permisssion to view.
169 """
170 valid_user = False
171 auth_err = ''
172 client_id = None
173
174 try:
175 client_id = oauth.get_client_id(framework_constants.OAUTH_SCOPE)
176 logging.info('Oauth client ID %s', client_id)
177 except oauth.Error as ex:
178 auth_err = 'oauth.Error: %s' % ex
179
180 if not requester:
181 try:
182 requester = oauth.get_current_user(framework_constants.OAUTH_SCOPE)
183 logging.info('Oauth requester %s', requester.email())
184 except oauth.Error as ex:
185 auth_err = 'oauth.Error: %s' % ex
186
187 if client_id and requester:
188 if client_id != 'anonymous':
189 if client_id in auth_client_ids:
190 valid_user = True
191 else:
192 auth_err = 'Client ID %s is not whitelisted' % client_id
193 # Some service accounts may have anonymous client ID
194 else:
195 if requester.email() in auth_emails:
196 valid_user = True
197 else:
198 auth_err = 'Client email %s is not whitelisted' % requester.email()
199
200 if not valid_user:
201 raise endpoints.UnauthorizedException('Auth error: %s' % auth_err)
202
203 project_name = None
204 if hasattr(request, 'projectId'):
205 project_name = request.projectId
206 issue_local_id = None
207 if hasattr(request, 'issueId'):
208 issue_local_id = request.issueId
209 # This could raise user_svc.NoSuchUserException
210 requester_id = services.user.LookupUserID(cnxn, requester.email())
211 requester_pb = services.user.GetUser(cnxn, requester_id)
212 requester_view = framework_views.UserView(
213 requester_id, requester.email(), requester_pb.obscure_email)
214 if permissions.IsBanned(requester_pb, requester_view):
215 raise permissions.BannedUserException(
216 'The user %s has been banned from using Monorail' %
217 requester.email())
218 if project_name:
219 project = services.project.GetProjectByName(
220 cnxn, project_name)
221 if not project:
222 raise project_svc.NoSuchProjectException(
223 'Project %s does not exist' % project_name)
224 if project.state != project_pb2.ProjectState.LIVE:
225 raise permissions.PermissionException(
226 'API may not access project %s because it is not live'
227 % project_name)
228 requester_effective_ids = services.usergroup.LookupMemberships(
229 cnxn, requester_id)
230 requester_effective_ids.add(requester_id)
231 if not permissions.UserCanViewProject(
232 requester_pb, requester_effective_ids, project):
233 raise permissions.PermissionException(
234 'The user %s has no permission for project %s' %
235 (requester.email(), project_name))
236 if issue_local_id:
237 # This may raise a NoSuchIssueException.
238 issue = services.issue.GetIssueByLocalID(
239 cnxn, project.project_id, issue_local_id)
240 perms = permissions.GetPermissions(
241 requester_pb, requester_effective_ids, project)
242 config = services.config.GetProjectConfig(cnxn, project.project_id)
243 granted_perms = tracker_bizobj.GetGrantedPerms(
244 issue, requester_effective_ids, config)
245 if not permissions.CanViewIssue(
246 requester_effective_ids, perms, project, issue,
247 granted_perms=granted_perms):
248 raise permissions.PermissionException(
249 'User is not allowed to view this issue %s:%d' %
250 (project_name, issue_local_id))
251
252
253 @endpoints.api(name=ENDPOINTS_API_NAME, version='v1',
254 description='Monorail API to manage issues.',
255 auth_level=endpoints.AUTH_LEVEL.NONE,
256 allowed_client_ids=endpoints.SKIP_CLIENT_ID_CHECK,
257 documentation=DOC_URL)
258 class MonorailApi(remote.Service):
259
260 # Class variables. Handy to mock.
261 _services = None
262 _mar = None
263
264 @classmethod
265 def _set_services(cls, services):
266 cls._services = services
267
268 def mar_factory(self, request):
269 if not self._mar:
270 self._mar = monorailrequest.MonorailApiRequest(request, self._services)
271 return self._mar
272
273 def aux_delete_comment(self, request, delete=True):
274 mar = self.mar_factory(request)
275 action_name = 'delete' if delete else 'undelete'
276
277 issue = self._services.issue.GetIssueByLocalID(
278 mar.cnxn, mar.project_id, request.issueId)
279 all_comments = self._services.issue.GetCommentsForIssue(
280 mar.cnxn, issue.issue_id)
281 try:
282 issue_comment = all_comments[request.commentId]
283 except IndexError:
284 raise issue_svc.NoSuchIssueException(
285 'The issue %s:%d does not have comment %d.' %
286 (mar.project_name, request.issueId, request.commentId))
287
288 if not permissions.CanDelete(
289 mar.auth.user_id, mar.auth.effective_ids, mar.perms,
290 issue_comment.deleted_by, issue_comment.user_id, mar.project,
291 permissions.GetRestrictions(issue), mar.granted_perms):
292 raise permissions.PermissionException(
293 'User is not allowed to %s the comment %d of issue %s:%d' %
294 (action_name, request.commentId, mar.project_name,
295 request.issueId))
296
297 self._services.issue.SoftDeleteComment(
298 mar.cnxn, mar.project_id, request.issueId, request.commentId,
299 mar.auth.user_id, self._services.user, delete=delete)
300 return api_pb2_v1.IssuesCommentsDeleteResponse()
301
302 def increment_request_limit(self, request):
303 """Check whether the requester has exceeded API quotas limit,
304 and increment request count.
305 """
306 mar = self.mar_factory(request)
307 # soft_limit == hard_limit for api_request, so this function either
308 # returns False if under limit, or raise ExcessiveActivityException
309 if not actionlimit.NeedCaptcha(
310 mar.auth.user_pb, actionlimit.API_REQUEST, skip_lifetime_check=True):
311 actionlimit.CountAction(
312 mar.auth.user_pb, actionlimit.API_REQUEST, delta=1)
313 self._services.user.UpdateUser(
314 mar.cnxn, mar.auth.user_id, mar.auth.user_pb)
315
316 @monorail_api_method(
317 api_pb2_v1.ISSUES_COMMENTS_DELETE_REQUEST_RESOURCE_CONTAINER,
318 api_pb2_v1.IssuesCommentsDeleteResponse,
319 path='projects/{projectId}/issues/{issueId}/comments/{commentId}',
320 http_method='DELETE',
321 name='issues.comments.delete')
322 def issues_comments_delete(self, request):
323 """Delete a comment."""
324 return self.aux_delete_comment(request, True)
325
326 @monorail_api_method(
327 api_pb2_v1.ISSUES_COMMENTS_INSERT_REQUEST_RESOURCE_CONTAINER,
328 api_pb2_v1.IssuesCommentsInsertResponse,
329 path='projects/{projectId}/issues/{issueId}/comments',
330 http_method='POST',
331 name='issues.comments.insert')
332 def issues_comments_insert(self, request):
333 """Add a comment."""
334 mar = self.mar_factory(request)
335 issue = self._services.issue.GetIssueByLocalID(
336 mar.cnxn, mar.project_id, request.issueId)
337 old_owner_id = tracker_bizobj.GetOwnerId(issue)
338 if not permissions.CanCommentIssue(
339 mar.auth.effective_ids, mar.perms, mar.project, issue,
340 mar.granted_perms):
341 raise permissions.PermissionException(
342 'User is not allowed to comment this issue (%s, %d)' %
343 (request.projectId, request.issueId))
344
345 updates_dict = {}
346 if request.updates:
347 if request.updates.moveToProject:
348 move_to = request.updates.moveToProject.lower()
349 move_to_project = issuedetail.CheckMoveIssueRequest(
350 self._services, mar, issue, True, move_to, mar.errors)
351 if mar.errors.AnyErrors():
352 raise endpoints.BadRequestException(mar.errors.move_to)
353 updates_dict['move_to_project'] = move_to_project
354
355 updates_dict['summary'] = request.updates.summary
356 updates_dict['status'] = request.updates.status
357 if request.updates.owner:
358 if request.updates.owner == framework_constants.NO_USER_NAME:
359 updates_dict['owner'] = framework_constants.NO_USER_SPECIFIED
360 else:
361 updates_dict['owner'] = self._services.user.LookupUserID(
362 mar.cnxn, request.updates.owner)
363 updates_dict['cc_add'], updates_dict['cc_remove'] = (
364 api_pb2_v1_helpers.split_remove_add(request.updates.cc))
365 updates_dict['cc_add'] = self._services.user.LookupUserIDs(
366 mar.cnxn, updates_dict['cc_add']).values()
367 updates_dict['cc_remove'] = self._services.user.LookupUserIDs(
368 mar.cnxn, updates_dict['cc_remove']).values()
369 updates_dict['labels_add'], updates_dict['labels_remove'] = (
370 api_pb2_v1_helpers.split_remove_add(request.updates.labels))
371 blocked_on_add_strs, blocked_on_remove_strs = (
372 api_pb2_v1_helpers.split_remove_add(request.updates.blockedOn))
373 updates_dict['blocked_on_add'] = api_pb2_v1_helpers.issue_global_ids(
374 blocked_on_add_strs, issue.project_id, mar,
375 self._services)
376 updates_dict['blocked_on_remove'] = api_pb2_v1_helpers.issue_global_ids(
377 blocked_on_remove_strs, issue.project_id, mar,
378 self._services)
379 blocking_add_strs, blocking_remove_strs = (
380 api_pb2_v1_helpers.split_remove_add(request.updates.blocking))
381 updates_dict['blocking_add'] = api_pb2_v1_helpers.issue_global_ids(
382 blocking_add_strs, issue.project_id, mar,
383 self._services)
384 updates_dict['blocking_remove'] = api_pb2_v1_helpers.issue_global_ids(
385 blocking_remove_strs, issue.project_id, mar,
386 self._services)
387 components_add_strs, components_remove_strs = (
388 api_pb2_v1_helpers.split_remove_add(request.updates.components))
389 updates_dict['components_add'] = (
390 api_pb2_v1_helpers.convert_component_ids(
391 mar.config, components_add_strs))
392 updates_dict['components_remove'] = (
393 api_pb2_v1_helpers.convert_component_ids(
394 mar.config, components_remove_strs))
395 if request.updates.mergedInto:
396 updates_dict['merged_into'] = self._services.issue.LookupIssueID(
397 mar.cnxn, issue.project_id, int(request.updates.mergedInto))
398 (updates_dict['field_vals_add'], updates_dict['field_vals_remove'],
399 updates_dict['fields_clear'], updates_dict['fields_labels_add'],
400 updates_dict['fields_labels_remove']) = (
401 api_pb2_v1_helpers.convert_field_values(
402 request.updates.fieldValues, mar, self._services))
403
404 field_helpers.ValidateCustomFields(
405 mar, self._services,
406 (updates_dict.get('field_vals_add', []) +
407 updates_dict.get('field_vals_remove', [])),
408 mar.config, mar.errors)
409 if mar.errors.AnyErrors():
410 raise endpoints.BadRequestException(
411 'Invalid field values: %s' % mar.errors.custom_fields)
412
413 _, comment = self._services.issue.DeltaUpdateIssue(
414 cnxn=mar.cnxn, services=self._services,
415 reporter_id=mar.auth.user_id,
416 project_id=mar.project_id, config=mar.config, issue=issue,
417 status=updates_dict.get('status'), owner_id=updates_dict.get('owner'),
418 cc_add=updates_dict.get('cc_add', []),
419 cc_remove=updates_dict.get('cc_remove', []),
420 comp_ids_add=updates_dict.get('components_add', []),
421 comp_ids_remove=updates_dict.get('components_remove', []),
422 labels_add=(updates_dict.get('labels_add', []) +
423 updates_dict.get('fields_labels_add', [])),
424 labels_remove=(updates_dict.get('labels_remove', []) +
425 updates_dict.get('fields_labels_remove', [])),
426 field_vals_add=updates_dict.get('field_vals_add', []),
427 field_vals_remove=updates_dict.get('field_vals_remove', []),
428 fields_clear=updates_dict.get('fields_clear', []),
429 blocked_on_add=updates_dict.get('blocked_on_add', []),
430 blocked_on_remove=updates_dict.get('blocked_on_remove', []),
431 blocking_add=updates_dict.get('blocking_add', []),
432 blocking_remove=updates_dict.get('blocking_remove', []),
433 merged_into=updates_dict.get('merged_into'),
434 index_now=False,
435 comment=request.content,
436 summary=updates_dict.get('summary'),
437 )
438
439 move_comment = None
440 if 'move_to_project' in updates_dict:
441 move_to_project = updates_dict['move_to_project']
442 old_text_ref = 'issue %s:%s' % (issue.project_name, issue.local_id)
443 tracker_fulltext.UnindexIssues([issue.issue_id])
444 moved_back_iids = self._services.issue.MoveIssues(
445 mar.cnxn, move_to_project, [issue], self._services.user)
446 new_text_ref = 'issue %s:%s' % (issue.project_name, issue.local_id)
447 if issue.issue_id in moved_back_iids:
448 content = 'Moved %s back to %s again.' % (old_text_ref, new_text_ref)
449 else:
450 content = 'Moved %s to now be %s.' % (old_text_ref, new_text_ref)
451 move_comment = self._services.issue.CreateIssueComment(
452 mar.cnxn, move_to_project.project_id, issue.local_id, mar.auth.user_id,
453 content, amendments=[
454 tracker_bizobj.MakeProjectAmendment(move_to_project.project_name)])
455
456 tracker_fulltext.IndexIssues(
457 mar.cnxn, [issue], self._services.user, self._services.issue,
458 self._services.config)
459
460 comment = comment or move_comment
461 if comment is None:
462 return api_pb2_v1.IssuesCommentsInsertResponse()
463
464 cmnts = self._services.issue.GetCommentsForIssue(mar.cnxn, issue.issue_id)
465 seq = len(cmnts) - 1
466
467 if request.sendEmail:
468 notify.PrepareAndSendIssueChangeNotification(
469 issue.project_id, issue.local_id, framework_helpers.GetHostPort(),
470 comment.user_id, seq, send_email=True, old_owner_id=old_owner_id)
471
472 can_delete = permissions.CanDelete(
473 mar.auth.user_id, mar.auth.effective_ids, mar.perms,
474 comment.deleted_by, comment.user_id, mar.project,
475 permissions.GetRestrictions(issue), granted_perms=mar.granted_perms)
476 return api_pb2_v1.IssuesCommentsInsertResponse(
477 id=seq,
478 kind='monorail#issueComment',
479 author=api_pb2_v1_helpers.convert_person(
480 comment.user_id, mar.cnxn, self._services),
481 content=comment.content,
482 published=datetime.datetime.fromtimestamp(comment.timestamp),
483 updates=api_pb2_v1_helpers.convert_amendments(
484 issue, comment.amendments, mar, self._services),
485 canDelete=can_delete)
486
487 @monorail_api_method(
488 api_pb2_v1.ISSUES_COMMENTS_LIST_REQUEST_RESOURCE_CONTAINER,
489 api_pb2_v1.IssuesCommentsListResponse,
490 path='projects/{projectId}/issues/{issueId}/comments',
491 http_method='GET',
492 name='issues.comments.list')
493 def issues_comments_list(self, request):
494 """List all comments for an issue."""
495 mar = self.mar_factory(request)
496 issue = self._services.issue.GetIssueByLocalID(
497 mar.cnxn, mar.project_id, request.issueId)
498 comments = self._services.issue.GetCommentsForIssue(
499 mar.cnxn, issue.issue_id)
500 visible_comments = []
501 for comment in comments[
502 request.startIndex:(request.startIndex + request.maxResults)]:
503 visible_comments.append(
504 api_pb2_v1_helpers.convert_comment(
505 issue, comment, mar, self._services, mar.granted_perms))
506
507 return api_pb2_v1.IssuesCommentsListResponse(
508 kind='monorail#issueCommentList',
509 totalResults=len(comments),
510 items=visible_comments)
511
512 @monorail_api_method(
513 api_pb2_v1.ISSUES_COMMENTS_DELETE_REQUEST_RESOURCE_CONTAINER,
514 api_pb2_v1.IssuesCommentsDeleteResponse,
515 path='projects/{projectId}/issues/{issueId}/comments/{commentId}',
516 http_method='POST',
517 name='issues.comments.undelete')
518 def issues_comments_undelete(self, request):
519 """Restore a deleted comment."""
520 return self.aux_delete_comment(request, False)
521
522 @monorail_api_method(
523 api_pb2_v1.USERS_GET_REQUEST_RESOURCE_CONTAINER,
524 api_pb2_v1.UsersGetResponse,
525 path='users/{userId}',
526 http_method='GET',
527 name='users.get')
528 def users_get(self, request):
529 """Get a user."""
530 owner_project_only = request.ownerProjectsOnly
531 mar = self.mar_factory(request)
532 (visible_ownership, visible_deleted, visible_membership,
533 visible_contrib) = sitewide_helpers.GetUserProjects(
534 mar.cnxn, self._services, mar.auth.user_pb, mar.auth.effective_ids,
535 mar.viewed_user_auth.effective_ids)
536
537 project_list = []
538 for proj in (visible_ownership + visible_deleted):
539 config = self._services.config.GetProjectConfig(
540 mar.cnxn, proj.project_id)
541 proj_result = api_pb2_v1_helpers.convert_project(
542 proj, config, api_pb2_v1.Role.owner)
543 project_list.append(proj_result)
544 if not owner_project_only:
545 for proj in visible_membership:
546 config = self._services.config.GetProjectConfig(
547 mar.cnxn, proj.project_id)
548 proj_result = api_pb2_v1_helpers.convert_project(
549 proj, config, api_pb2_v1.Role.member)
550 project_list.append(proj_result)
551 for proj in visible_contrib:
552 config = self._services.config.GetProjectConfig(
553 mar.cnxn, proj.project_id)
554 proj_result = api_pb2_v1_helpers.convert_project(
555 proj, config, api_pb2_v1.Role.contributor)
556 project_list.append(proj_result)
557
558 return api_pb2_v1.UsersGetResponse(
559 id=str(mar.viewed_user_auth.user_id),
560 kind='monorail#user',
561 projects=project_list,
562 )
563
564 @monorail_api_method(
565 api_pb2_v1.ISSUES_GET_REQUEST_RESOURCE_CONTAINER,
566 api_pb2_v1.IssuesGetInsertResponse,
567 path='projects/{projectId}/issues/{issueId}',
568 http_method='GET',
569 name='issues.get')
570 def issues_get(self, request):
571 """Get an issue."""
572 mar = self.mar_factory(request)
573 issue = self._services.issue.GetIssueByLocalID(
574 mar.cnxn, mar.project_id, request.issueId)
575
576 return api_pb2_v1_helpers.convert_issue(
577 api_pb2_v1.IssuesGetInsertResponse, issue, mar, self._services)
578
579 @monorail_api_method(
580 api_pb2_v1.ISSUES_INSERT_REQUEST_RESOURCE_CONTAINER,
581 api_pb2_v1.IssuesGetInsertResponse,
582 path='projects/{projectId}/issues',
583 http_method='POST',
584 name='issues.insert')
585 def issues_insert(self, request):
586 """Add a new issue."""
587 mar = self.mar_factory(request)
588 if not mar.perms.CanUsePerm(
589 permissions.CREATE_ISSUE, mar.auth.effective_ids, mar.project, []):
590 raise permissions.PermissionException(
591 'The requester %s is not allowed to create issues for project %s.' %
592 (mar.auth.email, mar.project_name))
593
594 owner_id = None
595 if request.owner:
596 try:
597 owner_id = self._services.user.LookupUserID(
598 mar.cnxn, request.owner.name)
599 except user_svc.NoSuchUserException:
600 raise endpoints.BadRequestException(
601 'The specified owner %s does not exist.' % request.owner.name)
602
603 cc_ids = []
604 if request.cc:
605 cc_ids = self._services.user.LookupUserIDs(
606 mar.cnxn, [ap.name for ap in request.cc]).values()
607 comp_ids = api_pb2_v1_helpers.convert_component_ids(
608 mar.config, request.components)
609 fields_add, _, _, fields_labels, _ = (
610 api_pb2_v1_helpers.convert_field_values(
611 request.fieldValues, mar, self._services))
612 field_helpers.ValidateCustomFields(
613 mar, self._services, fields_add, mar.config, mar.errors)
614 if mar.errors.AnyErrors():
615 raise endpoints.BadRequestException(
616 'Invalid field values: %s' % mar.errors.custom_fields)
617
618 local_id = self._services.issue.CreateIssue(
619 mar.cnxn, self._services, mar.project_id,
620 request.summary, request.status, owner_id,
621 cc_ids, request.labels + fields_labels, fields_add,
622 comp_ids, mar.auth.user_id, request.description,
623 blocked_on=api_pb2_v1_helpers.convert_issueref_pbs(
624 request.blockedOn, mar, self._services),
625 blocking=api_pb2_v1_helpers.convert_issueref_pbs(
626 request.blocking, mar, self._services))
627 new_issue = self._services.issue.GetIssueByLocalID(
628 mar.cnxn, mar.project_id, local_id)
629
630 if request.sendEmail:
631 notify.PrepareAndSendIssueChangeNotification(
632 mar.project_id, local_id, framework_helpers.GetHostPort(),
633 new_issue.reporter_id, 0)
634
635 return api_pb2_v1_helpers.convert_issue(
636 api_pb2_v1.IssuesGetInsertResponse, new_issue, mar, self._services)
637
638 @monorail_api_method(
639 api_pb2_v1.ISSUES_LIST_REQUEST_RESOURCE_CONTAINER,
640 api_pb2_v1.IssuesListResponse,
641 path='projects/{projectId}/issues',
642 http_method='GET',
643 name='issues.list')
644 def issues_list(self, request):
645 """List issues for projects."""
646 mar = self.mar_factory(request)
647
648 if request.additionalProject:
649 for project_name in request.additionalProject:
650 project = self._services.project.GetProjectByName(
651 mar.cnxn, project_name)
652 if project and not permissions.UserCanViewProject(
653 mar.auth.user_pb, mar.auth.effective_ids, project):
654 raise permissions.PermissionException(
655 'The user %s has no permission for project %s' %
656 (mar.auth.email, project_name))
657 prof = profiler.Profiler()
658 pipeline = frontendsearchpipeline.FrontendSearchPipeline(
659 mar, self._services, prof, mar.num)
660 if not mar.errors.AnyErrors():
661 pipeline.SearchForIIDs()
662 pipeline.MergeAndSortIssues()
663 pipeline.Paginate()
664 else:
665 raise endpoints.BadRequestException(mar.errors.query)
666
667 issue_list = [
668 api_pb2_v1_helpers.convert_issue(
669 api_pb2_v1.IssueWrapper, r, mar, self._services)
670 for r in pipeline.visible_results]
671 return api_pb2_v1.IssuesListResponse(
672 kind='monorail#issueList',
673 totalResults=pipeline.total_count,
674 items=issue_list)
675
676 @monorail_api_method(
677 api_pb2_v1.GROUPS_SETTINGS_LIST_REQUEST_RESOURCE_CONTAINER,
678 api_pb2_v1.GroupsSettingsListResponse,
679 path='groups/settings',
680 http_method='GET',
681 name='groups.settings.list')
682 def groups_settings_list(self, request):
683 """List all group settings."""
684 mar = self.mar_factory(request)
685 all_groups = self._services.usergroup.GetAllUserGroupsInfo(mar.cnxn)
686 group_settings = []
687 for g in all_groups:
688 setting = g[2]
689 wrapper = api_pb2_v1_helpers.convert_group_settings(g[0], setting)
690 if not request.importedGroupsOnly or wrapper.ext_group_type:
691 group_settings.append(wrapper)
692 return api_pb2_v1.GroupsSettingsListResponse(
693 groupSettings=group_settings)
694
695 @monorail_api_method(
696 api_pb2_v1.GROUPS_CREATE_REQUEST_RESOURCE_CONTAINER,
697 api_pb2_v1.GroupsCreateResponse,
698 path='groups',
699 http_method='POST',
700 name='groups.create')
701 def groups_create(self, request):
702 """Create a new user group."""
703 mar = self.mar_factory(request)
704 if not permissions.CanCreateGroup(mar.perms):
705 raise permissions.PermissionException(
706 'The user is not allowed to create groups.')
707
708 user_dict = self._services.user.LookupExistingUserIDs(
709 mar.cnxn, [request.groupName])
710 if request.groupName.lower() in user_dict:
711 raise usergroup_svc.GroupExistsException(
712 'group %s already exists' % request.groupName)
713
714 if request.ext_group_type:
715 ext_group_type = str(request.ext_group_type).lower()
716 else:
717 ext_group_type = None
718 group_id = self._services.usergroup.CreateGroup(
719 mar.cnxn, self._services, request.groupName,
720 str(request.who_can_view_members).lower(),
721 ext_group_type)
722
723 return api_pb2_v1.GroupsCreateResponse(
724 groupID=group_id)
725
726 @monorail_api_method(
727 api_pb2_v1.GROUPS_GET_REQUEST_RESOURCE_CONTAINER,
728 api_pb2_v1.GroupsGetResponse,
729 path='groups/{groupName}',
730 http_method='GET',
731 name='groups.get')
732 def groups_get(self, request):
733 """Get a group's settings and users."""
734 mar = self.mar_factory(request)
735 if not mar.viewed_user_auth:
736 raise user_svc.NoSuchUserException(request.groupName)
737 group_id = mar.viewed_user_auth.user_id
738 group_settings = self._services.usergroup.GetGroupSettings(
739 mar.cnxn, group_id)
740 member_ids, owner_ids = self._services.usergroup.LookupAllMembers(
741 mar.cnxn, [group_id])
742 (owned_project_ids, membered_project_ids,
743 contrib_project_ids) = self._services.project.GetUserRolesInAllProjects(
744 mar.cnxn, mar.auth.effective_ids)
745 project_ids = owned_project_ids.union(
746 membered_project_ids).union(contrib_project_ids)
747 if not permissions.CanViewGroup(
748 mar.perms, mar.auth.effective_ids, group_settings, member_ids[group_id],
749 owner_ids[group_id], project_ids):
750 raise permissions.PermissionException(
751 'The user is not allowed to view this group.')
752
753 member_ids, owner_ids = self._services.usergroup.LookupMembers(
754 mar.cnxn, [group_id])
755
756 member_emails = self._services.user.LookupUserEmails(
757 mar.cnxn, member_ids[group_id]).values()
758 owner_emails = self._services.user.LookupUserEmails(
759 mar.cnxn, owner_ids[group_id]).values()
760
761 return api_pb2_v1.GroupsGetResponse(
762 groupID=group_id,
763 groupSettings=api_pb2_v1_helpers.convert_group_settings(
764 request.groupName, group_settings),
765 groupOwners=owner_emails,
766 groupMembers=member_emails)
767
768 @monorail_api_method(
769 api_pb2_v1.GROUPS_UPDATE_REQUEST_RESOURCE_CONTAINER,
770 api_pb2_v1.GroupsUpdateResponse,
771 path='groups/{groupName}',
772 http_method='POST',
773 name='groups.update')
774 def groups_update(self, request):
775 """Update a group's settings and users."""
776 mar = self.mar_factory(request)
777 group_id = mar.viewed_user_auth.user_id
778 member_ids_dict, owner_ids_dict = self._services.usergroup.LookupMembers(
779 mar.cnxn, [group_id])
780 owner_ids = owner_ids_dict.get(group_id, [])
781 member_ids = member_ids_dict.get(group_id, [])
782 if not permissions.CanEditGroup(
783 mar.perms, mar.auth.effective_ids, owner_ids):
784 raise permissions.PermissionException(
785 'The user is not allowed to edit this group.')
786
787 group_settings = self._services.usergroup.GetGroupSettings(
788 mar.cnxn, group_id)
789 if (request.who_can_view_members or request.ext_group_type
790 or request.last_sync_time or request.friend_projects):
791 group_settings.who_can_view_members = (
792 request.who_can_view_members or group_settings.who_can_view_members)
793 group_settings.ext_group_type = (
794 request.ext_group_type or group_settings.ext_group_type)
795 group_settings.last_sync_time = (
796 request.last_sync_time or group_settings.last_sync_time)
797 if framework_constants.NO_VALUES in request.friend_projects:
798 group_settings.friend_projects = []
799 else:
800 id_dict = self._services.project.LookupProjectIDs(
801 mar.cnxn, request.friend_projects)
802 group_settings.friend_projects = (
803 id_dict.values() or group_settings.friend_projects)
804 self._services.usergroup.UpdateSettings(
805 mar.cnxn, group_id, group_settings)
806
807 if request.groupOwners or request.groupMembers:
808 self._services.usergroup.RemoveMembers(
809 mar.cnxn, group_id, owner_ids + member_ids)
810 owners_dict = self._services.user.LookupUserIDs(
811 mar.cnxn, request.groupOwners, True)
812 self._services.usergroup.UpdateMembers(
813 mar.cnxn, group_id, owners_dict.values(), 'owner')
814 members_dict = self._services.user.LookupUserIDs(
815 mar.cnxn, request.groupMembers, True)
816 self._services.usergroup.UpdateMembers(
817 mar.cnxn, group_id, members_dict.values(), 'member')
818
819 return api_pb2_v1.GroupsUpdateResponse()
820
821 @monorail_api_method(
822 api_pb2_v1.COMPONENTS_LIST_REQUEST_RESOURCE_CONTAINER,
823 api_pb2_v1.ComponentsListResponse,
824 path='projects/{projectId}/components',
825 http_method='GET',
826 name='components.list')
827 def components_list(self, request):
828 """List all components of a given project."""
829 mar = self.mar_factory(request)
830 config = self._services.config.GetProjectConfig(mar.cnxn, mar.project_id)
831 components = [api_pb2_v1_helpers.convert_component_def(
832 cd, mar, self._services) for cd in config.component_defs]
833 return api_pb2_v1.ComponentsListResponse(
834 components=components)
835
836 @monorail_api_method(
837 api_pb2_v1.COMPONENTS_CREATE_REQUEST_RESOURCE_CONTAINER,
838 api_pb2_v1.Component,
839 path='projects/{projectId}/components',
840 http_method='POST',
841 name='components.create')
842 def components_create(self, request):
843 """Create a component."""
844 mar = self.mar_factory(request)
845 if not mar.perms.CanUsePerm(
846 permissions.EDIT_PROJECT, mar.auth.effective_ids, mar.project, []):
847 raise permissions.PermissionException(
848 'User is not allowed to create components for this project')
849
850 config = self._services.config.GetProjectConfig(mar.cnxn, mar.project_id)
851 leaf_name = request.componentName
852 if not tracker_constants.COMPONENT_NAME_RE.match(leaf_name):
853 raise config_svc.InvalidComponentNameException(
854 'The component name %s is invalid.' % leaf_name)
855
856 parent_path = request.parentPath
857 if parent_path:
858 parent_def = tracker_bizobj.FindComponentDef(parent_path, config)
859 if not parent_def:
860 raise config_svc.NoSuchComponentException(
861 'Parent component %s does not exist.' % parent_path)
862 if not permissions.CanEditComponentDef(
863 mar.auth.effective_ids, mar.perms, mar.project, parent_def, config):
864 raise permissions.PermissionException(
865 'User is not allowed to add a subcomponent to component %s' %
866 parent_path)
867
868 path = '%s>%s' % (parent_path, leaf_name)
869 else:
870 path = leaf_name
871
872 if tracker_bizobj.FindComponentDef(path, config):
873 raise config_svc.InvalidComponentNameException(
874 'The name %s is already in use.' % path)
875
876 created = int(time.time())
877 user_emails = set()
878 user_emails.update([mar.auth.email] + request.admin + request.cc)
879 user_ids_dict = self._services.user.LookupUserIDs(
880 mar.cnxn, list(user_emails), autocreate=False)
881 admin_ids = [user_ids_dict[uname] for uname in request.admin]
882 cc_ids = [user_ids_dict[uname] for uname in request.cc]
883
884 component_id = self._services.config.CreateComponentDef(
885 mar.cnxn, mar.project_id, path, request.description, request.deprecated,
886 admin_ids, cc_ids, created, user_ids_dict[mar.auth.email])
887
888 return api_pb2_v1.Component(
889 componentId=component_id,
890 projectName=request.projectId,
891 componentPath=path,
892 description=request.description,
893 admin=request.admin,
894 cc=request.cc,
895 deprecated=request.deprecated,
896 created=datetime.datetime.fromtimestamp(created),
897 creator=mar.auth.email)
898
899 @monorail_api_method(
900 api_pb2_v1.COMPONENTS_DELETE_REQUEST_RESOURCE_CONTAINER,
901 message_types.VoidMessage,
902 path='projects/{projectId}/components/{componentPath}',
903 http_method='DELETE',
904 name='components.delete')
905 def components_delete(self, request):
906 """Delete a component."""
907 mar = self.mar_factory(request)
908 config = self._services.config.GetProjectConfig(mar.cnxn, mar.project_id)
909 component_path = request.componentPath
910 component_def = tracker_bizobj.FindComponentDef(
911 component_path, config)
912 if not component_def:
913 raise config_svc.NoSuchComponentException(
914 'The component %s does not exist.' % component_path)
915 if not permissions.CanViewComponentDef(
916 mar.auth.effective_ids, mar.perms, mar.project, component_def):
917 raise permissions.PermissionException(
918 'User is not allowed to view this component %s' % component_path)
919 if not permissions.CanEditComponentDef(
920 mar.auth.effective_ids, mar.perms, mar.project, component_def, config):
921 raise permissions.PermissionException(
922 'User is not allowed to delete this component %s' % component_path)
923
924 allow_delete = not tracker_bizobj.FindDescendantComponents(
925 config, component_def)
926 if not allow_delete:
927 raise permissions.PermissionException(
928 'User tried to delete component that had subcomponents')
929
930 self._services.issue.DeleteComponentReferences(
931 mar.cnxn, component_def.component_id)
932 self._services.config.DeleteComponentDef(
933 mar.cnxn, mar.project_id, component_def.component_id)
934 return message_types.VoidMessage()
935
936 @monorail_api_method(
937 api_pb2_v1.COMPONENTS_UPDATE_REQUEST_RESOURCE_CONTAINER,
938 message_types.VoidMessage,
939 path='projects/{projectId}/components/{componentPath}',
940 http_method='POST',
941 name='components.update')
942 def components_update(self, request):
943 """Update a component."""
944 mar = self.mar_factory(request)
945 config = self._services.config.GetProjectConfig(mar.cnxn, mar.project_id)
946 component_path = request.componentPath
947 component_def = tracker_bizobj.FindComponentDef(
948 component_path, config)
949 if not component_def:
950 raise config_svc.NoSuchComponentException(
951 'The component %s does not exist.' % component_path)
952 if not permissions.CanViewComponentDef(
953 mar.auth.effective_ids, mar.perms, mar.project, component_def):
954 raise permissions.PermissionException(
955 'User is not allowed to view this component %s' % component_path)
956 if not permissions.CanEditComponentDef(
957 mar.auth.effective_ids, mar.perms, mar.project, component_def, config):
958 raise permissions.PermissionException(
959 'User is not allowed to edit this component %s' % component_path)
960
961 original_path = component_def.path
962 new_path = component_def.path
963 new_docstring = component_def.docstring
964 new_deprecated = component_def.deprecated
965 new_admin_ids = component_def.admin_ids
966 new_cc_ids = component_def.cc_ids
967 update_filterrule = False
968 for update in request.updates:
969 if update.field == api_pb2_v1.ComponentUpdateFieldID.LEAF_NAME:
970 leaf_name = update.leafName
971 if not tracker_constants.COMPONENT_NAME_RE.match(leaf_name):
972 raise config_svc.InvalidComponentNameException(
973 'The component name %s is invalid.' % leaf_name)
974
975 if '>' in original_path:
976 parent_path = original_path[:original_path.rindex('>')]
977 new_path = '%s>%s' % (parent_path, leaf_name)
978 else:
979 new_path = leaf_name
980
981 conflict = tracker_bizobj.FindComponentDef(new_path, config)
982 if conflict and conflict.component_id != component_def.component_id:
983 raise config_svc.InvalidComponentNameException(
984 'The name %s is already in use.' % new_path)
985 update_filterrule = True
986 elif update.field == api_pb2_v1.ComponentUpdateFieldID.DESCRIPTION:
987 new_docstring = update.description
988 elif update.field == api_pb2_v1.ComponentUpdateFieldID.ADMIN:
989 user_ids_dict = self._services.user.LookupUserIDs(
990 mar.cnxn, list(update.admin), autocreate=False)
991 new_admin_ids = [user_ids_dict[email] for email in update.admin]
992 elif update.field == api_pb2_v1.ComponentUpdateFieldID.CC:
993 user_ids_dict = self._services.user.LookupUserIDs(
994 mar.cnxn, list(update.cc), autocreate=False)
995 new_cc_ids = [user_ids_dict[email] for email in update.cc]
996 update_filterrule = True
997 elif update.field == api_pb2_v1.ComponentUpdateFieldID.DEPRECATED:
998 new_deprecated = update.deprecated
999 else:
1000 logging.error('Unknown component field %r', update.field)
1001
1002 new_modified = int(time.time())
1003 new_modifier_id = self._services.user.LookupUserID(
1004 mar.cnxn, mar.auth.email, autocreate=False)
1005 logging.info(
1006 'Updating component id %d: path-%s, docstring-%s, deprecated-%s,'
1007 ' admin_ids-%s, cc_ids-%s modified by %s', component_def.component_id,
1008 new_path, new_docstring, new_deprecated, new_admin_ids, new_cc_ids,
1009 new_modifier_id)
1010 self._services.config.UpdateComponentDef(
1011 mar.cnxn, mar.project_id, component_def.component_id,
1012 path=new_path, docstring=new_docstring, deprecated=new_deprecated,
1013 admin_ids=new_admin_ids, cc_ids=new_cc_ids, modified=new_modified,
1014 modifier_id=new_modifier_id)
1015
1016 # TODO(sheyang): reuse the code in componentdetails
1017 if original_path != new_path:
1018 # If the name changed then update all of its subcomponents as well.
1019 subcomponent_ids = tracker_bizobj.FindMatchingComponentIDs(
1020 original_path, config, exact=False)
1021 for subcomponent_id in subcomponent_ids:
1022 if subcomponent_id == component_def.component_id:
1023 continue
1024 subcomponent_def = tracker_bizobj.FindComponentDefByID(
1025 subcomponent_id, config)
1026 subcomponent_new_path = subcomponent_def.path.replace(
1027 original_path, new_path, 1)
1028 self._services.config.UpdateComponentDef(
1029 mar.cnxn, mar.project_id, subcomponent_def.component_id,
1030 path=subcomponent_new_path)
1031
1032 if update_filterrule:
1033 filterrules_helpers.RecomputeAllDerivedFields(
1034 mar.cnxn, self._services, mar.project, config)
1035
1036 return message_types.VoidMessage()
1037
1038
1039 @endpoints.api(name='monorail_client_configs', version='v1',
1040 description='Monorail API client configs.')
1041 class ClientConfigApi(remote.Service):
1042
1043 # Class variables. Handy to mock.
1044 _services = None
1045 _mar = None
1046
1047 @classmethod
1048 def _set_services(cls, services):
1049 cls._services = services
1050
1051 def mar_factory(self, request):
1052 if not self._mar:
1053 self._mar = monorailrequest.MonorailApiRequest(request, self._services)
1054 return self._mar
1055
1056 @endpoints.method(
1057 message_types.VoidMessage,
1058 message_types.VoidMessage,
1059 path='client_configs',
1060 http_method='POST',
1061 name='client_configs.update')
1062 def client_configs_update(self, request):
1063 mar = self.mar_factory(request)
1064 if not mar.perms.HasPerm(permissions.ADMINISTER_SITE, None, None):
1065 raise permissions.PermissionException(
1066 'The requester %s is not allowed to update client configs.' %
1067 mar.auth.email)
1068
1069 ROLE_DICT = {
1070 1: permissions.COMMITTER_ROLE,
1071 2: permissions.CONTRIBUTOR_ROLE,
1072 }
1073
1074 client_config = client_config_svc.GetClientConfigSvc()
1075
1076 cfg = client_config.GetConfigs()
1077 if not cfg:
1078 msg = 'Failed to fetch client configs.'
1079 logging.error(msg)
1080 raise endpoints.InternalServerErrorException(msg)
1081
1082 for client in cfg.clients:
1083 if not client.client_email:
1084 continue
1085 # 1: create the user if non-existent
1086 user_id = self._services.user.LookupUserID(
1087 mar.cnxn, client.client_email, autocreate=True)
1088 user_pb = self._services.user.GetUser(mar.cnxn, user_id)
1089
1090 logging.info('User ID %d for email %s', user_id, client.client_email)
1091
1092 # 2: set period and lifetime limit
1093 # new_soft_limit, new_hard_limit, new_lifetime_limit
1094 new_limit_tuple = (
1095 client.period_limit, client.period_limit, client.lifetime_limit)
1096 action_limit_updates = {'api_request': new_limit_tuple}
1097 self._services.user.UpdateUserSettings(
1098 mar.cnxn, user_id, user_pb, action_limit_updates=action_limit_updates)
1099
1100 logging.info('Updated api request limit %r', new_limit_tuple)
1101
1102 # 3: Update project role and extra perms
1103 projects_dict = self._services.project.GetAllProjects(mar.cnxn)
1104 project_name_to_ids = {
1105 p.project_name: p.project_id for p in projects_dict.itervalues()}
1106
1107 # Set project role and extra perms
1108 for perm in client.project_permissions:
1109 project_ids = self._GetProjectIDs(perm.project, project_name_to_ids)
1110 logging.info('Matching projects %r for name %s',
1111 project_ids, perm.project)
1112
1113 role = ROLE_DICT[perm.role]
1114 for p_id in project_ids:
1115 project = projects_dict[p_id]
1116 people_list = []
1117 if role == 'owner':
1118 people_list = project.owner_ids
1119 elif role == 'committer':
1120 people_list = project.committer_ids
1121 elif role == 'contributor':
1122 people_list = project.contributor_ids
1123 # Onlu update role/extra perms iff changed
1124 if not user_id in people_list:
1125 logging.info('Update project %s role %s for user %s',
1126 project.project_name, role, client.client_email)
1127 owner_ids, committer_ids, contributor_ids = (
1128 project_helpers.MembersWith(project, {user_id}, role))
1129 self._services.project.UpdateProjectRoles(
1130 mar.cnxn, p_id, owner_ids, committer_ids,
1131 contributor_ids)
1132 if perm.extra_permissions:
1133 member_extra_perms = permissions.FindExtraPerms(project, user_id)
1134 if (member_extra_perms and
1135 set(member_extra_perms.perms) == set(perm.extra_permissions)):
1136 continue
1137 logging.info('Update project %s extra perm %s for user %s',
1138 project.project_name, perm.extra_permissions,
1139 client.client_email)
1140 self._services.project.UpdateExtraPerms(
1141 mar.cnxn, p_id, user_id, list(perm.extra_permissions))
1142
1143 return message_types.VoidMessage()
1144
1145 def _GetProjectIDs(self, project_str, project_name_to_ids):
1146 result = []
1147 if any(ch in project_str for ch in ['*', '+', '?', '.']):
1148 pattern = re.compile(project_str)
1149 for p_name in project_name_to_ids.iterkeys():
1150 if pattern.match(p_name):
1151 project_id = project_name_to_ids.get(p_name)
1152 if project_id:
1153 result.append(project_id)
1154 else:
1155 project_id = project_name_to_ids.get(project_str)
1156 if project_id:
1157 result.append(project_id)
1158
1159 if not result:
1160 logging.warning('Cannot find projects for specified name %s',
1161 project_str)
1162 return result
1163
1164
OLDNEW
« no previous file with comments | « appengine/monorail/services/api_pb2_v1_helpers.py ('k') | appengine/monorail/services/cachemanager_svc.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698