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