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