blob: afa015b55d99cd28f6c8ed949f5045df416c8267 [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"""Helper functions for issue template servlets"""
6from __future__ import print_function
7from __future__ import division
8from __future__ import absolute_import
9
10import collections
11import logging
12
13from framework import authdata
14from framework import exceptions
15from framework import framework_bizobj
16from framework import framework_helpers
17from tracker import field_helpers
18from tracker import tracker_bizobj
19from tracker import tracker_constants
20from tracker import tracker_helpers
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +010021from mrproto import tracker_pb2
Copybara854996b2021-09-07 19:36:02 +000022
23MAX_NUM_PHASES = 6
24
25PHASE_INPUTS = [
26 'phase_0', 'phase_1', 'phase_2', 'phase_3', 'phase_4', 'phase_5']
27
28_NO_PHASE_VALUE = 'no_phase'
29
30ParsedTemplate = collections.namedtuple(
31 'ParsedTemplate', 'name, members_only, summary, summary_must_be_edited, '
32 'content, status, owner_str, labels, field_val_strs, component_paths, '
33 'component_required, owner_defaults_to_member, admin_str, add_approvals, '
34 'phase_names, approvals_to_phase_idx, required_approval_ids')
35
36
37def ParseTemplateRequest(post_data, config):
38 """Parse an issue template."""
39
40 name = post_data.get('name', '')
41 members_only = (post_data.get('members_only') == 'on')
42 summary = post_data.get('summary', '')
43 summary_must_be_edited = (
44 post_data.get('summary_must_be_edited') == 'on')
45 content = post_data.get('content', '')
46 content = framework_helpers.WordWrapSuperLongLines(content, max_cols=75)
47 status = post_data.get('status', '')
48 owner_str = post_data.get('owner', '')
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +010049 labels = post_data.getlist('label')
Copybara854996b2021-09-07 19:36:02 +000050 field_val_strs = collections.defaultdict(list)
51 for fd in config.field_defs:
52 field_value_key = 'custom_%d' % fd.field_id
53 if post_data.get(field_value_key):
54 field_val_strs[fd.field_id].append(post_data[field_value_key])
55
56 component_paths = []
57 if post_data.get('components'):
58 for component_path in post_data.get('components').split(','):
59 if component_path.strip() not in component_paths:
60 component_paths.append(component_path.strip())
61 component_required = post_data.get('component_required') == 'on'
62
63 owner_defaults_to_member = post_data.get('owner_defaults_to_member') == 'on'
64
65 admin_str = post_data.get('admin_names', '')
66
67 add_approvals = post_data.get('add_approvals') == 'on'
68 phase_names = [post_data.get(phase_input, '') for phase_input in PHASE_INPUTS]
69
70 required_approval_ids = []
71 approvals_to_phase_idx = {}
72
73 for approval_def in config.approval_defs:
74 phase_num = post_data.get('approval_%d' % approval_def.approval_id, '')
75 if phase_num == _NO_PHASE_VALUE:
76 approvals_to_phase_idx[approval_def.approval_id] = None
77 else:
78 try:
79 idx = PHASE_INPUTS.index(phase_num)
80 approvals_to_phase_idx[approval_def.approval_id] = idx
81 except ValueError:
82 logging.info('approval %d was omitted' % approval_def.approval_id)
83 required_name = 'approval_%d_required' % approval_def.approval_id
84 if (post_data.get(required_name) == 'on'):
85 required_approval_ids.append(approval_def.approval_id)
86
87 return ParsedTemplate(
88 name, members_only, summary, summary_must_be_edited, content, status,
89 owner_str, labels, field_val_strs, component_paths, component_required,
90 owner_defaults_to_member, admin_str, add_approvals, phase_names,
91 approvals_to_phase_idx, required_approval_ids)
92
93
94def GetTemplateInfoFromParsed(mr, services, parsed, config):
95 """Get Template field info and PBs from a ParsedTemplate."""
96
97 admin_ids, _ = tracker_helpers.ParsePostDataUsers(
98 mr.cnxn, parsed.admin_str, services.user)
99
100 owner_id = 0
101 if parsed.owner_str:
102 try:
103 user_id = services.user.LookupUserID(mr.cnxn, parsed.owner_str)
104 auth = authdata.AuthData.FromUserID(mr.cnxn, user_id, services)
105 if framework_bizobj.UserIsInProject(mr.project, auth.effective_ids):
106 owner_id = user_id
107 else:
108 mr.errors.owner = 'User is not a member of this project.'
109 except exceptions.NoSuchUserException:
110 mr.errors.owner = 'Owner not found.'
111
112 component_ids = tracker_helpers.LookupComponentIDs(
113 parsed.component_paths, config, mr.errors)
114
115 # TODO(jojwang): monorail:4678 Process phase field values.
116 phase_field_val_strs = {}
117 field_values = field_helpers.ParseFieldValues(
118 mr.cnxn, services.user, parsed.field_val_strs,
119 phase_field_val_strs, config)
120 for fv in field_values:
121 logging.info('field_value is %r: %r',
122 fv.field_id, tracker_bizobj.GetFieldValue(fv, {}))
123
124 phases = []
125 approvals = []
126 if parsed.add_approvals:
127 phases, approvals = _GetPhasesAndApprovalsFromParsed(
128 mr, parsed.phase_names, parsed.approvals_to_phase_idx,
129 parsed.required_approval_ids)
130
131 return admin_ids, owner_id, component_ids, field_values, phases, approvals
132
133
134def _GetPhasesAndApprovalsFromParsed(
135 mr, phase_names, approvals_to_phase_idx, required_approval_ids):
136 """Get Phase PBs from a parsed phase_names and approvals_by_phase_idx."""
137
138 phases = []
139 approvals = []
140 valid_phase_names = []
141
142 for name in phase_names:
143 if name:
144 if not tracker_constants.PHASE_NAME_RE.match(name):
145 mr.errors.phase_approvals = 'Invalid gate name(s).'
146 return phases, approvals
147 valid_phase_names.append(name)
148 if len(valid_phase_names) != len(
149 set(name.lower() for name in valid_phase_names)):
150 mr.errors.phase_approvals = 'Duplicate gate names.'
151 return phases, approvals
152 valid_phase_idxs = [idx for idx, name in enumerate(phase_names) if name]
153 if set(valid_phase_idxs) != set([
154 idx for idx in approvals_to_phase_idx.values() if idx is not None]):
155 mr.errors.phase_approvals = 'Defined gates must have assigned approvals.'
156 return phases, approvals
157
158 # Distributing the ranks over a wider range is not necessary since
159 # any edits to template phases will cause a complete rewrite.
160 # phase_id is temporarily the idx for keeping track of which approvals
161 # belong to which phases.
162 for idx, phase_name in enumerate(phase_names):
163 if phase_name:
164 phase = tracker_pb2.Phase(name=phase_name, rank=idx, phase_id=idx)
165 phases.append(phase)
166
167 for approval_id, phase_idx in approvals_to_phase_idx.items():
168 av = tracker_pb2.ApprovalValue(
169 approval_id=approval_id, phase_id=phase_idx)
170 if approval_id in required_approval_ids:
171 av.status = tracker_pb2.ApprovalStatus.NEEDS_REVIEW
172 approvals.append(av)
173
174 return phases, approvals
175
176
177def FilterApprovalsAndPhases(approval_values, phases, config):
178 """Return lists without deleted approvals and empty phases."""
179 deleted_approval_ids = [fd.field_id for fd in config.field_defs if
180 fd.is_deleted and
181 fd.field_type is tracker_pb2.FieldTypes.APPROVAL_TYPE]
182 filtered_avs = [av for av in approval_values if
183 av.approval_id not in deleted_approval_ids]
184
185 av_phase_ids = list(set([av.phase_id for av in filtered_avs]))
186 filtered_phases = [phase for phase in phases if
187 phase.phase_id in av_phase_ids]
188 return filtered_avs, filtered_phases
189
190
191def GatherApprovalsPageData(approval_values, tmpl_phases, config):
192 """Create the page data necessary for filling in the launch-gates-table."""
193 filtered_avs, filtered_phases = FilterApprovalsAndPhases(
194 approval_values, tmpl_phases, config)
195 filtered_phases.sort(key=lambda phase: phase.rank)
196
197 required_approval_ids = []
198 prechecked_approvals = []
199
200 phase_idx_by_id = {
201 phase.phase_id:idx for idx, phase in enumerate(filtered_phases)}
202 for av in filtered_avs:
203 # approval is part of a phase and that phase can be found.
204 if phase_idx_by_id.get(av.phase_id) is not None:
205 idx = phase_idx_by_id.get(av.phase_id)
206 prechecked_approvals.append(
207 '%d_phase_%d' % (av.approval_id, idx))
208 else:
209 prechecked_approvals.append('%d' % av.approval_id)
210 if av.status is tracker_pb2.ApprovalStatus.NEEDS_REVIEW:
211 required_approval_ids.append(av.approval_id)
212
213 num_phases = len(filtered_phases)
214 filtered_phases.extend([tracker_pb2.Phase()] * (
215 MAX_NUM_PHASES - num_phases))
216 return prechecked_approvals, required_approval_ids, filtered_phases
217
218
219def GetCheckedApprovalsFromParsed(approvals_to_phase_idx):
220 checked_approvals = []
221 for approval_id, phs_idx in approvals_to_phase_idx.items():
222 if phs_idx is not None:
223 checked_approvals.append('%d_phase_%d' % (approval_id, phs_idx))
224 else:
225 checked_approvals.append('%d' % approval_id)
226 return checked_approvals
227
228
229def GetIssueFromTemplate(template, project_id, reporter_id):
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100230 # type: (mrproto.tracker_pb2.TemplateDef, int, int) ->
231 # mrproto.tracker_pb2.Issue
Copybara854996b2021-09-07 19:36:02 +0000232 """Build a templated issue from TemplateDef.
233
234 Args:
235 template: Template that issue creation is based on.
236 project_id: ID of the Project the template belongs to.
237 reporter_id: Requesting user's ID.
238
239 Returns:
240 protorpc Issue filled with data from given `template`.
241 """
242 owner_id = None
243 if template.owner_id:
244 owner_id = template.owner_id
245 elif template.owner_defaults_to_member:
246 owner_id = reporter_id
247
248 issue = tracker_pb2.Issue(
249 project_id=project_id,
250 summary=template.summary,
251 status=template.status,
252 owner_id=owner_id,
253 labels=template.labels,
254 component_ids=template.component_ids,
255 reporter_id=reporter_id,
256 field_values=template.field_values,
257 phases=template.phases,
258 approval_values=template.approval_values)
259
260 return issue