Update email templates to use new schema (#2837)

* Escalate accuracy verification notifications

* small comment change

* Remove unrelated change

* update templates to use new schema

* better type hinting

* update escalation check approach

* update intentpreview imports

* merge fix
This commit is contained in:
Daniel Smith 2023-03-23 11:47:15 -07:00 коммит произвёл GitHub
Родитель c10fc20c1d
Коммит 2d5bb78f0f
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
17 изменённых файлов: 483 добавлений и 169 удалений

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

@ -41,26 +41,33 @@ from internals.user_models import (
AppUser, BlinkComponent, FeatureOwner, UserPref)
def _determine_milestone_string(ship_stages: list[Stage]) -> str:
"""Determine the shipping milestone string to display in the template."""
# Get the earliest desktop and android milestones.
first_desktop = min(
(stage.milestones.desktop_first for stage in ship_stages
if stage.milestones and stage.milestones.desktop_first),
default=None)
first_android = min(
(stage.milestones.android_first for stage in ship_stages
if stage.milestones and stage.milestones.android_first),
default=None)
# Use the desktop milestone by default if it's available.
milestone_str = str(first_desktop)
# Use the android milestone with the android suffix if there are no
# desktop milestones.
if not first_desktop and first_android:
milestone_str = f'{first_android} (android)'
return milestone_str
def format_email_body(
is_update: bool, fe: FeatureEntry, fe_stages: dict[int, list[Stage]],
changes: list[dict[str, Any]]) -> str:
is_update: bool, fe: FeatureEntry, changes: list[dict[str, Any]]) -> str:
"""Return an HTML string for a notification email body."""
stage_type = core_enums.STAGE_TYPES_SHIPPING[fe.feature_type] or 0
ship_stages: list[Stage] = fe_stages.get(stage_type, [])
# TODO(danielrsmith): These notifications do not convey correct information
# for features with multiple shipping stages. Implement a new way to
# specify the shipping stage affected.
ship_milestones: MilestoneSet | None = (
ship_stages[0].milestones if len(ship_stages) > 0 else None)
milestone_str = 'not yet assigned'
if ship_milestones is not None:
if ship_milestones.desktop_first:
milestone_str = ship_milestones.desktop_first
elif (ship_milestones.desktop_first is None and
ship_milestones.android_first is not None):
milestone_str = f'{ship_milestones.android_first} (android)'
stage_info = stage_helpers.get_stage_info_for_templates(fe)
milestone_str = _determine_milestone_string(stage_info['ship_stages'])
moz_link_urls = [
link for link in fe.doc_links
@ -80,6 +87,8 @@ def format_email_body(
body_data = {
'feature': fe,
'stage_info': stage_info,
'should_render_mstone_table': stage_info['should_render_mstone_table'],
'creator_email': fe.creator_email,
'updater_email': fe.updater_email,
'id': fe.key.integer_id(),
@ -133,8 +142,7 @@ WEBVIEW_RULE_ADDRS = ['webview-leads-external@google.com']
def apply_subscription_rules(
fe: FeatureEntry, fe_stages: dict[int, list[Stage]],
changes: list) -> dict[str, list[str]]:
fe: FeatureEntry, changes: list) -> dict[str, list[str]]:
"""Return {"reason": [addrs]} for users who set up rules."""
# Note: for now this is hard-coded, but it will eventually be
# configurable through some kind of user preference.
@ -142,11 +150,9 @@ def apply_subscription_rules(
results: dict[str, list[str]] = {}
# Find an existing shipping stage with milestone info.
fe_stages = stage_helpers.get_feature_stages(fe.key.integer_id())
stage_type = core_enums.STAGE_TYPES_SHIPPING[fe.feature_type] or 0
ship_stages: list[Stage] = fe_stages.get(stage_type, [])
# TODO(danielrsmith): These notifications do not convey correct information
# for features with multiple shipping stages. Implement a new way to
# specify the shipping stage affected.
ship_milestones: MilestoneSet | None = (
ship_stages[0].milestones if len(ship_stages) > 0 else None)
@ -189,9 +195,7 @@ def make_feature_changes_email(fe: FeatureEntry, is_update: bool=False,
FeatureOwner.watching_all_features == True).fetch(None)
watcher_emails: list[str] = [watcher.email for watcher in watchers]
fe_stages = stage_helpers.get_feature_stages(fe.key.integer_id())
email_html = format_email_body(is_update, fe, fe_stages, changes)
email_html = format_email_body(is_update, fe, changes)
if is_update:
subject = 'updated feature: %s' % fe.name
triggering_user_email = fe.updater_email
@ -226,7 +230,7 @@ def make_feature_changes_email(fe: FeatureEntry, is_update: bool=False,
starrer_emails: list[str] = [user.email for user in starrers]
accumulate_reasons(addr_reasons, starrer_emails, 'You starred this feature')
rule_results = apply_subscription_rules(fe, fe_stages, changes)
rule_results = apply_subscription_rules(fe, changes)
for reason, sub_addrs in rule_results.items():
accumulate_reasons(addr_reasons, sub_addrs, reason)
@ -240,8 +244,7 @@ def make_review_requests_email(fe: FeatureEntry, gate_type: int, changes: Option
"""Return a list of task dicts to notify approvers of review requests."""
if changes is None:
changes = []
fe_stages = stage_helpers.get_feature_stages(fe.key.integer_id())
email_html = format_email_body(True, fe, fe_stages, changes)
email_html = format_email_body(True, fe, changes)
subject = 'Review Request for feature: %s' % fe.name
triggering_user_email = fe.updater_email

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

@ -100,12 +100,14 @@ class EmailFormattingTest(testing_config.CustomTestCase):
editor_emails=['feature_editor@example.com', 'owner_1@example.com'],
category=1, creator_email='creator_template@example.com',
updater_email='editor_template@example.com',
blink_components=['Blink'])
blink_components=['Blink'], feature_type=0)
self.template_ship_stage = Stage(feature_id=123, stage_type=160,
milestones=MilestoneSet(desktop_first=100))
self.template_ship_stage_2 = Stage(feature_id=123, stage_type=160,
milestones=MilestoneSet(desktop_first=103))
self.template_fe.put()
self.template_ship_stage.put()
self.template_stages = stage_helpers.get_feature_stages(123)
self.template_ship_stage_2.put()
self.template_fe.key = ndb.Key('FeatureEntry', 123)
self.template_fe.put()
@ -122,7 +124,7 @@ class EmailFormattingTest(testing_config.CustomTestCase):
"""We generate an email body for new features."""
with test_app.app_context():
body_html = notifier.format_email_body(
False, self.template_fe, self.template_stages, [])
False, self.template_fe, [])
# TESTDATA.make_golden(body_html, 'test_format_email_body__new.html')
self.assertEqual(body_html,
TESTDATA['test_format_email_body__new.html'])
@ -131,7 +133,7 @@ class EmailFormattingTest(testing_config.CustomTestCase):
"""We don't crash if the change list is emtpy."""
with test_app.app_context():
body_html = notifier.format_email_body(
True, self.template_fe, self.template_stages, [])
True, self.template_fe, [])
# TESTDATA.make_golden(body_html, 'test_format_email_body__update_no_changes.html')
self.assertEqual(body_html,
TESTDATA['test_format_email_body__update_no_changes.html'])
@ -140,7 +142,7 @@ class EmailFormattingTest(testing_config.CustomTestCase):
"""We generate an email body for an updated feature."""
with test_app.app_context():
body_html = notifier.format_email_body(
True, self.template_fe, self.template_stages, self.changes)
True, self.template_fe, self.changes)
# TESTDATA.make_golden(body_html, 'test_format_email_body__update_with_changes.html')
self.assertEqual(body_html,
TESTDATA['test_format_email_body__update_with_changes.html'])
@ -150,7 +152,7 @@ class EmailFormattingTest(testing_config.CustomTestCase):
self.fe_1.doc_links = ['https://developer.mozilla.org/look-here']
with test_app.app_context():
body_html = notifier.format_email_body(
True, self.template_fe, self.template_stages, self.changes)
True, self.template_fe, self.changes)
# TESTDATA.make_golden(body_html, 'test_format_email_body__mozdev_links_mozilla.html')
self.assertEqual(body_html,
TESTDATA['test_format_email_body__mozdev_links_mozilla.html'])
@ -159,7 +161,7 @@ class EmailFormattingTest(testing_config.CustomTestCase):
'https://hacker-site.org/developer.mozilla.org/look-here']
with test_app.app_context():
body_html = notifier.format_email_body(
True, self.template_fe, self.template_stages, self.changes)
True, self.template_fe, self.changes)
# TESTDATA.make_golden(body_html, 'test_format_email_body__mozdev_links_non_mozilla.html')
self.assertEqual(body_html,
TESTDATA['test_format_email_body__mozdev_links_non_mozilla.html'])
@ -235,7 +237,7 @@ class EmailFormattingTest(testing_config.CustomTestCase):
changes = [{'prop_name': 'shipped_android_milestone'}]
actual = notifier.apply_subscription_rules(
self.fe_1, self.fe_1_stages, changes)
self.fe_1, changes)
self.assertEqual(
{notifier.WEBVIEW_RULE_REASON: notifier.WEBVIEW_RULE_ADDRS},
@ -248,7 +250,7 @@ class EmailFormattingTest(testing_config.CustomTestCase):
changes = [{'prop_name': 'some_other_field'}] # irrelevant changesa
actual = notifier.apply_subscription_rules(
self.fe_1, self.fe_1_stages, changes)
self.fe_1, changes)
self.assertEqual({}, actual)
@ -258,14 +260,14 @@ class EmailFormattingTest(testing_config.CustomTestCase):
# No milestones of any kind set.
actual = notifier.apply_subscription_rules(
self.fe_1, self.fe_1_stages, changes)
self.fe_1, changes)
self.assertEqual({}, actual)
# Webview is also set
self.ship_stage.milestones.android_first = 88
self.ship_stage.milestones.webview_first = 89
actual = notifier.apply_subscription_rules(
self.fe_1, self.fe_1_stages, changes)
self.fe_1, changes)
self.assertEqual({}, actual)
@mock.patch('internals.notifier.format_email_body')
@ -323,7 +325,7 @@ class EmailFormattingTest(testing_config.CustomTestCase):
self.assertEqual('watcher_1@example.com', watcher_task['to'])
mock_f_e_b.assert_called_once_with(
False, self.fe_1, self.fe_1_stages, [])
False, self.fe_1, [])
@mock.patch('internals.notifier.format_email_body')
def test_make_feature_changes_email__update(self, mock_f_e_b):
@ -384,7 +386,7 @@ class EmailFormattingTest(testing_config.CustomTestCase):
self.assertEqual('watcher_1@example.com', watcher_task['to'])
mock_f_e_b.assert_called_once_with(
True, self.fe_1, self.fe_1_stages, self.changes)
True, self.fe_1, self.changes)
@mock.patch('internals.notifier.format_email_body')
@mock.patch('internals.approval_defs.get_approvers')
@ -415,7 +417,7 @@ class EmailFormattingTest(testing_config.CustomTestCase):
self.assertEqual('approver2@example.com', review_task_2['to'])
mock_f_e_b.assert_called_once_with(
True, self.fe_1, self.fe_1_stages, self.changes)
True, self.fe_1, self.changes)
mock_get_approvers.assert_called_once_with(1)
@mock.patch('internals.notifier.format_email_body')
@ -546,7 +548,7 @@ class EmailFormattingTest(testing_config.CustomTestCase):
self.assertEqual('watcher_1@example.com', watcher_task['to'])
mock_f_e_b.assert_called_once_with(
True, self.fe_1, self.fe_1_stages, self.changes)
True, self.fe_1, self.changes)
@mock.patch('internals.notifier.format_email_body')
@ -570,7 +572,7 @@ class EmailFormattingTest(testing_config.CustomTestCase):
self.assertEqual('owner_1@example.com', component_owner_task['to'])
self.assertEqual('watcher_1@example.com', watcher_task['to'])
mock_f_e_b.assert_called_once_with(
True, self.fe_2, self.fe_2_stages, self.changes)
True, self.fe_2, self.changes)
class FeatureStarTest(testing_config.CustomTestCase):

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

@ -15,6 +15,7 @@
from datetime import datetime, timedelta
import json
import logging
from typing import Any, Callable
import requests
from google.cloud import ndb # type: ignore
@ -22,10 +23,10 @@ from flask import render_template
from framework import basehandlers
from internals.core_models import FeatureEntry, MilestoneSet
from internals.legacy_models import Feature
from internals import notifier
from internals import stage_helpers
from internals.core_enums import STAGE_TYPES_BY_FIELD_MAPPING
from internals.core_enums import (
STAGE_TYPES_BY_FIELD_MAPPING)
import settings
@ -61,21 +62,27 @@ def choose_email_recipients(
def build_email_tasks(
features_to_notify, subject_format, body_template_path,
current_milestone_info, escalation_check):
email_tasks = []
features_to_notify: list[tuple[FeatureEntry, int]],
subject_format: str,
body_template_path: str,
current_milestone_info: dict,
escalation_check: Callable
) -> list[dict[str, Any]]:
email_tasks: list[dict[str, Any]] = []
beta_date = datetime.fromisoformat(current_milestone_info['earliest_beta'])
beta_date_str = beta_date.strftime('%Y-%m-%d')
for fe, mstone in features_to_notify:
# TODO(danielrsmith): the estimated-milestones-template.html is reliant
# on old Feature entity milestone fields and will need to be refactored
# to use Stage entity fields before removing this Feature use.
feature = Feature.get_by_id(fe.key.integer_id())
# Check if this notification should be escalated.
is_escalated = escalation_check(fe)
# Get stage information needed to display the template.
stage_info = stage_helpers.get_stage_info_for_templates(fe)
body_data = {
'id': fe.key.integer_id(),
'feature': feature,
'feature': fe,
'stage_info': stage_info,
'should_render_mstone_table': stage_info['should_render_mstone_table'],
'site_url': settings.SITE_URL,
'milestone': mstone,
'beta_date_str': beta_date_str,
@ -167,7 +174,10 @@ class AbstractReminderHandler(basehandlers.FlaskHandler):
return result
def determine_features_to_notify(self, current_milestone_info):
def determine_features_to_notify(
self,
current_milestone_info: dict
) -> list[tuple[FeatureEntry, int]]:
"""Get all features filter them by class-specific and milestone criteria."""
features = FeatureEntry.query(
FeatureEntry.deleted == False).fetch()

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

@ -14,11 +14,29 @@
# limitations under the License.
from collections import defaultdict
from typing import TypedDict
from api import converters
from internals.core_enums import INTENT_NONE
from internals.core_models import Stage
from internals.core_models import INTENT_STAGES_BY_STAGE_TYPE
from internals.core_enums import (
INTENT_NONE,
INTENT_STAGES_BY_STAGE_TYPE,
STAGE_TYPES_PROTOTYPE,
STAGE_TYPES_DEV_TRIAL,
STAGE_TYPES_ORIGIN_TRIAL,
STAGE_TYPES_EXTEND_ORIGIN_TRIAL,
STAGE_TYPES_SHIPPING)
from internals.core_models import FeatureEntry, MilestoneSet, Stage
# Type return value of get_stage_info_for_templates()
class StageTemplateInfo(TypedDict):
proto_stages: list[Stage]
dt_stages: list[Stage]
ot_stages: list[Stage]
extension_stages: list[Stage]
ship_stages: list[Stage]
should_render_mstone_table: bool
should_render_intents: bool
def get_feature_stages(feature_id: int) -> dict[int, list[Stage]]:
@ -58,7 +76,87 @@ def get_feature_stage_ids_list(feature_id: int) -> list[dict[str, int]]:
'intent_stage': INTENT_STAGES_BY_STAGE_TYPE.get(s.stage_type, INTENT_NONE)
} for s in q]
def get_ot_stage_extensions(ot_stage_id: int):
"""Return a list of extension stages associated with a stage in JSON format"""
q = Stage.query(Stage.ot_stage_id == ot_stage_id)
return [converters.stage_to_json_dict(stage) for stage in q]
def get_stage_info_for_templates(
fe: FeatureEntry) -> StageTemplateInfo:
"""Gather the information needed to display the estimated milestones table."""
# Only milestones from DevTrial, OT, or shipping stages are displayed.
id = fe.key.integer_id()
f_type = fe.feature_type or 0
proto_stage_type = STAGE_TYPES_PROTOTYPE[f_type]
dt_stage_type = STAGE_TYPES_DEV_TRIAL[f_type]
ot_stage_type = STAGE_TYPES_ORIGIN_TRIAL[f_type]
extension_stage_type = STAGE_TYPES_EXTEND_ORIGIN_TRIAL[f_type]
ship_stage_type = STAGE_TYPES_SHIPPING[f_type]
stage_info: StageTemplateInfo = {
'proto_stages': [],
'dt_stages': [],
'ot_stages': [],
'extension_stages': [],
'ship_stages': [],
# Note if any milestones that can be displayed are seen while organizing.
# This is used to check if rendering the milestone table is needed.
'should_render_mstone_table': False,
# Note if any intent URLs are seen while organizing.
# This is used to check if rendering the table is needed.
'should_render_intents': False,
}
for s in Stage.query(Stage.feature_id == id):
# Stage info is not needed if it's not the correct stage type.
if (s.stage_type != proto_stage_type and
s.stage_type != dt_stage_type and
s.stage_type != ot_stage_type and
s.stage_type != extension_stage_type and
s.stage_type != ship_stage_type):
continue
# If an intent thread is present in any stage,
# we should render the intents template.
if s.intent_thread_url is not None:
stage_info['should_render_intents'] = True
# Add stages to their respective lists.
if s.stage_type == proto_stage_type:
stage_info['proto_stages'].append(s)
# Make sure a MilestoneSet entity is referenced to avoid errors.
if s.milestones is None:
s.milestones = MilestoneSet()
m: MilestoneSet = s.milestones
if s.stage_type == dt_stage_type:
# Dev trial's announcement URL is rendered in templates like an intent.
if s.announcement_url is not None:
stage_info['should_render_intents'] = True
stage_info['dt_stages'].append(s)
if m.desktop_first or m.android_first or m.ios_first:
stage_info['should_render_mstone_table'] = True
if s.stage_type == ot_stage_type:
stage_info['ot_stages'].append(s)
if (m.desktop_first or m.android_first or m.webview_first or
m.desktop_last or m.android_last or m.webview_last):
stage_info['should_render_mstone_table'] = True
if s.stage_type == extension_stage_type:
stage_info['extension_stages'].append(s)
# Extension stages are not rendered
# in the milestones table; only for intents.
if s.stage_type == ship_stage_type:
stage_info['ship_stages'].append(s)
if m.desktop_first or m.android_first or m.webview_first or m.ios_first:
stage_info['should_render_mstone_table'] = True
# Returns a dictionary of stages needed for rendering info, as well as
# a boolean value representing whether or not the estimated milestones
# table will need to be rendered.
return stage_info

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

@ -12,7 +12,45 @@
<p><b>Estimated milestones</b>:
<p>No milestones specified</p>
<table>
<tr><td>Shipping on desktop</td>
<td>100</td></tr>
<tr><td>Shipping on desktop</td>
<td>103</td></tr>
</table>
<table>
</table>
<table>
</table>
<table>
</table>
</p>

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

@ -12,7 +12,45 @@
<p><b>Estimated milestones</b>:
<p>No milestones specified</p>
<table>
<tr><td>Shipping on desktop</td>
<td>100</td></tr>
<tr><td>Shipping on desktop</td>
<td>103</td></tr>
</table>
<table>
</table>
<table>
</table>
<table>
</table>
</p>

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

@ -10,7 +10,45 @@
<p><b>Estimated milestones</b>:
<p>No milestones specified</p>
<table>
<tr><td>Shipping on desktop</td>
<td>100</td></tr>
<tr><td>Shipping on desktop</td>
<td>103</td></tr>
</table>
<table>
</table>
<table>
</table>
<table>
</table>
</p>

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

@ -12,7 +12,45 @@
<p><b>Estimated milestones</b>:
<p>No milestones specified</p>
<table>
<tr><td>Shipping on desktop</td>
<td>100</td></tr>
<tr><td>Shipping on desktop</td>
<td>103</td></tr>
</table>
<table>
</table>
<table>
</table>
<table>
</table>
</p>

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

@ -12,7 +12,45 @@
<p><b>Estimated milestones</b>:
<p>No milestones specified</p>
<table>
<tr><td>Shipping on desktop</td>
<td>100</td></tr>
<tr><td>Shipping on desktop</td>
<td>103</td></tr>
</table>
<table>
</table>
<table>
</table>
<table>
</table>
</p>

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

@ -22,26 +22,19 @@
<table>
<tr><td>OriginTrial desktop first</td>
<td>100</td></tr>
</table>
<table>
</table>
@ -50,9 +43,6 @@
<table>
</table>
@ -61,7 +51,6 @@
<table>
</table>

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

@ -18,26 +18,19 @@
<table>
<tr><td>OriginTrial desktop first</td>
<td>100</td></tr>
</table>
<table>
</table>
@ -46,9 +39,6 @@
<table>
</table>
@ -57,7 +47,6 @@
<table>
</table>

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

@ -199,7 +199,7 @@ class FeatureEditHandlerTest(testing_config.CustomTestCase):
# rollout_platforms is not part of this form
with test_app.test_request_context(
'path', data={'form_fields': 'other,fields'}):
self.assertFalse(self.handler.touched('rollout_platforms', ['other, fields']))
self.assertFalse(self.handler.touched('rollout_platforms', ['other', 'fields']))
def test_post__anon(self):
"""Anon cannot edit features, gets a 403."""

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

@ -17,9 +17,10 @@
from api.converters import feature_entry_to_json_verbose
from internals import core_enums
from internals import processes
from internals import stage_helpers
from framework import basehandlers
from framework import permissions
from internals import processes
INTENT_PARAM = 'intent'
LAUNCH_PARAM = 'launch'
@ -49,9 +50,13 @@ class IntentEmailPreviewHandler(basehandlers.FlaskHandler):
def get_page_data(self, feature_id, f, intent_stage):
"""Return a dictionary of data used to render the page."""
stage_info = stage_helpers.get_stage_info_for_templates(f)
page_data = {
'subject_prefix': self.compute_subject_prefix(f, intent_stage),
'feature': feature_entry_to_json_verbose(f),
'stage_info': stage_info,
'should_render_mstone_table': stage_info['should_render_mstone_table'],
'should_render_intents': stage_info['should_render_intents'],
'sections_to_show': processes.INTENT_EMAIL_SECTIONS.get(
intent_stage, []),
'intent_stage': intent_stage,

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

@ -25,7 +25,7 @@ import settings
from google.cloud import ndb # type: ignore
from pages import intentpreview
from internals import core_enums
from internals import core_models
from internals.core_models import FeatureEntry, MilestoneSet, Stage
test_app = flask.Flask(__name__,
template_folder=settings.get_flask_template_path())
@ -36,10 +36,31 @@ TESTDATA = testing_config.Testdata(__file__)
class IntentEmailPreviewHandlerTest(testing_config.CustomTestCase):
def setUp(self):
self.feature_1 = core_models.FeatureEntry(
self.feature_1 = FeatureEntry(
name='feature one', summary='sum', owner_emails=['user1@google.com'],
category=1, intent_stage=core_enums.INTENT_IMPLEMENT)
category=1, intent_stage=core_enums.INTENT_IMPLEMENT,
feature_type=0)
self.feature_1.put()
# Write stages for the feature.
stage_types = [110, 120, 130, 140, 150, 151, 160, 1061]
for s_type in stage_types:
s = Stage(feature_id=self.feature_1.key.integer_id(), stage_type=s_type,
milestones=MilestoneSet(desktop_first=1,
android_first=1, desktop_last=2),
intent_thread_url=f'https://example.com/{s_type}')
# Add stage-specific fields based on the stage ID.
# 150 is the ID associated with the origin trial stage for feature type 0.
if s_type == 150:
s.experiment_goals = 'goals'
s.experiment_risks = 'risks'
s.announcement_url = 'https://example.com/announce'
# 151 is the stage ID associated with the origin trial extension.
elif s_type == 151:
s.experiment_extension_reason = 'reason'
# 151 is the ID associated with the shipping stage.
elif s_type == 160:
s.finch_url = 'https://example.com/finch'
s.put()
self.request_path = '/admin/features/launch/%d/%d?intent' % (
core_enums.INTENT_SHIP, self.feature_1.key.integer_id())
@ -191,7 +212,7 @@ class IntentEmailPreviewTemplateTest(testing_config.CustomTestCase):
def setUp(self):
super(IntentEmailPreviewTemplateTest, self).setUp()
self.feature_1 = core_models.FeatureEntry(
self.feature_1 = FeatureEntry(
name='feature one', summary='sum', owner_emails=['user1@google.com'],
category=1, intent_stage=core_enums.INTENT_IMPLEMENT)
# Hardcode the key for the template test

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

@ -198,7 +198,6 @@ limitations under the License.
<br><br><h4>Risks</h4>
<div style="margin-left: 4em;">
<br><br><h4>Interoperability and Compatibility</h4>

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

@ -82,11 +82,13 @@
{{feature.tag_review_status}}
{% endif %}
{% if feature.origin_trial_feedback_url %}
<br><br><h4>Link to origin trial feedback summary</h4>
{{feature.origin_trial_feedback_url}}
{% endif %}
{% for stage in stage_info.ot_stages %}
{% if stage.origin_trial_feedback_url %}
<br><br><h4>Link to origin trial feedback summary</h4>
{{stage.origin_trial_feedback_url}}
{% endif %}
{% endfor %}
<br><br><h4>Risks</h4>
<div style="margin-left: 4em;">
<br><br><h4>Interoperability and Compatibility</h4>
@ -156,8 +158,12 @@
{% endif %}
{% if 'extension_reason' in sections_to_show %}
<br><br><h4>Reason this experiment is being extended</h4>
<p class="preformatted">{{feature.experiment_extension_reason|urlize}}</p>
{% for stage in stage_info.extension_stages %}
{% if stage.experiment_extension_reason %}
<br><br><h4>Reason this experiment is being extended</h4>
<p class="preformatted">{{stage.experiment_extension_reason|urlize}}</p>
{% endif %}
{% endfor %}
{% endif %}
<br><br><h4>Ongoing technical constraints</h4>
@ -266,33 +272,34 @@
<a href="{{default_url}}">{{default_url}}</a>
{% if feature.intent_to_implement_url or feature.ready_for_trial_url or feature.intent_to_experiment_url or feature.intent_to_extend_experiment_url or feature.intent_to_ship_url %}
{% if should_render_intents %}
<br><br><h4>Links to previous Intent discussions</h4>
{% if feature.intent_to_implement_url %}
Intent to prototype: {{feature.intent_to_implement_url|urlize}}
<br>
{% endif %}
{% for stage in stage_info.proto_stages %}{% if stage.intent_thread_url %}
Intent to prototype: {{stage.intent_thread_url|urlize}}
{% if feature.ready_for_trial_url %}
Ready for Trial: {{feature.ready_for_trial_url|urlize}}
<br>
{% endif %}
{% endif %}{% endfor %}
{% for stage in stage_info.dt_stages %}{% if stage.announcement_url %}
Ready for Trial: {{stage.announcement_url|urlize}}
<br>
{% if feature.intent_to_experiment_url %}
Intent to Experiment: {{feature.intent_to_experiment_url|urlize}}
<br>
{% endif %}
{% endif %}{% endfor %}
{% for stage in stage_info.ot_stages %}{% if stage.intent_thread_url %}
Intent to Experiment: {{stage.intent_thread_url|urlize}}
<br>
{% if feature.intent_to_extend_experiment_url %}
Intent to Extend Experiment: {{feature.intent_to_extend_experiment_url|urlize}}
<br>
{% endif %}
{% endif %}{% endfor %}
{% for stage in stage_info.extension_stages %}
{% if stage.intent_thread_url %}
Intent to Extend Experiment: {{stage.intent_thread_url|urlize}}
<br>
{% if feature.intent_to_ship_url %}
Intent to Ship: {{feature.intent_to_ship_url|urlize}}
<br>
{% endif %}
{% endif %}{% endfor %}
{% for stage in stage_info.extension_stages %}{% if stage.intent_thread_url %}
Intent to Ship: {{stage.intent_thread_url|urlize}}
<br>
{% endif %}{% endfor %}
{% endif %}

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

@ -1,85 +1,86 @@
{% if feature.shipped_milestone or feature.ot_milestone_desktop_end or feature.ot_milestone_desktop_start or feature.dt_milestone_desktop_start or feature.shipped_android_milestone or feature.ot_milestone_android_end or feature.ot_milestone_android_start or feature.dt_milestone_android_start or feature.shipped_ios_milestone or feature.dt_milestone_ios_start or feature.shipped_webview_milestone or feature.ot_milestone_webview_end or feature.ot_milestone_webview_start %}
{% if should_render_mstone_table %}
<table>
{% if feature.shipped_milestone %}
{% for stage in stage_info.ship_stages %}{% if stage.milestones.desktop_first %}
<tr><td>Shipping on desktop</td>
<td>{{feature.shipped_milestone}}</td></tr>
{% endif %}
<td>{{stage.milestones.desktop_first}}</td></tr>
{% if feature.ot_milestone_desktop_end %}
{% endif %}{% endfor %}
{% for stage in stage_info.ot_stages %}{% if stage.milestones.desktop_last %}
<tr><td>OriginTrial desktop last</td>
<td>{{feature.ot_milestone_desktop_end}}</td></tr>
{% endif %}
<td>{{stage.milestones.desktop_last}}</td></tr>
{% if feature.ot_milestone_desktop_start %}
{% endif %}{% if stage.milestones.desktop_first %}
<tr><td>OriginTrial desktop first</td>
<td>{{feature.ot_milestone_desktop_start}}</td></tr>
{% endif %}
<td>{{stage.milestones.desktop_first}}</td></tr>
{% if feature.dt_milestone_desktop_start %}
{% endif %}{% endfor %}
{% for stage in stage_info.dt_stages %}{% if stage.milestones.desktop_first %}
<tr><td>DevTrial on desktop</td>
<td>{{feature.dt_milestone_desktop_start}}</td></tr>
{% endif %}
<td>{{stage.milestones.desktop_first}}</td></tr>
{% endif %}{% endfor %}
</table>
<table>
{% if feature.shipped_android_milestone %}
<tr><td>Shipping on Android</td>
<td>{{feature.shipped_android_milestone}}</td></tr>
{% endif %}
{% for stage in stage_info.ship_stages %}{% if stage.milestones.android_first %}
<tr><td>Shipping on Android</td>
<td>{{stage.milestones.android_first}}</td></tr>
{% if feature.ot_milestone_android_end %}
<tr><td>OriginTrial Android last</td>
<td>{{feature.ot_milestone_android_end}}</td></tr>
{% endif %}
{% endif %}{% endfor %}
{% for stage in stage_info.ot_stages %}{% if stage.milestones.android_last %}
<tr><td>OriginTrial Android last</td>
<td>{{stage.milestones.android_last}}</td></tr>
{% if feature.ot_milestone_android_start %}
<tr><td>OriginTrial Android first</td>
<td>{{feature.ot_milestone_android_start}}</td></tr>
{% endif %}
{% endif %}{% if stage.milestones.android_first %}
<tr><td>OriginTrial Android first</td>
<td>{{stage.milestones.android_first}}</td></tr>
{% if feature.dt_milestone_android_start %}
<tr><td>DevTrial on Android</td>
<td>{{feature.dt_milestone_android_start}}</td></tr>
{% endif %}
{% endif %}{% endfor %}
{% for stage in stage_info.dt_stages %}{% if stage.milestones.android_first %}
<tr><td>DevTrial on Android</td>
<td>{{stage.milestones.android_first}}</td></tr>
{% endif %}{% endfor %}
</table>
<table>
{% if feature.shipped_webview_milestone %}
{% for stage in stage_info.ship_stages %}{% if stage.milestones.webview_first %}
<tr><td>Shipping on WebView</td>
<td>{{feature.shipped_webview_milestone}}</td></tr>
{% endif %}
<td>{{stage.milestones.webview_first}}</td></tr>
{% if feature.ot_milestone_webview_end %}
<tr><td>OriginTrial webView last</td>
<td>{{feature.ot_milestone_webview_end}}</td></tr>
{% endif %}
{% endif %}{% endfor %}
{% for stage in stage_info.ot_stages %}{% if stage.milestones.webview_last %}
<tr><td>OriginTrial webView last</td>
<td>{{stage.milestones.webview_last}}</td></tr>
{% if feature.ot_milestone_webview_start %}
<tr><td>OriginTrial webView first</td>
<td>{{feature.ot_milestone_webview_start}}</td></tr>
{% endif %}
{% endif %}{% if stage.milestones.webview_first %}
<tr><td>OriginTrial webView first</td>
<td>{{stage.milestones.webview_first}}</td></tr>
{% endif %}{% endfor %}
</table>
<table>
{% if feature.shipped_ios_milestone %}
<tr><td>Shipping on iOS</td>
<td>{{feature.shipped_ios_milestone}}</td></tr>
{% endif %}
{% for stage in stage_info.ship_stages %}{% if stage.milestones.ios_first %}
<tr><td>Shipping on WebView</td>
<td>{{stage.milestones.ios_first}}</td></tr>
{% if feature.dt_milestone_ios_start %}
<tr><td>DevTrial on iOS</td>
<td>{{feature.dt_milestone_ios_start}}</td></tr>
{% endif %}
{% endif %}{% endfor %}
{% for stage in stage_info.dt_stages %}{% if stage.milestones.ios_first %}
<tr><td>DevTrial on iOS</td>
<td>{{stage.milestones.ios_first}}</td></tr>
{% endif %}{% endfor %}
</table>