Refactored reminders and added prepublication notice. (#2262)
* Refactored reminders and added prepublication notice. * Added links to references * Improved tests * Update internals/reminders.py Co-authored-by: Daniel Smith <56164590+DanielRyanSmith@users.noreply.github.com> * Updated comment Co-authored-by: Daniel Smith <56164590+DanielRyanSmith@users.noreply.github.com>
This commit is contained in:
Родитель
d4d0a9b063
Коммит
84cbd9016a
|
@ -17,6 +17,9 @@ cron:
|
|||
- description: Send reminders to verify the accuracy of feature data.
|
||||
url: /cron/send_accuracy_notifications
|
||||
schedule: every monday 09:00
|
||||
- description: Send reminder to check summary before publication.
|
||||
url: /cron/send_prepublication
|
||||
schedule: every tuesday 09:00
|
||||
- description: Notify any users that have been inactive for 6 months.
|
||||
url: /cron/warn_inactive_users
|
||||
schedule: 1st monday of month 9:00
|
||||
|
|
|
@ -18,13 +18,11 @@ __author__ = 'ericbidelman@chromium.org (Eric Bidelman)'
|
|||
from datetime import datetime, timedelta
|
||||
import collections
|
||||
import logging
|
||||
import json
|
||||
import os
|
||||
import urllib
|
||||
|
||||
from framework import permissions
|
||||
from google.cloud import ndb
|
||||
import requests
|
||||
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils.html import conditional_escape as escape
|
||||
|
@ -320,95 +318,6 @@ class NotifyInactiveUsersHandler(basehandlers.FlaskHandler):
|
|||
return email_tasks
|
||||
|
||||
|
||||
class FeatureAccuracyHandler(basehandlers.FlaskHandler):
|
||||
JSONIFY = True
|
||||
|
||||
CHROME_RELEASE_SCHEDULE_URL = (
|
||||
'https://chromiumdash.appspot.com/fetch_milestone_schedule')
|
||||
ACCURACY_AS_OF_WEEKS = 4
|
||||
MILESTONE_FIELDS = (
|
||||
'dt_milestone_android_start',
|
||||
'dt_milestone_desktop_start',
|
||||
'dt_milestone_ios_start',
|
||||
'dt_milestone_webview_start',
|
||||
'ot_milestone_android_start',
|
||||
'ot_milestone_desktop_start',
|
||||
'ot_milestone_webview_start',
|
||||
'shipped_android_milestone',
|
||||
'shipped_ios_milestone',
|
||||
'shipped_milestone',
|
||||
'shipped_webview_milestone'
|
||||
)
|
||||
EMAIL_TEMPLATE_PATH = 'accuracy_notice_email.html'
|
||||
|
||||
def get_template_data(self):
|
||||
"""Sends notifications to users requesting feature updates for accuracy."""
|
||||
self.require_cron_header()
|
||||
features_to_notify = self._determine_features_to_notify()
|
||||
email_tasks = self._build_email_tasks(features_to_notify)
|
||||
send_emails(email_tasks)
|
||||
return {'message': f'{len(email_tasks)} email(s) sent or logged.'}
|
||||
|
||||
def _determine_features_to_notify(self):
|
||||
# 'current' milestone is the next stable milestone that hasn't landed.
|
||||
# We send notifications to any feature planned for beta or stable launch
|
||||
# in the next 8 weeks. Beta for (current + 2) starts in roughly 8 weeks.
|
||||
# So we check if any features have launches in these 3 milestones
|
||||
# (current, current + 1, and current + 2).
|
||||
try:
|
||||
resp = requests.get(f'{self.CHROME_RELEASE_SCHEDULE_URL}?mstone=current')
|
||||
except requests.RequestException as e:
|
||||
raise e
|
||||
mstone_info = json.loads(resp.text)
|
||||
mstone = int(mstone_info['mstones'][0]['mstone'])
|
||||
|
||||
features = core_models.Feature.query(
|
||||
core_models.Feature.deleted == False).fetch(None)
|
||||
features_to_notify = []
|
||||
now = datetime.now()
|
||||
accuracy_as_of_delta = timedelta(weeks=self.ACCURACY_AS_OF_WEEKS)
|
||||
for feature in features:
|
||||
# If the data has been recently verified as accurate, no need for email.
|
||||
if (feature.accurate_as_of is not None and
|
||||
feature.accurate_as_of + accuracy_as_of_delta < now):
|
||||
continue
|
||||
|
||||
# Check each milestone field and see if it corresponds with
|
||||
# the next 3 milestones. Use the closest milestone for the email.
|
||||
closest_mstone = None
|
||||
for field in self.MILESTONE_FIELDS:
|
||||
launch_mstone = getattr(feature, field)
|
||||
if (launch_mstone is not None and
|
||||
launch_mstone >= mstone and launch_mstone <= mstone + 2):
|
||||
if closest_mstone is None:
|
||||
closest_mstone = launch_mstone
|
||||
else:
|
||||
closest_mstone = min(closest_mstone, launch_mstone)
|
||||
if closest_mstone is not None:
|
||||
features_to_notify.append((feature, closest_mstone))
|
||||
return features_to_notify
|
||||
|
||||
def _build_email_tasks(self, features_to_notify):
|
||||
email_tasks = []
|
||||
for feature, mstone in features_to_notify:
|
||||
body_data = {
|
||||
'id': feature.key.integer_id(),
|
||||
'feature': feature,
|
||||
'site_url': settings.SITE_URL,
|
||||
'milestone': mstone,
|
||||
}
|
||||
html = render_to_string(self.EMAIL_TEMPLATE_PATH, body_data)
|
||||
subject = f'[Action requested] Update {feature.name}'
|
||||
for owner in feature.owner:
|
||||
email_tasks.append({
|
||||
'to': owner,
|
||||
'subject': subject,
|
||||
'reply_to': None,
|
||||
'html': html
|
||||
})
|
||||
return email_tasks
|
||||
|
||||
|
||||
class FeatureChangeHandler(basehandlers.FlaskHandler):
|
||||
"""This task handles a feature creation or update by making email tasks."""
|
||||
|
||||
|
|
|
@ -505,77 +505,6 @@ class FeatureStarTest(testing_config.CustomTestCase):
|
|||
[au.email for au in actual])
|
||||
|
||||
|
||||
class MockResponse:
|
||||
"""Creates a fake response object for testing."""
|
||||
def __init__(self, status_code=200, text='{}'):
|
||||
self.status_code = status_code
|
||||
self.text = text
|
||||
|
||||
|
||||
class FeatureAccuracyHandlerTest(testing_config.CustomTestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.feature_1 = core_models.Feature(
|
||||
name='feature one', summary='sum', owner=['feature_owner@example.com'],
|
||||
category=1, visibility=1,
|
||||
standardization=1, web_dev_views=1, impl_status_chrome=1,
|
||||
ot_milestone_desktop_start=100)
|
||||
self.feature_1.put()
|
||||
self.feature_2 = core_models.Feature(
|
||||
name='feature two', summary='sum',
|
||||
owner=['owner_1@example.com', 'owner_2@example.com'],
|
||||
category=1, visibility=1, standardization=1,
|
||||
web_dev_views=1, impl_status_chrome=1, shipped_milestone=150)
|
||||
self.feature_2.put()
|
||||
self.feature_3 = core_models.Feature(
|
||||
name='feature three', summary='sum', category=1, visibility=1,
|
||||
standardization=1, web_dev_views=1, impl_status_chrome=1)
|
||||
self.feature_3.put()
|
||||
|
||||
def tearDown(self):
|
||||
self.feature_1.key.delete()
|
||||
self.feature_2.key.delete()
|
||||
self.feature_3.key.delete()
|
||||
|
||||
@mock.patch('requests.get')
|
||||
def test_determine_features_to_notify__no_features(self, mock_get):
|
||||
mock_return = MockResponse(text='{"mstones":[{"mstone": "40"}]}')
|
||||
mock_get.return_value = mock_return
|
||||
accuracy_notifier = notifier.FeatureAccuracyHandler()
|
||||
result = accuracy_notifier.get_template_data()
|
||||
expected = {'message': '0 email(s) sent or logged.'}
|
||||
self.assertEqual(result, expected)
|
||||
|
||||
@mock.patch('requests.get')
|
||||
def test_determine_features_to_notify__valid_features(self, mock_get):
|
||||
mock_return = MockResponse(text='{"mstones":[{"mstone": "100"}]}')
|
||||
mock_get.return_value = mock_return
|
||||
accuracy_notifier = notifier.FeatureAccuracyHandler()
|
||||
result = accuracy_notifier.get_template_data()
|
||||
expected = {'message': '1 email(s) sent or logged.'}
|
||||
self.assertEqual(result, expected)
|
||||
|
||||
@mock.patch('requests.get')
|
||||
def test_determine_features_to_notify__multiple_owners(self, mock_get):
|
||||
mock_return = MockResponse(text='{"mstones":[{"mstone": "148"}]}')
|
||||
mock_get.return_value = mock_return
|
||||
accuracy_notifier = notifier.FeatureAccuracyHandler()
|
||||
result = accuracy_notifier.get_template_data()
|
||||
expected = {'message': '2 email(s) sent or logged.'}
|
||||
self.assertEqual(result, expected)
|
||||
|
||||
def test_build_email_tasks(self):
|
||||
accuracy_notifier = notifier.FeatureAccuracyHandler()
|
||||
actual = accuracy_notifier._build_email_tasks([(self.feature_1, 100)])
|
||||
self.assertEqual(1, len(actual))
|
||||
task = actual[0]
|
||||
self.assertEqual('feature_owner@example.com', task['to'])
|
||||
self.assertEqual('[Action requested] Update feature one', task['subject'])
|
||||
self.assertEqual(None, task['reply_to'])
|
||||
self.assertIn('/guide/verify_accuracy/%d' % self.feature_1.key.integer_id(),
|
||||
task['html'])
|
||||
|
||||
|
||||
class NotifyInactiveUsersHandlerTest(testing_config.CustomTestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
|
|
@ -0,0 +1,177 @@
|
|||
# Copyright 2022 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 datetime import datetime, timedelta
|
||||
import json
|
||||
import requests
|
||||
|
||||
from django.template.loader import render_to_string
|
||||
|
||||
from framework import basehandlers
|
||||
from internals import core_models
|
||||
from internals import notifier
|
||||
import settings
|
||||
|
||||
|
||||
CHROME_RELEASE_SCHEDULE_URL = (
|
||||
'https://chromiumdash.appspot.com/fetch_milestone_schedule')
|
||||
|
||||
|
||||
def get_current_milestone_info():
|
||||
"""Return a dict of info about the next milestone reaching beta."""
|
||||
try:
|
||||
resp = requests.get(f'{CHROME_RELEASE_SCHEDULE_URL}?mstone=current')
|
||||
except requests.RequestException as e:
|
||||
raise e
|
||||
mstone_info = json.loads(resp.text)
|
||||
return mstone_info['mstones'][0]
|
||||
|
||||
|
||||
def build_email_tasks(
|
||||
features_to_notify, subject_format, body_template_path,
|
||||
current_milestone_info):
|
||||
email_tasks = []
|
||||
beta_date = datetime.fromisoformat(current_milestone_info['earliest_beta'])
|
||||
beta_date_str = beta_date.strftime('%Y-%m-%d')
|
||||
for feature, mstone in features_to_notify:
|
||||
body_data = {
|
||||
'id': feature.key.integer_id(),
|
||||
'feature': feature,
|
||||
'site_url': settings.SITE_URL,
|
||||
'milestone': mstone,
|
||||
'beta_date_str': beta_date_str,
|
||||
}
|
||||
html = render_to_string(body_template_path, body_data)
|
||||
subject = subject_format % feature.name
|
||||
for owner in feature.owner:
|
||||
email_tasks.append({
|
||||
'to': owner,
|
||||
'subject': subject,
|
||||
'reply_to': None,
|
||||
'html': html
|
||||
})
|
||||
return email_tasks
|
||||
|
||||
|
||||
class AbstractReminderHandler(basehandlers.FlaskHandler):
|
||||
JSONIFY = True
|
||||
SUBJECT_FORMAT = '%s'
|
||||
EMAIL_TEMPLATE_PATH = None # Subclasses must override
|
||||
FUTURE_MILESTONES_TO_CONSIDER = 0
|
||||
MILESTONE_FIELDS = None # Subclasses must override
|
||||
|
||||
def get_template_data(self):
|
||||
"""Sends notifications to users requesting feature updates for accuracy."""
|
||||
self.require_cron_header()
|
||||
current_milestone_info = get_current_milestone_info()
|
||||
features_to_notify = self.determine_features_to_notify(
|
||||
current_milestone_info)
|
||||
email_tasks = build_email_tasks(
|
||||
features_to_notify, self.SUBJECT_FORMAT, self.EMAIL_TEMPLATE_PATH,
|
||||
current_milestone_info)
|
||||
notifier.send_emails(email_tasks)
|
||||
return {'message': f'{len(email_tasks)} email(s) sent or logged.'}
|
||||
|
||||
def prefilter_features(self, current_milestone_info, features):
|
||||
"""Return a list of features that fit class-specific criteria."""
|
||||
return features # Defaults to no prefiltering.
|
||||
|
||||
def filter_by_milestones(self, current_milestone_info, features):
|
||||
"""Return [(feature, milestone)] for features with a milestone in range."""
|
||||
# 'current' milestone is the next stable milestone that hasn't landed.
|
||||
# We send notifications to any feature planned for beta or stable launch
|
||||
# in the next 4 * FUTURE_MILESTONES_TO_CONSIDER weeks.
|
||||
min_mstone = int(current_milestone_info['mstone'])
|
||||
max_mstone = min_mstone + self.FUTURE_MILESTONES_TO_CONSIDER
|
||||
|
||||
result = []
|
||||
for feature in features:
|
||||
field_values = [getattr(feature, field) for field in self.MILESTONE_FIELDS]
|
||||
matching_values = [
|
||||
m for m in field_values
|
||||
if m is not None and m >= min_mstone and m <= max_mstone]
|
||||
if matching_values:
|
||||
result.append((feature, min(matching_values)))
|
||||
|
||||
return result
|
||||
|
||||
def determine_features_to_notify(self, current_milestone_info):
|
||||
"""Get all features filter them by class-specific and milestone criteria."""
|
||||
features = core_models.Feature.query(
|
||||
core_models.Feature.deleted == False).fetch(None)
|
||||
prefiltered_features = self.prefilter_features(
|
||||
current_milestone_info, features)
|
||||
features_milestone_pairs = self.filter_by_milestones(
|
||||
current_milestone_info, prefiltered_features)
|
||||
return features_milestone_pairs
|
||||
|
||||
|
||||
class FeatureAccuracyHandler(AbstractReminderHandler):
|
||||
"""Periodically remind owners to verify the accuracy of their entries."""
|
||||
|
||||
ACCURACY_GRACE_PERIOD = timedelta(weeks=4)
|
||||
SUBJECT_FORMAT = '[Action requested] Update %s'
|
||||
EMAIL_TEMPLATE_PATH = 'accuracy_notice_email.html'
|
||||
FUTURE_MILESTONES_TO_CONSIDER = 2
|
||||
MILESTONE_FIELDS = (
|
||||
'dt_milestone_android_start',
|
||||
'dt_milestone_desktop_start',
|
||||
'dt_milestone_ios_start',
|
||||
'dt_milestone_webview_start',
|
||||
'ot_milestone_android_start',
|
||||
'ot_milestone_desktop_start',
|
||||
'ot_milestone_webview_start',
|
||||
'shipped_android_milestone',
|
||||
'shipped_ios_milestone',
|
||||
'shipped_milestone',
|
||||
'shipped_webview_milestone')
|
||||
|
||||
def prefilter_features(self, current_milestone_info, features):
|
||||
now = datetime.now()
|
||||
prefiltered_features = [
|
||||
feature for feature in features
|
||||
# It needs review if never reviewed, or if grace period has passed.
|
||||
if (feature.accurate_as_of is None or
|
||||
feature.accurate_as_of + self.ACCURACY_GRACE_PERIOD < now)]
|
||||
return prefiltered_features
|
||||
|
||||
|
||||
class PrepublicationHandler(AbstractReminderHandler):
|
||||
"""Give feature owners a final preview just before publication."""
|
||||
|
||||
SUBJECT_FORMAT = '[Action requested] Review %s'
|
||||
EMAIL_TEMPLATE_PATH = 'prepublication-notice-email.html'
|
||||
MILESTONE_FIELDS = (
|
||||
'shipped_android_milestone',
|
||||
'shipped_ios_milestone',
|
||||
'shipped_milestone',
|
||||
'shipped_webview_milestone')
|
||||
# Devrel copies summaries 1 week before the beta goes live.
|
||||
PUBLICATION_LEAD_TIME = timedelta(weeks=1)
|
||||
# We remind owners 1 week before that.
|
||||
REMINDER_WINDOW = timedelta(weeks=1)
|
||||
|
||||
def prefilter_features(self, current_milestone_info, features, now=None):
|
||||
earliest_beta = datetime.fromisoformat(
|
||||
current_milestone_info['earliest_beta'])
|
||||
|
||||
now = now or datetime.now()
|
||||
window_end = earliest_beta - self.PUBLICATION_LEAD_TIME
|
||||
window_start = window_end - self.REMINDER_WINDOW
|
||||
if now >= window_start and now <= window_end:
|
||||
# If we are in the reminder window, process all releveant features.
|
||||
return features
|
||||
else:
|
||||
# If this cron is running on an off week, do nothing.
|
||||
return []
|
|
@ -0,0 +1,147 @@
|
|||
# Copyright 2022 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License")
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import testing_config # Must be imported before the module under test.
|
||||
from datetime import datetime
|
||||
from unittest import mock
|
||||
|
||||
from internals import core_models
|
||||
from internals import reminders
|
||||
|
||||
|
||||
class MockResponse:
|
||||
"""Creates a fake response object for testing."""
|
||||
def __init__(self, status_code=200, text='{}'):
|
||||
self.status_code = status_code
|
||||
self.text = text
|
||||
|
||||
|
||||
def make_test_features():
|
||||
feature_1 = core_models.Feature(
|
||||
name='feature one', summary='sum', owner=['feature_owner@example.com'],
|
||||
category=1, visibility=1,
|
||||
standardization=1, web_dev_views=1, impl_status_chrome=1,
|
||||
ot_milestone_desktop_start=100)
|
||||
feature_1.put()
|
||||
feature_2 = core_models.Feature(
|
||||
name='feature two', summary='sum',
|
||||
owner=['owner_1@example.com', 'owner_2@example.com'],
|
||||
category=1, visibility=1, standardization=1,
|
||||
web_dev_views=1, impl_status_chrome=1, shipped_milestone=150)
|
||||
feature_2.put()
|
||||
feature_3 = core_models.Feature(
|
||||
name='feature three', summary='sum', category=1, visibility=1,
|
||||
standardization=1, web_dev_views=1, impl_status_chrome=1)
|
||||
feature_3.put()
|
||||
return feature_1, feature_2, feature_3
|
||||
|
||||
|
||||
class FunctionTest(testing_config.CustomTestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.feature_1, self.feature_2, self.feature_3 = make_test_features()
|
||||
self.current_milestone_info = {
|
||||
'earliest_beta': '2022-09-21T12:34:56',
|
||||
}
|
||||
|
||||
def tearDown(self):
|
||||
self.feature_1.key.delete()
|
||||
self.feature_2.key.delete()
|
||||
self.feature_3.key.delete()
|
||||
|
||||
def test_build_email_tasks(self):
|
||||
actual = reminders.build_email_tasks(
|
||||
[(self.feature_1, 100)], '[Action requested] Update %s',
|
||||
reminders.FeatureAccuracyHandler.EMAIL_TEMPLATE_PATH,
|
||||
self.current_milestone_info)
|
||||
self.assertEqual(1, len(actual))
|
||||
task = actual[0]
|
||||
self.assertEqual('feature_owner@example.com', task['to'])
|
||||
self.assertEqual('[Action requested] Update feature one', task['subject'])
|
||||
self.assertEqual(None, task['reply_to'])
|
||||
self.assertIn('/guide/verify_accuracy/%d' % self.feature_1.key.integer_id(),
|
||||
task['html'])
|
||||
|
||||
|
||||
class FeatureAccuracyHandlerTest(testing_config.CustomTestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.feature_1, self.feature_2, self.feature_3 = make_test_features()
|
||||
self.handler = reminders.FeatureAccuracyHandler()
|
||||
|
||||
def tearDown(self):
|
||||
self.feature_1.key.delete()
|
||||
self.feature_2.key.delete()
|
||||
self.feature_3.key.delete()
|
||||
|
||||
@mock.patch('requests.get')
|
||||
def test_determine_features_to_notify__no_features(self, mock_get):
|
||||
mock_return = MockResponse(
|
||||
text=('{"mstones":[{"mstone": "40", '
|
||||
'"earliest_beta": "2018-01-01T01:23:45"}]}'))
|
||||
mock_get.return_value = mock_return
|
||||
result = self.handler.get_template_data()
|
||||
expected = {'message': '0 email(s) sent or logged.'}
|
||||
self.assertEqual(result, expected)
|
||||
|
||||
@mock.patch('requests.get')
|
||||
def test_determine_features_to_notify__valid_features(self, mock_get):
|
||||
mock_return = MockResponse(
|
||||
text=('{"mstones":[{"mstone": "100", '
|
||||
'"earliest_beta": "2022-08-01T01:23:45"}]}'))
|
||||
mock_get.return_value = mock_return
|
||||
result = self.handler.get_template_data()
|
||||
expected = {'message': '1 email(s) sent or logged.'}
|
||||
self.assertEqual(result, expected)
|
||||
|
||||
@mock.patch('requests.get')
|
||||
def test_determine_features_to_notify__multiple_owners(self, mock_get):
|
||||
mock_return = MockResponse(
|
||||
text=('{"mstones":[{"mstone": "148", '
|
||||
'"earliest_beta": "2024-02-03T01:23:45"}]}'))
|
||||
mock_get.return_value = mock_return
|
||||
result = self.handler.get_template_data()
|
||||
expected = {'message': '2 email(s) sent or logged.'}
|
||||
self.assertEqual(result, expected)
|
||||
|
||||
|
||||
class PrepublicationHandlerTest(testing_config.CustomTestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.current_milestone_info = {
|
||||
'earliest_beta': '2022-09-21T12:34:56',
|
||||
}
|
||||
self.handler = reminders.PrepublicationHandler()
|
||||
|
||||
def test_prefilter_features__off_week(self):
|
||||
"""No reminders sent because the next beta is far future or past."""
|
||||
features = ['mock feature']
|
||||
|
||||
mock_now = datetime(2022, 9, 1) # Way before beta.
|
||||
actual = self.handler.prefilter_features(
|
||||
self.current_milestone_info, features, now=mock_now)
|
||||
self.assertEqual([], actual)
|
||||
|
||||
mock_now = datetime(2022, 9, 18) # After DevRel cut-off.
|
||||
actual = self.handler.prefilter_features(
|
||||
self.current_milestone_info, features, now=mock_now)
|
||||
self.assertEqual([], actual)
|
||||
|
||||
def test_prefilter_features__on_week(self):
|
||||
"""Reminders are sent because the next beta is coming up."""
|
||||
features = ['mock feature']
|
||||
mock_now = datetime(2022, 9, 12)
|
||||
actual = self.handler.prefilter_features(
|
||||
self.current_milestone_info, features, now=mock_now)
|
||||
self.assertEqual(['mock feature'], actual)
|
4
main.py
4
main.py
|
@ -38,6 +38,7 @@ from internals import data_backup
|
|||
from internals import inactive_users
|
||||
from internals import schema_migration
|
||||
from internals import deprecate_field
|
||||
from internals import reminders
|
||||
from pages import blink_handler
|
||||
from pages import featurelist
|
||||
from pages import guide
|
||||
|
@ -189,7 +190,8 @@ internals_routes = [
|
|||
('/cron/histograms', fetchmetrics.HistogramsHandler),
|
||||
('/cron/update_blink_components', fetchmetrics.BlinkComponentHandler),
|
||||
('/cron/export_backup', data_backup.BackupExportHandler),
|
||||
('/cron/send_accuracy_notifications', notifier.FeatureAccuracyHandler),
|
||||
('/cron/send_accuracy_notifications', reminders.FeatureAccuracyHandler),
|
||||
('/cron/send_prepublication', reminders.PrepublicationHandler),
|
||||
('/cron/warn_inactive_users', notifier.NotifyInactiveUsersHandler),
|
||||
('/cron/remove_inactive_users', inactive_users.RemoveInactiveUsersHandler),
|
||||
('/cron/schema_migration_comment_activity', schema_migration.MigrateCommentsToActivities),
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
<p>Your feature is slated to ship in M{{milestone}} which will reach the
|
||||
beta channel on {{beta_date_str}}.</p>
|
||||
|
||||
<p>About one week before that date, the summary field of your feature
|
||||
entry will be used verbatim (in blog posts and enterprise release
|
||||
notes) to announce this new milestone. These posts are widely read by
|
||||
web developers and IT admins, and they are sometimes used as the basis
|
||||
for popular press articles.</p>
|
||||
|
||||
<p>Please take a look at
|
||||
<a href="https://developer.chrome.com/tags/new-in-chrome/"
|
||||
>past blog posts</a> and
|
||||
<a href="https://support.google.com/chrome/a/answer/7679408"
|
||||
>enterprise release notes</a>,
|
||||
and then revise your feature summary to prepare it for
|
||||
publication.</p>
|
||||
|
||||
<p>Here's what we have now for publication:</p>
|
||||
<h3>{{feature.name}}<h3>
|
||||
<p>{{feature.summary}}</p>
|
||||
|
||||
<p><a href="{{site_url}}/guide/edit/{{id}}">Edit your feature</a></p>
|
||||
|
||||
|
||||
|
||||
<p>Guidelines for writing the summary:</p>
|
||||
<ul>
|
||||
|
||||
<li>Provide a one sentence description followed by one or two lines
|
||||
explaining how this feature works and how it helps web
|
||||
developers.</li>
|
||||
|
||||
<li>Write from a web developer's point of view, not a browser developer's</li>
|
||||
|
||||
<li>Do not use markup or markdown because they will not be rendered.</li>
|
||||
|
||||
<li>Do not use hard or soft returns because they will not be rendered.</li>
|
||||
|
||||
<li>Avoid phrases such as "a new feature". Every feature on the site
|
||||
was new when it was created. You don't need to repeat that
|
||||
information.</li>
|
||||
|
||||
<li>The first line should be a sentence fragment beginning with a
|
||||
verb. (See below.) This is the rare exception to the requirement to
|
||||
always use complete sentences.</li>
|
||||
|
||||
<li>"Conformance with spec" is not adequate. Most if not all
|
||||
features are in conformance to spec.</li>
|
||||
</ul>
|
||||
|
||||
<p>Example:</p>
|
||||
|
||||
<p style="border: 1px solid #444; padding: 1em; margin: 1em">
|
||||
Splits the HTTP cache using the top frame origin (and possibly
|
||||
subframe origin) to prevent documents from one origin from knowing
|
||||
whether a resource from another origin was cached. The HTTP cache is
|
||||
currently one per profile, with a single namespace for all resources
|
||||
and subresources regardless of origin or renderer process. Splitting
|
||||
the cache on top frame origins helps the browser deflect
|
||||
side-channel attacks where one site can detect resources in another
|
||||
site's cache.
|
||||
</p>
|
Загрузка…
Ссылка в новой задаче