Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 1 | # 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""" |
| 7 | from __future__ import print_function |
| 8 | from __future__ import division |
| 9 | from __future__ import absolute_import |
| 10 | |
| 11 | import collections |
| 12 | import logging |
| 13 | |
| 14 | from framework import authdata |
| 15 | from framework import exceptions |
| 16 | from framework import framework_bizobj |
| 17 | from framework import framework_helpers |
| 18 | from tracker import field_helpers |
| 19 | from tracker import tracker_bizobj |
| 20 | from tracker import tracker_constants |
| 21 | from tracker import tracker_helpers |
| 22 | from proto import tracker_pb2 |
| 23 | |
| 24 | MAX_NUM_PHASES = 6 |
| 25 | |
| 26 | PHASE_INPUTS = [ |
| 27 | 'phase_0', 'phase_1', 'phase_2', 'phase_3', 'phase_4', 'phase_5'] |
| 28 | |
| 29 | _NO_PHASE_VALUE = 'no_phase' |
| 30 | |
| 31 | ParsedTemplate = 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 | |
| 38 | def 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Ãnez | de94280 | 2022-07-15 14:06:55 +0200 | [diff] [blame^] | 50 | # TODO(crbug.com/monorail/10936): switch when convert /p to flask |
| 51 | # labels = post_data.getlist('label') |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 52 | 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 | |
| 97 | def 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 | |
| 137 | def _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 | |
| 180 | def 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 | |
| 194 | def 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 | |
| 222 | def 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 | |
| 232 | def 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 |