chromium-dashboard/pages/guide.py

574 строки
21 KiB
Python

# -*- coding: utf-8 -*-
# Copyright 2020 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License")
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import datetime
import flask
import json
import logging
import os
import re
import sys
from django import forms
from google.cloud import ndb
# Appengine imports.
from framework import ramcache
from framework import users
from framework import basehandlers
from framework import permissions
from framework import utils
from pages import guideforms
from internals import models
from internals import processes
# Forms to be used for each stage of each process.
# { feature_type_id: { stage_id: stage_specific_form} }
STAGE_FORMS = {
models.FEATURE_TYPE_INCUBATE_ID: {
models.INTENT_INCUBATE: guideforms.NewFeature_Incubate,
models.INTENT_IMPLEMENT: guideforms.NewFeature_Prototype,
models.INTENT_EXPERIMENT: guideforms.Any_DevTrial,
models.INTENT_IMPLEMENT_SHIP: guideforms.NewFeature_EvalReadinessToShip,
models.INTENT_EXTEND_TRIAL: guideforms.NewFeature_OriginTrial,
models.INTENT_SHIP: guideforms.Most_PrepareToShip,
models.INTENT_SHIPPED: guideforms.Any_Ship,
},
models.FEATURE_TYPE_EXISTING_ID: {
models.INTENT_IMPLEMENT: guideforms.Existing_Prototype,
models.INTENT_EXPERIMENT: guideforms.Any_DevTrial,
models.INTENT_EXTEND_TRIAL: guideforms.Existing_OriginTrial,
models.INTENT_SHIP: guideforms.Most_PrepareToShip,
models.INTENT_SHIPPED: guideforms.Any_Ship,
},
models.FEATURE_TYPE_CODE_CHANGE_ID: {
models.INTENT_IMPLEMENT: guideforms.PSA_Implement,
models.INTENT_EXPERIMENT: guideforms.Any_DevTrial,
models.INTENT_SHIP: guideforms.PSA_PrepareToShip,
models.INTENT_SHIPPED: guideforms.Any_Ship,
},
models.FEATURE_TYPE_DEPRECATION_ID: {
models.INTENT_IMPLEMENT: guideforms.Deprecation_Implement,
models.INTENT_EXPERIMENT: guideforms.Any_DevTrial,
models.INTENT_EXTEND_TRIAL: guideforms.Deprecation_DeprecationTrial,
models.INTENT_SHIP: guideforms.Deprecation_PrepareToShip,
models.INTENT_REMOVED: guideforms.Deprecation_Removed,
},
}
IMPL_STATUS_FORMS = {
models.INTENT_INCUBATE:
(None, guideforms.ImplStatus_Incubate),
models.INTENT_EXPERIMENT:
(models.BEHIND_A_FLAG, guideforms.ImplStatus_DevTrial),
models.INTENT_EXTEND_TRIAL:
(models.ORIGIN_TRIAL, guideforms.ImplStatus_OriginTrial),
models.INTENT_IMPLEMENT_SHIP:
(None, guideforms.ImplStatus_EvalReadinessToShip),
models.INTENT_SHIP:
(models.ENABLED_BY_DEFAULT, guideforms.ImplStatus_AllMilestones),
models.INTENT_SHIPPED:
(models.ENABLED_BY_DEFAULT, guideforms.ImplStatus_AllMilestones),
models.INTENT_REMOVED:
(models.REMOVED, guideforms.ImplStatus_AllMilestones),
}
# Forms to be used on the "Edit all" page that shows a flat list of fields.
# [('Section name': form_class)].
FLAT_FORMS = [
('Feature metadata', guideforms.Flat_Metadata),
('Identify the need', guideforms.Flat_Identify),
('Prototype a solution', guideforms.Flat_Implement),
('Dev trial', guideforms.Flat_DevTrial),
('Origin trial', guideforms.Flat_OriginTrial),
('Prepare to ship', guideforms.Flat_PrepareToShip),
('Ship', guideforms.Flat_Ship),
]
class FeatureNew(basehandlers.FlaskHandler):
TEMPLATE_PATH = 'guide/new.html'
@permissions.require_edit_feature
def get_template_data(self):
user = self.get_current_user()
new_feature_form = guideforms.NewFeatureForm(
initial={'owner': user.email()})
template_data = {
'overview_form': new_feature_form,
}
return template_data
@permissions.require_edit_feature
def process_post_data(self):
owners = self.split_emails('owner')
blink_components = (
self.split_input('blink_components', delim=',') or
[models.BlinkComponent.DEFAULT_COMPONENT])
# TODO(jrobbins): Validate input, even though it is done on client.
feature_type = int(self.form.get('feature_type', 0))
signed_in_user = ndb.User(
email=self.get_current_user().email(),
_auth_domain='gmail.com')
feature = models.Feature(
category=int(self.form.get('category')),
name=self.form.get('name'),
feature_type=feature_type,
intent_stage=models.INTENT_NONE,
summary=self.form.get('summary'),
owner=owners,
impl_status_chrome=models.NO_ACTIVE_DEV,
standardization=models.EDITORS_DRAFT,
unlisted=self.form.get('unlisted') == 'on',
web_dev_views=models.DEV_NO_SIGNALS,
blink_components=blink_components,
tag_review_status=processes.initial_tag_review_status(feature_type),
created_by=signed_in_user,
updated_by=signed_in_user)
key = feature.put()
# TODO(jrobbins): enumerate and remove only the relevant keys.
ramcache.flush_all()
redirect_url = '/guide/edit/' + str(key.integer_id())
return self.redirect(redirect_url)
class ProcessOverview(basehandlers.FlaskHandler):
TEMPLATE_PATH = 'guide/edit.html'
def detect_progress(self, f):
progress_so_far = {}
for progress_item, detector in list(processes.PROGRESS_DETECTORS.items()):
detected = detector(f)
if detected:
progress_so_far[progress_item] = str(detected)
return progress_so_far
@permissions.require_edit_feature
def get_template_data(self, feature_id):
f = models.Feature.get_by_id(int(feature_id))
if f is None:
self.abort(404, msg='Feature not found')
feature_process = processes.ALL_PROCESSES.get(
f.feature_type, processes.BLINK_LAUNCH_PROCESS)
template_data = {
'overview_form': guideforms.MetadataForm(f.format_for_edit()),
'process_json': json.dumps(processes.process_to_dict(feature_process)),
}
progress_so_far = self.detect_progress(f)
# Provide new or populated form to template.
template_data.update({
'feature': f.format_for_template(),
'feature_id': f.key.integer_id(),
'feature_json': json.dumps(f.format_for_template()),
'progress_so_far': json.dumps(progress_so_far),
})
return template_data
class FeatureEditStage(basehandlers.FlaskHandler):
TEMPLATE_PATH = 'guide/stage.html'
def touched(self, param_name):
"""Return True if the user edited the specified field."""
# TODO(jrobbins): for now we just consider everything on the current form
# to have been touched. Later we will add javascript to populate a
# hidden form field named "touched" that lists the names of all fields
# actually touched by the user.
# For now, checkboxes are always considered "touched", if they are
# present on the form.
# TODO(jrobbins): Simplify this after next deployment.
checkboxes = ('unlisted', 'all_platforms', 'wpt', 'prefixed', 'api_spec',
'requires_embedder_support')
if param_name in checkboxes:
form_fields_str = self.form.get('form_fields')
if form_fields_str:
form_fields = [field_name.strip()
for field_name in form_fields_str.split(',')]
return param_name in form_fields
else:
return True
return param_name in self.form
def get_blink_component_from_bug(self, blink_components, bug_url):
# TODO(jrobbins): Use monorail API instead of scrapping.
return []
def get_feature_and_process(self, feature_id):
"""Look up the feature that the user wants to edit, and its process."""
f = models.Feature.get_by_id(feature_id)
if f is None:
self.abort(404, msg='Feature not found')
feature_process = processes.ALL_PROCESSES.get(
f.feature_type, processes.BLINK_LAUNCH_PROCESS)
return f, feature_process
@permissions.require_edit_feature
def get_template_data(self, feature_id, stage_id):
f, feature_process = self.get_feature_and_process(feature_id)
stage_name = ''
for stage in feature_process.stages:
if stage.outgoing_stage == stage_id:
stage_name = stage.name
template_data = {
'stage_name': stage_name,
'stage_id': stage_id,
}
# TODO(jrobbins): show useful error if stage not found.
detail_form_class = STAGE_FORMS[f.feature_type][stage_id]
impl_status_offered, impl_status_form_class = IMPL_STATUS_FORMS.get(
stage_id, (None, None))
feature_edit_dict = f.format_for_edit()
detail_form = None
if detail_form_class:
detail_form = detail_form_class(feature_edit_dict)
impl_status_form = None
if impl_status_form_class:
impl_status_form = impl_status_form_class(feature_edit_dict)
# Provide new or populated form to template.
template_data.update({
'feature': f,
'feature_id': f.key.integer_id(),
'feature_form': detail_form,
'already_on_this_stage': stage_id == f.intent_stage,
'already_on_this_impl_status':
impl_status_offered == f.impl_status_chrome,
'impl_status_form': impl_status_form,
'impl_status_name': models.IMPLEMENTATION_STATUS.get(
impl_status_offered, None),
'impl_status_offered': impl_status_offered,
})
return template_data
@permissions.require_edit_feature
def process_post_data(self, feature_id, stage_id=0):
if feature_id:
feature = models.Feature.get_by_id(feature_id)
if feature is None:
self.abort(404, msg='Feature not found')
else:
feature.stash_values()
logging.info('POST is %r', self.form)
if self.touched('spec_link'):
feature.spec_link = self.parse_link('spec_link')
if self.touched('standard_maturity'):
feature.standard_maturity = self.parse_int('standard_maturity')
if self.touched('api_spec'):
feature.api_spec = self.form.get('api_spec') == 'on'
if self.touched('spec_mentors'):
feature.spec_mentors = self.split_emails('spec_mentors')
if self.touched('security_review_status'):
feature.security_review_status = self.parse_int('security_review_status')
if self.touched('privacy_review_status'):
feature.privacy_review_status = self.parse_int('privacy_review_status')
if self.touched('initial_public_proposal_url'):
feature.initial_public_proposal_url = self.parse_link(
'initial_public_proposal_url')
if self.touched('explainer_links'):
feature.explainer_links = self.split_input('explainer_links')
if self.touched('bug_url'):
feature.bug_url = self.parse_link('bug_url')
if self.touched('launch_bug_url'):
feature.launch_bug_url = self.parse_link('launch_bug_url')
if self.touched('intent_to_implement_url'):
feature.intent_to_implement_url = self.parse_link(
'intent_to_implement_url')
if self.touched('intent_to_ship_url'):
feature.intent_to_ship_url = self.parse_link(
'intent_to_ship_url')
if self.touched('ready_for_trial_url'):
feature.ready_for_trial_url = self.parse_link(
'ready_for_trial_url')
if self.touched('intent_to_experiment_url'):
feature.intent_to_experiment_url = self.parse_link(
'intent_to_experiment_url')
if self.touched('intent_to_extend_experiment_url'):
feature.intent_to_extend_experiment_url = self.parse_link(
'intent_to_extend_experiment_url')
if self.touched('origin_trial_feedback_url'):
feature.origin_trial_feedback_url = self.parse_link(
'origin_trial_feedback_url')
if self.touched('finch_url'):
feature.finch_url = self.parse_link('finch_url')
if self.touched('i2e_lgtms'):
feature.i2e_lgtms = self.split_emails('i2e_lgtms')
if self.touched('i2s_lgtms'):
feature.i2s_lgtms = self.split_emails('i2s_lgtms')
# Cast incoming milestones to ints.
# TODO(jrobbins): Consider supporting milestones that are not ints.
if self.touched('shipped_milestone'):
feature.shipped_milestone = self.parse_int('shipped_milestone')
if self.touched('shipped_android_milestone'):
feature.shipped_android_milestone = self.parse_int(
'shipped_android_milestone')
if self.touched('shipped_ios_milestone'):
feature.shipped_ios_milestone = self.parse_int('shipped_ios_milestone')
if self.touched('shipped_webview_milestone'):
feature.shipped_webview_milestone = self.parse_int(
'shipped_webview_milestone')
if self.touched('shipped_opera_milestone'):
feature.shipped_opera_milestone = (
self.parse_int('shipped_opera_milestone'))
if self.touched('shipped_opera_android'):
feature.shipped_opera_android_milestone = self.parse_int(
'shipped_opera_android_milestone')
if self.touched('ot_milestone_desktop_start'):
feature.ot_milestone_desktop_start = self.parse_int(
'ot_milestone_desktop_start')
if self.touched('ot_milestone_desktop_end'):
feature.ot_milestone_desktop_end = self.parse_int(
'ot_milestone_desktop_end')
if self.touched('ot_milestone_android_start'):
feature.ot_milestone_android_start = self.parse_int(
'ot_milestone_android_start')
if self.touched('ot_milestone_android_end'):
feature.ot_milestone_android_end = self.parse_int(
'ot_milestone_android_end')
if self.touched('requires_embedder_support'):
feature.requires_embedder_support = (
self.form.get('requires_embedder_support') == 'on')
if self.touched('devtrial_instructions'):
feature.devtrial_instructions = self.parse_link('devtrial_instructions')
if self.touched('dt_milestone_desktop_start'):
feature.dt_milestone_desktop_start = self.parse_int(
'dt_milestone_desktop_start')
if self.touched('dt_milestone_android_start'):
feature.dt_milestone_android_start = self.parse_int(
'dt_milestone_android_start')
if self.touched('dt_milestone_ios_start'):
feature.dt_milestone_ios_start = self.parse_int(
'dt_milestone_ios_start')
if self.touched('dt_milestone_webview_start'):
feature.dt_milestone_webview_start = self.parse_int(
'dt_milestone_webview_start')
if self.touched('flag_name'):
feature.flag_name = self.form.get('flag_name')
if self.touched('owner'):
feature.owner = self.split_emails('owner')
if self.touched('doc_links'):
feature.doc_links = self.split_input('doc_links')
if self.touched('measurement'):
feature.measurement = self.form.get('measurement')
if self.touched('sample_links'):
feature.sample_links = self.split_input('sample_links')
if self.touched('search_tags'):
feature.search_tags = self.split_input('search_tags', delim=',')
if self.touched('blink_components'):
feature.blink_components = (
self.split_input('blink_components', delim=',') or
[models.BlinkComponent.DEFAULT_COMPONENT])
if self.touched('devrel'):
feature.devrel = self.split_emails('devrel')
if self.touched('feature_type'):
feature.feature_type = int(self.form.get('feature_type'))
# intent_stage can be be set either by <select> or a checkbox
if self.touched('intent_stage'):
feature.intent_stage = int(self.form.get('intent_stage'))
elif self.form.get('set_stage') == 'on':
feature.intent_stage = stage_id
if self.touched('category'):
feature.category = int(self.form.get('category'))
if self.touched('name'):
feature.name = self.form.get('name')
if self.touched('summary'):
feature.summary = self.form.get('summary')
if self.touched('motivation'):
feature.motivation = self.form.get('motivation')
# impl_status_chrome can be be set either by <select> or a checkbox
if self.touched('impl_status_chrome'):
feature.impl_status_chrome = int(self.form.get('impl_status_chrome'))
elif self.form.get('set_impl_status') == 'on':
feature.impl_status_chrome = self.parse_int('impl_status_offered')
if self.touched('interop_compat_risks'):
feature.interop_compat_risks = self.form.get('interop_compat_risks')
if self.touched('ergonomics_risks'):
feature.ergonomics_risks = self.form.get('ergonomics_risks')
if self.touched('activation_risks'):
feature.activation_risks = self.form.get('activation_risks')
if self.touched('security_risks'):
feature.security_risks = self.form.get('security_risks')
if self.touched('debuggability'):
feature.debuggability = self.form.get('debuggability')
if self.touched('all_platforms'):
feature.all_platforms = self.form.get('all_platforms') == 'on'
if self.touched('all_platforms_descr'):
feature.all_platforms_descr = self.form.get('all_platforms_descr')
if self.touched('wpt'):
feature.wpt = self.form.get('wpt') == 'on'
if self.touched('wpt_descr'):
feature.wpt_descr = self.form.get('wpt_descr')
if self.touched('ff_views'):
feature.ff_views = int(self.form.get('ff_views'))
if self.touched('ff_views_link'):
feature.ff_views_link = self.parse_link('ff_views_link')
if self.touched('ff_views_notes'):
feature.ff_views_notes = self.form.get('ff_views_notes')
# TODO(jrobbins): Delete after the next deployment
if self.touched('ie_views'):
feature.ie_views = int(self.form.get('ie_views'))
if self.touched('ie_views_link'):
feature.ie_views_link = self.parse_link('ie_views_link')
if self.touched('ie_views_notes'):
feature.ie_views_notes = self.form.get('ie_views_notes')
if self.touched('safari_views'):
feature.safari_views = int(self.form.get('safari_views'))
if self.touched('safari_views_link'):
feature.safari_views_link = self.parse_link('safari_views_link')
if self.touched('safari_views_notes'):
feature.safari_views_notes = self.form.get('safari_views_notes')
if self.touched('web_dev_views'):
feature.web_dev_views = int(self.form.get('web_dev_views'))
if self.touched('web_dev_views'):
feature.web_dev_views_link = self.parse_link('web_dev_views_link')
if self.touched('web_dev_views_notes'):
feature.web_dev_views_notes = self.form.get('web_dev_views_notes')
if self.touched('other_views_notes'):
feature.other_views_notes = self.form.get('other_views_notes')
if self.touched('prefixed'):
feature.prefixed = self.form.get('prefixed') == 'on'
if self.touched('tag_review'):
feature.tag_review = self.form.get('tag_review')
if self.touched('tag_review_status'):
feature.tag_review_status = self.parse_int('tag_review_status')
if self.touched('standardization'):
feature.standardization = int(self.form.get('standardization'))
if self.touched('unlisted'):
feature.unlisted = self.form.get('unlisted') == 'on'
if self.touched('comments'):
feature.comments = self.form.get('comments')
if self.touched('experiment_goals'):
feature.experiment_goals = self.form.get('experiment_goals')
if self.touched('experiment_timeline'):
feature.experiment_timeline = self.form.get('experiment_timeline')
if self.touched('experiment_risks'):
feature.experiment_risks = self.form.get('experiment_risks')
if self.touched('experiment_extension_reason'):
feature.experiment_extension_reason = self.form.get(
'experiment_extension_reason')
if self.touched('ongoing_constraints'):
feature.ongoing_constraints = self.form.get('ongoing_constraints')
feature.updated_by = ndb.User(
email=self.get_current_user().email(),
_auth_domain='gmail.com')
key = feature.put()
# TODO(jrobbins): enumerate and remove only the relevant keys.
ramcache.flush_all()
redirect_url = '/guide/edit/' + str(key.integer_id())
return self.redirect(redirect_url)
class FeatureEditAllFields(FeatureEditStage):
"""Flat form page that lists all fields in seprate sections."""
TEMPLATE_PATH = 'guide/editall.html'
@permissions.require_edit_feature
def get_template_data(self, feature_id):
f, feature_process = self.get_feature_and_process(feature_id)
feature_edit_dict = f.format_for_edit()
# TODO(jrobbins): make flat forms process specific?
flat_form_section_list = FLAT_FORMS
flat_forms = [
(section_name, form_class(feature_edit_dict))
for section_name, form_class in flat_form_section_list]
template_data = {
'feature': f,
'feature_id': f.key.integer_id(),
'flat_forms': flat_forms,
}
return template_data