blob: edfde0564baf7bcf02439a1b32c1c69170312140 [file] [log] [blame]
Copybara854996b2021-09-07 19:36:02 +00001# Copyright 2018 The Chromium Authors. All rights reserved.
2# Use of this source code is governed 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"""The TemplateService class providing methods for template persistence."""
7from __future__ import print_function
8from __future__ import division
9from __future__ import absolute_import
10
11import collections
12import logging
13
14import settings
15
16from framework import exceptions
17from framework import sql
18from proto import tracker_pb2
19from services import caches
20from services import project_svc
21from tracker import tracker_bizobj
22from tracker import tracker_constants
23
24
25TEMPLATE_COLS = [
26 'id', 'project_id', 'name', 'content', 'summary', 'summary_must_be_edited',
27 'owner_id', 'status', 'members_only', 'owner_defaults_to_member',
28 'component_required']
29TEMPLATE2LABEL_COLS = ['template_id', 'label']
30TEMPLATE2COMPONENT_COLS = ['template_id', 'component_id']
31TEMPLATE2ADMIN_COLS = ['template_id', 'admin_id']
32TEMPLATE2FIELDVALUE_COLS = [
33 'template_id', 'field_id', 'int_value', 'str_value', 'user_id',
34 'date_value', 'url_value']
35ISSUEPHASEDEF_COLS = ['id', 'name', 'rank']
36TEMPLATE2APPROVALVALUE_COLS = [
37 'approval_id', 'template_id', 'phase_id', 'status']
38
39
40TEMPLATE_TABLE_NAME = 'Template'
41TEMPLATE2LABEL_TABLE_NAME = 'Template2Label'
42TEMPLATE2ADMIN_TABLE_NAME = 'Template2Admin'
43TEMPLATE2COMPONENT_TABLE_NAME = 'Template2Component'
44TEMPLATE2FIELDVALUE_TABLE_NAME = 'Template2FieldValue'
45ISSUEPHASEDEF_TABLE_NAME = 'IssuePhaseDef'
46TEMPLATE2APPROVALVALUE_TABLE_NAME = 'Template2ApprovalValue'
47
48
49class TemplateSetTwoLevelCache(caches.AbstractTwoLevelCache):
50 """Class to manage RAM and memcache for templates.
51
52 Holds a dictionary of {project_id: templateset} key value pairs,
53 where a templateset is a list of all templates in a project.
54 """
55
56 def __init__(self, cache_manager, template_service):
57 super(TemplateSetTwoLevelCache, self).__init__(
58 cache_manager, 'project', prefix='templateset:', pb_class=None)
59 self.template_service = template_service
60
61 def _MakeCache(self, cache_manager, kind, max_size=None):
62 """Make the RAM cache and register it with the cache_manager."""
63 return caches.RamCache(cache_manager, kind, max_size=max_size)
64
65 def FetchItems(self, cnxn, keys):
66 """On RAM and memcache miss, hit the database."""
67 template_set_dict = {}
68
69 for project_id in keys:
70 template_set_dict.setdefault(project_id, [])
71 template_rows = self.template_service.template_tbl.Select(
72 cnxn, cols=TEMPLATE_COLS, project_id=project_id,
73 order_by=[('name', [])])
74 for (template_id, _project_id, template_name, _content, _summary,
75 _summary_must_be_edited, _owner_id, _status, members_only,
76 _owner_defaults_to_member, _component_required) in template_rows:
77 template_set_row = (template_id, template_name, members_only)
78 template_set_dict[project_id].append(template_set_row)
79
80 return template_set_dict
81
82
83class TemplateDefTwoLevelCache(caches.AbstractTwoLevelCache):
84 """Class to manage RAM and memcache for individual TemplateDef.
85
86 Holds a dictionary of {template_id: TemplateDef} key value pairs.
87 """
88 def __init__(self, cache_manager, template_service):
89 super(TemplateDefTwoLevelCache, self).__init__(
90 cache_manager,
91 'template',
92 prefix='templatedef:',
93 pb_class=tracker_pb2.TemplateDef)
94 self.template_service = template_service
95
96 def _MakeCache(self, cache_manager, kind, max_size=None):
97 """Make the RAM cache and register it with the cache_manager."""
98 return caches.RamCache(cache_manager, kind, max_size=max_size)
99
100 def FetchItems(self, cnxn, keys):
101 """On RAM and memcache miss, hit the database.
102
103 Args:
104 cnxn: A MonorailConnection.
105 keys: A list of template IDs (ints).
106
107 Returns:
108 A dict of {template_id: TemplateDef}.
109 """
110 template_dict = {}
111
112 # Fetch template rows and relations.
113 template_rows = self.template_service.template_tbl.Select(
114 cnxn, cols=TEMPLATE_COLS, id=keys,
115 order_by=[('name', [])])
116
117 template2label_rows = self.template_service.\
118 template2label_tbl.Select(
119 cnxn, cols=TEMPLATE2LABEL_COLS, template_id=keys)
120 template2component_rows = self.template_service.\
121 template2component_tbl.Select(
122 cnxn, cols=TEMPLATE2COMPONENT_COLS, template_id=keys)
123 template2admin_rows = self.template_service.template2admin_tbl.Select(
124 cnxn, cols=TEMPLATE2ADMIN_COLS, template_id=keys)
125 template2fieldvalue_rows = self.template_service.\
126 template2fieldvalue_tbl.Select(
127 cnxn, cols=TEMPLATE2FIELDVALUE_COLS, template_id=keys)
128 template2approvalvalue_rows = self.template_service.\
129 template2approvalvalue_tbl.Select(
130 cnxn, cols=TEMPLATE2APPROVALVALUE_COLS, template_id=keys)
131 phase_ids = [av_row[2] for av_row in template2approvalvalue_rows]
132 phase_rows = []
133 if phase_ids:
134 phase_rows = self.template_service.issuephasedef_tbl.Select(
135 cnxn, cols=ISSUEPHASEDEF_COLS, id=list(set(phase_ids)))
136
137 # Build TemplateDef with all related data.
138 for template_row in template_rows:
139 template = UnpackTemplate(template_row)
140 template_dict[template.template_id] = template
141
142 for template2label_row in template2label_rows:
143 template_id, label = template2label_row
144 template = template_dict.get(template_id)
145 if template:
146 template.labels.append(label)
147
148 for template2component_row in template2component_rows:
149 template_id, component_id = template2component_row
150 template = template_dict.get(template_id)
151 if template:
152 template.component_ids.append(component_id)
153
154 for template2admin_row in template2admin_rows:
155 template_id, admin_id = template2admin_row
156 template = template_dict.get(template_id)
157 if template:
158 template.admin_ids.append(admin_id)
159
160 for fv_row in template2fieldvalue_rows:
161 (template_id, field_id, int_value, str_value, user_id,
162 date_value, url_value) = fv_row
163 fv = tracker_bizobj.MakeFieldValue(
164 field_id, int_value, str_value, user_id, date_value, url_value,
165 False)
166 template = template_dict.get(template_id)
167 if template:
168 template.field_values.append(fv)
169
170 phases_by_id = {}
171 for phase_row in phase_rows:
172 (phase_id, name, rank) = phase_row
173 phase = tracker_pb2.Phase(
174 phase_id=phase_id, name=name, rank=rank)
175 phases_by_id[phase_id] = phase
176
177 # Note: there is no templateapproval2approver_tbl.
178 for av_row in template2approvalvalue_rows:
179 (approval_id, template_id, phase_id, status) = av_row
180 approval_value = tracker_pb2.ApprovalValue(
181 approval_id=approval_id, phase_id=phase_id,
182 status=tracker_pb2.ApprovalStatus(status.upper()))
183 template = template_dict.get(template_id)
184 if template:
185 template.approval_values.append(approval_value)
186 phase = phases_by_id.get(phase_id)
187 if phase and phase not in template.phases:
188 template_dict.get(template_id).phases.append(phase)
189
190 return template_dict
191
192
193class TemplateService(object):
194
195 def __init__(self, cache_manager):
196 self.template_tbl = sql.SQLTableManager(TEMPLATE_TABLE_NAME)
197 self.template2label_tbl = sql.SQLTableManager(TEMPLATE2LABEL_TABLE_NAME)
198 self.template2component_tbl = sql.SQLTableManager(
199 TEMPLATE2COMPONENT_TABLE_NAME)
200 self.template2admin_tbl = sql.SQLTableManager(TEMPLATE2ADMIN_TABLE_NAME)
201 self.template2fieldvalue_tbl = sql.SQLTableManager(
202 TEMPLATE2FIELDVALUE_TABLE_NAME)
203 self.issuephasedef_tbl = sql.SQLTableManager(
204 ISSUEPHASEDEF_TABLE_NAME)
205 self.template2approvalvalue_tbl = sql.SQLTableManager(
206 TEMPLATE2APPROVALVALUE_TABLE_NAME)
207
208 self.template_set_2lc = TemplateSetTwoLevelCache(cache_manager, self)
209 self.template_def_2lc = TemplateDefTwoLevelCache(cache_manager, self)
210
211 def CreateDefaultProjectTemplates(self, cnxn, project_id):
212 """Create the default templates for a project.
213
214 Used only when creating a new project.
215
216 Args:
217 cnxn: A MonorailConnection instance.
218 project_id: The project ID under which to create the templates.
219 """
220 for tpl in tracker_constants.DEFAULT_TEMPLATES:
221 tpl = tracker_bizobj.ConvertDictToTemplate(tpl)
222 self.CreateIssueTemplateDef(cnxn, project_id, tpl.name, tpl.content,
223 tpl.summary, tpl.summary_must_be_edited, tpl.status, tpl.members_only,
224 tpl.owner_defaults_to_member, tpl.component_required, tpl.owner_id,
225 tpl.labels, tpl.component_ids, tpl.admin_ids, tpl.field_values,
226 tpl.phases)
227
228 def GetTemplateByName(self, cnxn, template_name, project_id):
229 """Retrieves a template by name and project_id.
230
231 Args:
232 template_name (string): name of template.
233 project_id (int): ID of project template is under.
234
235 Returns:
236 A Template PB if found, otherwise None.
237 """
238 template_set = self.GetTemplateSetForProject(cnxn, project_id)
239 for tpl_id, name, _members_only in template_set:
240 if template_name == name:
241 return self.GetTemplateById(cnxn, tpl_id)
242
243 def GetTemplateById(self, cnxn, template_id):
244 """Retrieves one template.
245
246 Args:
247 template_id (int): ID of the template.
248
249 Returns:
250 A TemplateDef PB if found, otherwise None.
251 """
252 result_dict, _ = self.template_def_2lc.GetAll(cnxn, [template_id])
253 try:
254 return result_dict[template_id]
255 except KeyError:
256 return None
257
258 def GetTemplatesById(self, cnxn, template_ids):
259 """Retrieves one or more templates by ID.
260
261 Args:
262 template_id (list<int>): IDs of the templates.
263
264 Returns:
265 A list containing any found TemplateDef PBs.
266 """
267 result_dict, _ = self.template_def_2lc.GetAll(cnxn, template_ids)
268 return list(result_dict.values())
269
270 def GetTemplateSetForProject(self, cnxn, project_id):
271 """Get the TemplateSet for a project."""
272 result_dict, _ = self.template_set_2lc.GetAll(cnxn, [project_id])
273 return result_dict[project_id]
274
275 def GetProjectTemplates(self, cnxn, project_id):
276 """Gets all templates in a given project.
277
278 Args:
279 cnxn: A MonorailConnection instance.
280 project_id: All templates for this project will be returned.
281
282 Returns:
283 A list of TemplateDefs.
284 """
285 template_set = self.GetTemplateSetForProject(cnxn, project_id)
286 template_ids = [row[0] for row in template_set]
287 return self.GetTemplatesById(cnxn, template_ids)
288
289 def TemplatesWithComponent(self, cnxn, component_id):
290 """Returns all templates with the specified component.
291
292 Args:
293 cnxn: connection to SQL database.
294 component_id: int component id.
295
296 Returns:
297 A list of TemplateDefs.
298 """
299 template2component_rows = self.template2component_tbl.Select(
300 cnxn, cols=['template_id'], component_id=component_id)
301 template_ids = [r[0] for r in template2component_rows]
302 return self.GetTemplatesById(cnxn, template_ids)
303
304 def CreateIssueTemplateDef(
305 self, cnxn, project_id, name, content, summary, summary_must_be_edited,
306 status, members_only, owner_defaults_to_member, component_required,
307 owner_id=None, labels=None, component_ids=None, admin_ids=None,
308 field_values=None, phases=None, approval_values=None):
309 """Create a new issue template definition with the given info.
310
311 Args:
312 cnxn: connection to SQL database.
313 project_id: int ID of the current project.
314 name: name of the new issue template.
315 content: string content of the issue template.
316 summary: string summary of the issue template.
317 summary_must_be_edited: True if the summary must be edited when this
318 issue template is used to make a new issue.
319 status: string default status of a new issue created with this template.
320 members_only: True if only members can view this issue template.
321 owner_defaults_to_member: True is issue owner should be set to member
322 creating the issue.
323 component_required: True if a component is required.
324 owner_id: user_id of default owner, if any.
325 labels: list of string labels for the new issue, if any.
326 component_ids: list of component_ids, if any.
327 admin_ids: list of admin_ids, if any.
328 field_values: list of FieldValue PBs, if any.
329 phases: list of Phase PBs, if any.
330 approval_values: list of ApprovalValue PBs, if any.
331
332 Returns:
333 Integer template_id of the new issue template definition.
334 """
335 template_id = self.template_tbl.InsertRow(
336 cnxn, project_id=project_id, name=name, content=content,
337 summary=summary, summary_must_be_edited=summary_must_be_edited,
338 owner_id=owner_id, status=status, members_only=members_only,
339 owner_defaults_to_member=owner_defaults_to_member,
340 component_required=component_required, commit=False)
341
342 if labels:
343 self.template2label_tbl.InsertRows(
344 cnxn, TEMPLATE2LABEL_COLS, [(template_id, label) for label in labels],
345 commit=False)
346 if component_ids:
347 self.template2component_tbl.InsertRows(
348 cnxn, TEMPLATE2COMPONENT_COLS, [(template_id, c_id) for
349 c_id in component_ids], commit=False)
350 if admin_ids:
351 self.template2admin_tbl.InsertRows(
352 cnxn, TEMPLATE2ADMIN_COLS, [(template_id, admin_id) for
353 admin_id in admin_ids], commit=False)
354 if field_values:
355 self.template2fieldvalue_tbl.InsertRows(
356 cnxn, TEMPLATE2FIELDVALUE_COLS, [
357 (template_id, fv.field_id, fv.int_value, fv.str_value, fv.user_id,
358 fv.date_value, fv.url_value) for fv in field_values],
359 commit=False)
360
361 # current phase_ids in approval_values and phases are temporary and were
362 # assigned based on the order of the phases. These temporary phase_ids are
363 # used to keep track of which approvals belong to which phases and are
364 # updated once all phases have their real phase_ids returned from InsertRow.
365 phase_id_by_tmp = {}
366 if phases:
367 for phase in phases:
368 phase_id = self.issuephasedef_tbl.InsertRow(
369 cnxn, name=phase.name, rank=phase.rank, commit=False)
370 phase_id_by_tmp[phase.phase_id] = phase_id
371
372 if approval_values:
373 self.template2approvalvalue_tbl.InsertRows(
374 cnxn, TEMPLATE2APPROVALVALUE_COLS,
375 [(av.approval_id, template_id,
376 phase_id_by_tmp.get(av.phase_id), av.status.name.lower())
377 for av in approval_values],
378 commit=False)
379
380 cnxn.Commit()
381 self.template_set_2lc.InvalidateKeys(cnxn, [project_id])
382 return template_id
383
384 def UpdateIssueTemplateDef(
385 self, cnxn, project_id, template_id, name=None, content=None,
386 summary=None, summary_must_be_edited=None, status=None, members_only=None,
387 owner_defaults_to_member=None, component_required=None, owner_id=None,
388 labels=None, component_ids=None, admin_ids=None, field_values=None,
389 phases=None, approval_values=None):
390 """Update an existing issue template definition with the given info.
391
392 Args:
393 cnxn: connection to SQL database.
394 project_id: int ID of the current project.
395 template_id: int ID of the issue template to update.
396 name: updated name of the new issue template.
397 content: updated string content of the issue template.
398 summary: updated string summary of the issue template.
399 summary_must_be_edited: True if the summary must be edited when this
400 issue template is used to make a new issue.
401 status: updated string default status of a new issue created with this
402 template.
403 members_only: True if only members can view this issue template.
404 owner_defaults_to_member: True is issue owner should be set to member
405 creating the issue.
406 component_required: True if a component is required.
407 owner_id: updated user_id of default owner, if any.
408 labels: updated list of string labels for the new issue, if any.
409 component_ids: updated list of component_ids, if any.
410 admin_ids: updated list of admin_ids, if any.
411 field_values: updated list of FieldValue PBs, if any.
412 phases: updated list of Phase PBs, if any.
413 approval_values: updated list of ApprovalValue PBs, if any.
414 """
415 new_values = {}
416 if name is not None:
417 new_values['name'] = name
418 if content is not None:
419 new_values['content'] = content
420 if summary is not None:
421 new_values['summary'] = summary
422 if summary_must_be_edited is not None:
423 new_values['summary_must_be_edited'] = bool(summary_must_be_edited)
424 if status is not None:
425 new_values['status'] = status
426 if members_only is not None:
427 new_values['members_only'] = bool(members_only)
428 if owner_defaults_to_member is not None:
429 new_values['owner_defaults_to_member'] = bool(owner_defaults_to_member)
430 if component_required is not None:
431 new_values['component_required'] = bool(component_required)
432 if owner_id is not None:
433 new_values['owner_id'] = owner_id
434
435 self.template_tbl.Update(cnxn, new_values, id=template_id, commit=False)
436
437 if labels is not None:
438 self.template2label_tbl.Delete(
439 cnxn, template_id=template_id, commit=False)
440 self.template2label_tbl.InsertRows(
441 cnxn, TEMPLATE2LABEL_COLS, [(template_id, label) for label in labels],
442 commit=False)
443 if component_ids is not None:
444 self.template2component_tbl.Delete(
445 cnxn, template_id=template_id, commit=False)
446 self.template2component_tbl.InsertRows(
447 cnxn, TEMPLATE2COMPONENT_COLS, [(template_id, c_id) for
448 c_id in component_ids],
449 commit=False)
450 if admin_ids is not None:
451 self.template2admin_tbl.Delete(
452 cnxn, template_id=template_id, commit=False)
453 self.template2admin_tbl.InsertRows(
454 cnxn, TEMPLATE2ADMIN_COLS, [(template_id, admin_id) for
455 admin_id in admin_ids],
456 commit=False)
457 if field_values is not None:
458 self.template2fieldvalue_tbl.Delete(
459 cnxn, template_id=template_id, commit=False)
460 self.template2fieldvalue_tbl.InsertRows(
461 cnxn, TEMPLATE2FIELDVALUE_COLS, [
462 (template_id, fv.field_id, fv.int_value, fv.str_value, fv.user_id,
463 fv.date_value, fv.url_value) for fv in field_values],
464 commit=False)
465
466 # we need to keep track of tmp phase_ids created at the servlet.
467 phase_id_by_tmp = {}
468 if phases is not None:
469 self.template2approvalvalue_tbl.Delete(
470 cnxn, template_id=template_id, commit=False)
471 for phase in phases:
472 phase_id = self.issuephasedef_tbl.InsertRow(
473 cnxn, name=phase.name, rank=phase.rank, commit=False)
474 phase_id_by_tmp[phase.phase_id] = phase_id
475
476 self.template2approvalvalue_tbl.InsertRows(
477 cnxn, TEMPLATE2APPROVALVALUE_COLS,
478 [(av.approval_id, template_id,
479 phase_id_by_tmp.get(av.phase_id), av.status.name.lower())
480 for av in approval_values], commit=False)
481
482 cnxn.Commit()
483 self.template_set_2lc.InvalidateKeys(cnxn, [project_id])
484 self.template_def_2lc.InvalidateKeys(cnxn, [template_id])
485
486 def DeleteIssueTemplateDef(self, cnxn, project_id, template_id):
487 """Delete the specified issue template definition."""
488 self.template2label_tbl.Delete(cnxn, template_id=template_id, commit=False)
489 self.template2component_tbl.Delete(
490 cnxn, template_id=template_id, commit=False)
491 self.template2admin_tbl.Delete(cnxn, template_id=template_id, commit=False)
492 self.template2fieldvalue_tbl.Delete(
493 cnxn, template_id=template_id, commit=False)
494 self.template2approvalvalue_tbl.Delete(
495 cnxn, template_id=template_id, commit=False)
496 # We do not delete issuephasedef rows becuase these rows will be used by
497 # issues that were created with this template. template2approvalvalue rows
498 # can be deleted because those rows are copied over to issue2approvalvalue
499 # during issue creation.
500 self.template_tbl.Delete(cnxn, id=template_id, commit=False)
501
502 cnxn.Commit()
503 self.template_set_2lc.InvalidateKeys(cnxn, [project_id])
504 self.template_def_2lc.InvalidateKeys(cnxn, [template_id])
505
506 def ExpungeProjectTemplates(self, cnxn, project_id):
507 template_id_rows = self.template_tbl.Select(
508 cnxn, cols=['id'], project_id=project_id)
509 template_ids = [row[0] for row in template_id_rows]
510 self.template2label_tbl.Delete(cnxn, template_id=template_ids)
511 self.template2component_tbl.Delete(cnxn, template_id=template_ids)
512 # TODO(3816): Delete all other relations here.
513 self.template_tbl.Delete(cnxn, project_id=project_id)
514
515 def ExpungeUsersInTemplates(self, cnxn, user_ids, limit=None):
516 """Wipes a user from the templates system.
517
518 This method will not commit the operation. This method will
519 not make changes to in-memory data.
520 """
521 self.template2admin_tbl.Delete(
522 cnxn, admin_id=user_ids, commit=False, limit=limit)
523 self.template2fieldvalue_tbl.Delete(
524 cnxn, user_id=user_ids, commit=False, limit=limit)
525 # template_tbl's owner_id does not reference User. All appropriate rows
526 # should be deleted before rows can be safely deleted from User. No limit
527 # will be applied.
528 self.template_tbl.Update(
529 cnxn, {'owner_id': None}, owner_id=user_ids, commit=False)
530
531
532def UnpackTemplate(template_row):
533 """Partially construct a template object using info from a DB row."""
534 (template_id, _project_id, name, content, summary,
535 summary_must_be_edited, owner_id, status,
536 members_only, owner_defaults_to_member, component_required) = template_row
537 template = tracker_pb2.TemplateDef()
538 template.template_id = template_id
539 template.name = name
540 template.content = content
541 template.summary = summary
542 template.summary_must_be_edited = bool(
543 summary_must_be_edited)
544 template.owner_id = owner_id or 0
545 template.status = status
546 template.members_only = bool(members_only)
547 template.owner_defaults_to_member = bool(owner_defaults_to_member)
548 template.component_required = bool(component_required)
549
550 return template