Delete code for the legacy feature editing UI. (#1231)
This commit is contained in:
Родитель
3ccb4624c1
Коммит
288467837d
351
admin.py
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)
|
||||
|
|
|
@ -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'}
|
|
@ -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
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 & 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 & 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)
|
||||
|
|
Загрузка…
Ссылка в новой задаче