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