OLD | NEW |
(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 |
OLD | NEW |