Delete code for the legacy feature editing UI. (#1231)

This commit is contained in:
Jason Robbins 2021-03-23 09:28:46 -07:00 коммит произвёл GitHub
Родитель 3ccb4624c1
Коммит 288467837d
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
15 изменённых файлов: 264 добавлений и 1540 удалений

351
admin.py
Просмотреть файл

@ -340,354 +340,6 @@ class IntentEmailPreviewHandler(basehandlers.FlaskHandler):
return 'Intent stage "%s"' % models.INTENT_STAGES[intent_stage]
class BaseFeatureHandler(basehandlers.FlaskHandler):
"""Shared form processing for new and edit forms."""
ADD_NEW_URL = '/admin/features/new'
EDIT_URL = '/admin/features/edit'
LAUNCH_URL = '/admin/features/launch'
def process_post_data(self, feature_id=None):
user = users.get_current_user()
if user is None or (user and not self.user_can_edit(user)):
self.abort(403)
spec_link = self.parse_link('spec_link')
api_spec = self.request.form.get('api_spec') == 'on'
explainer_links = self.request.form.get('explainer_links') or []
if explainer_links:
explainer_links = filter(bool, [x.strip() for x in re.split('\\r?\\n', explainer_links)])
bug_url = self.parse_link('bug_url')
launch_bug_url = self.parse_link('launch_bug_url')
initial_public_proposal_url = self.parse_link(
'initial_public_proposal_url')
intent_to_implement_url = self.parse_link('intent_to_implement_url')
intent_to_ship_url = self.parse_link('intent_to_ship_url')
ready_for_trial_url = self.parse_link('ready_for_trial_url')
intent_to_experiment_url = self.parse_link('intent_to_experiment_url')
origin_trial_feedback_url = self.parse_link('origin_trial_feedback_url')
i2e_lgtms = self.request.form.get('i2e_lgtms') or []
if i2e_lgtms:
i2e_lgtms = [db.Email(x.strip()) for x in i2e_lgtms.split(',')]
i2s_lgtms = self.request.form.get('i2s_lgtms') or []
if i2s_lgtms:
i2s_lgtms = [db.Email(x.strip()) for x in i2s_lgtms.split(',')]
ff_views_link = self.parse_link('ff_views_link')
ie_views_link = self.parse_link('ie_views_link')
safari_views_link = self.parse_link('safari_views_link')
web_dev_views_link = self.parse_link('web_dev_views_link')
# Cast incoming milestones to ints.
shipped_milestone = self.parse_int('shipped_milestone')
shipped_android_milestone = self.parse_int('shipped_android_milestone')
shipped_ios_milestone = self.parse_int('shipped_ios_milestone')
shipped_webview_milestone = self.parse_int('shipped_webview_milestone')
shipped_opera_milestone = self.parse_int('shipped_opera_milestone')
shipped_opera_android_milestone = self.parse_int('shipped_opera_android_milestone')
owners = self.request.form.get('owner') or []
if owners:
owners = [db.Email(x.strip()) for x in owners.split(',')]
doc_links = self.request.form.get('doc_links') or []
if doc_links:
doc_links = filter(bool, [x.strip() for x in re.split('\\r?\\n', doc_links)])
sample_links = self.request.form.get('sample_links') or []
if sample_links:
sample_links = filter(bool, [x.strip() for x in re.split('\\r?\\n', sample_links)])
search_tags = self.request.form.get('search_tags') or []
if search_tags:
search_tags = filter(bool, [x.strip() for x in search_tags.split(',')])
blink_components = self.request.form.get('blink_components') or models.BlinkComponent.DEFAULT_COMPONENT
if blink_components:
blink_components = filter(bool, [x.strip() for x in blink_components.split(',')])
devrel = self.request.form.get('devrel') or []
if devrel:
devrel = [db.Email(x.strip()) for x in devrel.split(',')]
try:
intent_stage = int(self.request.form.get('intent_stage'))
except:
logging.error('Invalid intent_stage \'{}\'' \
.format(self.request.form.get('intent_stage')))
# Default the intent stage to 1 (Prototype) if we failed to get a valid
# intent stage from the request. This should be removed once we
# understand what causes this.
intent_stage = 1
if feature_id: # /admin/edit/1234
feature = models.Feature.get_by_id(feature_id)
if feature is None:
return self.redirect(self.request.path)
# Update properties of existing feature.
feature.category = int(self.request.form.get('category'))
feature.name = self.request.form.get('name')
feature.intent_stage = intent_stage
feature.summary = self.request.form.get('summary')
feature.unlisted = self.request.form.get('unlisted') == 'on'
feature.intent_to_implement_url = intent_to_implement_url
feature.intent_to_ship_url = intent_to_ship_url
feature.ready_for_trial_url = ready_for_trial_url
feature.intent_to_experiment_url = intent_to_experiment_url
feature.origin_trial_feedback_url = origin_trial_feedback_url
feature.i2e_lgtms = i2e_lgtms
feature.i2s_lgtms = i2s_lgtms
feature.motivation = self.request.form.get('motivation')
feature.explainer_links = explainer_links
feature.owner = owners
feature.bug_url = bug_url
feature.launch_bug_url = launch_bug_url
feature.initial_public_proposal_url = initial_public_proposal_url
feature.blink_components = blink_components
feature.devrel = devrel
feature.impl_status_chrome = int(self.request.form.get('impl_status_chrome'))
feature.shipped_milestone = shipped_milestone
feature.shipped_android_milestone = shipped_android_milestone
feature.shipped_ios_milestone = shipped_ios_milestone
feature.shipped_webview_milestone = shipped_webview_milestone
feature.shipped_opera_milestone = shipped_opera_milestone
feature.shipped_opera_android_milestone = shipped_opera_android_milestone
feature.interop_compat_risks = self.request.form.get('interop_compat_risks')
feature.ergonomics_risks = self.request.form.get('ergonomics_risks')
feature.activation_risks = self.request.form.get('activation_risks')
feature.security_risks = self.request.form.get('security_risks')
feature.debuggability = self.request.form.get('debuggability')
feature.all_platforms = self.request.form.get('all_platforms') == 'on'
feature.all_platforms_descr = self.request.form.get('all_platforms_descr')
feature.wpt = self.request.form.get('wpt') == 'on'
feature.wpt_descr = self.request.form.get('wpt_descr')
feature.ff_views = int(self.request.form.get('ff_views'))
feature.ff_views_link = ff_views_link
feature.ff_views_notes = self.request.form.get('ff_views_notes')
feature.ie_views = int(self.request.form.get('ie_views'))
feature.ie_views_link = ie_views_link
feature.ie_views_notes = self.request.form.get('ie_views_notes')
feature.safari_views = int(self.request.form.get('safari_views'))
feature.safari_views_link = safari_views_link
feature.safari_views_notes = self.request.form.get('safari_views_notes')
feature.web_dev_views = int(self.request.form.get('web_dev_views'))
feature.web_dev_views_link = web_dev_views_link
feature.web_dev_views_notes = self.request.form.get('web_dev_views_notes')
feature.prefixed = self.request.form.get('prefixed') == 'on'
feature.spec_link = spec_link
feature.api_spec = api_spec
feature.security_review_status = int(self.request.form.get(
'security_review_status', models.REVIEW_PENDING))
feature.privacy_review_status = int(self.request.form.get(
'privacy_review_status', models.REVIEW_PENDING))
feature.tag_review = self.request.form.get('tag_review')
feature.tag_review_status = int(self.request.form.get(
'tag_review_status', models.REVIEW_PENDING))
feature.standardization = int(self.request.form.get('standardization'))
feature.doc_links = doc_links
feature.measurement = self.request.form.get('measurement')
feature.sample_links = sample_links
feature.search_tags = search_tags
feature.comments = self.request.form.get('comments')
feature.experiment_goals = self.request.form.get('experiment_goals')
feature.experiment_timeline = self.request.form.get('experiment_timeline')
feature.experiment_risks = self.request.form.get('experiment_risks')
feature.experiment_extension_reason = self.request.form.get('experiment_extension_reason')
feature.ongoing_constraints = self.request.form.get('ongoing_constraints')
else:
# Check bug for existing blink component(s) used to label the bug. If
# found, use the first component name instead of the generic "Blink" name.
try:
blink_components = self.__get_blink_component_from_bug(blink_components, bug_url)
except Exception:
pass
feature = models.Feature(
category=int(self.request.form.get('category')),
name=self.request.form.get('name'),
intent_stage=intent_stage,
summary=self.request.form.get('summary'),
intent_to_implement_url=intent_to_implement_url,
intent_to_ship_url=intent_to_ship_url,
ready_for_trial_url=ready_for_trial_url,
intent_to_experiment_url=intent_to_experiment_url,
origin_trial_feedback_url=origin_trial_feedback_url,
i2e_lgtms=i2e_lgtms,
i2s_lgtms=i2s_lgtms,
motivation=self.request.form.get('motivation'),
explainer_links=explainer_links,
owner=owners,
bug_url=bug_url,
launch_bug_url=launch_bug_url,
initial_public_proposal_url=initial_public_proposal_url,
blink_components=blink_components,
devrel=devrel,
impl_status_chrome=int(self.request.form.get('impl_status_chrome')),
shipped_milestone=shipped_milestone,
shipped_android_milestone=shipped_android_milestone,
shipped_ios_milestone=shipped_ios_milestone,
shipped_webview_milestone=shipped_webview_milestone,
shipped_opera_milestone=shipped_opera_milestone,
shipped_opera_android_milestone=shipped_opera_android_milestone,
interop_compat_risks=self.request.form.get('interop_compat_risks'),
ergonomics_risks=self.request.form.get('ergonomics_risks'),
activation_risks=self.request.form.get('activation_risks'),
security_risks=self.request.form.get('security_risks'),
debuggability=self.request.form.get('debuggability'),
all_platforms=self.request.form.get('all_platforms') == 'on',
all_platforms_descr=self.request.form.get('all_platforms_descr'),
wpt=self.request.form.get('wpt') == 'on',
wpt_descr=self.request.form.get('wpt_descr'),
ff_views=int(self.request.form.get('ff_views')),
ff_views_link=ff_views_link,
ff_views_notes=self.request.form.get('ff_views_notes'),
ie_views=int(self.request.form.get('ie_views')),
ie_views_link=ie_views_link,
ie_views_notes=self.request.form.get('ie_views_notes'),
safari_views=int(self.request.form.get('safari_views')),
safari_views_link=safari_views_link,
safari_views_notes=self.request.form.get('safari_views_notes'),
web_dev_views=int(self.request.form.get('web_dev_views')),
web_dev_views_link=web_dev_views_link,
web_dev_views_notes=self.request.form.get('web_dev_views_notes'),
prefixed=self.request.form.get('prefixed') == 'on',
spec_link=spec_link,
api_spec=api_spec,
security_review_status=int(self.request.form.get(
'security_review_status', models.REVIEW_PENDING)),
privacy_review_status=int(self.request.form.get(
'privacy_review_status', models.REVIEW_PENDING)),
tag_review=self.request.form.get('tag_review'),
tag_review_status=int(self.request.form.get(
'tag_review_status', models.REVIEW_PENDING)),
standardization=int(self.request.form.get('standardization')),
doc_links=doc_links,
measurement=self.request.form.get('measurement'),
sample_links=sample_links,
search_tags=search_tags,
comments=self.request.form.get('comments'),
experiment_goals=self.request.form.get('experiment_goals'),
experiment_timeline=self.request.form.get('experiment_timeline'),
experiment_risks=self.request.form.get('experiment_risks'),
experiment_extension_reason=self.request.form.get('experiment_extension_reason'),
ongoing_constraints=self.request.form.get('ongoing_constraints'),
)
if self.request.form.get('flag_name'):
feature.flag_name = self.request.form.get('flag_name')
if self.request.form.get('ot_milestone_desktop_start'):
feature.ot_milestone_desktop_start = int(self.request.form.get(
'ot_milestone_desktop_start'))
if self.request.form.get('ot_milestone_desktop_end'):
feature.ot_milestone_desktop_end = int(self.request.form.get(
'ot_milestone_desktop_end'))
if self.request.form.get('ot_milestone_android_start'):
feature.ot_milestone_android_start = int(self.request.form.get(
'ot_milestone_android_start'))
if self.request.form.get('ot_milestone_android_end'):
feature.ot_milestone_android_end = int(self.request.form.get(
'ot_milestone_android_end'))
params = []
if self.request.form.get('create_launch_bug') == 'on':
params.append(LAUNCH_PARAM)
if self.request.form.get('intent_to_implement') == 'on':
params.append(INTENT_PARAM)
feature.intent_template_use_count += 1
key = feature.put()
# TODO(ericbidelman): enumerate and remove only the relevant keys.
ramcache.flush_all()
redirect_url = '/feature/' + str(key.id())
if len(params):
redirect_url = '%s/%s?%s' % (self.LAUNCH_URL, key.id(),
'&'.join(params))
return self.redirect(redirect_url)
class NewFeatureHandler(BaseFeatureHandler):
TEMPLATE_PATH = 'admin/features/new.html'
def get_template_data(self):
user = users.get_current_user()
if user is None:
return self.redirect(users.create_login_url(self.request.path))
if not self.user_can_edit(user):
self.abort(403)
template_data = {
'feature_form': models.FeatureForm(),
}
return template_data
class EditFeatureHandler(BaseFeatureHandler):
TEMPLATE_PATH = 'admin/features/edit.html'
def __get_blink_component_from_bug(self, blink_components, bug_url):
if blink_components[0] == models.BlinkComponent.DEFAULT_COMPONENT and bug_url:
# Scraping the bug URL no longer works because monorail uses
# web components and APIs rather than details in an HTML page.
return []
return blink_components
def get_template_data(self, feature_id=None):
user = users.get_current_user()
if user is None:
# Redirect to public URL for unauthenticated users.
return self.redirect(VIEW_FEATURE_URL + '/' + str(feature_id))
if not self.user_can_edit(user):
self.abort(403)
f = models.Feature.get_by_id(feature_id)
if f is None:
return self.redirect(self.ADD_NEW_URL)
template_data = {
'feature': f.format_for_template(),
'feature_form': models.FeatureForm(f.format_for_edit()),
'default_url': '%s://%s%s/%s' % (self.request.scheme, self.request.host,
VIEW_FEATURE_URL, feature_id),
'edit_url': '%s://%s%s/%s' % (self.request.scheme, self.request.host,
self.EDIT_URL, feature_id),
}
return template_data
# TODO(jrobbins): implement undelete UI. For now, use cloud console.
class DeleteFeatureHandler(basehandlers.FlaskHandler):
def process_post_data(self, feature_id):
user = users.get_current_user()
if user is None:
# Redirect to public URL for unauthenticated users.
return self.redirect(VIEW_FEATURE_URL + '/' + str(feature_id))
if not self.user_can_edit(user):
self.abort(403)
feature = models.Feature.get_by_id(feature_id)
feature.deleted = True
feature.put()
ramcache.flush_all()
return 'Success'
class BlinkComponentHandler(basehandlers.FlaskHandler):
"""Updates the list of Blink components in the db."""
def get_template_data(self):
@ -702,7 +354,4 @@ app = basehandlers.FlaskApplication([
('/admin/features/launch/<int:feature_id>', IntentEmailPreviewHandler),
('/admin/features/launch/<int:feature_id>/<int:stage_id>',
IntentEmailPreviewHandler),
('/admin/features/new', NewFeatureHandler),
('/admin/features/edit/<int:feature_id>', EditFeatureHandler),
('/admin/features/delete/<int:feature_id>', DeleteFeatureHandler),
], debug=settings.DEBUG)

53
api/features_api.py Normal file
Просмотреть файл

@ -0,0 +1,53 @@
# -*- coding: utf-8 -*-
# Copyright 2021 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.
from __future__ import division
from __future__ import print_function
import logging
from framework import basehandlers
from framework import permissions
from framework import ramcache
import models
class FeaturesAPI(basehandlers.APIHandler):
"""Features are the the main records that we track."""
# TODO(jrobbins): do_get
# TODO(jrobbins): do_post
# TODO(jrobbins): do_patch
@permissions.require_admin_site
def do_delete(self, feature_id):
"""Delete the specified feature."""
# TODO(jrobbins): implement undelete UI. For now, use cloud console.
if not feature_id:
logging.info('feature_id not specified')
self.abort(400)
feature = models.Feature.get_by_id(feature_id)
if not feature:
logging.info('feature %r not found' % feature_id)
self.abort(404)
feature.deleted = True
feature.put()
ramcache.flush_all()
# Callers don't use the JSON response for this API call.
return {'message': 'Done'}

88
api/features_api_test.py Normal file
Просмотреть файл

@ -0,0 +1,88 @@
# 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.
from __future__ import division
from __future__ import print_function
import unittest
import testing_config # Must be imported before the module under test.
import flask
import mock
import werkzeug.exceptions # Flask HTTP stuff.
from api import features_api
from api import register
import models
class FeaturesAPITest(unittest.TestCase):
def setUp(self):
self.feature_1 = models.Feature(
name='feature one', summary='sum', category=1, visibility=1,
standardization=1, web_dev_views=1, impl_status_chrome=1,
intent_stage=models.INTENT_IMPLEMENT)
self.feature_1.put()
self.feature_id = self.feature_1.key().id()
self.request_path = '/api/v0/features/%d' % self.feature_id
self.handler = features_api.FeaturesAPI()
def tearDown(self):
self.feature_1.delete()
def test_delete__valid(self):
"""Admin wants to soft-delete a feature."""
testing_config.sign_in('admin@example.com', 123567890, is_admin=True)
with register.app.test_request_context(self.request_path):
actual_json = self.handler.do_delete(self.feature_id)
self.assertEqual({'message': 'Done'}, actual_json)
revised_feature = models.Feature.get_by_id(self.feature_id)
self.assertTrue(revised_feature.deleted)
def test_delete__forbidden(self):
"""Regular user cannot soft-delete a feature."""
testing_config.sign_in('one@example.com', 123567890)
with register.app.test_request_context(self.request_path):
with self.assertRaises(werkzeug.exceptions.Forbidden):
self.handler.do_delete(self.feature_id)
revised_feature = models.Feature.get_by_id(self.feature_id)
self.assertFalse(revised_feature.deleted)
def test_delete__invalid(self):
"""We cannot soft-delete a feature without a feature_id."""
testing_config.sign_in('admin@example.com', 123567890, is_admin=True)
with register.app.test_request_context(self.request_path):
with self.assertRaises(werkzeug.exceptions.BadRequest):
self.handler.do_delete(None)
revised_feature = models.Feature.get_by_id(self.feature_id)
self.assertFalse(revised_feature.deleted)
def test_delete__not_found(self):
"""We cannot soft-delete a feature with the wrong feature_id."""
testing_config.sign_in('admin@example.com', 123567890, is_admin=True)
with register.app.test_request_context(self.request_path):
with self.assertRaises(werkzeug.exceptions.NotFound):
self.handler.do_delete(self.feature_id + 1)
revised_feature = models.Feature.get_by_id(self.feature_id)
self.assertFalse(revised_feature.deleted)

Просмотреть файл

@ -18,6 +18,7 @@ from __future__ import print_function
import settings
from api import cues_api
from api import features_api
from api import stars_api
from framework import basehandlers
@ -26,8 +27,8 @@ from framework import basehandlers
API_BASE = '/api/v0'
app = basehandlers.FlaskApplication([
# ('/features', TODO),
# ('/features/<int:feature_id>', TODO),
('/features', features_api.FeaturesAPI),
('/features/<int:feature_id>', features_api.FeaturesAPI),
# ('/features/<int:feature_id>/approvals/<int:field_id>', TODO),
# ('/features/<int:feature_id>/approvals/<int:field_id>/comments', TODO),

Просмотреть файл

@ -22,6 +22,22 @@ import flask
from google.appengine.api import users
def can_admin_site():
"""Return true if the user is a site admin."""
return users.is_current_user_admin()
def require_admin_site(handler):
"""Handler decorator to require the user have edit permission."""
def check_login(self, *args, **kwargs):
if not can_admin_site():
flask.abort(403)
return
return handler(self, *args, **kwargs) # Call the handler method
return check_login
def require_edit_permission(handler):
"""Handler decorator to require the user have edit permission."""
def check_login(self, *args, **kwargs):

Просмотреть файл

@ -0,0 +1,78 @@
# 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.
from __future__ import division
from __future__ import print_function
import unittest
import testing_config # Must be imported before the module under test.
import mock
import werkzeug.exceptions # Flask HTTP stuff.
from framework import permissions
class MockHandler(object):
def __init__(self):
self.called_with = None
@permissions.require_admin_site
def do_get(self, *args):
self.called_with = args
class CanAdminSiteTests(unittest.TestCase):
def test_can_admin_site__normal_user(self):
"""A normal user is not allowed to administer the site."""
testing_config.sign_in('user@example.com', 123)
self.assertFalse(permissions.can_admin_site())
def test_can_admin_site__admin_user(self):
"""An admin user is allowed to administer the site."""
testing_config.sign_in('user@example.com', 123, is_admin=True)
self.assertTrue(permissions.can_admin_site())
def test_can_admin_site__anon(self):
"""An anon visitor is not allowed to administer the site."""
testing_config.sign_out()
self.assertFalse(permissions.can_admin_site())
class RequireAdminSiteTests(unittest.TestCase):
def test_require_admin_site__normal_user(self):
"""Wrapped method rejects call from normal user."""
handler = MockHandler()
testing_config.sign_in('user@example.com', 123)
with self.assertRaises(werkzeug.exceptions.Forbidden):
handler.do_get()
self.assertEqual(handler.called_with, None)
def test_require_admin_site__normal_user(self):
"""Wrapped method rejects call from normal user."""
handler = MockHandler()
testing_config.sign_in('admin@example.com', 123, is_admin=True)
handler.do_get()
self.assertEqual(handler.called_with, tuple())
def test_require_admin_site__anon(self):
"""Wrapped method rejects call from anon."""
handler = MockHandler()
testing_config.sign_out()
with self.assertRaises(werkzeug.exceptions.Forbidden):
handler.do_get()
self.assertEqual(handler.called_with, None)

445
models.py
Просмотреть файл

@ -1078,451 +1078,6 @@ class Feature(DictModel):
star_count = db.IntegerProperty(default=0)
class PlaceholderCharField(forms.CharField):
def __init__(self, *args, **kwargs):
#super(forms.CharField, self).__init__(*args, **kwargs)
attrs = {}
if kwargs.get('placeholder'):
attrs['placeholder'] = kwargs.get('placeholder')
del kwargs['placeholder']
label = kwargs.get('label') or ''
if label:
del kwargs['label']
self.max_length = kwargs.get('max_length') or None
super(forms.CharField, self).__init__(label=label,
widget=forms.TextInput(attrs=attrs), *args, **kwargs)
# class PlaceholderForm(forms.Form):
# def __init__(self, *args, **kwargs):
# super(PlaceholderForm, self).__init__(*args, **kwargs)
# for field_name in self.fields:
# field = self.fields.get(field_name)
# if field:
# if type(field.widget) in (forms.TextInput, forms.DateInput):
# field.widget = forms.TextInput(attrs={'placeholder': field.label})
class FeatureForm(forms.Form):
SHIPPED_HELP_TXT = ('First milestone to ship with this '
'status. Applies to: Enabled by default, In developer trial, '
'Browser Intervention, and Deprecated. If '
'the flag is \'test\' rather than \'experimental\' set '
'status to In development.')
SHIPPED_WEBVIEW_HELP_TXT = ('First milestone to ship with this status. '
'Applies to Enabled by default, Browser '
'Intervention, and Deprecated.\n\n NOTE: for '
'statuses In developer trial and Origin trial this '
'MUST be blank.')
SUMMARY_PLACEHOLDER_TXT = (
'NOTE: This text describes this feature in the eventual beta release post '
'as well as possibly in other external documents.\n\n'
'Begin with one line explaining what the feature does. Add one or two '
'lines explaining how this feature helps developers. Avoid language such '
'as "a new feature". They all are or have been new features.\n\n'
'Follow the example link below for more guidance.')
# Note that the "required" argument in the following field definitions only
# mean so much in practice. There's various code in js/admin/feature_form.js,
# including intentStageChanged(), that adjusts what fields are required (as
# well as visible at all). IOW, when making changes to what form fields are or
# are not required, look both in the definitions here as well as in
# js/admin/feature_form.js and make sure the code works as intended.
#name = PlaceholderCharField(required=True, placeholder='Feature name')
name = forms.CharField(
required=True, label='Feature name',
# Use a specific autocomplete value to avoid "name" autofill.
# https://bugs.chromium.org/p/chromium/issues/detail?id=468153#c164
widget=forms.TextInput(attrs={'autocomplete': 'feature-name'}),
help_text='Capitalize only the first letter and the beginnings of proper nouns.')
summary = forms.CharField(label='Summary', required=True,
widget=forms.Textarea(
attrs={'cols': 50, 'maxlength': 500, 'placeholder': SUMMARY_PLACEHOLDER_TXT }),
help_text='<a target="_blank" href="'
'https://github.com/GoogleChrome/chromium-dashboard/wiki/'
'EditingHelp#summary-example">Guidelines and example</a>.')
unlisted = forms.BooleanField(
required=False, initial=False,
help_text=('Check this box for draft features that should not appear '
'on the public feature list. Anyone with the link will be able to '
'view the feature on the detail page.'))
owner = forms.EmailField(
required=True, label='Contact emails',
widget=forms.EmailInput(
attrs={'multiple': True, 'placeholder': 'email, email'}),
help_text='Comma separated list of full email addresses. Prefer @chromium.org.')
blink_components = forms.ChoiceField(
required=False,
label='Blink component',
help_text='Select the most specific component. If unsure, leave as "%s".' % BlinkComponent.DEFAULT_COMPONENT,
choices=[(x, x) for x in BlinkComponent.fetch_all_components()],
initial=[BlinkComponent.DEFAULT_COMPONENT])
category = forms.ChoiceField(
required=False,
help_text='Select the most specific category. If unsure, leave as "%s".' % FEATURE_CATEGORIES[MISC],
initial=MISC,
choices=sorted(FEATURE_CATEGORIES.items(), key=lambda x: x[1]))
intent_stage = forms.ChoiceField(
required=False,
label='Intent stage', help_text='Select the appropriate intent stage.',
initial=INTENT_IMPLEMENT,
choices=INTENT_STAGES.items())
motivation = forms.CharField(label='Motivation', required=True,
widget=forms.Textarea(attrs={'cols': 50, 'maxlength': 1480}),
help_text='Explain why the web needs this change. It may be useful to describe what web developers are forced to do without it. When possible, include links to back up your claims in the explainer.')
explainer_links = forms.CharField(label='Explainer link(s)', required=False,
widget=forms.Textarea(
attrs={'rows': 4, 'cols': 50, 'maxlength': 500,
'placeholder': 'https://\nhttps://'}),
help_text='Link to explainer(s) (one URL per line). You should have at least an explainer in hand and have shared it on a public forum before sending an Intent to Prototype in order to enable discussion with other browser vendors, standards bodies, or other interested parties.')
intent_to_implement_url = forms.URLField(
required=False, label='Intent to Prototype link',
widget=forms.URLInput(attrs={'placeholder': 'https://'}),
help_text=('After you have started the "Intent to Prototype" discussion '
'thread, link to it here.'))
intent_to_ship_url = forms.URLField(
required=False, label='Intent to Ship link',
widget=forms.URLInput(attrs={'placeholder': 'https://'}),
help_text=('After you have started the "Intent to Ship" discussion '
'thread, link to it here.'))
ready_for_trial_url = forms.URLField(
required=False, label='Ready for Trial link',
widget=forms.URLInput(attrs={'placeholder': 'https://'}),
help_text=('After you have started the "Ready for Trial" discussion '
'thread, link to it here.'))
intent_to_experiment_url = forms.URLField(
required=False, label='Intent to Experiment link',
widget=forms.URLInput(attrs={'placeholder': 'https://'}),
help_text=('After you have started the "Intent to Experiment" discussion '
'thread, link to it here.'))
origin_trial_feedback_url = forms.URLField(
required=False, label='Origin trial feedback summary',
widget=forms.URLInput(attrs={'placeholder': 'https://'}),
help_text='If your feature was available as an origin trial, link to a summary of usage and developer feedback. If not, leave this empty.')
i2e_lgtms = forms.EmailField(
required=False, label='Intent to Experiment LGTM by',
widget=forms.EmailInput(
attrs={'multiple': True, 'placeholder': 'email'}),
help_text=('Full email address of API owner who LGTM\'d the '
'Intent to Experiment email thread.'))
i2s_lgtms = forms.EmailField(
required=False, label='Intent to Ship LGTMs by',
widget=forms.EmailInput(
attrs={'multiple': True, 'placeholder': 'email, email, email'}),
help_text=('Comma separated list of '
'full email addresses of API owners who LGTM\'d '
'the Intent to Ship email thread.'))
doc_links = forms.CharField(label='Doc link(s)', required=False,
widget=forms.Textarea(
attrs={'rows': 4, 'cols': 50, 'maxlength': 500,
'placeholder': 'https://\nhttps://'}),
help_text='Links to design doc(s) (one URL per line), if and when available. [This is not required to send out an Intent to Prototype. Please update the intent thread with the design doc when ready]. An explainer and/or design doc is sufficient to start this process. [Note: Please include links and data, where possible, to support any claims.]')
measurement = forms.CharField(label='Measurement', required=False,
widget=forms.Textarea(
attrs={'rows': 4, 'cols': 50, 'maxlength': 500}),
help_text=(
'It\'s important to measure the adoption and success of web-exposed '
'features. Note here what measurements you have added to track the '
'success of this feature, such as a link to the UseCounter(s) you '
'have set up.'))
standardization = forms.ChoiceField(
required=False,
label='Standardization', choices=STANDARDIZATION.items(),
initial=EDITORS_DRAFT,
help_text=("The standardization status of the API. In bodies that don't "
"use this nomenclature, use the closest equivalent."))
spec_link = forms.URLField(
required=False, label='Spec link',
widget=forms.URLInput(attrs={'placeholder': 'https://'}),
help_text="Link to spec, if and when available. Please update the chromestatus.com entry and the intent thread(s) with the spec link when available.")
api_spec = forms.BooleanField(
required=False, initial=False, label='API spec',
help_text=('The spec document has details in a specification language '
'such as Web IDL, or there is an existing MDN page.'))
security_review_status = forms.ChoiceField(
required=False,
choices=REVIEW_STATUS_CHOICES.items(),
initial=REVIEW_PENDING,
help_text=('Status of the security review.'))
privacy_review_status = forms.ChoiceField(
required=False,
choices=REVIEW_STATUS_CHOICES.items(),
initial=REVIEW_PENDING,
help_text=('Status of the privacy review.'))
tag_review = forms.CharField(label='TAG Review', required=True,
widget=forms.Textarea(attrs={'rows': 2, 'cols': 50, 'maxlength': 1480}),
help_text='Link(s) to TAG review(s), or explanation why this is not needed.')
tag_review_status = forms.ChoiceField(
required=False,
choices=REVIEW_STATUS_CHOICES.items(),
initial=REVIEW_PENDING,
help_text=('Status of the tag review.'))
interop_compat_risks = forms.CharField(label='Interoperability and Compatibility Risks', required=True,
widget=forms.Textarea(attrs={'cols': 50, 'maxlength': 1480}),
help_text='Describe the degree of <a target="_blank" href="https://www.chromium.org/blink/guidelines/web-platform-changes-guidelines#TOC-Finding-balance">interoperability risk</a>. For a new feature, the main risk is that it fails to become an interoperable part of the web platform if other browsers do not implement it. For a removal, please review our <a target="_blank" href="https://docs.google.com/document/d/1RC-pBBvsazYfCNNUSkPqAVpSpNJ96U8trhNkfV0v9fk/edit">principles of web compatibility</a>.<br><br>Please include citation links below where possible. Examples include resolutions from relevant standards bodies (e.g. W3C working group), tracking bugs, or links to online conversations.')
safari_views = forms.ChoiceField(
required=False, label='Safari views',
choices=VENDOR_VIEWS_WEBKIT.items(),
initial=NO_PUBLIC_SIGNALS,
help_text=(
'See <a target="_blank" href="https://bit.ly/blink-signals">'
'https://bit.ly/blink-signals</a>'))
safari_views_link = forms.URLField(
required=False, label='',
widget=forms.URLInput(attrs={'placeholder': 'https://'}),
help_text='Citation link.')
safari_views_notes = forms.CharField(required=False, label='',
widget=forms.Textarea(attrs={'rows': 2, 'cols': 50, 'placeholder': 'Notes', 'maxlength': 1480}))
ff_views = forms.ChoiceField(
required=False, label='Firefox views',
choices=VENDOR_VIEWS_GECKO.items(),
initial=NO_PUBLIC_SIGNALS,
help_text=(
'See <a target="_blank" href="https://bit.ly/blink-signals">'
'https://bit.ly/blink-signals</a>'))
ff_views_link = forms.URLField(
required=False, label='',
widget=forms.URLInput(attrs={'placeholder': 'https://'}),
help_text='Citation link.')
ff_views_notes = forms.CharField(required=False, label='',
widget=forms.Textarea(attrs={'rows': 2, 'cols': 50, 'placeholder': 'Notes', 'maxlength': 1480}))
ie_views = forms.ChoiceField(
required=False, label='Edge views',
choices=VENDOR_VIEWS_EDGE.items(),
initial=NO_PUBLIC_SIGNALS)
ie_views_link = forms.URLField(
required=False, label='',
widget=forms.URLInput(attrs={'placeholder': 'https://'}),
help_text='Citation link.')
ie_views_notes = forms.CharField(required=False, label='',
widget=forms.Textarea(attrs={'rows': 2, 'cols': 50, 'placeholder': 'Notes', 'maxlength': 1480}))
web_dev_views = forms.ChoiceField(
required=False, label='Web / Framework developer views',
choices=WEB_DEV_VIEWS.items(),
initial=DEV_NO_SIGNALS,
help_text=(
'If unsure, default to "No signals". '
'See <a target="_blank" href="https://goo.gle/developer-signals">'
'https://goo.gle/developer-signals</a>'))
web_dev_views_link = forms.URLField(
required=False, label='',
widget=forms.URLInput(attrs={'placeholder': 'https://'}),
help_text='Citation link.')
web_dev_views_notes = forms.CharField(required=False, label='',
widget=forms.Textarea(attrs={'rows': 2, 'cols': 50, 'placeholder': 'Notes', 'maxlength': 1480}),
help_text='Reference known representative examples of opinion, both positive and negative.')
ergonomics_risks = forms.CharField(label='Ergonomics Risks', required=False,
widget=forms.Textarea(attrs={'cols': 50, 'maxlength': 1480}),
help_text='Are there any other platform APIs this feature will frequently be used in tandem with? Could the default usage of this API make it hard for Chrome to maintain good performance (i.e. synchronous return, must run on a certain thread, guaranteed return timing)?')
activation_risks = forms.CharField(label='Activation Risks', required=False,
widget=forms.Textarea(attrs={'cols': 50, 'maxlength': 1480}),
help_text='Will it be challenging for developers to take advantage of this feature immediately, as-is? Would this feature benefit from having polyfills, significant documentation and outreach, and/or libraries built on top of it to make it easier to use?')
security_risks = forms.CharField(label='Security Risks', required=False,
widget=forms.Textarea(attrs={'cols': 50, 'maxlength': 1480}),
help_text='List any security considerations that were taken into account when deigning this feature.')
experiment_goals = forms.CharField(label='Experiment Goals', required=False,
widget=forms.Textarea(attrs={'cols': 50, 'maxlength': 1480}),
help_text='Which pieces of the API surface are you looking to gain insight on? What metrics/measurement/feedback will you be using to validate designs? Double check that your experiment makes sense given that a large developer (e.g. a Google product or Facebook) likely can\'t use it in production due to the limits enforced by origin trials.\n\nIf Intent to Extend Origin Trial, highlight new/different areas for experimentation. Should not be an exact copy of goals from the first Intent to Experiment.')
# TODO(jrobbins): Phase out this field.
experiment_timeline = forms.CharField(
label='Experiment Timeline', required=False,
widget=forms.Textarea(attrs={
'rows': 2, 'cols': 50, 'maxlength': 1480,
'placeholder': 'This field is deprecated',
'disabled': 'disabled'}),
help_text=('When does the experiment start and expire? '
'Deprecated: '
'Please use the following numeric fields instead.'))
# TODO(jrobbins and jmedley): Refine help text.
ot_milestone_desktop_start = forms.IntegerField(
required=False, label='OT desktop start',
widget=forms.NumberInput(attrs={'placeholder': 'Milestone #'}),
help_text=('First desktop milestone that will support an origin '
'trial of this feature.'))
ot_milestone_desktop_end = forms.IntegerField(
required=False, label='OT milestone end',
widget=forms.NumberInput(attrs={'placeholder': 'Milestone #'}),
help_text=('Last desktop milestone that will support an origin '
'trial of this feature.'))
ot_milestone_android_start = forms.IntegerField(
required=False, label='OT android start',
widget=forms.NumberInput(attrs={'placeholder': 'Milestone #'}),
help_text=('First android milestone that will support an origin '
'trial of this feature.'))
ot_milestone_android_end = forms.IntegerField(
required=False, label='OT android end',
widget=forms.NumberInput(attrs={'placeholder': 'Milestone #'}),
help_text=('Last android milestone that will support an origin '
'trial of this feature.'))
experiment_risks = forms.CharField(label='Experiment Risks', required=False,
widget=forms.Textarea(attrs={'cols': 50, 'maxlength': 1480}),
help_text='When this experiment comes to an end are there any risks to the sites that were using it, for example losing access to important storage due to an experimental storage API?')
experiment_extension_reason = forms.CharField(label='Experiment Extension Reason', required=False,
widget=forms.Textarea(attrs={'cols': 50, 'maxlength': 1480}),
help_text='If this is a repeat experiment, link to the previous Intent to Experiment thread and explain why you want to extend this experiment.')
ongoing_constraints = forms.CharField(label='Ongoing Constraints', required=False,
widget=forms.Textarea(attrs={'cols': 50, 'maxlength': 1480}),
help_text='Do you anticipate adding any ongoing technical constraints to the codebase while implementing this feature? We prefer to avoid features which require or assume a specific architecture. For most features, the answer here is "None."')
debuggability = forms.CharField(label='Debuggability', required=False,
widget=forms.Textarea(attrs={'cols': 50, 'maxlength': 1480}),
help_text='Description of the desired DevTools debugging support for your feature. Consider emailing the <a href="https://groups.google.com/forum/?fromgroups#!forum/google-chrome-developer-tools">google-chrome-developer-tools</a> list for additional help. For new language features in V8 specifically, refer to the debugger support checklist. If your feature doesn\'t require changes to DevTools in order to provide a good debugging experience, feel free to leave this section empty.')
all_platforms = forms.BooleanField(required=False, initial=False, label='Supported on all platforms?',
help_text='Will this feature be supported on all six Blink platforms (Windows, Mac, Linux, Chrome OS, Android, and Android WebView)?'
)
all_platforms_descr = forms.CharField(label='Platform Support Explanation', required=False,
widget=forms.Textarea(attrs={'rows': 2, 'cols': 50, 'maxlength': 2000}),
help_text='Explanation for why this feature is, or is not, supported on all platforms.')
wpt = forms.BooleanField(required=False, initial=False, label='Web Platform Tests', help_text='Is this feature fully tested in Web Platform Tests?')
wpt_descr = forms.CharField(label='Web Platform Tests Description', required=True,
widget=forms.Textarea(attrs={'cols': 50, 'maxlength': 1480}),
help_text='Please link to the <a href="https://wpt.fyi/results">results on wpt.fyi</a>. If any part of the feature is not tested by web-platform-tests, please include links to issues, e.g. a web-platform-tests issue with the "infra" label explaining why a certain thing cannot be tested (<a href="https://github.com/w3c/web-platform-tests/issues/3867">example</a>), a spec issue for some change that would make it possible to test. (<a href="https://github.com/whatwg/fullscreen/issues/70">example</a>), or a Chromium issue to upstream some existing tests (<a href="https://bugs.chromium.org/p/chromium/issues/detail?id=695486">example</a>).')
sample_links = forms.CharField(label='Samples links', required=False,
widget=forms.Textarea(
attrs={'cols': 50, 'maxlength': 500,
'placeholder': 'https://\nhttps://'}),
help_text='Links to samples (one URL per line).')
bug_url = forms.URLField(
required=False, label='Tracking bug URL',
widget=forms.URLInput(attrs={'placeholder': 'https://'}),
help_text='Tracking bug url (https://bugs.chromium.org/...). This bug should have "Type=Feature" set and be world readable.')
launch_bug_url = forms.URLField(
required=False, label='Launch bug URL',
widget=forms.URLInput(attrs={'placeholder': 'https://'}),
help_text=(
'Launch bug url (https://bugs.chromium.org/...) to track launch '
'approvals. '
'<a href="https://bugs.chromium.org/p/chromium/issues/entry?template=Chrome+Launch+Feature" '
'target="_blank" '
'>Create launch bug<a>'))
initial_public_proposal_url = forms.URLField(
required=False, label='Initial public proposal URL',
widget=forms.URLInput(attrs={'placeholder': 'https://'}),
help_text=(
'Link to the first public proposal to create this feature, e.g., '
'a WICG discourse post.'))
devrel = forms.EmailField(
required=False, label='Developer relations emails',
widget=forms.EmailInput(
attrs={'multiple': True, 'placeholder': 'email, email'}),
help_text='Comma separated list of full email addresses.')
impl_status_chrome = forms.ChoiceField(
required=False,
label='Implementation status', choices=IMPLEMENTATION_STATUS.items(),
help_text='Implementation status in Chromium')
#shipped_milestone = PlaceholderCharField(required=False,
# placeholder='First milestone the feature shipped with this status (either enabled by default or experimental)')
shipped_milestone = forms.IntegerField(
required=False, label='',
widget=forms.NumberInput(attrs={'placeholder': 'Milestone #'}),
help_text='Desktop:<br/>' + SHIPPED_HELP_TXT)
shipped_android_milestone = forms.IntegerField(
required=False, label='',
widget=forms.NumberInput(attrs={'placeholder': 'Milestone #'}),
help_text='Chrome for Android:</br/>' + SHIPPED_HELP_TXT)
shipped_ios_milestone = forms.IntegerField(
required=False, label='',
widget=forms.NumberInput(attrs={'placeholder': 'Milestone #'}),
help_text='Chrome for iOS (RARE):<br/>' + SHIPPED_HELP_TXT)
shipped_webview_milestone = forms.IntegerField(
required=False, label='',
widget=forms.NumberInput(attrs={'placeholder': 'Milestone #'}),
help_text='Android WebView:<br/>' + SHIPPED_WEBVIEW_HELP_TXT)
flag_name = forms.CharField(label='Flag name', required=False,
help_text='Name of the flag that enables this feature.')
prefixed = forms.BooleanField(required=False, initial=False, label='Prefixed?')
search_tags = forms.CharField(label='Search tags', required=False,
help_text='Comma separated keywords used only in search')
comments = forms.CharField(label='Comments', required=False,
widget=forms.Textarea(attrs={
'cols': 50, 'rows': 4, 'maxlength': 1480}),
help_text='Additional comments, caveats, info...')
class Meta:
model = Feature
#exclude = ('shipped_webview_milestone',)
def __init__(self, *args, **keyargs):
super(FeatureForm, self).__init__(*args, **keyargs)
meta = getattr(self, 'Meta', None)
exclude = getattr(meta, 'exclude', [])
for field_name in exclude:
if field_name in self.fields:
del self.fields[field_name]
for field, val in self.fields.iteritems():
if val.required:
self.fields[field].widget.attrs['required'] = 'required'
class UserPref(DictModel):
"""Describes a user's application preferences."""

Просмотреть файл

@ -199,7 +199,7 @@ class ProcessOverview(basehandlers.FlaskHandler):
# Provide new or populated form to template.
template_data.update({
'feature': f.format_for_template(),
'feature_id': f.key().id,
'feature_id': f.key().id(),
'feature_json': json.dumps(f.format_for_template()),
'progress_so_far': json.dumps(progress_so_far),
})
@ -284,7 +284,7 @@ class FeatureEditStage(basehandlers.FlaskHandler):
# Provide new or populated form to template.
template_data.update({
'feature': f,
'feature_id': f.key().id,
'feature_id': f.key().id(),
'feature_form': detail_form,
'already_on_this_stage': stage_id == f.intent_stage,
'already_on_this_impl_status':
@ -552,7 +552,7 @@ class FeatureEditAllFields(FeatureEditStage):
for section_name, form_class in flat_form_section_list]
template_data = {
'feature': f,
'feature_id': f.key().id,
'feature_id': f.key().id(),
'flat_forms': flat_forms,
}
return template_data
@ -561,7 +561,6 @@ class FeatureEditAllFields(FeatureEditStage):
app = basehandlers.FlaskApplication([
('/guide/new', FeatureNew),
('/guide/edit/<int:feature_id>', ProcessOverview),
# TODO(jrobbins): ('/guide/delete/<int:feature_id>', FeatureDelete),
('/guide/stage/<int:feature_id>/<int:stage_id>', FeatureEditStage),
('/guide/editall/<int:feature_id>', FeatureEditAllFields),
], debug=settings.DEBUG)

Просмотреть файл

@ -31,6 +31,21 @@ SHIPPED_HELP_TXT = (
'status to In development. If the flag is for an origin trial set status '
'to Origin trial.')
SHIPPED_WEBVIEW_HELP_TXT = ('First milestone to ship with this status. '
'Applies to Enabled by default, Browser '
'Intervention, and Deprecated.\n\n NOTE: for '
'statuses In developer trial and Origin trial this '
'MUST be blank.')
SUMMARY_PLACEHOLDER_TXT = (
'NOTE: This text describes this feature in the eventual beta release post '
'as well as possibly in other external documents.\n\n'
'Begin with one line explaining what the feature does. Add one or two '
'lines explaining how this feature helps developers. Avoid language such '
'as "a new feature". They all are or have been new features.\n\n'
'Follow the example link below for more guidance.')
# We define all form fields here so that they can be include in one or more
# stage-specific fields without repeating the details and help text.
ALL_FIELDS = {
@ -53,7 +68,8 @@ ALL_FIELDS = {
'summary': forms.CharField(
required=True,
widget=forms.Textarea(
attrs={'cols': 50, 'maxlength': 500, 'placeholder': models.FeatureForm.SUMMARY_PLACEHOLDER_TXT}),
attrs={'cols': 50, 'maxlength': 500,
'placeholder': SUMMARY_PLACEHOLDER_TXT}),
help_text=
('<a target="_blank" href="'
'https://github.com/GoogleChrome/chromium-dashboard/wiki/'
@ -564,7 +580,7 @@ ALL_FIELDS = {
'shipped_webview_milestone': forms.IntegerField(
required=False, label='Android Webview',
widget=forms.NumberInput(attrs={'placeholder': 'Milestone #'}),
help_text=SHIPPED_HELP_TXT),
help_text=SHIPPED_WEBVIEW_HELP_TXT),
'flag_name': forms.CharField(
label='Flag name', required=False,

Просмотреть файл

@ -8,43 +8,6 @@ for (let i = 0; i < fields.length; ++i) {
});
}
// TODO(ericbidelman): These values are brittle if changed in the db later on.
const MIN_MILESTONE_TO_BE_ACTIVE = 3;
const NO_LONGER_PURSUING = 1000;
document.querySelector('[name=feature_form]').addEventListener('change', (e) => {
switch (e.target.tagName.toLowerCase()) {
case 'select':
if (e.target.id === 'id_impl_status_chrome') {
toggleMilestones(e.target);
} else if (e.target.id === 'id_intent_stage' ||
e.target.id === 'id_category') {
intentStageChanged();
}
break;
default:
break;
}
});
// Only admins see this button
if (document.querySelector('.delete-button')) {
document.querySelector('.delete-button').addEventListener('click', (e) => {
if (!confirm('Delete feature?')) {
return;
}
fetch(`/admin/features/delete/${e.currentTarget.dataset.id}`, {
method: 'POST',
credentials: 'include',
}).then((resp) => {
if (resp.status === 200) {
location.href = '/features';
}
});
});
}
if (document.querySelector('#cancel-button')) {
document.querySelector('#cancel-button').addEventListener('click', (e) => {
@ -52,553 +15,8 @@ if (document.querySelector('#cancel-button')) {
});
}
/**
* Toggles the chrome milestone inputs.
* @param {HTMLInputElement} status Input element.
*/
function toggleMilestones(status) {
const val = parseInt(status.value, 10);
const disabled = (val <= MIN_MILESTONE_TO_BE_ACTIVE ||
val === NO_LONGER_PURSUING);
const shippedInputs = document.querySelectorAll('[name^="shipped_"]');
[].forEach.call(shippedInputs, function(input) {
input.disabled = disabled;
input.parentElement.parentElement.hidden = input.disabled;
});
// var milestone = document.querySelector('#id_shipped_milestone');
// milestone.disabled = parseInt(status.value) <= MIN_MILESTONE_TO_BE_ACTIVE;
// milestone.parentElement.parentElement.hidden = milestone.disabled;
}
const INTENT_NONE = 0;
const INTENT_IMPLEMENT = 1;
const INTENT_EXPERIMENT = 2;
const INTENT_EXTEND_TRIAL = 3;
const INTENT_IMPLEMENT_SHIP = 4;
const INTENT_SHIP = 5;
const INTENT_REMOVE = 6;
let INTENT_IDENTIFIER_NAMES = {};
INTENT_IDENTIFIER_NAMES[INTENT_NONE] = 'INTENT_NONE';
INTENT_IDENTIFIER_NAMES[INTENT_IMPLEMENT] = 'INTENT_IMPLEMENT';
INTENT_IDENTIFIER_NAMES[INTENT_EXPERIMENT] = 'INTENT_EXPERIMENT';
INTENT_IDENTIFIER_NAMES[INTENT_EXTEND_TRIAL] = 'INTENT_EXTEND_TRIAL';
INTENT_IDENTIFIER_NAMES[INTENT_IMPLEMENT_SHIP] = 'INTENT_IMPLEMENT_SHIP';
INTENT_IDENTIFIER_NAMES[INTENT_SHIP] = 'INTENT_SHIP';
INTENT_IDENTIFIER_NAMES[INTENT_REMOVE] = 'INTENT_REMOVE';
// An object graph mapping form fields to implementation status and whether or
// not the form field is shown or hidden (first 1/0) and required or optional
// (second 1/0).
const FORM_FIELD_SHOW_POS = 0;
const FORM_FIELD_REQUIRED_POS = 1;
const HIDDEN = [0, 0];
const VISIBLE_OPTIONAL = [1, 0];
const VISIBLE_REQUIRED = [1, 1];
const FORM_FIELD_GRAPH = {
'intent_to_implement_url': {
INTENT_NONE: HIDDEN,
INTENT_IMPLEMENT: HIDDEN,
INTENT_EXPERIMENT: VISIBLE_OPTIONAL,
INTENT_EXTEND_TRIAL: VISIBLE_OPTIONAL,
INTENT_IMPLEMENT_SHIP: HIDDEN,
INTENT_SHIP: VISIBLE_REQUIRED,
INTENT_REMOVE: VISIBLE_OPTIONAL,
},
'intent_to_ship_url': {
INTENT_NONE: HIDDEN,
INTENT_IMPLEMENT: HIDDEN,
INTENT_EXPERIMENT: HIDDEN,
INTENT_EXTEND_TRIAL: HIDDEN,
INTENT_IMPLEMENT_SHIP: VISIBLE_OPTIONAL,
INTENT_SHIP: VISIBLE_OPTIONAL,
INTENT_REMOVE: VISIBLE_OPTIONAL,
},
'ready_for_trial_url': {
INTENT_NONE: HIDDEN,
INTENT_IMPLEMENT: HIDDEN,
INTENT_EXPERIMENT: VISIBLE_OPTIONAL,
INTENT_EXTEND_TRIAL: VISIBLE_OPTIONAL,
INTENT_IMPLEMENT_SHIP: HIDDEN,
INTENT_SHIP: HIDDEN,
INTENT_REMOVE: VISIBLE_OPTIONAL,
},
'intent_to_experiment_url': {
INTENT_NONE: HIDDEN,
INTENT_IMPLEMENT: HIDDEN,
INTENT_EXPERIMENT: VISIBLE_OPTIONAL,
INTENT_EXTEND_TRIAL: VISIBLE_OPTIONAL,
INTENT_IMPLEMENT_SHIP: VISIBLE_OPTIONAL,
INTENT_SHIP: VISIBLE_OPTIONAL,
INTENT_REMOVE: VISIBLE_OPTIONAL,
},
'explainer_links': {
INTENT_NONE: HIDDEN,
INTENT_IMPLEMENT: VISIBLE_REQUIRED,
INTENT_EXPERIMENT: VISIBLE_OPTIONAL,
INTENT_EXTEND_TRIAL: HIDDEN,
INTENT_IMPLEMENT_SHIP: VISIBLE_REQUIRED,
INTENT_SHIP: VISIBLE_REQUIRED,
INTENT_REMOVE: HIDDEN,
},
'doc_links': {
INTENT_NONE: VISIBLE_OPTIONAL,
INTENT_IMPLEMENT: VISIBLE_OPTIONAL,
INTENT_EXPERIMENT: VISIBLE_OPTIONAL,
INTENT_EXTEND_TRIAL: VISIBLE_OPTIONAL,
INTENT_IMPLEMENT_SHIP: VISIBLE_OPTIONAL,
INTENT_SHIP: VISIBLE_OPTIONAL,
INTENT_REMOVE: VISIBLE_OPTIONAL,
},
'spec_link': {
INTENT_NONE: VISIBLE_OPTIONAL,
INTENT_IMPLEMENT: VISIBLE_OPTIONAL,
INTENT_EXPERIMENT: VISIBLE_OPTIONAL,
INTENT_EXTEND_TRIAL: VISIBLE_OPTIONAL,
INTENT_IMPLEMENT_SHIP: VISIBLE_REQUIRED,
INTENT_SHIP: VISIBLE_REQUIRED,
INTENT_REMOVE: VISIBLE_OPTIONAL,
},
'standardization': {
INTENT_NONE: VISIBLE_OPTIONAL,
INTENT_IMPLEMENT: VISIBLE_OPTIONAL,
INTENT_EXPERIMENT: HIDDEN,
INTENT_EXTEND_TRIAL: HIDDEN,
INTENT_IMPLEMENT_SHIP: VISIBLE_REQUIRED,
INTENT_SHIP: VISIBLE_REQUIRED,
INTENT_REMOVE: VISIBLE_REQUIRED,
},
'tag_review': {
INTENT_NONE: HIDDEN,
INTENT_IMPLEMENT: VISIBLE_OPTIONAL,
INTENT_EXPERIMENT: HIDDEN,
INTENT_EXTEND_TRIAL: HIDDEN,
INTENT_IMPLEMENT_SHIP: VISIBLE_REQUIRED,
INTENT_SHIP: VISIBLE_REQUIRED,
INTENT_REMOVE: HIDDEN,
},
'wpt': {
INTENT_NONE: HIDDEN,
INTENT_IMPLEMENT: VISIBLE_OPTIONAL,
INTENT_EXPERIMENT: HIDDEN,
INTENT_EXTEND_TRIAL: HIDDEN,
INTENT_IMPLEMENT_SHIP: VISIBLE_OPTIONAL,
INTENT_SHIP: VISIBLE_OPTIONAL,
INTENT_REMOVE: VISIBLE_OPTIONAL,
},
'wpt_descr': {
INTENT_NONE: HIDDEN,
INTENT_IMPLEMENT: VISIBLE_REQUIRED,
INTENT_EXPERIMENT: HIDDEN,
INTENT_EXTEND_TRIAL: HIDDEN,
INTENT_IMPLEMENT_SHIP: VISIBLE_REQUIRED,
INTENT_SHIP: VISIBLE_REQUIRED,
INTENT_REMOVE: VISIBLE_REQUIRED,
},
'sample_links': {
INTENT_NONE: VISIBLE_OPTIONAL,
INTENT_IMPLEMENT: VISIBLE_OPTIONAL,
INTENT_EXPERIMENT: HIDDEN,
INTENT_EXTEND_TRIAL: HIDDEN,
INTENT_IMPLEMENT_SHIP: VISIBLE_OPTIONAL,
INTENT_SHIP: VISIBLE_OPTIONAL,
INTENT_REMOVE: HIDDEN,
},
'bug_url': {
INTENT_NONE: VISIBLE_OPTIONAL,
INTENT_IMPLEMENT: VISIBLE_REQUIRED,
INTENT_EXPERIMENT: HIDDEN,
INTENT_EXTEND_TRIAL: HIDDEN,
INTENT_IMPLEMENT_SHIP: VISIBLE_REQUIRED,
INTENT_SHIP: VISIBLE_REQUIRED,
INTENT_REMOVE: VISIBLE_REQUIRED,
},
'blink_components': {
INTENT_NONE: VISIBLE_OPTIONAL,
INTENT_IMPLEMENT: VISIBLE_REQUIRED,
INTENT_EXPERIMENT: HIDDEN,
INTENT_EXTEND_TRIAL: HIDDEN,
INTENT_IMPLEMENT_SHIP: VISIBLE_REQUIRED,
INTENT_SHIP: VISIBLE_REQUIRED,
INTENT_REMOVE: VISIBLE_REQUIRED,
},
'impl_status_chrome': {
INTENT_NONE: VISIBLE_OPTIONAL,
INTENT_IMPLEMENT: VISIBLE_REQUIRED,
INTENT_EXPERIMENT: VISIBLE_OPTIONAL,
INTENT_EXTEND_TRIAL: HIDDEN,
INTENT_IMPLEMENT_SHIP: VISIBLE_REQUIRED,
INTENT_SHIP: VISIBLE_REQUIRED,
INTENT_REMOVE: VISIBLE_REQUIRED,
},
'prefixed': {
INTENT_NONE: VISIBLE_OPTIONAL,
INTENT_IMPLEMENT: VISIBLE_OPTIONAL,
INTENT_EXPERIMENT: VISIBLE_OPTIONAL,
INTENT_EXTEND_TRIAL: HIDDEN,
INTENT_IMPLEMENT_SHIP: VISIBLE_OPTIONAL,
INTENT_SHIP: VISIBLE_OPTIONAL,
INTENT_REMOVE: VISIBLE_OPTIONAL,
},
'interop_compat_risks': {
INTENT_NONE: HIDDEN,
INTENT_IMPLEMENT: VISIBLE_REQUIRED,
INTENT_EXPERIMENT: HIDDEN,
INTENT_EXTEND_TRIAL: HIDDEN,
INTENT_IMPLEMENT_SHIP: VISIBLE_REQUIRED,
INTENT_SHIP: VISIBLE_REQUIRED,
INTENT_REMOVE: VISIBLE_REQUIRED,
},
'motivation': {
INTENT_NONE: HIDDEN,
INTENT_IMPLEMENT: VISIBLE_REQUIRED,
INTENT_EXPERIMENT: HIDDEN,
INTENT_EXTEND_TRIAL: HIDDEN,
INTENT_IMPLEMENT_SHIP: VISIBLE_REQUIRED,
INTENT_SHIP: HIDDEN,
INTENT_REMOVE: VISIBLE_REQUIRED,
},
'ff_views': {
INTENT_NONE: VISIBLE_REQUIRED,
INTENT_IMPLEMENT: VISIBLE_REQUIRED,
INTENT_EXPERIMENT: HIDDEN,
INTENT_EXTEND_TRIAL: HIDDEN,
INTENT_IMPLEMENT_SHIP: VISIBLE_REQUIRED,
INTENT_SHIP: VISIBLE_REQUIRED,
INTENT_REMOVE: VISIBLE_REQUIRED,
},
'ie_views': {
INTENT_NONE: VISIBLE_REQUIRED,
INTENT_IMPLEMENT: VISIBLE_REQUIRED,
INTENT_EXPERIMENT: HIDDEN,
INTENT_EXTEND_TRIAL: HIDDEN,
INTENT_IMPLEMENT_SHIP: VISIBLE_REQUIRED,
INTENT_SHIP: VISIBLE_REQUIRED,
INTENT_REMOVE: VISIBLE_REQUIRED,
},
'safari_views': {
INTENT_NONE: VISIBLE_REQUIRED,
INTENT_IMPLEMENT: VISIBLE_REQUIRED,
INTENT_EXPERIMENT: HIDDEN,
INTENT_EXTEND_TRIAL: HIDDEN,
INTENT_IMPLEMENT_SHIP: VISIBLE_REQUIRED,
INTENT_SHIP: VISIBLE_REQUIRED,
INTENT_REMOVE: VISIBLE_REQUIRED,
},
'web_dev_views': {
INTENT_NONE: VISIBLE_REQUIRED,
INTENT_IMPLEMENT: VISIBLE_REQUIRED,
INTENT_EXPERIMENT: HIDDEN,
INTENT_EXTEND_TRIAL: HIDDEN,
INTENT_IMPLEMENT_SHIP: VISIBLE_REQUIRED,
INTENT_SHIP: VISIBLE_REQUIRED,
INTENT_REMOVE: VISIBLE_REQUIRED,
},
'ff_views_link': {
INTENT_NONE: VISIBLE_OPTIONAL,
INTENT_IMPLEMENT: VISIBLE_OPTIONAL,
INTENT_EXPERIMENT: HIDDEN,
INTENT_EXTEND_TRIAL: HIDDEN,
INTENT_IMPLEMENT_SHIP: VISIBLE_OPTIONAL,
INTENT_SHIP: VISIBLE_OPTIONAL,
INTENT_REMOVE: VISIBLE_OPTIONAL,
},
'ie_views_link': {
INTENT_NONE: VISIBLE_OPTIONAL,
INTENT_IMPLEMENT: VISIBLE_OPTIONAL,
INTENT_EXPERIMENT: HIDDEN,
INTENT_EXTEND_TRIAL: HIDDEN,
INTENT_IMPLEMENT_SHIP: VISIBLE_OPTIONAL,
INTENT_SHIP: VISIBLE_OPTIONAL,
INTENT_REMOVE: VISIBLE_OPTIONAL,
},
'safari_views_link': {
INTENT_NONE: VISIBLE_OPTIONAL,
INTENT_IMPLEMENT: VISIBLE_OPTIONAL,
INTENT_EXPERIMENT: HIDDEN,
INTENT_EXTEND_TRIAL: HIDDEN,
INTENT_IMPLEMENT_SHIP: VISIBLE_OPTIONAL,
INTENT_SHIP: VISIBLE_OPTIONAL,
INTENT_REMOVE: VISIBLE_OPTIONAL,
},
'web_dev_views_link': {
INTENT_NONE: VISIBLE_OPTIONAL,
INTENT_IMPLEMENT: VISIBLE_OPTIONAL,
INTENT_EXPERIMENT: HIDDEN,
INTENT_EXTEND_TRIAL: HIDDEN,
INTENT_IMPLEMENT_SHIP: VISIBLE_OPTIONAL,
INTENT_SHIP: VISIBLE_OPTIONAL,
INTENT_REMOVE: VISIBLE_OPTIONAL,
},
'ff_views_notes': {
INTENT_NONE: VISIBLE_OPTIONAL,
INTENT_IMPLEMENT: VISIBLE_OPTIONAL,
INTENT_EXPERIMENT: HIDDEN,
INTENT_EXTEND_TRIAL: HIDDEN,
INTENT_IMPLEMENT_SHIP: VISIBLE_OPTIONAL,
INTENT_SHIP: VISIBLE_OPTIONAL,
INTENT_REMOVE: VISIBLE_OPTIONAL,
},
'ie_views_notes': {
INTENT_NONE: VISIBLE_OPTIONAL,
INTENT_IMPLEMENT: VISIBLE_OPTIONAL,
INTENT_EXPERIMENT: HIDDEN,
INTENT_EXTEND_TRIAL: HIDDEN,
INTENT_IMPLEMENT_SHIP: VISIBLE_OPTIONAL,
INTENT_SHIP: VISIBLE_OPTIONAL,
INTENT_REMOVE: VISIBLE_OPTIONAL,
},
'safari_views_notes': {
INTENT_NONE: VISIBLE_OPTIONAL,
INTENT_IMPLEMENT: VISIBLE_OPTIONAL,
INTENT_EXPERIMENT: HIDDEN,
INTENT_EXTEND_TRIAL: HIDDEN,
INTENT_IMPLEMENT_SHIP: VISIBLE_OPTIONAL,
INTENT_SHIP: VISIBLE_OPTIONAL,
INTENT_REMOVE: VISIBLE_OPTIONAL,
},
'web_dev_views_notes': {
INTENT_NONE: VISIBLE_OPTIONAL,
INTENT_IMPLEMENT: VISIBLE_OPTIONAL,
INTENT_EXPERIMENT: HIDDEN,
INTENT_EXTEND_TRIAL: HIDDEN,
INTENT_IMPLEMENT_SHIP: VISIBLE_OPTIONAL,
INTENT_SHIP: VISIBLE_OPTIONAL,
INTENT_REMOVE: VISIBLE_OPTIONAL,
},
'experiment_goals': {
INTENT_NONE: HIDDEN,
INTENT_IMPLEMENT: HIDDEN,
INTENT_EXPERIMENT: VISIBLE_REQUIRED,
INTENT_EXTEND_TRIAL: VISIBLE_REQUIRED,
INTENT_IMPLEMENT_SHIP: HIDDEN,
INTENT_SHIP: HIDDEN,
INTENT_REMOVE: HIDDEN,
},
'experiment_timeline': {
INTENT_NONE: HIDDEN,
INTENT_IMPLEMENT: HIDDEN,
INTENT_EXPERIMENT: VISIBLE_REQUIRED,
INTENT_EXTEND_TRIAL: VISIBLE_REQUIRED,
INTENT_IMPLEMENT_SHIP: HIDDEN,
INTENT_SHIP: HIDDEN,
INTENT_REMOVE: HIDDEN,
},
'experiment_risks': {
INTENT_NONE: HIDDEN,
INTENT_IMPLEMENT: HIDDEN,
INTENT_EXPERIMENT: VISIBLE_REQUIRED,
INTENT_EXTEND_TRIAL: VISIBLE_REQUIRED,
INTENT_IMPLEMENT_SHIP: HIDDEN,
INTENT_SHIP: HIDDEN,
INTENT_REMOVE: HIDDEN,
},
'experiment_extension_reason': {
INTENT_NONE: HIDDEN,
INTENT_IMPLEMENT: HIDDEN,
INTENT_EXPERIMENT: HIDDEN,
INTENT_EXTEND_TRIAL: VISIBLE_REQUIRED,
INTENT_IMPLEMENT_SHIP: HIDDEN,
INTENT_SHIP: HIDDEN,
INTENT_REMOVE: HIDDEN,
},
'ongoing_constraints': {
INTENT_NONE: HIDDEN,
INTENT_IMPLEMENT: HIDDEN,
INTENT_EXPERIMENT: VISIBLE_OPTIONAL,
INTENT_EXTEND_TRIAL: VISIBLE_OPTIONAL,
INTENT_IMPLEMENT_SHIP: HIDDEN,
INTENT_SHIP: HIDDEN,
INTENT_REMOVE: HIDDEN,
},
'all_platforms': {
INTENT_NONE: HIDDEN,
INTENT_IMPLEMENT: VISIBLE_OPTIONAL,
INTENT_EXPERIMENT: HIDDEN,
INTENT_EXTEND_TRIAL: HIDDEN,
INTENT_IMPLEMENT_SHIP: VISIBLE_OPTIONAL,
INTENT_SHIP: VISIBLE_OPTIONAL,
INTENT_REMOVE: VISIBLE_OPTIONAL,
},
'all_platforms_descr': {
INTENT_NONE: HIDDEN,
INTENT_IMPLEMENT: VISIBLE_OPTIONAL,
INTENT_EXPERIMENT: HIDDEN,
INTENT_EXTEND_TRIAL: HIDDEN,
INTENT_IMPLEMENT_SHIP: VISIBLE_OPTIONAL,
INTENT_SHIP: VISIBLE_OPTIONAL,
INTENT_REMOVE: VISIBLE_OPTIONAL,
},
'ergonomics_risks': {
INTENT_NONE: HIDDEN,
INTENT_IMPLEMENT: VISIBLE_OPTIONAL,
INTENT_EXPERIMENT: HIDDEN,
INTENT_EXTEND_TRIAL: HIDDEN,
INTENT_IMPLEMENT_SHIP: VISIBLE_OPTIONAL,
INTENT_SHIP: VISIBLE_OPTIONAL,
INTENT_REMOVE: VISIBLE_OPTIONAL,
},
'activation_risks': {
INTENT_NONE: HIDDEN,
INTENT_IMPLEMENT: VISIBLE_OPTIONAL,
INTENT_EXPERIMENT: HIDDEN,
INTENT_EXTEND_TRIAL: HIDDEN,
INTENT_IMPLEMENT_SHIP: VISIBLE_OPTIONAL,
INTENT_SHIP: VISIBLE_OPTIONAL,
INTENT_REMOVE: VISIBLE_OPTIONAL,
},
'security_risks': {
INTENT_NONE: HIDDEN,
INTENT_IMPLEMENT: VISIBLE_OPTIONAL,
INTENT_EXPERIMENT: HIDDEN,
INTENT_EXTEND_TRIAL: HIDDEN,
INTENT_IMPLEMENT_SHIP: VISIBLE_OPTIONAL,
INTENT_SHIP: VISIBLE_OPTIONAL,
INTENT_REMOVE: VISIBLE_OPTIONAL,
},
'origin_trial_feedback_url': {
INTENT_NONE: HIDDEN,
INTENT_IMPLEMENT: HIDDEN,
INTENT_EXPERIMENT: HIDDEN,
INTENT_EXTEND_TRIAL: HIDDEN,
INTENT_IMPLEMENT_SHIP: HIDDEN,
INTENT_SHIP: VISIBLE_OPTIONAL,
INTENT_REMOVE: VISIBLE_OPTIONAL,
},
'debuggability': {
INTENT_NONE: HIDDEN,
INTENT_IMPLEMENT: VISIBLE_OPTIONAL,
INTENT_EXPERIMENT: VISIBLE_OPTIONAL,
INTENT_EXTEND_TRIAL: VISIBLE_OPTIONAL,
INTENT_IMPLEMENT_SHIP: VISIBLE_OPTIONAL,
INTENT_SHIP: VISIBLE_OPTIONAL,
INTENT_REMOVE: VISIBLE_OPTIONAL,
},
};
// Exceptions by category to the above set of values
const FORM_FIELD_CATEGORY_EXCEPTIONS =
{
'JavaScript': {
'tag_review': {
INTENT_NONE: VISIBLE_OPTIONAL,
INTENT_IMPLEMENT: VISIBLE_OPTIONAL,
INTENT_EXPERIMENT: VISIBLE_OPTIONAL,
INTENT_EXTEND_TRIAL: VISIBLE_OPTIONAL,
INTENT_IMPLEMENT_SHIP: VISIBLE_OPTIONAL,
INTENT_SHIP: VISIBLE_OPTIONAL,
INTENT_REMOVE: VISIBLE_OPTIONAL,
},
'wpt': {
INTENT_NONE: VISIBLE_OPTIONAL,
INTENT_IMPLEMENT: VISIBLE_OPTIONAL,
INTENT_EXPERIMENT: VISIBLE_OPTIONAL,
INTENT_EXTEND_TRIAL: VISIBLE_OPTIONAL,
INTENT_IMPLEMENT_SHIP: VISIBLE_OPTIONAL,
INTENT_SHIP: VISIBLE_OPTIONAL,
INTENT_REMOVE: VISIBLE_OPTIONAL,
},
'wpt_descr': {
INTENT_NONE: VISIBLE_OPTIONAL,
INTENT_IMPLEMENT: VISIBLE_OPTIONAL,
INTENT_EXPERIMENT: VISIBLE_OPTIONAL,
INTENT_EXTEND_TRIAL: VISIBLE_OPTIONAL,
INTENT_IMPLEMENT_SHIP: VISIBLE_OPTIONAL,
INTENT_SHIP: VISIBLE_OPTIONAL,
INTENT_REMOVE: VISIBLE_OPTIONAL,
},
},
};
function intentStageChanged() {
function setRequired(id, required) {
if (required) {
document.querySelector('#id_' + id).setAttribute('required', 'required');
} else {
document.querySelector('#id_' + id).removeAttribute('required');
}
}
function show(id) {
// Clear the inline style for the control's table row.
document.querySelector('#id_' + id).parentNode.parentNode.style = '';
}
function hide(id) {
// Set the control's table row style to display: none.
document.querySelector('#id_' + id).parentNode.parentNode.style.display = 'none';
}
const stageEl = document.querySelector('#id_intent_stage');
const stageIndex = Number(stageEl.value);
const stageIdentifier = INTENT_IDENTIFIER_NAMES[stageIndex];
const category = document.querySelector('#id_category').selectedOptions[0].textContent;
for (let id in FORM_FIELD_GRAPH) {
if (!FORM_FIELD_GRAPH.hasOwnProperty(id)) {
continue;
}
let formFieldValues = FORM_FIELD_GRAPH[id][stageIdentifier];
if (category in FORM_FIELD_CATEGORY_EXCEPTIONS &&
id in FORM_FIELD_CATEGORY_EXCEPTIONS[category] &&
stageIdentifier in FORM_FIELD_CATEGORY_EXCEPTIONS[category][id]) {
formFieldValues = FORM_FIELD_CATEGORY_EXCEPTIONS[category][id][stageIdentifier];
}
if (formFieldValues[FORM_FIELD_SHOW_POS]) {
show(id);
} else {
hide(id);
}
if (formFieldValues[FORM_FIELD_REQUIRED_POS]) {
setRequired(id, true);
} else {
setRequired(id, false);
}
}
// Update the "Intent to <X>" wording in the form to match the intent stage.
let intentStageNameEl = document.querySelector('#id_intent_stage_name');
if (intentStageNameEl) {
if (stageIndex != INTENT_NONE) {
intentStageNameEl.textContent =
stageEl.options[stageEl.options.selectedIndex].textContent;
} else {
intentStageNameEl.textContent = '...';
}
}
// Disable the "Generate Intent to..." checkbox when the intent stage is
// "None" (i.e. for entries created before the notion of an intent stage was
// known to Features).
let intentToImplementEl = document.querySelector('#id_intent_to_implement');
if (intentToImplementEl) {
if (stageIndex == INTENT_NONE) {
intentToImplementEl.disabled = true;
intentToImplementEl.checked = false;
} else {
intentToImplementEl.disabled = false;
}
}
}
document.addEventListener('DOMContentLoaded', function() {
document.body.classList.remove('loading');
toggleMilestones(document.querySelector('#id_impl_status_chrome'));
intentStageChanged();
});
})();

Просмотреть файл

@ -8,8 +8,8 @@ if (document.querySelector('.delete-button')) {
return;
}
fetch(`/admin/features/delete/${e.currentTarget.dataset.id}`, {
method: 'POST',
fetch(`/api/v0/features/${e.currentTarget.dataset.id}`, {
method: 'DELETE',
credentials: 'include',
}).then((resp) => {
if (resp.status === 200) {

Просмотреть файл

@ -1,48 +0,0 @@
{% extends "_base.html" %}
{% block css %}
<link rel="stylesheet" href="/static/css/forms.css">
<style>
.delete-button {
margin-left: 20px;
}
</style>
{% endblock %}
{% block subheader %}
<div id="subheader">
<h2>Edit a feature</h2>
{% if user.is_admin %}
<button class="delete-button" data-id="{{feature.id}}">Delete</button>
{% endif %}
<a style="flex-grow:1; text-align:right"
href="/guide/edit/{{ feature.id }}">Try new UI</a>
</div>
{% endblock %}
{% block content %}
<section>
<form name="feature_form" method="POST" action="{{current_path}}">
<table>
{{ feature_form }}
</table>
<ul>
<li>
<input type="checkbox" name="intent_to_implement" id="id_intent_to_implement">
<label for="id_intent_to_implement">Generate "Intent to <span id="id_intent_stage_name"></span>" email on next page (you will copy &amp; paste it to send)</label>
</li>
<li>
<input type="checkbox" name="create_launch_bug" disabled id="id_create_launch_bug">
<label for="id_create_launch_bug">Generate a pre-populated launch-tracking bug link on the next page (you will file it manually)</label>
</li>
<li></li>
</ul>
<input type="submit" value="Submit">
</form>
</section>
{% endblock %}
{% block js %}
<script src="/static/js/admin/feature_form.min.js"></script>
{% endblock %}

Просмотреть файл

@ -1,42 +0,0 @@
{% extends "_base.html" %}
{% block css %}
<link rel="stylesheet" href="/static/css/forms.css">
{% endblock %}
{% block drawer %}
{% endblock %}
{% block subheader %}
<div id="subheader">
<h2>Add a feature</h2>
<a style="flex-grow:1; text-align:right"
href="/guide/new">Try new UI</a>
</div>
{% endblock %}
{% block content %}
<section>
<form name="feature_form" method="POST" action="{{current_path}}">
<table>
{{ feature_form }}
<tr><td colspan="2"><input type="checkbox" disabled name="email_owners_on_update" id="id_email_owners_on_update"><label for="id_email_owners_on_update">Email feature owner(s) when this data is updated.</label></td></tr>
</table>
<h3>Next steps for the Blink <a href="http://www.chromium.org/blink#launch-process">launch process</a>:</h3>
<ul>
<li><input type="checkbox" name="intent_to_implement" id="id_intent_to_implement"><label for="id_intent_to_implement">Generate "Intent to <span id="id_intent_stage_name"></span>" email on next page (you will copy &amp; paste it to send)</label></li>
<li><input type="checkbox" name="create_launch_bug" disabled id="id_create_launch_bug"><label for="id_create_launch_bug">Generate a pre-populated launch-tracking bug link on the next page (you will file it manually)</label></li>
<li></li>
</ul>
<input type="submit" value="Submit">
</form>
</section>
{% endblock %}
{% block js %}
<script src="/static/js/admin/feature_form.min.js"></script>
{% endblock %}

Просмотреть файл

@ -114,12 +114,13 @@ class Blank(object):
def sign_out():
"""Set env variables to represent a signed out user."""
ourTestbed.setup_env(
user_email='', user_id='', overwrite=True)
user_email='', user_id='', user_is_admin='0', overwrite=True)
def sign_in(user_email, user_id):
def sign_in(user_email, user_id, is_admin=False):
"""Set env variables to represent a signed out user."""
ourTestbed.setup_env(
user_email=user_email,
user_id=str(user_id),
user_is_admin='1' if is_admin else '0',
overwrite=True)

Просмотреть файл

@ -265,63 +265,3 @@ class IntentEmailPreviewHandlerTest(unittest.TestCase):
'Request for Deprecation Trial',
self.handler.compute_subject_prefix(
self.feature_1, models.INTENT_EXTEND_TRIAL))
class BaseFeatureHandlerTest(unittest.TestCase):
def setUp(self):
self.feature_1 = models.Feature(
name='feature one', summary='sum', category=1, visibility=1,
standardization=1, web_dev_views=1, impl_status_chrome=1)
self.feature_1.put()
self.request_path = (
'/admin/features/edit/%d' % self.feature_1.key().id())
self.handler = admin.BaseFeatureHandler()
def tearDown(self):
self.feature_1.delete()
def test_post__anon(self):
"""Anon cannot edit features, gets a 401."""
testing_config.sign_out()
feature_id = self.feature_1.key().id()
with admin.app.test_request_context(self.request_path):
with self.assertRaises(werkzeug.exceptions.Forbidden):
self.handler.process_post_data(feature_id=feature_id)
def test_post__no_existing(self):
"""Trying to edit a feature that does not exist redirects."""
testing_config.sign_in('user1@google.com', 123567890)
bad_feature_id = self.feature_1.key().id() + 1
params = { "intent_stage": "1" }
path = '/admin/features/edit/%d' % bad_feature_id
with admin.app.test_request_context(path, data=params):
actual_response = self.handler.process_post_data(feature_id=bad_feature_id)
self.assertEqual('302 FOUND', actual_response.status)
def test_post__normal(self):
"""Allowed user can edit a feature."""
testing_config.sign_in('user1@google.com', 123567890)
feature_id = self.feature_1.key().id()
params = {
"category": "1",
"name": "name",
"summary": "sum",
"intent_stage": "1",
"impl_status_chrome": "1",
"visibility": "1",
"ff_views": "1",
"ie_views": "1",
"safari_views": "1",
"web_dev_views": "1",
"standardization": "1",
"experiment_goals": "Measure something",
}
path = '/admin/features/edit/%d' % feature_id
with admin.app.test_request_context(path, method='POST', data=params):
actual_response = self.handler.process_post_data(feature_id=feature_id)
self.assertEqual('302 FOUND', actual_response.status)
updated_feature = models.Feature.get_by_id(feature_id)
self.assertEqual('Measure something', updated_feature.experiment_goals)