diff --git a/api/intents_api.py b/api/intents_api.py new file mode 100644 index 00000000..8c47e0a0 --- /dev/null +++ b/api/intents_api.py @@ -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.'} diff --git a/client-src/elements/chromedash-guide-intent-preview.js b/client-src/elements/chromedash-guide-intent-preview.js index ce2a474a..4081eada 100644 --- a/client-src/elements/chromedash-guide-intent-preview.js +++ b/client-src/elements/chromedash-guide-intent-preview.js @@ -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()}" /> +

+ TODO(DanielRyanSmith): add confirmation plus CC email selection. +

+ `; + } + + render() { + return this.renderDialog(); + } +} + +customElements.define( + 'chromedash-post-intent-dialog', + ChromedashPostIntentDialog +); diff --git a/client-src/js-src/cs-client.js b/client-src/js-src/cs-client.js index 7b3d0ca0..6c7888c1 100644 --- a/client-src/js-src/cs-client.js +++ b/client-src/js-src/cs-client.js @@ -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`); diff --git a/internals/notifier.py b/internals/notifier.py index fc51d99d..a7f2f181 100644 --- a/internals/notifier.py +++ b/internals/notifier.py @@ -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' diff --git a/main.py b/main.py index be900dcd..155758cc 100644 --- a/main.py +++ b/main.py @@ -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), diff --git a/pages/intentpreview.py b/pages/intentpreview.py index 1717d8c2..d10e0288 100644 --- a/pages/intentpreview.py +++ b/pages/intentpreview.py @@ -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] diff --git a/templates/admin/features/launch.html b/templates/admin/features/launch.html index 3ed514f5..3b40fcb1 100644 --- a/templates/admin/features/launch.html +++ b/templates/admin/features/launch.html @@ -44,8 +44,36 @@ {% if intent %}
-

Copy and send this text for your "Intent to ..." email

-{% include "blink/intent_to_implement.html" %} +

Copy and send this text for your "Intent to ..." email

+

Email to

+
+ blink-dev@chromium.org +
+ +

Subject

+
+ {{subject_prefix}}: + {{feature.name}} +
+ + {# + Insted of vertical margins,
elements are used to create line breaks + that can be copied and pasted into a text editor. + #} + +

Body + + + + + +

+ +
{% endif %} diff --git a/templates/blink/intent_to_implement.html b/templates/blink/intent_to_implement.html index 0cfe7146..756436ea 100644 --- a/templates/blink/intent_to_implement.html +++ b/templates/blink/intent_to_implement.html @@ -1,31 +1,3 @@ -

Email to

-
- blink-dev@chromium.org -
- -

Subject

-
- {{subject_prefix}}: - {{feature.name}} -
- -{# - Insted of vertical margins,
elements are used to create line breaks - that can be copied and pasted into a text editor. -#} - -

Body - - - - - -

- -
-

Contact emails

{% 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 Chrome Platform Status.
- -