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

Side by Side Diff: appengine/monorail/services/project_svc.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
« no previous file with comments | « appengine/monorail/services/issue_svc.py ('k') | appengine/monorail/services/secrets_svc.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
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 """A set of functions that provide persistence for projects.
7
8 This module provides functions to get, update, create, and (in some
9 cases) delete each type of project business object. It provides
10 a logical persistence layer on top of the database.
11
12 Business objects are described in project_pb2.py.
13 """
14
15 import collections
16 import logging
17 import time
18
19 import settings
20 from framework import framework_bizobj
21 from framework import permissions
22 from framework import sql
23 from services import caches
24 from proto import project_pb2
25
26
27 PROJECT_TABLE_NAME = 'Project'
28 USER2PROJECT_TABLE_NAME = 'User2Project'
29 EXTRAPERM_TABLE_NAME = 'ExtraPerm'
30 MEMBERNOTES_TABLE_NAME = 'MemberNotes'
31 USERGROUPPROJECTS_TABLE_NAME = 'Group2Project'
32
33 PROJECT_COLS = [
34 'project_id', 'project_name', 'summary', 'description', 'state', 'access',
35 'read_only_reason', 'state_reason', 'delete_time', 'issue_notify_address',
36 'attachment_bytes_used', 'attachment_quota',
37 'cached_content_timestamp', 'recent_activity_timestamp', 'moved_to',
38 'process_inbound_email', 'only_owners_remove_restrictions',
39 'only_owners_see_contributors', 'revision_url_format',
40 'home_page', 'docs_url', 'logo_gcs_id', 'logo_file_name']
41 USER2PROJECT_COLS = ['project_id', 'user_id', 'role_name']
42 EXTRAPERM_COLS = ['project_id', 'user_id', 'perm']
43 MEMBERNOTES_COLS = ['project_id', 'user_id', 'notes']
44
45
46 class ProjectTwoLevelCache(caches.AbstractTwoLevelCache):
47 """Class to manage both RAM and memcache for Project PBs."""
48
49 def __init__(self, cachemanager, project_service):
50 super(ProjectTwoLevelCache, self).__init__(
51 cachemanager, 'project', 'project:', project_pb2.Project)
52 self.project_service = project_service
53
54 def _DeserializeProjects(
55 self, project_rows, role_rows, extraperm_rows):
56 """Convert database rows into a dictionary of Project PB keyed by ID."""
57 project_dict = {}
58
59 for project_row in project_rows:
60 (project_id, project_name, summary, description, state_name,
61 access_name, read_only_reason, state_reason, delete_time,
62 issue_notify_address, attachment_bytes_used, attachment_quota, cct,
63 recent_activity_timestamp, moved_to, process_inbound_email,
64 oorr, oosc, revision_url_format, home_page, docs_url,
65 logo_gcs_id, logo_file_name) = project_row
66 project = project_pb2.Project()
67 project.project_id = project_id
68 project.project_name = project_name
69 project.summary = summary
70 project.description = description
71 project.state = project_pb2.ProjectState(state_name.upper())
72 project.state_reason = state_reason or ''
73 project.access = project_pb2.ProjectAccess(access_name.upper())
74 project.read_only_reason = read_only_reason or ''
75 project.issue_notify_address = issue_notify_address or ''
76 project.attachment_bytes_used = attachment_bytes_used or 0
77 project.attachment_quota = attachment_quota
78 project.recent_activity = recent_activity_timestamp or 0
79 project.cached_content_timestamp = cct or 0
80 project.delete_time = delete_time or 0
81 project.moved_to = moved_to or ''
82 project.process_inbound_email = bool(process_inbound_email)
83 project.only_owners_remove_restrictions = bool(oorr)
84 project.only_owners_see_contributors = bool(oosc)
85 project.revision_url_format = revision_url_format or ''
86 project.home_page = home_page or ''
87 project.docs_url = docs_url or ''
88 project.logo_gcs_id = logo_gcs_id or ''
89 project.logo_file_name = logo_file_name or ''
90 project_dict[project_id] = project
91
92 for project_id, user_id, role_name in role_rows:
93 project = project_dict[project_id]
94 if role_name == 'owner':
95 project.owner_ids.append(user_id)
96 elif role_name == 'committer':
97 project.committer_ids.append(user_id)
98 elif role_name == 'contributor':
99 project.contributor_ids.append(user_id)
100
101 for project_id, user_id, perm in extraperm_rows:
102 project = project_dict[project_id]
103 extra_perms = permissions.FindExtraPerms(project, user_id)
104 if not extra_perms:
105 extra_perms = project_pb2.Project.ExtraPerms(
106 member_id=user_id)
107 project.extra_perms.append(extra_perms)
108
109 extra_perms.perms.append(perm)
110
111 return project_dict
112
113 def FetchItems(self, cnxn, keys):
114 """On RAM and memcache miss, hit the database to get missing projects."""
115 project_rows = self.project_service.project_tbl.Select(
116 cnxn, cols=PROJECT_COLS, project_id=keys)
117 role_rows = self.project_service.user2project_tbl.Select(
118 cnxn, cols=['project_id', 'user_id', 'role_name'],
119 project_id=keys)
120 extraperm_rows = self.project_service.extraperm_tbl.Select(
121 cnxn, cols=EXTRAPERM_COLS, project_id=keys)
122 retrieved_dict = self._DeserializeProjects(
123 project_rows, role_rows, extraperm_rows)
124 return retrieved_dict
125
126
127 class ProjectService(object):
128 """The persistence layer for project data."""
129
130 def __init__(self, cache_manager):
131 """Initialize this module so that it is ready to use.
132
133 Args:
134 cache_manager: local cache with distributed invalidation.
135 """
136 self.project_tbl = sql.SQLTableManager(PROJECT_TABLE_NAME)
137 self.user2project_tbl = sql.SQLTableManager(USER2PROJECT_TABLE_NAME)
138 self.extraperm_tbl = sql.SQLTableManager(EXTRAPERM_TABLE_NAME)
139 self.membernotes_tbl = sql.SQLTableManager(MEMBERNOTES_TABLE_NAME)
140 self.usergroupprojects_tbl = sql.SQLTableManager(
141 USERGROUPPROJECTS_TABLE_NAME)
142
143 # Like a dictionary {project_id: project}
144 self.project_2lc = ProjectTwoLevelCache(cache_manager, self)
145
146 # The project name to ID cache can never be invalidated by individual
147 # project changes because it is keyed by strings instead of ints. In
148 # the case of rare operations like deleting a project (or a future
149 # project renaming feature), we just InvalidateAll().
150 self.project_names_to_ids = cache_manager.MakeCache('project')
151
152 ### Creating projects
153
154 def CreateProject(
155 self, cnxn, project_name, owner_ids, committer_ids, contributor_ids,
156 summary, description, state=project_pb2.ProjectState.LIVE,
157 access=None, read_only=None, home_page=None, docs_url=None,
158 logo_gcs_id=None, logo_file_name=None):
159 """Create and store a Project with the given attributes.
160
161 Args:
162 cnxn: connection to SQL database.
163 project_name: a valid project name, all lower case.
164 owner_ids: a list of user IDs for the project owners.
165 committer_ids: a list of user IDs for the project members.
166 contributor_ids: a list of user IDs for the project contributors.
167 summary: one-line explanation of the project.
168 description: one-page explanation of the project.
169 state: a project state enum defined in project_pb2.
170 access: optional project access enum defined in project.proto.
171 read_only: if given, provides a status message and marks the project as
172 read-only.
173 home_page: home page of the project
174 docs_url: url to redirect to for wiki/documentation links
175 logo_gcs_id: google storage object id of the project's logo
176 logo_file_name: uploaded file name of the project's logo
177
178 Returns:
179 The int project_id of the new project.
180
181 Raises:
182 ProjectAlreadyExists: if a project with that name already exists.
183 """
184 assert framework_bizobj.IsValidProjectName(project_name)
185 if self.LookupProjectIDs(cnxn, [project_name]):
186 raise ProjectAlreadyExists()
187
188 project = project_pb2.MakeProject(
189 project_name, state=state, access=access,
190 description=description, summary=summary,
191 owner_ids=owner_ids, committer_ids=committer_ids,
192 contributor_ids=contributor_ids, read_only=read_only,
193 home_page=home_page, docs_url=docs_url, logo_gcs_id=logo_gcs_id,
194 logo_file_name=logo_file_name)
195
196 project.project_id = self._InsertProject(cnxn, project)
197 return project.project_id
198
199 def _InsertProject(self, cnxn, project):
200 """Insert the given project into the database."""
201 # Note: project_id is not specified because it is auto_increment.
202 project_id = self.project_tbl.InsertRow(
203 cnxn, project_name=project.project_name,
204 summary=project.summary, description=project.description,
205 state=str(project.state), access=str(project.access),
206 home_page=project.home_page, docs_url=project.docs_url,
207 logo_gcs_id=project.logo_gcs_id, logo_file_name=project.logo_file_name)
208 logging.info('stored project was given project_id %d', project_id)
209
210 self.user2project_tbl.InsertRows(
211 cnxn, ['project_id', 'user_id', 'role_name'],
212 [(project_id, user_id, 'owner')
213 for user_id in project.owner_ids] +
214 [(project_id, user_id, 'committer')
215 for user_id in project.committer_ids] +
216 [(project_id, user_id, 'contributor')
217 for user_id in project.contributor_ids])
218
219 return project_id
220
221 ### Lookup project names and IDs
222
223 def LookupProjectIDs(self, cnxn, project_names):
224 """Return a list of project IDs for the specified projects."""
225 id_dict, missed_names = self.project_names_to_ids.GetAll(project_names)
226 if missed_names:
227 rows = self.project_tbl.Select(
228 cnxn, cols=['project_name', 'project_id'], project_name=missed_names)
229 retrieved_dict = dict(rows)
230 self.project_names_to_ids.CacheAll(retrieved_dict)
231 id_dict.update(retrieved_dict)
232
233 return id_dict
234
235 def LookupProjectNames(self, cnxn, project_ids):
236 """Lookup the names of the projects with the given IDs."""
237 projects_dict = self.GetProjects(cnxn, project_ids)
238 return {p.project_id: p.project_name
239 for p in projects_dict.itervalues()}
240
241 ### Retrieving projects
242
243 def GetAllProjects(self, cnxn, use_cache=True):
244 """Return A dict mapping IDs to all live project PBs."""
245 project_rows = self.project_tbl.Select(
246 cnxn, cols=['project_id'], state=project_pb2.ProjectState.LIVE)
247 project_ids = [row[0] for row in project_rows]
248 projects_dict = self.GetProjects(cnxn, project_ids, use_cache=use_cache)
249
250 return projects_dict
251
252 def GetVisibleLiveProjects(self, cnxn, logged_in_user, effective_ids,
253 use_cache=True):
254 """Return all user visible live project ids.
255
256 Args:
257 cnxn: connection to SQL database.
258 logged_in_user: protocol buffer of the logged in user. Can be None.
259 effective_ids: set of user IDs for this user. Can be None.
260 use_cache: pass False to force database query to find Project protocol
261 buffers.
262
263 Returns:
264 A list of project ids of user visible live projects sorted by the names
265 of the projects.
266 """
267 project_rows = self.project_tbl.Select(
268 cnxn, cols=['project_id'], state=project_pb2.ProjectState.LIVE)
269 project_ids = [row[0] for row in project_rows]
270 projects_dict = self.GetProjects(cnxn, project_ids, use_cache=use_cache)
271
272 visible_projects = [project for project in projects_dict.values()
273 if permissions.UserCanViewProject(
274 logged_in_user, effective_ids, project)]
275 visible_projects.sort(key=lambda p: p.project_name)
276
277 return [project.project_id for project in visible_projects]
278
279 def GetProjects(self, cnxn, project_ids, use_cache=True):
280 """Load all the Project PBs for the given projects.
281
282 Args:
283 cnxn: connection to SQL database.
284 project_ids: list of int project IDs
285 use_cache: pass False to force database query.
286
287 Returns:
288 A dict mapping IDs to the corresponding Project protocol buffers.
289
290 Raises:
291 NoSuchProjectException: if any of the projects was not found.
292 """
293 project_dict, missed_ids = self.project_2lc.GetAll(
294 cnxn, project_ids, use_cache=use_cache)
295
296 # Also, update the project name cache.
297 self.project_names_to_ids.CacheAll(
298 {p.project_name: p.project_id for p in project_dict.itervalues()})
299
300 if missed_ids:
301 raise NoSuchProjectException()
302
303 return project_dict
304
305 def GetProject(self, cnxn, project_id, use_cache=True):
306 """Load the specified project from the database."""
307 project_id_dict = self.GetProjects(cnxn, [project_id], use_cache=use_cache)
308 return project_id_dict[project_id]
309
310 def GetProjectsByName(self, cnxn, project_names, use_cache=True):
311 """Load all the Project PBs for the given projects.
312
313 Args:
314 cnxn: connection to SQL database.
315 project_names: list of project names.
316 use_cache: specifify False to force database query.
317
318 Returns:
319 A dict mapping names to the corresponding Project protocol buffers.
320 """
321 project_ids = self.LookupProjectIDs(cnxn, project_names).values()
322 projects = self.GetProjects(cnxn, project_ids, use_cache=use_cache)
323 return {p.project_name: p for p in projects.itervalues()}
324
325 def GetProjectByName(self, cnxn, project_name, use_cache=True):
326 """Load the specified project from the database, None if does not exist."""
327 project_dict = self.GetProjectsByName(
328 cnxn, [project_name], use_cache=use_cache)
329 return project_dict.get(project_name)
330
331 ### Deleting projects
332
333 def ExpungeProject(self, cnxn, project_id):
334 """Wipes a project from the system."""
335 logging.info('expunging project %r', project_id)
336 self.user2project_tbl.Delete(cnxn, project_id=project_id)
337 self.usergroupprojects_tbl.Delete(cnxn, project_id=project_id)
338 self.extraperm_tbl.Delete(cnxn, project_id=project_id)
339 self.membernotes_tbl.Delete(cnxn, project_id=project_id)
340 self.project_tbl.Delete(cnxn, project_id=project_id)
341
342 ### Updating projects
343
344 def UpdateProject(
345 self, cnxn, project_id, summary=None, description=None,
346 state=None, state_reason=None, access=None, issue_notify_address=None,
347 attachment_bytes_used=None, attachment_quota=None, moved_to=None,
348 process_inbound_email=None, only_owners_remove_restrictions=None,
349 read_only_reason=None, cached_content_timestamp=None,
350 only_owners_see_contributors=None, delete_time=None,
351 recent_activity=None, revision_url_format=None, home_page=None,
352 docs_url=None, logo_gcs_id=None, logo_file_name=None):
353 """Update the DB with the given project information."""
354 # This will be a newly constructed object, not from the cache and not
355 # shared with any other thread.
356 project = self.GetProject(cnxn, project_id, use_cache=False)
357 if not project:
358 raise NoSuchProjectException()
359
360 delta = {}
361 if summary is not None:
362 delta['summary'] = summary
363 if description is not None:
364 delta['description'] = description
365 if state is not None:
366 delta['state'] = str(state).lower()
367 if state is not None:
368 delta['state_reason'] = state_reason
369 if access is not None:
370 delta['access'] = str(access).lower()
371 if read_only_reason is not None:
372 delta['read_only_reason'] = read_only_reason
373 if issue_notify_address is not None:
374 delta['issue_notify_address'] = issue_notify_address
375 if attachment_bytes_used is not None:
376 delta['attachment_bytes_used'] = attachment_bytes_used
377 if attachment_quota is not None:
378 delta['attachment_quota'] = attachment_quota
379 if moved_to is not None:
380 delta['moved_to'] = moved_to
381 if process_inbound_email is not None:
382 delta['process_inbound_email'] = process_inbound_email
383 if only_owners_remove_restrictions is not None:
384 delta['only_owners_remove_restrictions'] = (
385 only_owners_remove_restrictions)
386 if only_owners_see_contributors is not None:
387 delta['only_owners_see_contributors'] = only_owners_see_contributors
388 if delete_time is not None:
389 delta['delete_time'] = delete_time
390 if recent_activity is not None:
391 delta['recent_activity_timestamp'] = recent_activity
392 if revision_url_format is not None:
393 delta['revision_url_format'] = revision_url_format
394 if home_page is not None:
395 delta['home_page'] = home_page
396 if docs_url is not None:
397 delta['docs_url'] = docs_url
398 if logo_gcs_id is not None:
399 delta['logo_gcs_id'] = logo_gcs_id
400 if logo_file_name is not None:
401 delta['logo_file_name'] = logo_file_name
402 if cached_content_timestamp is not None:
403 delta['cached_content_timestamp'] = cached_content_timestamp
404 self.project_tbl.Update(cnxn, delta, project_id=project_id)
405
406 self.project_2lc.InvalidateKeys(cnxn, [project_id])
407
408 # Now update the full-text index.
409 if summary is not None:
410 project.summary = summary
411 if description is not None:
412 project.description = description
413 if state is not None:
414 project.state = state
415 if access is not None:
416 project.access = access
417 if only_owners_remove_restrictions is not None:
418 project.only_owners_remove_restrictions = (
419 only_owners_remove_restrictions)
420 if only_owners_see_contributors is not None:
421 project.only_owners_see_contributors = only_owners_see_contributors
422
423 def UpdateProjectRoles(
424 self, cnxn, project_id, owner_ids, committer_ids, contributor_ids,
425 now=None):
426 """Store the project's roles in the DB and set cached_content_timestamp."""
427 # This will be a newly constructed object, not from the cache and not
428 # shared with any other thread.
429 project = self.GetProject(cnxn, project_id, use_cache=False)
430 if not project:
431 raise NoSuchProjectException()
432
433 now = now or int(time.time())
434 self.project_tbl.Update(
435 cnxn, {'cached_content_timestamp': now},
436 project_id=project_id)
437
438 self.user2project_tbl.Delete(
439 cnxn, project_id=project_id, role_name='owner', commit=False)
440 self.user2project_tbl.Delete(
441 cnxn, project_id=project_id, role_name='committer', commit=False)
442 self.user2project_tbl.Delete(
443 cnxn, project_id=project_id, role_name='contributor', commit=False)
444
445 self.user2project_tbl.InsertRows(
446 cnxn, ['project_id', 'user_id', 'role_name'],
447 [(project_id, user_id, 'owner') for user_id in owner_ids],
448 commit=False)
449 self.user2project_tbl.InsertRows(
450 cnxn, ['project_id', 'user_id', 'role_name'],
451 [(project_id, user_id, 'committer')
452 for user_id in committer_ids], commit=False)
453
454 self.user2project_tbl.InsertRows(
455 cnxn, ['project_id', 'user_id', 'role_name'],
456 [(project_id, user_id, 'contributor')
457 for user_id in contributor_ids], commit=False)
458
459 cnxn.Commit()
460 self.project_2lc.InvalidateKeys(cnxn, [project_id])
461
462 project.owner_ids = owner_ids
463 project.committer_ids = committer_ids
464 project.contributor_ids = contributor_ids
465
466 def MarkProjectDeletable(self, cnxn, project_id, config_service):
467 """Update the project's state to make it DELETABLE and free up the name.
468
469 Args:
470 cnxn: connection to SQL database.
471 project_id: int ID of the project that will be deleted soon.
472 config_service: issue tracker configuration persistence service, needed
473 to invalidate cached issue tracker results.
474 """
475 generated_name = 'DELETABLE_%d' % project_id
476 delta = {'project_name': generated_name, 'state': 'deletable'}
477 self.project_tbl.Update(cnxn, delta, project_id=project_id)
478
479 self.project_2lc.InvalidateKeys(cnxn, [project_id])
480 # We cannot invalidate a specific part of the name->proj cache by name,
481 # So, tell every job to just drop the whole cache. It should refill
482 # efficiently and incrementally from memcache.
483 self.project_2lc.InvalidateAllRamEntries(cnxn)
484 config_service.InvalidateMemcacheForEntireProject(project_id)
485
486 def UpdateRecentActivity(self, cnxn, project_id, now=None):
487 """Set the project's recent_activity to the current time."""
488 now = now or int(time.time())
489 self.UpdateProject(cnxn, project_id, recent_activity=now)
490
491 ### Roles and extra perms
492
493 def GetUserRolesInAllProjects(self, cnxn, effective_ids):
494 """Return three sets of project IDs where the user has a role."""
495 owned_project_ids = set()
496 membered_project_ids = set()
497 contrib_project_ids = set()
498
499 rows = self.user2project_tbl.Select(
500 cnxn, cols=['project_id', 'role_name'], user_id=effective_ids)
501
502 for project_id, role_name in rows:
503 if role_name == 'owner':
504 owned_project_ids.add(project_id)
505 elif role_name == 'committer':
506 membered_project_ids.add(project_id)
507 elif role_name == 'contributor':
508 contrib_project_ids.add(project_id)
509 else:
510 logging.warn('Unexpected role name %r', role_name)
511
512 return owned_project_ids, membered_project_ids, contrib_project_ids
513
514 def UpdateExtraPerms(
515 self, cnxn, project_id, member_id, extra_perms, now=None):
516 """Load the project, update the member's extra perms, and store.
517
518 Args:
519 cnxn: connection to SQL database.
520 project_id: int ID of the current project.
521 member_id: int user id of the user that was edited.
522 extra_perms: list of strings for perms that the member
523 should have over-and-above what their role gives them.
524 now: fake int(time.time()) value passed in during unit testing.
525 """
526 # This will be a newly constructed object, not from the cache and not
527 # shared with any other thread.
528 project = self.GetProject(cnxn, project_id, use_cache=False)
529
530 member_extra_perms = permissions.FindExtraPerms(project, member_id)
531 if not member_extra_perms and not extra_perms:
532 return
533 if member_extra_perms and list(member_extra_perms.perms) == extra_perms:
534 return
535
536 if member_extra_perms:
537 member_extra_perms.perms = extra_perms
538 else:
539 member_extra_perms = project_pb2.Project.ExtraPerms(
540 member_id=member_id, perms=extra_perms)
541 project.extra_perms.append(member_extra_perms)
542
543 self.extraperm_tbl.Delete(
544 cnxn, project_id=project_id, user_id=member_id, commit=False)
545 self.extraperm_tbl.InsertRows(
546 cnxn, EXTRAPERM_COLS,
547 [(project_id, member_id, perm) for perm in extra_perms],
548 commit=False)
549 now = now or int(time.time())
550 project.cached_content_timestamp = now
551 self.project_tbl.Update(
552 cnxn, {'cached_content_timestamp': project.cached_content_timestamp},
553 project_id=project_id, commit=False)
554 cnxn.Commit()
555
556 self.project_2lc.InvalidateKeys(cnxn, [project_id])
557
558 ### Project Commitments
559
560 def GetProjectCommitments(self, cnxn, project_id):
561 """Get the project commitments (notes) from the DB.
562
563 Args:
564 cnxn: connection to SQL database.
565 project_id: int project ID.
566
567 Returns:
568 A the specified project's ProjectCommitments instance, or an empty one,
569 if the project doesn't exist, or has not documented member
570 commitments.
571 """
572 # Get the notes. Don't get the project_id column
573 # since we already know that value.
574 notes_rows = self.membernotes_tbl.Select(
575 cnxn, cols=['user_id', 'notes'], project_id=project_id)
576 notes_dict = dict(notes_rows)
577
578 project_commitments = project_pb2.ProjectCommitments()
579 project_commitments.project_id = project_id
580 for user_id in notes_dict.keys():
581 commitment = project_pb2.ProjectCommitments.MemberCommitment(
582 member_id=user_id,
583 notes=notes_dict.get(user_id, ''))
584 project_commitments.commitments.append(commitment)
585
586 return project_commitments
587
588 def _StoreProjectCommitments(self, cnxn, project_commitments):
589 """Store an updated set of project commitments in the DB.
590
591 Args:
592 cnxn: connection to SQL database.
593 project_commitments: ProjectCommitments PB
594 """
595 project_id = project_commitments.project_id
596 notes_rows = []
597 for commitment in project_commitments.commitments:
598 notes_rows.append(
599 (project_id, commitment.member_id, commitment.notes))
600
601 # TODO(jrobbins): this should be in a transaction.
602 self.membernotes_tbl.Delete(cnxn, project_id=project_id)
603 self.membernotes_tbl.InsertRows(
604 cnxn, MEMBERNOTES_COLS, notes_rows, ignore=True)
605
606 def UpdateCommitments(self, cnxn, project_id, member_id, notes):
607 """Update the member's commitments in the specified project.
608
609 Args:
610 cnxn: connection to SQL database.
611 project_id: int ID of the current project.
612 member_id: int user ID of the user that was edited.
613 notes: further notes on the member's expected involvment
614 in the project.
615 """
616 project_commitments = self.GetProjectCommitments(cnxn, project_id)
617
618 commitment = None
619 for c in project_commitments.commitments:
620 if c.member_id == member_id:
621 commitment = c
622 break
623 else:
624 commitment = project_pb2.ProjectCommitments.MemberCommitment(
625 member_id=member_id)
626 project_commitments.commitments.append(commitment)
627
628 dirty = False
629
630 if commitment.notes != notes:
631 commitment.notes = notes
632 dirty = True
633
634 if dirty:
635 self._StoreProjectCommitments(cnxn, project_commitments)
636
637
638 class Error(Exception):
639 """Base exception class for this package."""
640
641
642 class ProjectAlreadyExists(Error):
643 """Tried to create a project that already exists."""
644
645
646 class NoSuchProjectException(Error):
647 """No project with the specified name exists."""
648 pass
OLDNEW
« no previous file with comments | « appengine/monorail/services/issue_svc.py ('k') | appengine/monorail/services/secrets_svc.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698