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

Side by Side Diff: appengine/monorail/services/config_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
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 """Classes and functions for persistence of issue tracker configuration.
7
8 This module provides functions to get, update, create, and (in some
9 cases) delete each type of business object. It provides a logical
10 persistence layer on top of an SQL database.
11
12 Business objects are described in tracker_pb2.py and tracker_bizobj.py.
13 """
14
15 import collections
16 import logging
17
18 from google.appengine.api import memcache
19
20 import settings
21 from framework import sql
22 from proto import tracker_pb2
23 from services import caches
24 from tracker import tracker_bizobj
25
26
27 TEMPLATE_TABLE_NAME = 'Template'
28 TEMPLATE2LABEL_TABLE_NAME = 'Template2Label'
29 TEMPLATE2ADMIN_TABLE_NAME = 'Template2Admin'
30 TEMPLATE2COMPONENT_TABLE_NAME = 'Template2Component'
31 TEMPLATE2FIELDVALUE_TABLE_NAME = 'Template2FieldValue'
32 PROJECTISSUECONFIG_TABLE_NAME = 'ProjectIssueConfig'
33 LABELDEF_TABLE_NAME = 'LabelDef'
34 FIELDDEF_TABLE_NAME = 'FieldDef'
35 FIELDDEF2ADMIN_TABLE_NAME = 'FieldDef2Admin'
36 COMPONENTDEF_TABLE_NAME = 'ComponentDef'
37 COMPONENT2ADMIN_TABLE_NAME = 'Component2Admin'
38 COMPONENT2CC_TABLE_NAME = 'Component2Cc'
39 STATUSDEF_TABLE_NAME = 'StatusDef'
40
41 TEMPLATE_COLS = [
42 'id', 'project_id', 'name', 'content', 'summary', 'summary_must_be_edited',
43 'owner_id', 'status', 'members_only', 'owner_defaults_to_member',
44 'component_required']
45 TEMPLATE2LABEL_COLS = ['template_id', 'label']
46 TEMPLATE2COMPONENT_COLS = ['template_id', 'component_id']
47 TEMPLATE2ADMIN_COLS = ['template_id', 'admin_id']
48 TEMPLATE2FIELDVALUE_COLS = [
49 'template_id', 'field_id', 'int_value', 'str_value', 'user_id']
50 PROJECTISSUECONFIG_COLS = [
51 'project_id', 'statuses_offer_merge', 'exclusive_label_prefixes',
52 'default_template_for_developers', 'default_template_for_users',
53 'default_col_spec', 'default_sort_spec', 'default_x_attr',
54 'default_y_attr', 'custom_issue_entry_url']
55 STATUSDEF_COLS = [
56 'id', 'project_id', 'rank', 'status', 'means_open', 'docstring',
57 'deprecated']
58 LABELDEF_COLS = [
59 'id', 'project_id', 'rank', 'label', 'docstring', 'deprecated']
60 FIELDDEF_COLS = [
61 'id', 'project_id', 'rank', 'field_name', 'field_type', 'applicable_type',
62 'applicable_predicate', 'is_required', 'is_multivalued',
63 'min_value', 'max_value', 'regex', 'needs_member', 'needs_perm',
64 'grants_perm', 'notify_on', 'docstring', 'is_deleted']
65 FIELDDEF2ADMIN_COLS = ['field_id', 'admin_id']
66 COMPONENTDEF_COLS = ['id', 'project_id', 'path', 'docstring', 'deprecated',
67 'created', 'creator_id', 'modified', 'modifier_id']
68 COMPONENT2ADMIN_COLS = ['component_id', 'admin_id']
69 COMPONENT2CC_COLS = ['component_id', 'cc_id']
70
71 NOTIFY_ON_ENUM = ['never', 'any_comment']
72
73
74 class LabelRowTwoLevelCache(caches.AbstractTwoLevelCache):
75 """Class to manage RAM and memcache for label rows.
76
77 Label rows exist for every label used in a project, even those labels
78 that were added to issues in an ad hoc way without being defined in the
79 config ahead of time.
80 """
81
82 def __init__(self, cache_manager, config_service):
83 super(LabelRowTwoLevelCache, self).__init__(
84 cache_manager, 'project', 'label_rows:', None)
85 self.config_service = config_service
86
87 def _DeserializeLabelRows(self, label_def_rows):
88 """Convert DB result rows into a dict {project_id: [row, ...]}."""
89 result_dict = collections.defaultdict(list)
90 for label_id, project_id, rank, label, docstr, deprecated in label_def_rows:
91 result_dict[project_id].append(
92 (label_id, project_id, rank, label, docstr, deprecated))
93
94 return result_dict
95
96 def FetchItems(self, cnxn, keys):
97 """On RAM and memcache miss, hit the database."""
98 label_def_rows = self.config_service.labeldef_tbl.Select(
99 cnxn, cols=LABELDEF_COLS, project_id=keys,
100 order_by=[('rank DESC', []), ('label DESC', [])])
101 label_rows_dict = self._DeserializeLabelRows(label_def_rows)
102
103 # Make sure that every requested project is represented in the result
104 for project_id in keys:
105 label_rows_dict.setdefault(project_id, [])
106
107 return label_rows_dict
108
109
110 class StatusRowTwoLevelCache(caches.AbstractTwoLevelCache):
111 """Class to manage RAM and memcache for status rows."""
112
113 def __init__(self, cache_manager, config_service):
114 super(StatusRowTwoLevelCache, self).__init__(
115 cache_manager, 'project', 'status_rows:', None)
116 self.config_service = config_service
117
118 def _DeserializeStatusRows(self, def_rows):
119 """Convert status definition rows into {project_id: [row, ...]}."""
120 result_dict = collections.defaultdict(list)
121 for (status_id, project_id, rank, status,
122 means_open, docstr, deprecated) in def_rows:
123 result_dict[project_id].append(
124 (status_id, project_id, rank, status, means_open, docstr, deprecated))
125
126 return result_dict
127
128 def FetchItems(self, cnxn, keys):
129 """On cache miss, get status definition rows from the DB."""
130 status_def_rows = self.config_service.statusdef_tbl.Select(
131 cnxn, cols=STATUSDEF_COLS, project_id=keys,
132 order_by=[('rank DESC', []), ('status DESC', [])])
133 status_rows_dict = self._DeserializeStatusRows(status_def_rows)
134
135 # Make sure that every requested project is represented in the result
136 for project_id in keys:
137 status_rows_dict.setdefault(project_id, [])
138
139 return status_rows_dict
140
141
142 class FieldRowTwoLevelCache(caches.AbstractTwoLevelCache):
143 """Class to manage RAM and memcache for field rows.
144
145 Field rows exist for every field used in a project, since they cannot be
146 created through ad-hoc means.
147 """
148
149 def __init__(self, cache_manager, config_service):
150 super(FieldRowTwoLevelCache, self).__init__(
151 cache_manager, 'project', 'field_rows:', None)
152 self.config_service = config_service
153
154 def _DeserializeFieldRows(self, field_def_rows):
155 """Convert DB result rows into a dict {project_id: [row, ...]}."""
156 result_dict = collections.defaultdict(list)
157 # TODO(agable): Actually process the rest of the items.
158 for (field_id, project_id, rank, field_name, _field_type, _applicable_type,
159 _applicable_predicate, _is_required, _is_multivalued, _min_value,
160 _max_value, _regex, _needs_member, _needs_perm, _grants_perm,
161 _notify_on, docstring, _is_deleted) in field_def_rows:
162 result_dict[project_id].append(
163 (field_id, project_id, rank, field_name, docstring))
164
165 return result_dict
166
167 def FetchItems(self, cnxn, keys):
168 """On RAM and memcache miss, hit the database."""
169 field_def_rows = self.config_service.fielddef_tbl.Select(
170 cnxn, cols=FIELDDEF_COLS, project_id=keys,
171 order_by=[('rank DESC', []), ('field_name DESC', [])])
172 field_rows_dict = self._DeserializeFieldRows(field_def_rows)
173
174 # Make sure that every requested project is represented in the result
175 for project_id in keys:
176 field_rows_dict.setdefault(project_id, [])
177
178 return field_rows_dict
179
180
181 class ConfigTwoLevelCache(caches.AbstractTwoLevelCache):
182 """Class to manage RAM and memcache for IssueProjectConfig PBs."""
183
184 def __init__(self, cache_manager, config_service):
185 super(ConfigTwoLevelCache, self).__init__(
186 cache_manager, 'project', 'config:', tracker_pb2.ProjectIssueConfig)
187 self.config_service = config_service
188
189 def _UnpackProjectIssueConfig(self, config_row):
190 """Partially construct a config object using info from a DB row."""
191 (project_id, statuses_offer_merge, exclusive_label_prefixes,
192 default_template_for_developers, default_template_for_users,
193 default_col_spec, default_sort_spec, default_x_attr, default_y_attr,
194 custom_issue_entry_url) = config_row
195 config = tracker_pb2.ProjectIssueConfig()
196 config.project_id = project_id
197 config.statuses_offer_merge.extend(statuses_offer_merge.split())
198 config.exclusive_label_prefixes.extend(exclusive_label_prefixes.split())
199 config.default_template_for_developers = default_template_for_developers
200 config.default_template_for_users = default_template_for_users
201 config.default_col_spec = default_col_spec
202 config.default_sort_spec = default_sort_spec
203 config.default_x_attr = default_x_attr
204 config.default_y_attr = default_y_attr
205 if custom_issue_entry_url is not None:
206 config.custom_issue_entry_url = custom_issue_entry_url
207
208 return config
209
210 def _UnpackTemplate(self, template_row):
211 """Partially construct a template object using info from a DB row."""
212 (template_id, project_id, name, content, summary,
213 summary_must_be_edited, owner_id, status,
214 members_only, owner_defaults_to_member, component_required) = template_row
215 template = tracker_pb2.TemplateDef()
216 template.template_id = template_id
217 template.name = name
218 template.content = content
219 template.summary = summary
220 template.summary_must_be_edited = bool(
221 summary_must_be_edited)
222 template.owner_id = owner_id or 0
223 template.status = status
224 template.members_only = bool(members_only)
225 template.owner_defaults_to_member = bool(owner_defaults_to_member)
226 template.component_required = bool(component_required)
227
228 return template, project_id
229
230 def _UnpackFieldDef(self, fielddef_row):
231 """Partially construct a FieldDef object using info from a DB row."""
232 (field_id, project_id, _rank, field_name, field_type,
233 applic_type, applic_pred, is_required, is_multivalued,
234 min_value, max_value, regex, needs_member, needs_perm,
235 grants_perm, notify_on_str, docstring, is_deleted) = fielddef_row
236 if notify_on_str == 'any_comment':
237 notify_on = tracker_pb2.NotifyTriggers.ANY_COMMENT
238 else:
239 notify_on = tracker_pb2.NotifyTriggers.NEVER
240
241 return tracker_bizobj.MakeFieldDef(
242 field_id, project_id, field_name,
243 tracker_pb2.FieldTypes(field_type.upper()), applic_type, applic_pred,
244 is_required, is_multivalued, min_value, max_value, regex,
245 needs_member, needs_perm, grants_perm, notify_on, docstring,
246 is_deleted)
247
248 def _UnpackComponentDef(
249 self, cd_row, component2admin_rows, component2cc_rows):
250 """Partially construct a FieldDef object using info from a DB row."""
251 (component_id, project_id, path, docstring, deprecated, created,
252 creator_id, modified, modifier_id) = cd_row
253 cd = tracker_bizobj.MakeComponentDef(
254 component_id, project_id, path, docstring, deprecated,
255 [admin_id for comp_id, admin_id in component2admin_rows
256 if comp_id == component_id],
257 [cc_id for comp_id, cc_id in component2cc_rows
258 if comp_id == component_id],
259 created, creator_id, modified, modifier_id)
260
261 return cd
262
263 def _DeserializeIssueConfigs(
264 self, config_rows, template_rows, template2label_rows,
265 template2component_rows, template2admin_rows, template2fieldvalue_rows,
266 statusdef_rows, labeldef_rows, fielddef_rows, fielddef2admin_rows,
267 componentdef_rows, component2admin_rows, component2cc_rows):
268 """Convert the given row tuples into a dict of ProjectIssueConfig PBs."""
269 result_dict = {}
270 template_dict = {}
271 fielddef_dict = {}
272
273 for config_row in config_rows:
274 config = self._UnpackProjectIssueConfig(config_row)
275 result_dict[config.project_id] = config
276
277 for template_row in template_rows:
278 template, project_id = self._UnpackTemplate(template_row)
279 if project_id in result_dict:
280 result_dict[project_id].templates.append(template)
281 template_dict[template.template_id] = template
282
283 for template2label_row in template2label_rows:
284 template_id, label = template2label_row
285 template = template_dict.get(template_id)
286 if template:
287 template.labels.append(label)
288
289 for template2component_row in template2component_rows:
290 template_id, component_id = template2component_row
291 template = template_dict.get(template_id)
292 if template:
293 template.component_ids.append(component_id)
294
295 for template2admin_row in template2admin_rows:
296 template_id, admin_id = template2admin_row
297 template = template_dict.get(template_id)
298 if template:
299 template.admin_ids.append(admin_id)
300
301 for fv_row in template2fieldvalue_rows:
302 template_id, field_id, int_value, str_value, user_id = fv_row
303 fv = tracker_bizobj.MakeFieldValue(
304 field_id, int_value, str_value, user_id, False)
305 template = template_dict.get(template_id)
306 if template:
307 template.field_values.append(fv)
308
309 for statusdef_row in statusdef_rows:
310 (_, project_id, _rank, status,
311 means_open, docstring, deprecated) = statusdef_row
312 if project_id in result_dict:
313 wks = tracker_pb2.StatusDef(
314 status=status, means_open=bool(means_open),
315 status_docstring=docstring or '', deprecated=bool(deprecated))
316 result_dict[project_id].well_known_statuses.append(wks)
317
318 for labeldef_row in labeldef_rows:
319 _, project_id, _rank, label, docstring, deprecated = labeldef_row
320 if project_id in result_dict:
321 wkl = tracker_pb2.LabelDef(
322 label=label, label_docstring=docstring or '',
323 deprecated=bool(deprecated))
324 result_dict[project_id].well_known_labels.append(wkl)
325
326 for fd_row in fielddef_rows:
327 fd = self._UnpackFieldDef(fd_row)
328 result_dict[fd.project_id].field_defs.append(fd)
329 fielddef_dict[fd.field_id] = fd
330
331 for fd2admin_row in fielddef2admin_rows:
332 field_id, admin_id = fd2admin_row
333 fd = fielddef_dict.get(field_id)
334 if fd:
335 fd.admin_ids.append(admin_id)
336
337 for cd_row in componentdef_rows:
338 cd = self._UnpackComponentDef(
339 cd_row, component2admin_rows, component2cc_rows)
340 result_dict[cd.project_id].component_defs.append(cd)
341
342 return result_dict
343
344 def _FetchConfigs(self, cnxn, project_ids):
345 """On RAM and memcache miss, hit the database."""
346 config_rows = self.config_service.projectissueconfig_tbl.Select(
347 cnxn, cols=PROJECTISSUECONFIG_COLS, project_id=project_ids)
348 template_rows = self.config_service.template_tbl.Select(
349 cnxn, cols=TEMPLATE_COLS, project_id=project_ids,
350 order_by=[('name', [])])
351 template_ids = [row[0] for row in template_rows]
352 template2label_rows = self.config_service.template2label_tbl.Select(
353 cnxn, cols=TEMPLATE2LABEL_COLS, template_id=template_ids)
354 template2component_rows = self.config_service.template2component_tbl.Select(
355 cnxn, cols=TEMPLATE2COMPONENT_COLS, template_id=template_ids)
356 template2admin_rows = self.config_service.template2admin_tbl.Select(
357 cnxn, cols=TEMPLATE2ADMIN_COLS, template_id=template_ids)
358 template2fv_rows = self.config_service.template2fieldvalue_tbl.Select(
359 cnxn, cols=TEMPLATE2FIELDVALUE_COLS, template_id=template_ids)
360 logging.info('t2fv is %r', template2fv_rows)
361 statusdef_rows = self.config_service.statusdef_tbl.Select(
362 cnxn, cols=STATUSDEF_COLS, project_id=project_ids,
363 where=[('rank IS NOT NULL', [])], order_by=[('rank', [])])
364 labeldef_rows = self.config_service.labeldef_tbl.Select(
365 cnxn, cols=LABELDEF_COLS, project_id=project_ids,
366 where=[('rank IS NOT NULL', [])], order_by=[('rank', [])])
367 # TODO(jrobbins): For now, sort by field name, but someday allow admins
368 # to adjust the rank to group and order field definitions logically.
369 fielddef_rows = self.config_service.fielddef_tbl.Select(
370 cnxn, cols=FIELDDEF_COLS, project_id=project_ids,
371 order_by=[('field_name', [])])
372 field_ids = [row[0] for row in fielddef_rows]
373 fielddef2admin_rows = self.config_service.fielddef2admin_tbl.Select(
374 cnxn, cols=FIELDDEF2ADMIN_COLS, field_id=field_ids)
375 componentdef_rows = self.config_service.componentdef_tbl.Select(
376 cnxn, cols=COMPONENTDEF_COLS, project_id=project_ids,
377 order_by=[('LOWER(path)', [])])
378 component_ids = [cd_row[0] for cd_row in componentdef_rows]
379 component2admin_rows = self.config_service.component2admin_tbl.Select(
380 cnxn, cols=COMPONENT2ADMIN_COLS, component_id=component_ids)
381 component2cc_rows = self.config_service.component2cc_tbl.Select(
382 cnxn, cols=COMPONENT2CC_COLS, component_id=component_ids)
383
384 retrieved_dict = self._DeserializeIssueConfigs(
385 config_rows, template_rows, template2label_rows,
386 template2component_rows, template2admin_rows,
387 template2fv_rows, statusdef_rows, labeldef_rows,
388 fielddef_rows, fielddef2admin_rows, componentdef_rows,
389 component2admin_rows, component2cc_rows)
390 return retrieved_dict
391
392 def FetchItems(self, cnxn, keys):
393 """On RAM and memcache miss, hit the database."""
394 retrieved_dict = self._FetchConfigs(cnxn, keys)
395
396 # Any projects which don't have stored configs should use a default
397 # config instead.
398 for project_id in keys:
399 if project_id not in retrieved_dict:
400 config = tracker_bizobj.MakeDefaultProjectIssueConfig(project_id)
401 retrieved_dict[project_id] = config
402
403 return retrieved_dict
404
405
406 class ConfigService(object):
407 """The persistence layer for Monorail's issue tracker configuration data."""
408
409 def __init__(self, cache_manager):
410 """Initialize this object so that it is ready to use.
411
412 Args:
413 cache_manager: manages local caches with distributed invalidation.
414 """
415 self.template_tbl = sql.SQLTableManager(TEMPLATE_TABLE_NAME)
416 self.template2label_tbl = sql.SQLTableManager(TEMPLATE2LABEL_TABLE_NAME)
417 self.template2component_tbl = sql.SQLTableManager(
418 TEMPLATE2COMPONENT_TABLE_NAME)
419 self.template2admin_tbl = sql.SQLTableManager(TEMPLATE2ADMIN_TABLE_NAME)
420 self.template2fieldvalue_tbl = sql.SQLTableManager(
421 TEMPLATE2FIELDVALUE_TABLE_NAME)
422 self.projectissueconfig_tbl = sql.SQLTableManager(
423 PROJECTISSUECONFIG_TABLE_NAME)
424 self.statusdef_tbl = sql.SQLTableManager(STATUSDEF_TABLE_NAME)
425 self.labeldef_tbl = sql.SQLTableManager(LABELDEF_TABLE_NAME)
426 self.fielddef_tbl = sql.SQLTableManager(FIELDDEF_TABLE_NAME)
427 self.fielddef2admin_tbl = sql.SQLTableManager(FIELDDEF2ADMIN_TABLE_NAME)
428 self.componentdef_tbl = sql.SQLTableManager(COMPONENTDEF_TABLE_NAME)
429 self.component2admin_tbl = sql.SQLTableManager(COMPONENT2ADMIN_TABLE_NAME)
430 self.component2cc_tbl = sql.SQLTableManager(COMPONENT2CC_TABLE_NAME)
431
432 self.config_2lc = ConfigTwoLevelCache(cache_manager, self)
433 self.label_row_2lc = LabelRowTwoLevelCache(cache_manager, self)
434 self.label_cache = cache_manager.MakeCache('project')
435 self.status_row_2lc = StatusRowTwoLevelCache(cache_manager, self)
436 self.status_cache = cache_manager.MakeCache('project')
437 self.field_row_2lc = FieldRowTwoLevelCache(cache_manager, self)
438 self.field_cache = cache_manager.MakeCache('project')
439
440 ### Label lookups
441
442 def GetLabelDefRows(self, cnxn, project_id):
443 """Get SQL result rows for all labels used in the specified project."""
444 pids_to_label_rows, misses = self.label_row_2lc.GetAll(cnxn, [project_id])
445 assert not misses
446 return pids_to_label_rows[project_id]
447
448 def GetLabelDefRowsAnyProject(self, cnxn, where=None):
449 """Get all LabelDef rows for the whole site. Used in whole-site search."""
450 # TODO(jrobbins): maybe add caching for these too.
451 label_def_rows = self.labeldef_tbl.Select(
452 cnxn, cols=LABELDEF_COLS, where=where,
453 order_by=[('rank DESC', []), ('label DESC', [])])
454 return label_def_rows
455
456 def _DeserializeLabels(self, def_rows):
457 """Convert label defs into bi-directional mappings of names and IDs."""
458 label_id_to_name = {
459 label_id: label for
460 label_id, _pid, _rank, label, _doc, _deprecated
461 in def_rows}
462 label_name_to_id = {
463 label.lower(): label_id
464 for label_id, label in label_id_to_name.iteritems()}
465
466 return label_id_to_name, label_name_to_id
467
468 def _EnsureLabelCacheEntry(self, cnxn, project_id):
469 """Make sure that self.label_cache has an entry for project_id."""
470 if not self.label_cache.HasItem(project_id):
471 def_rows = self.GetLabelDefRows(cnxn, project_id)
472 self.label_cache.CacheItem(project_id, self._DeserializeLabels(def_rows))
473
474 def LookupLabel(self, cnxn, project_id, label_id):
475 """Lookup a label string given the label_id.
476
477 Args:
478 cnxn: connection to SQL database.
479 project_id: int ID of the project where the label is defined or used.
480 label_id: int label ID.
481
482 Returns:
483 Label name string for the given label_id, or None.
484 """
485 self._EnsureLabelCacheEntry(cnxn, project_id)
486 label_id_to_name, _label_name_to_id = self.label_cache.GetItem(
487 project_id)
488 return label_id_to_name.get(label_id)
489
490 def LookupLabelID(self, cnxn, project_id, label, autocreate=True):
491 """Look up a label ID, optionally interning it.
492
493 Args:
494 cnxn: connection to SQL database.
495 project_id: int ID of the project where the statuses are defined.
496 label: label string.
497 autocreate: if not already in the DB, store it and generate a new ID.
498
499 Returns:
500 The label ID for the given label string.
501 """
502 self._EnsureLabelCacheEntry(cnxn, project_id)
503 _label_id_to_name, label_name_to_id = self.label_cache.GetItem(
504 project_id)
505 if label.lower() in label_name_to_id:
506 return label_name_to_id[label.lower()]
507
508 if autocreate:
509 logging.info('No label %r is known in project %d, so intern it.',
510 label, project_id)
511 label_id = self.labeldef_tbl.InsertRow(
512 cnxn, project_id=project_id, label=label)
513 self.label_row_2lc.InvalidateKeys(cnxn, [project_id])
514 self.label_cache.Invalidate(cnxn, project_id)
515 return label_id
516
517 return None # It was not found and we don't want to create it.
518
519 def LookupLabelIDs(self, cnxn, project_id, labels, autocreate=False):
520 """Look up several label IDs.
521
522 Args:
523 cnxn: connection to SQL database.
524 project_id: int ID of the project where the statuses are defined.
525 labels: list of label strings.
526 autocreate: if not already in the DB, store it and generate a new ID.
527
528 Returns:
529 Returns a list of int label IDs for the given label strings.
530 """
531 result = []
532 for lab in labels:
533 label_id = self.LookupLabelID(
534 cnxn, project_id, lab, autocreate=autocreate)
535 if label_id is not None:
536 result.append(label_id)
537
538 return result
539
540 def LookupIDsOfLabelsMatching(self, cnxn, project_id, regex):
541 """Look up the IDs of all labels in a project that match the regex.
542
543 Args:
544 cnxn: connection to SQL database.
545 project_id: int ID of the project where the statuses are defined.
546 regex: regular expression object to match against the label strings.
547
548 Returns:
549 List of label IDs for labels that match the regex.
550 """
551 self._EnsureLabelCacheEntry(cnxn, project_id)
552 label_id_to_name, _label_name_to_id = self.label_cache.GetItem(
553 project_id)
554 result = [label_id for label_id, label in label_id_to_name.iteritems()
555 if regex.match(label)]
556
557 return result
558
559 def LookupLabelIDsAnyProject(self, cnxn, label):
560 """Return the IDs of labels with the given name in any project.
561
562 Args:
563 cnxn: connection to SQL database.
564 label: string label to look up. Case sensitive.
565
566 Returns:
567 A list of int label IDs of all labels matching the given string.
568 """
569 # TODO(jrobbins): maybe add caching for these too.
570 label_id_rows = self.labeldef_tbl.Select(
571 cnxn, cols=['id'], label=label)
572 label_ids = [row[0] for row in label_id_rows]
573 return label_ids
574
575 def LookupIDsOfLabelsMatchingAnyProject(self, cnxn, regex):
576 """Return the IDs of matching labels in any project."""
577 label_rows = self.labeldef_tbl.Select(
578 cnxn, cols=['id', 'label'])
579 matching_ids = [
580 label_id for label_id, label in label_rows if regex.match(label)]
581 return matching_ids
582
583 ### Status lookups
584
585 def GetStatusDefRows(self, cnxn, project_id):
586 """Return a list of status definition rows for the specified project."""
587 pids_to_status_rows, misses = self.status_row_2lc.GetAll(
588 cnxn, [project_id])
589 assert not misses
590 return pids_to_status_rows[project_id]
591
592 def GetStatusDefRowsAnyProject(self, cnxn):
593 """Return all status definition rows on the whole site."""
594 # TODO(jrobbins): maybe add caching for these too.
595 status_def_rows = self.statusdef_tbl.Select(
596 cnxn, cols=STATUSDEF_COLS,
597 order_by=[('rank DESC', []), ('status DESC', [])])
598 return status_def_rows
599
600 def _DeserializeStatuses(self, def_rows):
601 """Convert status defs into bi-directional mappings of names and IDs."""
602 status_id_to_name = {
603 status_id: status
604 for (status_id, _pid, _rank, status, _means_open,
605 _doc, _deprecated) in def_rows}
606 status_name_to_id = {
607 status.lower(): status_id
608 for status_id, status in status_id_to_name.iteritems()}
609 closed_status_ids = [
610 status_id
611 for (status_id, _pid, _rank, _status, means_open,
612 _doc, _deprecated) in def_rows
613 if means_open == 0] # Only 0 means closed. NULL/None means open.
614
615 return status_id_to_name, status_name_to_id, closed_status_ids
616
617 def _EnsureStatusCacheEntry(self, cnxn, project_id):
618 """Make sure that self.status_cache has an entry for project_id."""
619 if not self.status_cache.HasItem(project_id):
620 def_rows = self.GetStatusDefRows(cnxn, project_id)
621 self.status_cache.CacheItem(
622 project_id, self._DeserializeStatuses(def_rows))
623
624 def LookupStatus(self, cnxn, project_id, status_id):
625 """Look up a status string for the given status ID.
626
627 Args:
628 cnxn: connection to SQL database.
629 project_id: int ID of the project where the statuses are defined.
630 status_id: int ID of the status value.
631
632 Returns:
633 A status string, or None.
634 """
635 if status_id == 0:
636 return ''
637
638 self._EnsureStatusCacheEntry(cnxn, project_id)
639 (status_id_to_name, _status_name_to_id,
640 _closed_status_ids) = self.status_cache.GetItem(project_id)
641
642 return status_id_to_name.get(status_id)
643
644 def LookupStatusID(self, cnxn, project_id, status, autocreate=True):
645 """Look up a status ID for the given status string.
646
647 Args:
648 cnxn: connection to SQL database.
649 project_id: int ID of the project where the statuses are defined.
650 status: status string.
651 autocreate: if not already in the DB, store it and generate a new ID.
652
653 Returns:
654 The status ID for the given status string, or None.
655 """
656 if not status:
657 return None
658
659 self._EnsureStatusCacheEntry(cnxn, project_id)
660 (_status_id_to_name, status_name_to_id,
661 _closed_status_ids) = self.status_cache.GetItem(project_id)
662 if status.lower() in status_name_to_id:
663 return status_name_to_id[status.lower()]
664
665 if autocreate:
666 logging.info('No status %r is known in project %d, so intern it.',
667 status, project_id)
668 status_id = self.statusdef_tbl.InsertRow(
669 cnxn, project_id=project_id, status=status)
670 self.status_row_2lc.InvalidateKeys(cnxn, [project_id])
671 self.status_cache.Invalidate(cnxn, project_id)
672 return status_id
673
674 return None # It was not found and we don't want to create it.
675
676 def LookupStatusIDs(self, cnxn, project_id, statuses):
677 """Look up several status IDs for the given status strings.
678
679 Args:
680 cnxn: connection to SQL database.
681 project_id: int ID of the project where the statuses are defined.
682 statuses: list of status strings.
683
684 Returns:
685 A list of int status IDs.
686 """
687 result = []
688 for stat in statuses:
689 status_id = self.LookupStatusID(cnxn, project_id, stat, autocreate=False)
690 if status_id:
691 result.append(status_id)
692
693 return result
694
695 def LookupClosedStatusIDs(self, cnxn, project_id):
696 """Return the IDs of closed statuses defined in the given project."""
697 self._EnsureStatusCacheEntry(cnxn, project_id)
698 (_status_id_to_name, _status_name_to_id,
699 closed_status_ids) = self.status_cache.GetItem(project_id)
700
701 return closed_status_ids
702
703 def LookupClosedStatusIDsAnyProject(self, cnxn):
704 """Return the IDs of closed statuses defined in any project."""
705 status_id_rows = self.statusdef_tbl.Select(
706 cnxn, cols=['id'], means_open=False)
707 status_ids = [row[0] for row in status_id_rows]
708 return status_ids
709
710 def LookupStatusIDsAnyProject(self, cnxn, status):
711 """Return the IDs of statues with the given name in any project."""
712 status_id_rows = self.statusdef_tbl.Select(
713 cnxn, cols=['id'], status=status)
714 status_ids = [row[0] for row in status_id_rows]
715 return status_ids
716
717 # TODO(jrobbins): regex matching for status values.
718
719 ### Issue tracker configuration objects
720
721 def GetProjectConfigs(self, cnxn, project_ids, use_cache=True):
722 """Get several project issue config objects."""
723 config_dict, missed_ids = self.config_2lc.GetAll(
724 cnxn, project_ids, use_cache=use_cache)
725 assert not missed_ids
726 return config_dict
727
728 def GetProjectConfig(self, cnxn, project_id, use_cache=True):
729 """Load a ProjectIssueConfig for the specified project from the database.
730
731 Args:
732 cnxn: connection to SQL database.
733 project_id: int ID of the current project.
734 use_cache: if False, always hit the database.
735
736 Returns:
737 A ProjectIssueConfig describing how the issue tracker in the specified
738 project is configured. Projects only have a stored ProjectIssueConfig if
739 a project owner has edited the configuration. Other projects use a
740 default configuration.
741 """
742 config_dict = self.GetProjectConfigs(
743 cnxn, [project_id], use_cache=use_cache)
744 return config_dict[project_id]
745
746 def TemplatesWithComponent(self, cnxn, component_id, config):
747 """Returns all templates with the specified component.
748
749 Args:
750 cnxn: connection to SQL database.
751 component_id: int component id.
752 config: ProjectIssueConfig instance.
753 """
754 template2component_rows = self.template2component_tbl.Select(
755 cnxn, cols=['template_id'], component_id=component_id)
756 template_ids = [r[0] for r in template2component_rows]
757 return [t for t in config.templates if t.template_id in template_ids]
758
759 def StoreConfig(self, cnxn, config):
760 """Update an issue config in the database.
761
762 Args:
763 cnxn: connection to SQL database.
764 config: ProjectIssueConfig PB to update.
765 """
766 # TODO(jrobbins): Convert default template index values into foreign
767 # key references. Updating an entire config might require (1) adding
768 # new templates, (2) updating the config with new foreign key values,
769 # and finally (3) deleting only the specific templates that should be
770 # deleted.
771 self.projectissueconfig_tbl.InsertRow(
772 cnxn, replace=True,
773 project_id=config.project_id,
774 statuses_offer_merge=' '.join(config.statuses_offer_merge),
775 exclusive_label_prefixes=' '.join(config.exclusive_label_prefixes),
776 default_template_for_developers=config.default_template_for_developers,
777 default_template_for_users=config.default_template_for_users,
778 default_col_spec=config.default_col_spec,
779 default_sort_spec=config.default_sort_spec,
780 default_x_attr=config.default_x_attr,
781 default_y_attr=config.default_y_attr,
782 custom_issue_entry_url=config.custom_issue_entry_url,
783 commit=False)
784
785 self._UpdateTemplates(cnxn, config)
786 self._UpdateWellKnownLabels(cnxn, config)
787 self._UpdateWellKnownStatuses(cnxn, config)
788 cnxn.Commit()
789
790 def _UpdateTemplates(self, cnxn, config):
791 """Update the templates part of a project's issue configuration.
792
793 Args:
794 cnxn: connection to SQL database.
795 config: ProjectIssueConfig PB to update in the DB.
796 """
797 # Delete dependent rows of existing templates. It is all rewritten below.
798 template_id_rows = self.template_tbl.Select(
799 cnxn, cols=['id'], project_id=config.project_id)
800 template_ids = [row[0] for row in template_id_rows]
801 self.template2label_tbl.Delete(
802 cnxn, template_id=template_ids, commit=False)
803 self.template2component_tbl.Delete(
804 cnxn, template_id=template_ids, commit=False)
805 self.template2admin_tbl.Delete(
806 cnxn, template_id=template_ids, commit=False)
807 self.template2fieldvalue_tbl.Delete(
808 cnxn, template_id=template_ids, commit=False)
809 self.template_tbl.Delete(
810 cnxn, project_id=config.project_id, commit=False)
811
812 # Now, update existing ones and add new ones.
813 template_rows = []
814 for template in config.templates:
815 row = (template.template_id,
816 config.project_id,
817 template.name,
818 template.content,
819 template.summary,
820 template.summary_must_be_edited,
821 template.owner_id or None,
822 template.status,
823 template.members_only,
824 template.owner_defaults_to_member,
825 template.component_required)
826 template_rows.append(row)
827
828 # Maybe first insert ones that have a template_id and then insert new ones
829 # separately.
830 generated_ids = self.template_tbl.InsertRows(
831 cnxn, TEMPLATE_COLS, template_rows, replace=True, commit=False,
832 return_generated_ids=True)
833 logging.info('generated_ids is %r', generated_ids)
834 for template in config.templates:
835 if not template.template_id:
836 # Get IDs from the back of the list because the original template IDs
837 # have already been added to template_rows.
838 template.template_id = generated_ids.pop()
839
840 template2label_rows = []
841 template2component_rows = []
842 template2admin_rows = []
843 template2fieldvalue_rows = []
844 for template in config.templates:
845 for label in template.labels:
846 if label:
847 template2label_rows.append((template.template_id, label))
848 for component_id in template.component_ids:
849 template2component_rows.append((template.template_id, component_id))
850 for admin_id in template.admin_ids:
851 template2admin_rows.append((template.template_id, admin_id))
852 for fv in template.field_values:
853 template2fieldvalue_rows.append(
854 (template.template_id, fv.field_id, fv.int_value, fv.str_value,
855 fv.user_id or None))
856
857 self.template2label_tbl.InsertRows(
858 cnxn, TEMPLATE2LABEL_COLS, template2label_rows, ignore=True,
859 commit=False)
860 self.template2component_tbl.InsertRows(
861 cnxn, TEMPLATE2COMPONENT_COLS, template2component_rows, commit=False)
862 self.template2admin_tbl.InsertRows(
863 cnxn, TEMPLATE2ADMIN_COLS, template2admin_rows, commit=False)
864 self.template2fieldvalue_tbl.InsertRows(
865 cnxn, TEMPLATE2FIELDVALUE_COLS, template2fieldvalue_rows, commit=False)
866
867 def _UpdateWellKnownLabels(self, cnxn, config):
868 """Update the labels part of a project's issue configuration.
869
870 Args:
871 cnxn: connection to SQL database.
872 config: ProjectIssueConfig PB to update in the DB.
873 """
874 update_labeldef_rows = []
875 new_labeldef_rows = []
876 for rank, wkl in enumerate(config.well_known_labels):
877 # We must specify label ID when replacing, otherwise a new ID is made.
878 label_id = self.LookupLabelID(
879 cnxn, config.project_id, wkl.label, autocreate=False)
880 if label_id:
881 row = (label_id, config.project_id, rank, wkl.label,
882 wkl.label_docstring, wkl.deprecated)
883 update_labeldef_rows.append(row)
884 else:
885 row = (
886 config.project_id, rank, wkl.label, wkl.label_docstring,
887 wkl.deprecated)
888 new_labeldef_rows.append(row)
889
890 self.labeldef_tbl.Update(
891 cnxn, {'rank': None}, project_id=config.project_id, commit=False)
892 self.labeldef_tbl.InsertRows(
893 cnxn, LABELDEF_COLS, update_labeldef_rows, replace=True, commit=False)
894 self.labeldef_tbl.InsertRows(
895 cnxn, LABELDEF_COLS[1:], new_labeldef_rows, commit=False)
896 self.label_row_2lc.InvalidateKeys(cnxn, [config.project_id])
897 self.label_cache.Invalidate(cnxn, config.project_id)
898
899 def _UpdateWellKnownStatuses(self, cnxn, config):
900 """Update the status part of a project's issue configuration.
901
902 Args:
903 cnxn: connection to SQL database.
904 config: ProjectIssueConfig PB to update in the DB.
905 """
906 update_statusdef_rows = []
907 new_statusdef_rows = []
908 for rank, wks in enumerate(config.well_known_statuses):
909 # We must specify label ID when replacing, otherwise a new ID is made.
910 status_id = self.LookupStatusID(cnxn, config.project_id, wks.status,
911 autocreate=False)
912 if status_id is not None:
913 row = (status_id, config.project_id, rank, wks.status,
914 bool(wks.means_open), wks.status_docstring, wks.deprecated)
915 update_statusdef_rows.append(row)
916 else:
917 row = (config.project_id, rank, wks.status,
918 bool(wks.means_open), wks.status_docstring, wks.deprecated)
919 new_statusdef_rows.append(row)
920
921 self.statusdef_tbl.Update(
922 cnxn, {'rank': None}, project_id=config.project_id, commit=False)
923 self.statusdef_tbl.InsertRows(
924 cnxn, STATUSDEF_COLS, update_statusdef_rows, replace=True,
925 commit=False)
926 self.statusdef_tbl.InsertRows(
927 cnxn, STATUSDEF_COLS[1:], new_statusdef_rows, commit=False)
928 self.status_row_2lc.InvalidateKeys(cnxn, [config.project_id])
929 self.status_cache.Invalidate(cnxn, config.project_id)
930
931 def UpdateConfig(
932 self, cnxn, project, well_known_statuses=None,
933 statuses_offer_merge=None, well_known_labels=None,
934 excl_label_prefixes=None, templates=None,
935 default_template_for_developers=None, default_template_for_users=None,
936 list_prefs=None, restrict_to_known=None):
937 """Update project's issue tracker configuration with the given info.
938
939 Args:
940 cnxn: connection to SQL database.
941 project: the project in which to update the issue tracker config.
942 well_known_statuses: [(status_name, docstring, means_open, deprecated),..]
943 statuses_offer_merge: list of status values that trigger UI to merge.
944 well_known_labels: [(label_name, docstring, deprecated),...]
945 excl_label_prefixes: list of prefix strings. Each issue should
946 have only one label with each of these prefixed.
947 templates: List of PBs for issue templates.
948 default_template_for_developers: int ID of template to use for devs.
949 default_template_for_users: int ID of template to use for non-members.
950 list_prefs: defaults for columns and sorting.
951 restrict_to_known: optional bool to allow project owners
952 to limit issue status and label values to only the well-known ones.
953
954 Returns:
955 The updated ProjectIssueConfig PB.
956 """
957 project_id = project.project_id
958 project_config = self.GetProjectConfig(cnxn, project_id, use_cache=False)
959
960 if well_known_statuses is not None:
961 tracker_bizobj.SetConfigStatuses(project_config, well_known_statuses)
962
963 if statuses_offer_merge is not None:
964 project_config.statuses_offer_merge = statuses_offer_merge
965
966 if well_known_labels is not None:
967 tracker_bizobj.SetConfigLabels(project_config, well_known_labels)
968
969 if excl_label_prefixes is not None:
970 project_config.exclusive_label_prefixes = excl_label_prefixes
971
972 if templates is not None:
973 project_config.templates = templates
974
975 if default_template_for_developers is not None:
976 project_config.default_template_for_developers = (
977 default_template_for_developers)
978 if default_template_for_users is not None:
979 project_config.default_template_for_users = default_template_for_users
980
981 if list_prefs:
982 default_col_spec, default_sort_spec, x_attr, y_attr = list_prefs
983 project_config.default_col_spec = default_col_spec
984 project_config.default_sort_spec = default_sort_spec
985 project_config.default_x_attr = x_attr
986 project_config.default_y_attr = y_attr
987
988 if restrict_to_known is not None:
989 project_config.restrict_to_known = restrict_to_known
990
991 self.StoreConfig(cnxn, project_config)
992 self.config_2lc.InvalidateKeys(cnxn, [project_id])
993 self.InvalidateMemcacheForEntireProject(project_id)
994 # Invalidate all issue caches in all frontends to clear out
995 # sorting.art_values_cache which now has wrong sort orders.
996 cache_manager = self.config_2lc.cache.cache_manager
997 cache_manager.StoreInvalidateAll(cnxn, 'issue')
998
999 return project_config
1000
1001 def ExpungeConfig(self, cnxn, project_id):
1002 """Completely delete the specified project config from the database."""
1003 logging.info('expunging the config for %r', project_id)
1004 template_id_rows = self.template_tbl.Select(
1005 cnxn, cols=['id'], project_id=project_id)
1006 template_ids = [row[0] for row in template_id_rows]
1007 self.template2label_tbl.Delete(cnxn, template_id=template_ids)
1008 self.template2component_tbl.Delete(cnxn, template_id=template_ids)
1009 self.template_tbl.Delete(cnxn, project_id=project_id)
1010 self.statusdef_tbl.Delete(cnxn, project_id=project_id)
1011 self.labeldef_tbl.Delete(cnxn, project_id=project_id)
1012 self.projectissueconfig_tbl.Delete(cnxn, project_id=project_id)
1013
1014 self.config_2lc.InvalidateKeys(cnxn, [project_id])
1015
1016 ### Custom field definitions
1017
1018 def CreateFieldDef(
1019 self, cnxn, project_id, field_name, field_type_str, applic_type,
1020 applic_pred, is_required, is_multivalued,
1021 min_value, max_value, regex, needs_member, needs_perm,
1022 grants_perm, notify_on, docstring, admin_ids):
1023 """Create a new field definition with the given info.
1024
1025 Args:
1026 cnxn: connection to SQL database.
1027 project_id: int ID of the current project.
1028 field_name: name of the new custom field.
1029 field_type_str: string identifying the type of the custom field.
1030 applic_type: string specifying issue type the field is applicable to.
1031 applic_pred: string condition to test if the field is applicable.
1032 is_required: True if the field should be required on issues.
1033 is_multivalued: True if the field can occur multiple times on one issue.
1034 min_value: optional validation for int_type fields.
1035 max_value: optional validation for int_type fields.
1036 regex: optional validation for str_type fields.
1037 needs_member: optional validation for user_type fields.
1038 needs_perm: optional validation for user_type fields.
1039 grants_perm: optional string for perm to grant any user named in field.
1040 notify_on: int enum of when to notify users named in field.
1041 docstring: string describing this field.
1042 admin_ids: list of additional user IDs who can edit this field def.
1043
1044 Returns:
1045 Integer field_id of the new field definition.
1046 """
1047 field_id = self.fielddef_tbl.InsertRow(
1048 cnxn, project_id=project_id,
1049 field_name=field_name, field_type=field_type_str,
1050 applicable_type=applic_type, applicable_predicate=applic_pred,
1051 is_required=is_required, is_multivalued=is_multivalued,
1052 min_value=min_value, max_value=max_value, regex=regex,
1053 needs_member=needs_member, needs_perm=needs_perm,
1054 grants_perm=grants_perm, notify_on=NOTIFY_ON_ENUM[notify_on],
1055 docstring=docstring, commit=False)
1056 self.fielddef2admin_tbl.InsertRows(
1057 cnxn, FIELDDEF2ADMIN_COLS,
1058 [(field_id, admin_id) for admin_id in admin_ids],
1059 commit=False)
1060 cnxn.Commit()
1061 self.config_2lc.InvalidateKeys(cnxn, [project_id])
1062 self.InvalidateMemcacheForEntireProject(project_id)
1063 return field_id
1064
1065 def _DeserializeFields(self, def_rows):
1066 """Convert field defs into bi-directional mappings of names and IDs."""
1067 field_id_to_name = {
1068 field_id: field
1069 for field_id, _pid, _rank, field, _doc in def_rows}
1070 field_name_to_id = {
1071 field.lower(): field_id
1072 for field_id, field in field_id_to_name.iteritems()}
1073
1074 return field_id_to_name, field_name_to_id
1075
1076 def GetFieldDefRows(self, cnxn, project_id):
1077 """Get SQL result rows for all fields used in the specified project."""
1078 pids_to_field_rows, misses = self.field_row_2lc.GetAll(cnxn, [project_id])
1079 assert not misses
1080 return pids_to_field_rows[project_id]
1081
1082 def _EnsureFieldCacheEntry(self, cnxn, project_id):
1083 """Make sure that self.field_cache has an entry for project_id."""
1084 if not self.field_cache.HasItem(project_id):
1085 def_rows = self.GetFieldDefRows(cnxn, project_id)
1086 self.field_cache.CacheItem(
1087 project_id, self._DeserializeFields(def_rows))
1088
1089 def LookupField(self, cnxn, project_id, field_id):
1090 """Lookup a field string given the field_id.
1091
1092 Args:
1093 cnxn: connection to SQL database.
1094 project_id: int ID of the project where the label is defined or used.
1095 field_id: int field ID.
1096
1097 Returns:
1098 Field name string for the given field_id, or None.
1099 """
1100 self._EnsureFieldCacheEntry(cnxn, project_id)
1101 field_id_to_name, _field_name_to_id = self.field_cache.GetItem(
1102 project_id)
1103 return field_id_to_name.get(field_id)
1104
1105 def LookupFieldID(self, cnxn, project_id, field):
1106 """Look up a field ID.
1107
1108 Args:
1109 cnxn: connection to SQL database.
1110 project_id: int ID of the project where the fields are defined.
1111 field: field string.
1112
1113 Returns:
1114 The field ID for the given field string.
1115 """
1116 self._EnsureFieldCacheEntry(cnxn, project_id)
1117 _field_id_to_name, field_name_to_id = self.field_cache.GetItem(
1118 project_id)
1119 return field_name_to_id.get(field.lower())
1120
1121 def SoftDeleteFieldDef(self, cnxn, project_id, field_id):
1122 """Mark the specified field as deleted, it will be reaped later."""
1123 self.fielddef_tbl.Update(cnxn, {'is_deleted': True}, id=field_id)
1124 self.config_2lc.InvalidateKeys(cnxn, [project_id])
1125 self.InvalidateMemcacheForEntireProject(project_id)
1126
1127 # TODO(jrobbins): GC deleted field defs after field values are gone.
1128
1129 def UpdateFieldDef(
1130 self, cnxn, project_id, field_id, field_name=None,
1131 applicable_type=None, applicable_predicate=None, is_required=None,
1132 is_multivalued=None, min_value=None, max_value=None, regex=None,
1133 needs_member=None, needs_perm=None, grants_perm=None, notify_on=None,
1134 docstring=None, admin_ids=None):
1135 """Update the specified field definition."""
1136 new_values = {}
1137 if field_name is not None:
1138 new_values['field_name'] = field_name
1139 if applicable_type is not None:
1140 new_values['applicable_type'] = applicable_type
1141 if applicable_predicate is not None:
1142 new_values['applicable_predicate'] = applicable_predicate
1143 if is_required is not None:
1144 new_values['is_required'] = bool(is_required)
1145 if is_multivalued is not None:
1146 new_values['is_multivalued'] = bool(is_multivalued)
1147 if min_value is not None:
1148 new_values['min_value'] = min_value
1149 if max_value is not None:
1150 new_values['max_value'] = max_value
1151 if regex is not None:
1152 new_values['regex'] = regex
1153 if needs_member is not None:
1154 new_values['needs_member'] = needs_member
1155 if needs_perm is not None:
1156 new_values['needs_perm'] = needs_perm
1157 if grants_perm is not None:
1158 new_values['grants_perm'] = grants_perm
1159 if notify_on is not None:
1160 new_values['notify_on'] = NOTIFY_ON_ENUM[notify_on]
1161 if docstring is not None:
1162 new_values['docstring'] = docstring
1163
1164 self.fielddef_tbl.Update(cnxn, new_values, id=field_id, commit=False)
1165 self.fielddef2admin_tbl.Delete(cnxn, field_id=field_id, commit=False)
1166 self.fielddef2admin_tbl.InsertRows(
1167 cnxn, FIELDDEF2ADMIN_COLS,
1168 [(field_id, admin_id) for admin_id in admin_ids],
1169 commit=False)
1170 cnxn.Commit()
1171 self.config_2lc.InvalidateKeys(cnxn, [project_id])
1172 self.InvalidateMemcacheForEntireProject(project_id)
1173
1174 ### Component definitions
1175
1176 def FindMatchingComponentIDsAnyProject(self, cnxn, path_list, exact=True):
1177 """Look up component IDs across projects.
1178
1179 Args:
1180 cnxn: connection to SQL database.
1181 path_list: list of component path prefixes.
1182 exact: set to False to include all components which have one of the
1183 given paths as their ancestor, instead of exact matches.
1184
1185 Returns:
1186 A list of component IDs of component's whose paths match path_list.
1187 """
1188 or_terms = []
1189 args = []
1190 for path in path_list:
1191 or_terms.append('path = %s')
1192 args.append(path)
1193
1194 if not exact:
1195 for path in path_list:
1196 or_terms.append('path LIKE %s')
1197 args.append(path + '>%')
1198
1199 cond_str = '(' + ' OR '.join(or_terms) + ')'
1200 rows = self.componentdef_tbl.Select(
1201 cnxn, cols=['id'], where=[(cond_str, args)])
1202 return [row[0] for row in rows]
1203
1204 def CreateComponentDef(
1205 self, cnxn, project_id, path, docstring, deprecated, admin_ids, cc_ids,
1206 created, creator_id):
1207 """Create a new component definition with the given info.
1208
1209 Args:
1210 cnxn: connection to SQL database.
1211 project_id: int ID of the current project.
1212 path: string pathname of the new component.
1213 docstring: string describing this field.
1214 deprecated: whether or not this should be autocompleted
1215 admin_ids: list of int IDs of users who can administer.
1216 cc_ids: list of int IDs of users to notify when an issue in
1217 this component is updated.
1218 created: timestamp this component was created at.
1219 creator_id: int ID of user who created this component.
1220
1221 Returns:
1222 Integer component_id of the new component definition.
1223 """
1224 component_id = self.componentdef_tbl.InsertRow(
1225 cnxn, project_id=project_id, path=path, docstring=docstring,
1226 deprecated=deprecated, created=created, creator_id=creator_id,
1227 commit=False)
1228 self.component2admin_tbl.InsertRows(
1229 cnxn, COMPONENT2ADMIN_COLS,
1230 [(component_id, admin_id) for admin_id in admin_ids],
1231 commit=False)
1232 self.component2cc_tbl.InsertRows(
1233 cnxn, COMPONENT2CC_COLS,
1234 [(component_id, cc_id) for cc_id in cc_ids],
1235 commit=False)
1236 cnxn.Commit()
1237 self.config_2lc.InvalidateKeys(cnxn, [project_id])
1238 self.InvalidateMemcacheForEntireProject(project_id)
1239 return component_id
1240
1241 def UpdateComponentDef(
1242 self, cnxn, project_id, component_id, path=None, docstring=None,
1243 deprecated=None, admin_ids=None, cc_ids=None, created=None,
1244 creator_id=None, modified=None, modifier_id=None):
1245 """Update the specified component definition."""
1246 new_values = {}
1247 if path is not None:
1248 assert path
1249 new_values['path'] = path
1250 if docstring is not None:
1251 new_values['docstring'] = docstring
1252 if deprecated is not None:
1253 new_values['deprecated'] = deprecated
1254 if created is not None:
1255 new_values['created'] = created
1256 if creator_id is not None:
1257 new_values['creator_id'] = creator_id
1258 if modified is not None:
1259 new_values['modified'] = modified
1260 if modifier_id is not None:
1261 new_values['modifier_id'] = modifier_id
1262
1263 if admin_ids is not None:
1264 self.component2admin_tbl.Delete(
1265 cnxn, component_id=component_id, commit=False)
1266 self.component2admin_tbl.InsertRows(
1267 cnxn, COMPONENT2ADMIN_COLS,
1268 [(component_id, admin_id) for admin_id in admin_ids],
1269 commit=False)
1270
1271 if cc_ids is not None:
1272 self.component2cc_tbl.Delete(
1273 cnxn, component_id=component_id, commit=False)
1274 self.component2cc_tbl.InsertRows(
1275 cnxn, COMPONENT2CC_COLS,
1276 [(component_id, cc_id) for cc_id in cc_ids],
1277 commit=False)
1278
1279 self.componentdef_tbl.Update(
1280 cnxn, new_values, id=component_id, commit=False)
1281 cnxn.Commit()
1282 self.config_2lc.InvalidateKeys(cnxn, [project_id])
1283 self.InvalidateMemcacheForEntireProject(project_id)
1284
1285 def DeleteComponentDef(self, cnxn, project_id, component_id):
1286 """Delete the specified component definition."""
1287 self.component2cc_tbl.Delete(
1288 cnxn, component_id=component_id, commit=False)
1289 self.component2admin_tbl.Delete(
1290 cnxn, component_id=component_id, commit=False)
1291 self.componentdef_tbl.Delete(cnxn, id=component_id, commit=False)
1292 cnxn.Commit()
1293 self.config_2lc.InvalidateKeys(cnxn, [project_id])
1294 self.InvalidateMemcacheForEntireProject(project_id)
1295
1296 ### Memcache management
1297
1298 def InvalidateMemcache(self, issues, key_prefix=''):
1299 """Delete the memcache entries for issues and their project-shard pairs."""
1300 memcache.delete_multi(
1301 [str(issue.issue_id) for issue in issues], key_prefix='issue:')
1302 project_shards = set(
1303 (issue.project_id, issue.issue_id % settings.num_logical_shards)
1304 for issue in issues)
1305 self._InvalidateMemcacheShards(project_shards, key_prefix=key_prefix)
1306
1307 def _InvalidateMemcacheShards(self, project_shards, key_prefix=''):
1308 """Delete the memcache entries for the given project-shard pairs.
1309
1310 Deleting these rows does not delete the actual cached search results
1311 but it does mean that they will be considered stale and thus not used.
1312
1313 Args:
1314 project_shards: list of (pid, sid) pairs.
1315 key_prefix: string to pass as memcache key prefix.
1316 """
1317 cache_entries = ['%d;%d' % ps for ps in project_shards]
1318 # Whenever any project is invalidated, also invalidate the 'all'
1319 # entry that is used in site-wide searches.
1320 shard_id_set = {sid for _pid, sid in project_shards}
1321 cache_entries.extend(('all;%d' % sid) for sid in shard_id_set)
1322
1323 memcache.delete_multi(cache_entries, key_prefix=key_prefix)
1324
1325 def InvalidateMemcacheForEntireProject(self, project_id):
1326 """Delete the memcache entries for all searches in a project."""
1327 project_shards = set((project_id, shard_id)
1328 for shard_id in range(settings.num_logical_shards))
1329 self._InvalidateMemcacheShards(project_shards)
1330 memcache.delete_multi([str(project_id)], key_prefix='config:')
1331 memcache.delete_multi([str(project_id)], key_prefix='label_rows:')
1332 memcache.delete_multi([str(project_id)], key_prefix='status_rows:')
1333 memcache.delete_multi([str(project_id)], key_prefix='field_rows:')
1334
1335
1336 class Error(Exception):
1337 """Base class for errors from this module."""
1338 pass
1339
1340
1341 class NoSuchComponentException(Error):
1342 """No component with the specified name exists."""
1343 pass
1344
1345
1346 class InvalidComponentNameException(Error):
1347 """The component name is invalid."""
1348 pass
OLDNEW
« no previous file with comments | « appengine/monorail/services/client_config_svc.py ('k') | appengine/monorail/services/features_svc.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698