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:
Родитель
c10fc20c1d
Коммит
2d5bb78f0f
|
@ -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>
|
||||
|
||||
|
|
Загрузка…
Ссылка в новой задаче