Add new endpoint to trigger intent post

This commit is contained in:
Daniel Smith 2024-07-07 18:01:32 -07:00
Родитель f87312fe95
Коммит 6d4614de01
9 изменённых файлов: 269 добавлений и 64 удалений

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

@ -0,0 +1,97 @@
# Copyright 2024 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 typing import TypedDict
from api import converters
from framework import basehandlers
from framework import cloud_tasks_helpers
from framework import permissions
from internals import processes
from internals import stage_helpers
from internals.core_enums import INTENT_STAGES_BY_STAGE_TYPE
from internals.core_models import FeatureEntry, Stage
from internals.data_types import VerboseFeatureDict
from internals.review_models import Gate
from pages.intentpreview import compute_subject_prefix
class IntentOptions(TypedDict):
subject: str
feature: VerboseFeatureDict
stage_info: stage_helpers.StageTemplateInfo
should_render_mstone_table: bool
should_render_intents: bool
sections_to_show: list[str]
intent_stage: int|None
default_url: str
intent_cc_emails: list[str]
class IntentsAPI(basehandlers.APIHandler):
def do_post(self, **kwargs):
"""Submit an intent email directly to blink-dev."""
feature_id = int(kwargs['feature_id'])
# Check that feature ID is valid.
if not feature_id:
self.abort(404, msg='No feature specified.')
feature: FeatureEntry|None = FeatureEntry.get_by_id(feature_id)
if feature is None:
self.abort(404, msg=f'Feature {feature_id} not found')
body = self.get_json_param_dict()
# Check that stage ID is valid.
stage_id = body.get('stage_id')
if not stage_id:
self.abort(404, msg='No stage specified')
stage: Stage|None = Stage.get_by_id(stage_id)
if stage is None:
self.abort(404, msg=f'Stage {stage_id} not found')
# Check that the user has feature edit permissions.
redirect_resp = permissions.validate_feature_edit_permission(
self, feature_id)
if redirect_resp:
return redirect_resp
intent_stage = INTENT_STAGES_BY_STAGE_TYPE[stage.stage_type]
stage_info = stage_helpers.get_stage_info_for_templates(feature)
default_url = (f'{self.request.scheme}://{self.request.host}'
f'/feature/{feature_id}')
# Add gate to Chromestatus URL query string if it is found.
gate: Gate|None = None
if body.get('gate_id'):
gate = Gate.get_by_id(body['gate_id'])
if gate:
default_url += f'?gate={gate.key.integer_id()}'
params: IntentOptions = {
'subject': compute_subject_prefix(feature, intent_stage),
'feature': converters.feature_entry_to_json_verbose(feature),
'stage_info': stage_helpers.get_stage_info_for_templates(feature),
'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,
'default_url': default_url,
'intent_cc_emails': body.get('intent_cc_emails', [])
}
cloud_tasks_helpers.enqueue_task('/tasks/email-intent-to-blink-dev',
params)
return {'message': 'Email task submitted successfully.'}

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

@ -1,8 +1,9 @@
import {LitElement, css, html, nothing} from 'lit';
import {SHARED_STYLES} from '../css/shared-css.js';
import {showToastMessage} from './utils';
import './chromedash-intent-template';
import {openPostIntentDialog} from './chromedash-post-intent-dialog.js'
import {STAGE_TYPES_SHIPPING} from './form-field-enums.js';
import './chromedash-intent-template';
class ChromedashGuideIntentPreview extends LitElement {
static get properties() {
@ -149,6 +150,7 @@ class ChromedashGuideIntentPreview extends LitElement {
class="button inline"
type="submit"
value="Post directly to blink-dev"
@click="${() => openPostIntentDialog()}"
/>
<chromedash-intent-template
appTitle="${this.appTitle}"

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

@ -0,0 +1,69 @@
import {LitElement, css, html} from 'lit';
import {SHARED_STYLES} from '../css/shared-css.js';
let dialogEl;
export async function openPostIntentDialog() {
if (!dialogEl) {
dialogEl = document.createElement('chromedash-post-intent-dialog');
document.body.appendChild(dialogEl);
await dialogEl.updateComplete;
}
dialogEl.show();
}
class ChromedashPostIntentDialog extends LitElement {
static get properties() {
return {
};
}
constructor() {
super();
}
static get styles() {
return [
...SHARED_STYLES,
css`
#prereqs-list li {
margin-left: 8px;
margin-bottom: 8px;
list-style: circle;
}
#prereqs-header {
margin-bottom: 8px;
}
#update-button {
margin-right: 8px;
}
.float-right {
float: right;
}
`,
];
}
show() {
this.shadowRoot!.querySelector('sl-dialog')!.show();
}
renderDialog() {
return html` <sl-dialog label="Post intent to blink-dev">
<p>
TODO(DanielRyanSmith): add confirmation plus CC email selection.
</p>
</sl-dialog>`;
}
render() {
return this.renderDialog();
}
}
customElements.define(
'chromedash-post-intent-dialog',
ChromedashPostIntentDialog
);

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

@ -645,6 +645,11 @@ export class ChromeStatusClient {
return this.doGet(`/features/${featureId}/process`);
}
// Intents API
async postIntentToBlinkDev(featureId, body) {
return this.doPost(`features/${featureId}/postintent`, body);
}
// Progress API
async getFeatureProgress(featureId) {
return this.doGet(`/features/${featureId}/progress`);

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

@ -45,6 +45,7 @@ from internals.user_models import (
OT_SUPPORT_EMAIL = 'origin-trials-support@google.com'
BLINK_DEV_EMAIL = 'blink-dev@chromium.org'
def _determine_milestone_string(ship_stages: list[Stage]) -> str:
@ -864,6 +865,37 @@ class OTExtensionApprovedHandler(basehandlers.FlaskHandler):
}
class IntentToBlinkDevHandler(basehandlers.FlaskHandler):
"""Submit an intent email directly to blink-dev."""
IS_INTERNAL_HANDLER = True
EMAIL_TEMPLATE_PATH = 'templates/blink/intent_to_implement.html'
def process_post_data(self, **kwargs):
self.require_task_header()
send_emails([self.build_email()])
return {'message': 'OK'}
def build_email(self):
json_data = self.get_json_param_dict()
template_data = {
'feature': json_data['feature'],
'stage_info': json_data['stage_info'],
'should_render_mston_table': json_data['should_render_mston_table'],
'should_render_intents': json_data['should_render_intents'],
'intent_stage': json_data['intent_stage'],
'default_url': json_data['default_url'],
}
body = render_template(self.EMAIL_TEMPLATE_PATH, **template_data)
return {
'to': BLINK_DEV_EMAIL,
'cc': json_data['intent_cc_emails'],
'subject': json_data['subject'],
'reply_to': None,
'html': body,
}
GLOBAL_OT_PROCESS_REMINDER_CC_LIST = [
OT_SUPPORT_EMAIL,
'origin-trials-timeline-updates@google.com'

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

@ -304,6 +304,7 @@ internals_routes: list[Route] = [
Route('/tasks/email-ot-extended', notifier.OTExtendedHandler),
Route('/tasks/email-ot-extension-approved',
notifier.OTExtensionApprovedHandler),
Route('/tasks/email-intent-to-blink-dev', notifier.IntentToBlinkDevHandler),
# OT process reminder emails
Route('/tasks/email-ot-first-branch', notifier.OTFirstBranchReminderHandler),

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

@ -29,6 +29,37 @@ LAUNCH_PARAM = 'launch'
VIEW_FEATURE_URL = '/feature'
def compute_subject_prefix(feature, intent_stage):
"""Return part of the subject line for an intent email."""
if intent_stage == core_enums.INTENT_IMPLEMENT:
if feature.feature_type == core_enums.FEATURE_TYPE_DEPRECATION_ID:
return 'Intent to Deprecate and Remove'
else:
return 'Intent to Prototype'
elif intent_stage == core_enums.INTENT_EXPERIMENT:
return 'Ready for Developer Testing'
elif intent_stage == core_enums.INTENT_ORIGIN_TRIAL:
if feature.feature_type == core_enums.FEATURE_TYPE_DEPRECATION_ID:
return 'Request for Deprecation Trial'
else:
return 'Intent to Experiment'
elif intent_stage == core_enums.INTENT_EXTEND_ORIGIN_TRIAL:
if feature.feature_type == core_enums.FEATURE_TYPE_DEPRECATION_ID:
return 'Intent to Extend Deprecation Trial'
else:
return 'Intent to Extend Experiment'
elif intent_stage == core_enums.INTENT_SHIP:
if feature.feature_type == core_enums.FEATURE_TYPE_CODE_CHANGE_ID:
return 'Web-Facing Change PSA'
else:
return 'Intent to Ship'
elif intent_stage == core_enums.INTENT_REMOVED:
return 'Intent to Extend Deprecation Trial'
return f'Intent stage "{core_enums.INTENT_STAGES[intent_stage]}"'
class IntentEmailPreviewHandler(basehandlers.FlaskHandler):
"""Show a preview of an intent email, as appropriate to the feature stage."""
@ -72,7 +103,7 @@ class IntentEmailPreviewHandler(basehandlers.FlaskHandler):
stage_info = stage_helpers.get_stage_info_for_templates(f)
page_data = {
'subject_prefix': self.compute_subject_prefix(f, intent_stage),
'subject_prefix': 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'],
@ -89,33 +120,3 @@ class IntentEmailPreviewHandler(basehandlers.FlaskHandler):
page_data[INTENT_PARAM] = True
return page_data
def compute_subject_prefix(self, feature, intent_stage):
"""Return part of the subject line for an intent email."""
if intent_stage == core_enums.INTENT_IMPLEMENT:
if feature.feature_type == core_enums.FEATURE_TYPE_DEPRECATION_ID:
return 'Intent to Deprecate and Remove'
else:
return 'Intent to Prototype'
elif intent_stage == core_enums.INTENT_EXPERIMENT:
return 'Ready for Developer Testing'
elif intent_stage == core_enums.INTENT_ORIGIN_TRIAL:
if feature.feature_type == core_enums.FEATURE_TYPE_DEPRECATION_ID:
return 'Request for Deprecation Trial'
else:
return 'Intent to Experiment'
elif intent_stage == core_enums.INTENT_EXTEND_ORIGIN_TRIAL:
if feature.feature_type == core_enums.FEATURE_TYPE_DEPRECATION_ID:
return 'Intent to Extend Deprecation Trial'
else:
return 'Intent to Extend Experiment'
elif intent_stage == core_enums.INTENT_SHIP:
if feature.feature_type == core_enums.FEATURE_TYPE_CODE_CHANGE_ID:
return 'Web-Facing Change PSA'
else:
return 'Intent to Ship'
elif intent_stage == core_enums.INTENT_REMOVED:
return 'Intent to Extend Deprecation Trial'
return 'Intent stage "%s"' % core_enums.INTENT_STAGES[intent_stage]

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

@ -45,7 +45,35 @@
{% if intent %}
<section>
<h3>Copy and send this text for your "Intent to ..." email</h3>
<p>Email to</p>
<div class="subject">
blink-dev@chromium.org
</div>
<p>Subject</p>
<div class="subject">
{{subject_prefix}}:
{{feature.name}}
</div>
{#
Insted of vertical margins, <br> elements are used to create line breaks
that can be copied and pasted into a text editor.
#}
<p>Body
<span class="tooltip copy-text" style="float:right"
title="Copy text to clipboard">
<a href="#" data-tooltip>
<iron-icon icon="chromestatus:content_copy"
id="copy-email-body"></iron-icon>
</a>
</span>
</p>
<div class="email">
{% include "blink/intent_to_implement.html" %}
</div> <!-- end email body div -->
</section>
{% endif %}

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

@ -1,31 +1,3 @@
<p>Email to</p>
<div class="subject">
blink-dev@chromium.org
</div>
<p>Subject</p>
<div class="subject">
{{subject_prefix}}:
{{feature.name}}
</div>
{#
Insted of vertical margins, <br> elements are used to create line breaks
that can be copied and pasted into a text editor.
#}
<p>Body
<span class="tooltip copy-text" style="float:right"
title="Copy text to clipboard">
<a href="#" data-tooltip>
<iron-icon icon="chromestatus:content_copy"
id="copy-email-body"></iron-icon>
</a>
</span>
</p>
<div class="email">
<h4>Contact emails</h4>
{% if not feature.browsers.chrome.owners %}None{% endif %}
{% for owner in feature.browsers.chrome.owners %}
@ -336,5 +308,3 @@
This intent message was generated by
<a href="https://chromestatus.com">Chrome Platform Status</a>.
</small></div>
</div> <!-- end email body div -->