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