Adrià Vilanova Martínez | f19ea43 | 2024-01-23 20:20:52 +0100 | [diff] [blame] | 1 | # 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. |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 4 | |
| 5 | """Helper functions for issue template servlets""" |
| 6 | from __future__ import print_function |
| 7 | from __future__ import division |
| 8 | from __future__ import absolute_import |
| 9 | |
| 10 | import collections |
| 11 | import logging |
| 12 | |
| 13 | from framework import authdata |
| 14 | from framework import exceptions |
| 15 | from framework import framework_bizobj |
| 16 | from framework import framework_helpers |
| 17 | from tracker import field_helpers |
| 18 | from tracker import tracker_bizobj |
| 19 | from tracker import tracker_constants |
| 20 | from tracker import tracker_helpers |
Adrià Vilanova Martínez | f19ea43 | 2024-01-23 20:20:52 +0100 | [diff] [blame] | 21 | from mrproto import tracker_pb2 |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 22 | |
| 23 | MAX_NUM_PHASES = 6 |
| 24 | |
| 25 | PHASE_INPUTS = [ |
| 26 | 'phase_0', 'phase_1', 'phase_2', 'phase_3', 'phase_4', 'phase_5'] |
| 27 | |
| 28 | _NO_PHASE_VALUE = 'no_phase' |
| 29 | |
| 30 | ParsedTemplate = 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 | |
| 37 | def 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ínez | f19ea43 | 2024-01-23 20:20:52 +0100 | [diff] [blame] | 49 | labels = post_data.getlist('label') |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 50 | 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 | |
| 94 | def 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 | |
| 134 | def _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 | |
| 177 | def 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 | |
| 191 | def 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 | |
| 219 | def 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 | |
| 229 | def GetIssueFromTemplate(template, project_id, reporter_id): |
Adrià Vilanova Martínez | f19ea43 | 2024-01-23 20:20:52 +0100 | [diff] [blame] | 230 | # type: (mrproto.tracker_pb2.TemplateDef, int, int) -> |
| 231 | # mrproto.tracker_pb2.Issue |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 232 | """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 |