blob: 1f15bccf8bef7cf16453c18a6a16661fbcb7688c [file] [log] [blame]
# 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', '')
# TODO(crbug.com/monorail/10936): switch when convert /p to flask
# labels = post_data.getlist('label')
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