| # Copyright 2018 The Chromium Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style |
| # license that can be found in the LICENSE file or at |
| # https://developers.google.com/open-source/licenses/bsd |
| |
| """Helper functions for issue template servlets""" |
| from __future__ import print_function |
| from __future__ import division |
| from __future__ import absolute_import |
| |
| import collections |
| import logging |
| |
| from framework import authdata |
| from framework import exceptions |
| from framework import framework_bizobj |
| from framework import framework_helpers |
| from tracker import field_helpers |
| from tracker import tracker_bizobj |
| from tracker import tracker_constants |
| from tracker import tracker_helpers |
| from proto import tracker_pb2 |
| |
| MAX_NUM_PHASES = 6 |
| |
| PHASE_INPUTS = [ |
| 'phase_0', 'phase_1', 'phase_2', 'phase_3', 'phase_4', 'phase_5'] |
| |
| _NO_PHASE_VALUE = 'no_phase' |
| |
| ParsedTemplate = collections.namedtuple( |
| 'ParsedTemplate', 'name, members_only, summary, summary_must_be_edited, ' |
| 'content, status, owner_str, labels, field_val_strs, component_paths, ' |
| 'component_required, owner_defaults_to_member, admin_str, add_approvals, ' |
| 'phase_names, approvals_to_phase_idx, required_approval_ids') |
| |
| |
| def ParseTemplateRequest(post_data, config): |
| """Parse an issue template.""" |
| |
| name = post_data.get('name', '') |
| members_only = (post_data.get('members_only') == 'on') |
| summary = post_data.get('summary', '') |
| summary_must_be_edited = ( |
| post_data.get('summary_must_be_edited') == 'on') |
| content = post_data.get('content', '') |
| content = framework_helpers.WordWrapSuperLongLines(content, max_cols=75) |
| status = post_data.get('status', '') |
| owner_str = post_data.get('owner', '') |
| labels = post_data.getall('label') |
| field_val_strs = collections.defaultdict(list) |
| for fd in config.field_defs: |
| field_value_key = 'custom_%d' % fd.field_id |
| if post_data.get(field_value_key): |
| field_val_strs[fd.field_id].append(post_data[field_value_key]) |
| |
| component_paths = [] |
| if post_data.get('components'): |
| for component_path in post_data.get('components').split(','): |
| if component_path.strip() not in component_paths: |
| component_paths.append(component_path.strip()) |
| component_required = post_data.get('component_required') == 'on' |
| |
| owner_defaults_to_member = post_data.get('owner_defaults_to_member') == 'on' |
| |
| admin_str = post_data.get('admin_names', '') |
| |
| add_approvals = post_data.get('add_approvals') == 'on' |
| phase_names = [post_data.get(phase_input, '') for phase_input in PHASE_INPUTS] |
| |
| required_approval_ids = [] |
| approvals_to_phase_idx = {} |
| |
| for approval_def in config.approval_defs: |
| phase_num = post_data.get('approval_%d' % approval_def.approval_id, '') |
| if phase_num == _NO_PHASE_VALUE: |
| approvals_to_phase_idx[approval_def.approval_id] = None |
| else: |
| try: |
| idx = PHASE_INPUTS.index(phase_num) |
| approvals_to_phase_idx[approval_def.approval_id] = idx |
| except ValueError: |
| logging.info('approval %d was omitted' % approval_def.approval_id) |
| required_name = 'approval_%d_required' % approval_def.approval_id |
| if (post_data.get(required_name) == 'on'): |
| required_approval_ids.append(approval_def.approval_id) |
| |
| return ParsedTemplate( |
| name, members_only, summary, summary_must_be_edited, content, status, |
| owner_str, labels, field_val_strs, component_paths, component_required, |
| owner_defaults_to_member, admin_str, add_approvals, phase_names, |
| approvals_to_phase_idx, required_approval_ids) |
| |
| |
| def GetTemplateInfoFromParsed(mr, services, parsed, config): |
| """Get Template field info and PBs from a ParsedTemplate.""" |
| |
| admin_ids, _ = tracker_helpers.ParsePostDataUsers( |
| mr.cnxn, parsed.admin_str, services.user) |
| |
| owner_id = 0 |
| if parsed.owner_str: |
| try: |
| user_id = services.user.LookupUserID(mr.cnxn, parsed.owner_str) |
| auth = authdata.AuthData.FromUserID(mr.cnxn, user_id, services) |
| if framework_bizobj.UserIsInProject(mr.project, auth.effective_ids): |
| owner_id = user_id |
| else: |
| mr.errors.owner = 'User is not a member of this project.' |
| except exceptions.NoSuchUserException: |
| mr.errors.owner = 'Owner not found.' |
| |
| component_ids = tracker_helpers.LookupComponentIDs( |
| parsed.component_paths, config, mr.errors) |
| |
| # TODO(jojwang): monorail:4678 Process phase field values. |
| phase_field_val_strs = {} |
| field_values = field_helpers.ParseFieldValues( |
| mr.cnxn, services.user, parsed.field_val_strs, |
| phase_field_val_strs, config) |
| for fv in field_values: |
| logging.info('field_value is %r: %r', |
| fv.field_id, tracker_bizobj.GetFieldValue(fv, {})) |
| |
| phases = [] |
| approvals = [] |
| if parsed.add_approvals: |
| phases, approvals = _GetPhasesAndApprovalsFromParsed( |
| mr, parsed.phase_names, parsed.approvals_to_phase_idx, |
| parsed.required_approval_ids) |
| |
| return admin_ids, owner_id, component_ids, field_values, phases, approvals |
| |
| |
| def _GetPhasesAndApprovalsFromParsed( |
| mr, phase_names, approvals_to_phase_idx, required_approval_ids): |
| """Get Phase PBs from a parsed phase_names and approvals_by_phase_idx.""" |
| |
| phases = [] |
| approvals = [] |
| valid_phase_names = [] |
| |
| for name in phase_names: |
| if name: |
| if not tracker_constants.PHASE_NAME_RE.match(name): |
| mr.errors.phase_approvals = 'Invalid gate name(s).' |
| return phases, approvals |
| valid_phase_names.append(name) |
| if len(valid_phase_names) != len( |
| set(name.lower() for name in valid_phase_names)): |
| mr.errors.phase_approvals = 'Duplicate gate names.' |
| return phases, approvals |
| valid_phase_idxs = [idx for idx, name in enumerate(phase_names) if name] |
| if set(valid_phase_idxs) != set([ |
| idx for idx in approvals_to_phase_idx.values() if idx is not None]): |
| mr.errors.phase_approvals = 'Defined gates must have assigned approvals.' |
| return phases, approvals |
| |
| # Distributing the ranks over a wider range is not necessary since |
| # any edits to template phases will cause a complete rewrite. |
| # phase_id is temporarily the idx for keeping track of which approvals |
| # belong to which phases. |
| for idx, phase_name in enumerate(phase_names): |
| if phase_name: |
| phase = tracker_pb2.Phase(name=phase_name, rank=idx, phase_id=idx) |
| phases.append(phase) |
| |
| for approval_id, phase_idx in approvals_to_phase_idx.items(): |
| av = tracker_pb2.ApprovalValue( |
| approval_id=approval_id, phase_id=phase_idx) |
| if approval_id in required_approval_ids: |
| av.status = tracker_pb2.ApprovalStatus.NEEDS_REVIEW |
| approvals.append(av) |
| |
| return phases, approvals |
| |
| |
| def FilterApprovalsAndPhases(approval_values, phases, config): |
| """Return lists without deleted approvals and empty phases.""" |
| deleted_approval_ids = [fd.field_id for fd in config.field_defs if |
| fd.is_deleted and |
| fd.field_type is tracker_pb2.FieldTypes.APPROVAL_TYPE] |
| filtered_avs = [av for av in approval_values if |
| av.approval_id not in deleted_approval_ids] |
| |
| av_phase_ids = list(set([av.phase_id for av in filtered_avs])) |
| filtered_phases = [phase for phase in phases if |
| phase.phase_id in av_phase_ids] |
| return filtered_avs, filtered_phases |
| |
| |
| def GatherApprovalsPageData(approval_values, tmpl_phases, config): |
| """Create the page data necessary for filling in the launch-gates-table.""" |
| filtered_avs, filtered_phases = FilterApprovalsAndPhases( |
| approval_values, tmpl_phases, config) |
| filtered_phases.sort(key=lambda phase: phase.rank) |
| |
| required_approval_ids = [] |
| prechecked_approvals = [] |
| |
| phase_idx_by_id = { |
| phase.phase_id:idx for idx, phase in enumerate(filtered_phases)} |
| for av in filtered_avs: |
| # approval is part of a phase and that phase can be found. |
| if phase_idx_by_id.get(av.phase_id) is not None: |
| idx = phase_idx_by_id.get(av.phase_id) |
| prechecked_approvals.append( |
| '%d_phase_%d' % (av.approval_id, idx)) |
| else: |
| prechecked_approvals.append('%d' % av.approval_id) |
| if av.status is tracker_pb2.ApprovalStatus.NEEDS_REVIEW: |
| required_approval_ids.append(av.approval_id) |
| |
| num_phases = len(filtered_phases) |
| filtered_phases.extend([tracker_pb2.Phase()] * ( |
| MAX_NUM_PHASES - num_phases)) |
| return prechecked_approvals, required_approval_ids, filtered_phases |
| |
| |
| def GetCheckedApprovalsFromParsed(approvals_to_phase_idx): |
| checked_approvals = [] |
| for approval_id, phs_idx in approvals_to_phase_idx.items(): |
| if phs_idx is not None: |
| checked_approvals.append('%d_phase_%d' % (approval_id, phs_idx)) |
| else: |
| checked_approvals.append('%d' % approval_id) |
| return checked_approvals |
| |
| |
| def GetIssueFromTemplate(template, project_id, reporter_id): |
| # type: (proto.tracker_pb2.TemplateDef, int, int) -> |
| # proto.tracker_pb2.Issue |
| """Build a templated issue from TemplateDef. |
| |
| Args: |
| template: Template that issue creation is based on. |
| project_id: ID of the Project the template belongs to. |
| reporter_id: Requesting user's ID. |
| |
| Returns: |
| protorpc Issue filled with data from given `template`. |
| """ |
| owner_id = None |
| if template.owner_id: |
| owner_id = template.owner_id |
| elif template.owner_defaults_to_member: |
| owner_id = reporter_id |
| |
| issue = tracker_pb2.Issue( |
| project_id=project_id, |
| summary=template.summary, |
| status=template.status, |
| owner_id=owner_id, |
| labels=template.labels, |
| component_ids=template.component_ids, |
| reporter_id=reporter_id, |
| field_values=template.field_values, |
| phases=template.phases, |
| approval_values=template.approval_values) |
| |
| return issue |