diff --git a/api/converters.py b/api/converters.py index 00642815..dcd9ffc5 100644 --- a/api/converters.py +++ b/api/converters.py @@ -14,11 +14,12 @@ # limitations under the License. import datetime -from typing import Any +from typing import Any, TypedDict from google.cloud import ndb # type: ignore from internals.core_enums import * from internals.core_models import FeatureEntry, MilestoneSet, Stage +from internals.data_types import StageDict, VerboseFeatureDict from internals.review_models import Vote, Gate from internals import approval_defs @@ -72,29 +73,38 @@ def _date_to_str(date: Optional[datetime.datetime]) -> Optional[str]: return str(date) if date is not None else None -def _val_to_list(items: Optional[list]) -> list: +def _val_to_list(items: list | None) -> list: """Returns the given list, or returns an empty list if null.""" return items if items is not None else [] -def _stage_attr( - stage: Optional[Stage], field: str, is_mstone: bool=False) -> Optional[Any]: +def _stage_attr(stage: Stage | None, field: str) -> Any: """Returns a specified field of a Stage entity.""" - if stage is None: - return None - if not is_mstone: - return getattr(stage, field) + return None if stage is None else getattr(stage, field) - if stage.milestones is None: +def _get_milestone_attr(stage: Stage | None, field: str) -> int | None: + """Returns a specified milestone field of a Stage entity.""" + if stage is None or stage.milestones is None: return None return getattr(stage.milestones, field) -def _prep_stage_gate_info( - fe: FeatureEntry, d: dict, +# Return type for _prep_stage_info function. +class StagePrepResponse(TypedDict): + proto: Stage | None + dev_trial: Stage | None + ot: Stage | None + extend: Stage | None + ship: Stage | None + rollout: Stage | None + all_stages: list[StageDict] + + +def _prep_stage_info( + fe: FeatureEntry, prefetched_stages: list[Stage] | None=None - ) -> dict[str, Optional[Stage]]: - """Adds stage and gate info to the dict and returns major stage info.""" + ) -> StagePrepResponse: + """Prepares stage info of a feature to help create JSON dictionaries.""" proto_type = STAGE_TYPES_PROTOTYPE[fe.feature_type] dev_trial_type = STAGE_TYPES_DEV_TRIAL[fe.feature_type] ot_type = STAGE_TYPES_ORIGIN_TRIAL[fe.feature_type] @@ -107,18 +117,18 @@ def _prep_stage_gate_info( prefetched_stages.sort(key=lambda s: s.stage_type) stages = prefetched_stages else: - stages = Stage.query(Stage.feature_id == d['id']).order(Stage.stage_type) + stages = Stage.query( + Stage.feature_id == fe.key.integer_id()).order(Stage.stage_type) - major_stages: dict[str, Optional[Stage]] = { + stage_info: StagePrepResponse = { 'proto': None, 'dev_trial': None, 'ot': None, 'extend': None, 'ship': None, - 'rollout': None} - - # Write a list of stages associated with the feature. - d['stages'] = [] + 'rollout': None, + # Write a list of all stages associated with the feature. + 'all_stages': []} # Keep track of trial stage indexes so that we can add trial extension # stages as a property of the trial stage later. @@ -127,296 +137,368 @@ def _prep_stage_gate_info( stage_dict = stage_to_json_dict(s, fe.feature_type) # Keep major stages for referencing additional fields. if s.stage_type == proto_type: - major_stages['proto'] = s + stage_info['proto'] = s elif s.stage_type == dev_trial_type: - major_stages['dev_trial'] = s + stage_info['dev_trial'] = s elif s.stage_type == ot_type: # Keep the stage's index to add trial extensions later. - ot_stage_indexes[s.key.integer_id()] = len(d['stages']) + ot_stage_indexes[s.key.integer_id()] = len(stage_info['all_stages']) stage_dict['extensions'] = [] - major_stages['ot'] = s + stage_info['ot'] = s elif s.stage_type == extend_type: # Trial extensions are kept as a list on the associated trial stage dict. if s.ot_stage_id and s.ot_stage_id in ot_stage_indexes: - (d['stages'][ot_stage_indexes[s.ot_stage_id]]['extensions'] + (stage_info['all_stages'][ot_stage_indexes[s.ot_stage_id]]['extensions'] .append(stage_dict)) - major_stages['extend'] = s + stage_info['extend'] = s # No need to append the extension stage to the overall stages list. continue elif s.stage_type == ship_type: - major_stages['ship'] = s + stage_info['ship'] = s elif s.stage_type == rollout_type: - major_stages['rollout'] = s - d['stages'].append(stage_dict) + stage_info['rollout'] = s + stage_info['all_stages'].append(stage_dict) - return major_stages - - -# This function is a workaround to reference extension stage info on -# origin trial stages. -# TODO(danielrsmith): Remove this once users can manually add trial extension -# stages. -def _add_ot_extension_fields(d: dict): - """Adds info from the trial extension stage to the OT stage dict.""" - extension_stage = Stage.query( - Stage.ot_stage_id == d['id']).get() - if extension_stage is None: - return - d['experiment_extension_reason'] = extension_stage.experiment_extension_reason - d['intent_to_extend_experiment_url'] = ( - extension_stage.intent_thread_url) + return stage_info def stage_to_json_dict( - stage: Stage, feature_type: int | None=None) -> dict[str, Any]: + stage: Stage, feature_type: int | None=None) -> StageDict: """Convert a stage entity into a JSON dict.""" # Get feature type if not supplied. if feature_type is None: f = FeatureEntry.get_by_id(stage.feature_id) feature_type = f.feature_type + d: StageDict = { + 'id': stage.key.integer_id(), + 'feature_id': stage.feature_id, + 'stage_type': stage.stage_type, + 'intent_stage': INTENT_STAGES_BY_STAGE_TYPE.get( + stage.stage_type, INTENT_NONE), + 'pm_emails': stage.pm_emails, + 'tl_emails': stage.tl_emails, + 'ux_emails': stage.ux_emails, + 'te_emails': stage.te_emails, + 'intent_thread_url': stage.intent_thread_url, - d: dict[str, Any] = {} - d['id'] = stage.key.integer_id() - d['feature_id'] = stage.feature_id - d['stage_type'] = stage.stage_type - d['intent_stage'] = INTENT_STAGES_BY_STAGE_TYPE.get( - d['stage_type'], INTENT_NONE) + 'announcement_url': stage.announcement_url, + 'intent_to_experiment_url': stage.intent_thread_url, + 'experiment_goals': stage.experiment_goals, + 'experiment_risks': stage.experiment_risks, + 'origin_trial_feedback_url': stage.origin_trial_feedback_url, + 'extensions': [], + 'experiment_extension_reason': stage.experiment_extension_reason, + 'intent_to_extend_experiment_url': stage.intent_thread_url, + 'ot_stage_id': stage.ot_stage_id, + 'intent_to_ship_url': stage.intent_thread_url, + 'finch_url': stage.finch_url, - # Determine the stage type and handle stage-specific fields. - # TODO(danielrsmith): Change client to use new field names. - milestone_field_names: list[dict] | None = None + 'rollout_milestone': stage.rollout_milestone, + 'rollout_platforms': stage.rollout_platforms, + 'rollout_details': stage.rollout_details, + 'rollout_impact': stage.rollout_impact, + 'enterprise_policies': stage.enterprise_policies, + + # Milestone fields to be populated later. + 'desktop_first': None, + 'android_first': None, + 'ios_first': None, + 'webview_first': None, + 'desktop_last': None, + 'android_last': None, + 'ios_last': None, + 'webview_last': None, + + # TODO(danielrsmith): Change client to use new field names. + # Legacy field names. + 'intent_to_implement_url': None, + 'intent_to_experiment_url': None, + 'intent_to_extend_experiment_url': None, + 'intent_to_ship_url': None, + 'ready_for_trial_url': stage.announcement_url, + + # Legacy milestone field names. + 'shipped_milestone': None, + 'shipped_android_milestone': None, + 'shipped_ios_milestone': None, + 'shipped_webview_milestone': None, + 'ot_milestone_desktop_start': None, + 'ot_milestone_desktop_end': None, + 'ot_milestone_android_start': None, + 'ot_milestone_android_end': None, + 'ot_milestone_webview_start': None, + 'ot_milestone_webview_end': None, + 'dt_milestone_desktop_start': None, + 'dt_milestone_android_start': None, + 'dt_milestone_ios_start': None, + 'dt_milestone_webview_start': None, + } + + # Determine milestone fields to use and intent fields to populate. + milestone_field_names: list[dict[str, str]] | None = None if d['stage_type'] == STAGE_TYPES_PROTOTYPE[feature_type]: d['intent_to_implement_url'] = stage.intent_thread_url elif d['stage_type'] == STAGE_TYPES_DEV_TRIAL[feature_type]: - d['ready_for_trial_url'] = stage.announcement_url milestone_field_names = MilestoneSet.DEV_TRIAL_MILESTONE_FIELD_NAMES elif d['stage_type'] == STAGE_TYPES_ORIGIN_TRIAL[feature_type]: d['intent_to_experiment_url'] = stage.intent_thread_url - d['experiment_goals'] = stage.experiment_goals - d['experiment_risks'] = stage.experiment_risks - d['origin_trial_feedback_url'] = stage.origin_trial_feedback_url - _add_ot_extension_fields(d) milestone_field_names = MilestoneSet.OT_MILESTONE_FIELD_NAMES elif d['stage_type'] == STAGE_TYPES_EXTEND_ORIGIN_TRIAL[feature_type]: - d['experiment_extension_reason'] = stage.experiment_extension_reason d['intent_to_extend_experiment_url'] = stage.intent_thread_url - d['ot_stage_id'] = stage.ot_stage_id milestone_field_names = MilestoneSet.OT_EXTENSION_MILESTONE_FIELD_NAMES elif d['stage_type'] == STAGE_TYPES_SHIPPING[feature_type]: d['intent_to_ship_url'] = stage.intent_thread_url - d['finch_url'] = stage.finch_url milestone_field_names = MilestoneSet.SHIPPING_MILESTONE_FIELD_NAMES - elif d['stage_type'] == STAGE_TYPES_ROLLOUT[feature_type]: - d['rollout_impact'] = stage.rollout_impact - d['rollout_milestone'] = stage.rollout_milestone - d['rollout_platforms'] = stage.rollout_platforms - d['rollout_details'] = stage.rollout_details - d['enterprise_policies'] = stage.enterprise_policies - # Add milestone fields + # Add milestone fields. if stage.milestones is not None and milestone_field_names is not None: for name_info in milestone_field_names: # The old val name is still used on the client side. # TODO(danielrsmith): Change client to use new field names. - d[name_info['old']] = getattr(stage.milestones, name_info['new']) - d[name_info['new']] = getattr(stage.milestones, name_info['new']) - - d['pm_emails'] = stage.pm_emails - d['tl_emails'] = stage.tl_emails - d['ux_emails'] = stage.ux_emails - d['te_emails'] = stage.te_emails - d['intent_thread_url'] = stage.intent_thread_url + # Must be type ignored until this is (eventually) removed. + d[name_info['old']] = getattr(stage.milestones, name_info['new']) # type: ignore + d[name_info['new']] = getattr(stage.milestones, name_info['new']) # type: ignore return d def feature_entry_to_json_verbose( fe: FeatureEntry, prefetched_stages: list[Stage] | None=None - ) -> dict[str, Any]: + ) -> VerboseFeatureDict: """Returns a verbose dictionary with all info about a feature.""" # Do not convert to JSON if the entity has not been saved. if not fe.key: - return {} + raise Exception('Unsaved FeatureEntry cannot be converted.') - d: dict[str, Any] = fe.to_dict() + id: int = fe.key.integer_id() - d['id'] = fe.key.integer_id() + # Get stage info, returning it to be more explicitly added. + stage_info = _prep_stage_info( + fe, prefetched_stages=prefetched_stages) - # Get stage and gate info, returning stage info to be more explicitly added. - stages = _prep_stage_gate_info(fe, d, prefetched_stages=prefetched_stages) - # Prototype stage fields. - d['intent_to_implement_url'] = _stage_attr( - stages['proto'], 'intent_thread_url') - - # Dev trial stage fields. - d['dt_milestone_desktop_start'] = _stage_attr( - stages['dev_trial'], 'desktop_first', True) - d['dt_milestone_android_start'] = _stage_attr( - stages['dev_trial'], 'android_first', True) - d['dt_milestone_ios_start'] = _stage_attr( - stages['dev_trial'], 'ios_first', True) - d['dt_milestone_webview_start'] = _stage_attr( - stages['dev_trial'], 'webview_first', True) - d['ready_for_trial_url'] = _stage_attr( - stages['dev_trial'], 'announcement_url') - - # Origin trial stage fields. - d['ot_milestone_desktop_start'] = _stage_attr( - stages['ot'], 'desktop_first', True) - d['ot_milestone_android_start'] = _stage_attr( - stages['ot'], 'android_first', True) - d['ot_milestone_webview_start'] = _stage_attr( - stages['ot'], 'webview_first', True) - d['ot_milestone_desktop_end'] = _stage_attr( - stages['ot'], 'desktop_last', True) - d['ot_milestone_android_end'] = _stage_attr( - stages['ot'], 'android_last', True) - d['ot_milestone_webview_end'] = _stage_attr( - stages['ot'], 'webview_last', True) - d['origin_trial_feeback_url'] = _stage_attr( - stages['ot'], 'origin_trial_feedback_url') - d['intent_to_experiment_url'] = _stage_attr( - stages['ot'], 'intent_thread_url') - d['experiment_goals'] = _stage_attr(stages['ot'], 'experiment_goals') - d['experiment_risks'] = _stage_attr(stages['ot'], 'experiment_risks') - d['announcement_url'] = _stage_attr(stages['ot'], 'announcement_url') - - # Extend origin trial stage fields. - d['experiment_extension_reason'] = _stage_attr( - stages['extend'], 'experiment_extension_reason') - d['intent_to_extend_experiment_url'] = _stage_attr( - stages['extend'], 'intent_thread_url') - - # Ship stage fields. - d['intent_to_ship_url'] = _stage_attr(stages['ship'], 'intent_thread_url') - d['finch_url'] = _stage_attr(stages['ship'], 'finch_url') - d['rollout_impact'] = _stage_attr(stages['rollout'], 'rollout_impact') - d['rollout_milestone'] = _stage_attr(stages['rollout'], 'rollout_milestone') - d['rollout_platforms'] = _stage_attr(stages['rollout'], 'rollout_platforms') - d['rollout_details'] = _stage_attr(stages['rollout'], 'rollout_details') - d['enterprise_policies'] = _stage_attr(stages['rollout'], 'enterprise_policies') - - # TODO(danielrsmith): Adjust the references to this JSON to use - # the new renamed field names. - impl_status_chrome = d.pop('impl_status_chrome', None) - standard_maturity = d.pop('standard_maturity', None) - d['is_released'] = fe.impl_status_chrome in RELEASE_IMPL_STATES - d['category'] = FEATURE_CATEGORIES[fe.category] - d['category_int'] = fe.category - d['enterprise_feature_categories'] = d.pop('enterprise_feature_categories', []) - if fe.feature_type is not None: - d['feature_type'] = FEATURE_TYPES[fe.feature_type] - d['feature_type_int'] = fe.feature_type - d['is_enterprise_feature'] = fe.feature_type == FEATURE_TYPE_ENTERPRISE_ID - if fe.intent_stage is not None: - d['intent_stage'] = INTENT_STAGES.get( - fe.intent_stage, INTENT_STAGES[INTENT_NONE]) - d['intent_stage_int'] = fe.intent_stage - d['active_stage_id'] = fe.active_stage_id - d['created'] = { - 'by': d.pop('creator_email', None), - 'when': _date_to_str(fe.created), - } - d['updated'] = { - 'by': d.pop('updater_email', None), - 'when': _date_to_str(fe.updated), - } - d['accurate_as_of'] = _date_to_str(fe.accurate_as_of) - d['standards'] = { - 'spec': fe.spec_link, - 'maturity': { - 'text': STANDARD_MATURITY_CHOICES.get(standard_maturity), - 'short_text': STANDARD_MATURITY_SHORT.get(standard_maturity), - 'val': standard_maturity, + d: VerboseFeatureDict = { + 'id': id, + 'name': fe.name, + 'summary': fe.summary, + 'blink_components': fe.blink_components or [], + 'star_count': fe.star_count, + 'search_tags': fe.search_tags or [], + 'created': { + 'by': fe.creator_email, + 'when': _date_to_str(fe.created), }, - } - d['spec_mentors'] = fe.spec_mentor_emails - d['tag_review_status'] = REVIEW_STATUS_CHOICES[fe.tag_review_status] - d['tag_review_status_int'] = fe.tag_review_status - d['security_review_status'] = REVIEW_STATUS_CHOICES[ - fe.security_review_status] - d['security_review_status_int'] = fe.security_review_status - d['privacy_review_status'] = REVIEW_STATUS_CHOICES[fe.privacy_review_status] - d['privacy_review_status_int'] = fe.privacy_review_status - d['resources'] = { - 'samples': _val_to_list(fe.sample_links), - 'docs': _val_to_list(fe.doc_links), - } - d['tags'] = d.pop('search_tags', None) - d['editors'] = d.pop('editor_emails', []) - d['cc_recipients'] = d.pop('cc_emails', []) - d['creator'] = fe.creator_email - d['comments'] = d.pop('feature_notes', None) + 'updated': { + 'by': fe.updater_email, + 'when': _date_to_str(fe.updated), + }, + 'category': FEATURE_CATEGORIES[fe.category], + 'category_int': fe.category, + 'feature_notes': fe.feature_notes, + 'enterprise_feature_categories': fe.enterprise_feature_categories or [], + 'stages': stage_info['all_stages'], + 'accurate_as_of': _date_to_str(fe.accurate_as_of), + 'creator_email': fe.creator_email, + 'updater_email': fe.updater_email, + 'owner_emails': fe.owner_emails or [], + 'editor_emails': fe.editor_emails or [], + 'cc_emails': fe.cc_emails or [], + 'spec_mentor_emails': fe.spec_mentor_emails or [], + 'unlisted': fe.unlisted, + 'deleted': fe.deleted, + 'editors': fe.editor_emails or [], + 'cc_recipients': fe.cc_emails or [], + 'spec_mentors': fe.spec_mentor_emails or [], + 'creator': fe.creator_email, + 'feature_type': FEATURE_TYPES[fe.feature_type], + 'feature_type_int': fe.feature_type, + 'intent_stage': INTENT_STAGES.get( + fe.intent_stage, INTENT_STAGES[INTENT_NONE]), + 'intent_stage_int': fe.intent_stage, + 'active_stage_id': fe.active_stage_id, + 'bug_url': fe.bug_url, + 'launch_bug_url': fe.launch_bug_url, + 'new_crbug_url': None, + 'breaking_change': fe.breaking_change, + 'flag_name': fe.flag_name, + 'ongoing_constraints': fe.ongoing_constraints, + 'motivation': fe.motivation, + 'devtrial_instructions': fe.devtrial_instructions, + 'activation_risks': fe.activation_risks, + 'measurement': fe.measurement, + 'availability_expectation': fe.availability_expectation, + 'adoption_expectation': fe.adoption_expectation, + 'adoption_plan': fe.adoption_plan, + 'initial_public_proposal_url': fe.initial_public_proposal_url, + 'explainer_links': fe.explainer_links, + 'requires_embedder_support': fe.requires_embedder_support, + 'spec_link': fe.spec_link, + 'api_spec': fe.api_spec, + 'interop_compat_risks': fe.interop_compat_risks, + 'all_platforms': fe.all_platforms, + 'all_platforms_descr': fe.all_platforms_descr, + 'non_oss_deps': fe.non_oss_deps, + 'anticipated_spec_changes': fe.anticipated_spec_changes, + 'security_risks': fe.security_risks, + 'ergonomics_risks': fe.ergonomics_risks, + 'wpt': fe.wpt, + 'wpt_descr': fe.wpt_descr, + 'webview_risks': fe.webview_risks, + 'devrel_emails': fe.devrel_emails or [], + 'debuggability': fe.debuggability, + 'doc_links': fe.doc_links or [], + 'sample_links': fe.sample_links or [], + 'prefixed': fe.prefixed, + 'tags': fe.search_tags, + 'tag_review': fe.tag_review, + 'tag_review_status': REVIEW_STATUS_CHOICES[fe.tag_review_status], + 'tag_review_status_int': fe.tag_review_status, + 'security_review_status': REVIEW_STATUS_CHOICES[fe.security_review_status], + 'security_review_status_int': fe.security_review_status, + 'privacy_review_status': REVIEW_STATUS_CHOICES[fe.privacy_review_status], + 'privacy_review_status_int': fe.privacy_review_status, + 'updated_display': None, + 'resources': { + 'samples': fe.sample_links or [], + 'docs': fe.doc_links or [], + }, + 'comments': fe.feature_notes, + 'ff_views': fe.ff_views or NO_PUBLIC_SIGNALS, + 'safari_views': fe.safari_views or NO_PUBLIC_SIGNALS, + 'web_dev_views': fe.web_dev_views or DEV_NO_SIGNALS, + 'browsers': { + 'chrome': { + 'bug': fe.bug_url, + 'blink_components': fe.blink_components or [], + 'devrel': fe.devrel_emails or [], + 'owners': fe.owner_emails or [], + 'origintrial': fe.impl_status_chrome == ORIGIN_TRIAL, + 'intervention': fe.impl_status_chrome == INTERVENTION, + 'prefixed': fe.prefixed, + 'flag': fe.impl_status_chrome == BEHIND_A_FLAG, + 'status': { + 'text': IMPLEMENTATION_STATUS[fe.impl_status_chrome], + 'val': fe.impl_status_chrome, + 'milestone_str': None + }, + + # TODO(danielrsmith): Find out if these are used and delete if not. + 'desktop': _get_milestone_attr(stage_info['ship'], 'desktop_first'), + 'android': _get_milestone_attr(stage_info['ship'], 'android_first'), + 'webview': _get_milestone_attr(stage_info['ship'], 'webview_first'), + 'ios': _get_milestone_attr(stage_info['ship'], 'ios_first'), - ff_views = d.pop('ff_views', NO_PUBLIC_SIGNALS) - safari_views = d.pop('safari_views', NO_PUBLIC_SIGNALS) - web_dev_views = d.pop('web_dev_views', DEV_NO_SIGNALS) - d['browsers'] = { - 'chrome': { - 'bug': fe.bug_url, - 'blink_components': d.pop('blink_components', []), - 'devrel': _val_to_list(fe.devrel_emails), - 'owners': d.pop('owner_emails', []), - 'origintrial': fe.impl_status_chrome == ORIGIN_TRIAL, - 'intervention': fe.impl_status_chrome == INTERVENTION, - 'prefixed': fe.prefixed, - 'flag': fe.impl_status_chrome == BEHIND_A_FLAG, - 'status': { - 'text': IMPLEMENTATION_STATUS[impl_status_chrome], - 'val': impl_status_chrome }, - 'desktop': _stage_attr(stages['ship'], 'desktop_first', True), - 'android': _stage_attr(stages['ship'], 'android_first', True), - 'webview': _stage_attr(stages['ship'], 'webview_first', True), - 'ios': _stage_attr(stages['ship'], 'ios_first', True), + 'ff': { + 'view': { + 'text': VENDOR_VIEWS.get( + fe.ff_views, VENDOR_VIEWS_COMMON[NO_PUBLIC_SIGNALS]), + 'val': fe.ff_views if fe.ff_views in VENDOR_VIEWS else NO_PUBLIC_SIGNALS, + 'url': fe.ff_views_link, + 'notes': fe.ff_views_notes, + }, + }, + 'safari': { + 'view': { + 'text': VENDOR_VIEWS.get( + fe.safari_views,VENDOR_VIEWS_COMMON[NO_PUBLIC_SIGNALS]), + 'val': (fe.safari_views if fe.safari_views in VENDOR_VIEWS + else NO_PUBLIC_SIGNALS), + 'url': fe.safari_views_link, + 'notes': fe.safari_views_notes, + }, + }, + 'webdev': { + 'view': { + 'text': WEB_DEV_VIEWS.get(fe.web_dev_views, WEB_DEV_VIEWS[DEV_NO_SIGNALS]), + 'val': (fe.web_dev_views if fe.web_dev_views in WEB_DEV_VIEWS + else DEV_NO_SIGNALS), + 'url': fe.web_dev_views_link, + 'notes': fe.web_dev_views_notes, + }, + }, + 'other': { + 'view': { + 'text': None, + 'val': None, + 'url': None, + 'notes':fe.other_views_notes, + }, + }, }, - 'ff': { - 'view': { - 'text': VENDOR_VIEWS.get(ff_views, - VENDOR_VIEWS_COMMON[NO_PUBLIC_SIGNALS]), - 'val': ff_views if ff_views in VENDOR_VIEWS else NO_PUBLIC_SIGNALS, - 'url': d.pop('ff_views_link', None), - 'notes': d.pop('ff_views_notes'), - } - }, - 'safari': { - 'view': { - 'text': VENDOR_VIEWS.get(safari_views, - VENDOR_VIEWS_COMMON[NO_PUBLIC_SIGNALS]), - 'val': (safari_views if safari_views in VENDOR_VIEWS - else NO_PUBLIC_SIGNALS), - 'url': d.pop('safari_views_link', None), - 'notes': d.pop('safari_views_notes', None), - } - }, - 'webdev': { - 'view': { - 'text': WEB_DEV_VIEWS.get(web_dev_views, - WEB_DEV_VIEWS[DEV_NO_SIGNALS]), - 'val': (web_dev_views if web_dev_views in WEB_DEV_VIEWS - else DEV_NO_SIGNALS), - 'url': d.pop('web_dev_views_link', None), - 'notes': d.pop('web_dev_views_notes', None), - } - }, - 'other': { - 'view': { - 'notes': d.pop('other_views_notes', None), - } + 'enterprise_feature_categories': fe.enterprise_feature_categories or [], + 'standards': { + 'spec': fe.spec_link, + 'maturity': { + 'text': STANDARD_MATURITY_CHOICES.get(fe.standard_maturity), + 'short_text': STANDARD_MATURITY_SHORT.get(fe.standard_maturity), + 'val': fe.standard_maturity, + }, }, + 'is_released': fe.impl_status_chrome in RELEASE_IMPL_STATES, + 'is_enterprise_feature': fe.feature_type == FEATURE_TYPE_ENTERPRISE_ID, + + 'experiment_timeline': fe.experiment_timeline, + # TODO(danielrsmith): Adjust the references to this JSON to use + # the new renamed field names. + # Prototype stage fields. + 'intent_to_implement_url': _stage_attr( + stage_info['proto'], 'intent_thread_url'), + # Dev trial stage fields. + 'dt_milestone_desktop_start': _get_milestone_attr( + stage_info['dev_trial'], 'desktop_first'), + 'dt_milestone_android_start': _get_milestone_attr( + stage_info['dev_trial'], 'android_first'), + 'dt_milestone_ios_start': _get_milestone_attr( + stage_info['dev_trial'], 'ios_first'), + 'dt_milestone_webview_start': _get_milestone_attr( + stage_info['dev_trial'], 'webview_first'), + 'ready_for_trial_url': _stage_attr( + stage_info['dev_trial'], 'announcement_url'), + # Origin trial stage fields. + 'ot_milestone_desktop_start': _get_milestone_attr( + stage_info['ot'], 'desktop_first'), + 'ot_milestone_android_start': _get_milestone_attr( + stage_info['ot'], 'android_first'), + 'ot_milestone_webview_start': _get_milestone_attr( + stage_info['ot'], 'webview_first'), + 'ot_milestone_desktop_end': _get_milestone_attr( + stage_info['ot'], 'desktop_last'), + 'ot_milestone_android_end': _get_milestone_attr( + stage_info['ot'], 'android_last'), + 'ot_milestone_webview_end': _get_milestone_attr( + stage_info['ot'], 'webview_last'), + 'origin_trial_feeback_url': _stage_attr( + stage_info['ot'], 'origin_trial_feedback_url'), + 'intent_to_experiment_url': _stage_attr( + stage_info['ot'], 'intent_thread_url'), + 'experiment_goals': _stage_attr(stage_info['ot'], 'experiment_goals'), + 'experiment_risks': _stage_attr(stage_info['ot'], 'experiment_risks'), + 'announcement_url': _stage_attr(stage_info['ot'], 'announcement_url'), + # Extend origin trial stage fields. + 'experiment_extension_reason': _stage_attr( + stage_info['extend'], 'experiment_extension_reason'), + 'intent_to_extend_experiment_url': _stage_attr( + stage_info['extend'], 'intent_thread_url'), + # Ship stage fields. + 'intent_to_ship_url': _stage_attr(stage_info['ship'], 'intent_thread_url'), + 'finch_url': _stage_attr(stage_info['ship'], 'finch_url'), + 'rollout_milestone': _stage_attr(stage_info['rollout'], 'rollout_milestone'), + 'rollout_details': _stage_attr(stage_info['rollout'], 'rollout_details'), + 'rollout_impact': _stage_attr(stage_info['rollout'], 'rollout_impact'), } - if d['is_released'] and _stage_attr(stages['ship'], 'desktop_first', True): - d['browsers']['chrome']['status']['milestone_str'] = ( - _stage_attr(stages['ship'], 'desktop_first', True)) - elif d['is_released'] and _stage_attr(stages['ship'], 'android_first', True): - d['browsers']['chrome']['status']['milestone_str'] = ( - _stage_attr(stages['ship'], 'android_first', True)) + if (d['is_released'] and + _get_milestone_attr(stage_info['ship'], 'desktop_first')): + d['browsers']['chrome']['status']['milestone_str'] = str( + _get_milestone_attr(stage_info['ship'], 'desktop_first')) + elif (d['is_released'] and + _get_milestone_attr(stage_info['ship'], 'android_first')): + d['browsers']['chrome']['status']['milestone_str'] = str( + _get_milestone_attr(stage_info['ship'], 'android_first')) else: d['browsers']['chrome']['status']['milestone_str'] = ( d['browsers']['chrome']['status']['text']) - del_none(d) # Further prune response by removing null/[] values. return d diff --git a/api/converters_test.py b/api/converters_test.py index ae3a8698..759222ee 100644 --- a/api/converters_test.py +++ b/api/converters_test.py @@ -248,9 +248,9 @@ class FeatureConvertersTest(testing_config.CustomTestCase): 'other': { 'view': { 'notes': 'other notes', - } - } - } + }, + }, + }, } self.assertEqual(result, expected) @@ -333,7 +333,6 @@ class FeatureConvertersTest(testing_config.CustomTestCase): 'when': str(self.date) }, 'accurate_as_of': str(self.date), - 'outstanding_notifications': 0, 'resources': { 'samples': ['https://example.com/samples'], 'docs': ['https://example.com/docs'], @@ -347,6 +346,64 @@ class FeatureConvertersTest(testing_config.CustomTestCase): 'val': 1, }, }, + + 'activation_risks': None, + 'active_stage_id': None, + 'adoption_expectation': None, + 'adoption_plan': None, + 'all_platforms': None, + 'all_platforms_descr': None, + 'anticipated_spec_changes': None, + 'availability_expectation': None, + 'blink_components': ['Blink'], + + 'cc_emails': [], + 'cc_recipients': [], + 'creator_email': 'creator@example.com', + 'debuggability': None, + 'devtrial_instructions': None, + 'dt_milestone_ios_start': None, + 'dt_milestone_webview_start': None, + 'editor_emails': ['feature_editor@example.com', 'owner_1@example.com'], + 'enterprise_feature_categories': [], + 'ergonomics_risks': None, + 'experiment_timeline': None, + 'explainer_links': [], + 'feature_notes': 'notes', + 'ff_views': 5, + 'flag_name': None, + 'initial_public_proposal_url': None, + 'interop_compat_risks': None, + 'measurement': None, + 'motivation': None, + 'new_crbug_url': None, + 'non_oss_deps': None, + 'ongoing_constraints': None, + 'origin_trial_feeback_url': None, + 'ot_milestone_android_end': None, + 'ot_milestone_webview_end': None, + 'ot_milestone_webview_start': None, + 'owner_emails': ['feature_owner@example.com'], + 'ot_milestone_webview_end': None, + 'ot_milestone_webview_start': None, + 'owner_emails': ['feature_owner@example.com'], + 'ready_for_trial_url': None, + 'rollout_details': None, + 'rollout_milestone': None, + 'safari_views': 1, + 'search_tags': [], + 'security_risks': None, + 'spec_mentor_emails': [], + 'spec_mentors': [], + 'tag_review': None, + 'tags': [], + 'updated_display': None, + 'updater_email': 'updater@example.com', + 'web_dev_views': 1, + 'webview_risks': None, + 'wpt': None, + 'wpt_descr': None, + 'tag_review_status': 'Pending', 'tag_review_status_int': 1, 'security_review_status': 'Issues open', @@ -364,12 +421,15 @@ class FeatureConvertersTest(testing_config.CustomTestCase): 'owners':['feature_owner@example.com'], 'desktop': 1, 'android': 1, + 'ios': None, + 'origintrial': False, 'intervention': False, 'prefixed': False, 'flag': False, + 'webview': None, 'status': { - 'milestone_str': 1, + 'milestone_str': '1', 'text': 'Enabled by default', 'val': 5 } @@ -392,6 +452,7 @@ class FeatureConvertersTest(testing_config.CustomTestCase): }, 'webdev': { 'view': { + 'notes': None, 'text': 'Strongly positive', 'val': 1, 'url': 'https://example.com/web_dev', @@ -399,10 +460,13 @@ class FeatureConvertersTest(testing_config.CustomTestCase): }, 'other': { 'view': { - 'notes': 'other notes', - } - } - } + 'notes': 'other notes', + 'text': None, + 'url': None, + 'val': None, + }, + }, + }, } self.assertEqual(result, expected) @@ -430,9 +494,8 @@ class FeatureConvertersTest(testing_config.CustomTestCase): """Function handles an empty feature.""" empty_fe = FeatureEntry() - result = converters.feature_entry_to_json_verbose(empty_fe) - - self.assertEqual(result, {}) + with self.assertRaises(Exception): + converters.feature_entry_to_json_verbose(empty_fe) class VoteConvertersTest(testing_config.CustomTestCase): diff --git a/api/features_api.py b/api/features_api.py index 5cf8109e..b1c60723 100644 --- a/api/features_api.py +++ b/api/features_api.py @@ -13,7 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import Any, Optional +from typing import Optional, TypedDict from api import converters from framework import basehandlers @@ -22,19 +22,21 @@ from framework import rediscache from framework import users from internals.core_enums import * from internals.core_models import FeatureEntry +from internals.data_types import VerboseFeatureDict from internals import feature_helpers from internals import search + class FeaturesAPI(basehandlers.APIHandler): """Features are the the main records that we track.""" - def get_one_feature(self, feature_id: int) -> dict[str, Any]: + def get_one_feature(self, feature_id: int) -> VerboseFeatureDict: feature = FeatureEntry.get_by_id(feature_id) if not feature: self.abort(404, msg='Feature %r not found' % feature_id) return converters.feature_entry_to_json_verbose(feature) - def do_search(self) -> dict[str, Any]: + def do_search(self): user = users.get_current_user() # Show unlisted features to site editors or admins. show_unlisted_features = permissions.can_edit_any_feature(user) @@ -69,8 +71,11 @@ class FeaturesAPI(basehandlers.APIHandler): 'features': features_on_page, } - def do_get(self, **kwargs) -> dict[str, Any]: + def do_get(self, **kwargs): """Handle GET requests for a single feature or a search.""" + # TODO(danielrsmith): This request gives two independent return types + # based on whether a feature_id was specified. Determine the best + # way to handle this in a strictly-typed manner and implement it. feature_id = kwargs.get('feature_id', None) if feature_id: return self.get_one_feature(feature_id) diff --git a/api/stages_api_test.py b/api/stages_api_test.py index d18c7937..2525ec80 100644 --- a/api/stages_api_test.py +++ b/api/stages_api_test.py @@ -74,31 +74,55 @@ class StagesAPITest(testing_config.CustomTestCase): self.stage_5.put() self.expected_stage_1 = { - 'id': 10, - 'feature_id': 1, - 'stage_type': 150, - 'intent_stage': 3, - 'pm_emails': [], - 'tl_emails': [], - 'ux_emails': ['ux_person@example.com'], - 'te_emails': [], - 'intent_thread_url': 'https://example.com/intent', - 'intent_to_experiment_url': 'https://example.com/intent', - 'desktop_first': 100, - 'desktop_last': None, 'android_first': None, 'android_last': None, - 'webview_first': None, - 'webview_last': None, + 'announcement_url': None, + 'desktop_first': 100, + 'desktop_last': None, + 'dt_milestone_android_start': None, + 'dt_milestone_desktop_start': None, + 'dt_milestone_ios_start': None, + 'dt_milestone_webview_start': None, + 'enterprise_policies': [], + 'experiment_extension_reason': None, 'experiment_goals': 'To be the very best.', 'experiment_risks': None, + 'extensions': [], + 'feature_id': 1, + 'finch_url': None, + 'id': 10, + 'intent_stage': 3, + 'intent_thread_url': 'https://example.com/intent', + 'intent_to_experiment_url': 'https://example.com/intent', + 'intent_to_extend_experiment_url': None, + 'intent_to_implement_url': None, + 'intent_to_ship_url': None, + 'ios_first': None, + 'ios_last': None, + 'origin_trial_feedback_url': None, 'ot_milestone_android_end': None, 'ot_milestone_android_start': None, 'ot_milestone_desktop_end': None, 'ot_milestone_desktop_start': 100, 'ot_milestone_webview_end': None, 'ot_milestone_webview_start': None, - 'origin_trial_feedback_url': None} + 'ot_stage_id': None, + 'pm_emails': [], + 'ready_for_trial_url': None, + 'rollout_details': None, + 'rollout_impact': 2, + 'rollout_milestone': None, + 'rollout_platforms': [], + 'shipped_android_milestone': None, + 'shipped_ios_milestone': None, + 'shipped_milestone': None, + 'shipped_webview_milestone': None, + 'stage_type': 150, + 'te_emails': [], + 'tl_emails': [], + 'ux_emails': ['ux_person@example.com'], + 'webview_first': None, + 'webview_last': None} self.handler = stages_api.StagesAPI() self.request_path = '/api/v0/features/' @@ -164,7 +188,31 @@ class StagesAPITest(testing_config.CustomTestCase): 'ot_milestone_desktop_start': 100, 'ot_milestone_webview_end': None, 'ot_milestone_webview_start': None, - 'origin_trial_feedback_url': None} + 'origin_trial_feedback_url': None, + 'announcement_url': None, + 'dt_milestone_android_start': None, + 'dt_milestone_desktop_start': None, + 'dt_milestone_ios_start': None, + 'dt_milestone_webview_start': None, + 'enterprise_policies': [], + 'experiment_extension_reason': None, + 'extensions': [], + 'finch_url': None, + 'intent_to_extend_experiment_url': None, + 'intent_to_implement_url': None, + 'intent_to_ship_url': None, + 'ios_first': None, + 'ios_last': None, + 'ot_stage_id': 40, + 'ready_for_trial_url': None, + 'rollout_details': None, + 'rollout_impact': 2, + 'rollout_milestone': None, + 'rollout_platforms': [], + 'shipped_android_milestone': None, + 'shipped_ios_milestone': None, + 'shipped_milestone': None, + 'shipped_webview_milestone': None} expect = { 'id': 40, @@ -177,7 +225,7 @@ class StagesAPITest(testing_config.CustomTestCase): 'te_emails': [], 'intent_thread_url': 'https://example.com/intent', 'intent_to_experiment_url': 'https://example.com/intent', - 'intent_to_extend_experiment_url': 'https://example.com/intent', + 'intent_to_extend_experiment_url': None, 'desktop_first': 100, 'desktop_last': None, 'android_first': None, @@ -188,13 +236,36 @@ class StagesAPITest(testing_config.CustomTestCase): 'experiment_goals': 'To be the very best.', 'experiment_risks': None, 'extensions': [extension], + 'announcement_url': None, 'ot_milestone_android_end': None, 'ot_milestone_android_start': None, 'ot_milestone_desktop_end': None, 'ot_milestone_desktop_start': 100, 'ot_milestone_webview_end': None, 'ot_milestone_webview_start': None, - 'origin_trial_feedback_url': None} + 'dt_milestone_android_start': None, + 'dt_milestone_desktop_start': None, + 'dt_milestone_ios_start': None, + 'dt_milestone_webview_start': None, + 'enterprise_policies': [], + 'origin_trial_feedback_url': None, + 'ready_for_trial_url': None, + 'rollout_details': None, + 'rollout_impact': 2, + 'rollout_milestone': None, + 'rollout_platforms': [], + 'shipped_android_milestone': None, + 'shipped_ios_milestone': None, + 'shipped_milestone': None, + 'shipped_webview_milestone': None, + 'ot_stage_id': None, + 'intent_to_implement_url': None, + 'intent_to_ship_url': None, + 'ios_first': None, + 'ios_last': None, + 'finch_url': None, + +} with test_app.test_request_context(f'{self.request_path}1/stages/10'): actual = self.handler.do_get(feature_id=1, stage_id=40) diff --git a/internals/core_models.py b/internals/core_models.py index 8871d3fd..6762418e 100644 --- a/internals/core_models.py +++ b/internals/core_models.py @@ -171,8 +171,6 @@ class MilestoneSet(ndb.Model): # copy from milestone fields of Feature 'ot_milestone_desktop_end': 'desktop_last', 'ot_milestone_android_start': 'android_first', 'ot_milestone_android_end': 'android_last', - 'ot_milestone_ios_start': 'ios_first', - 'ot_milestone_ios_end': 'ios_last', 'ot_milestone_webview_start': 'webview_first', 'ot_milestone_webview_end': 'webview_last', 'dt_milestone_desktop_start': 'desktop_first', diff --git a/internals/data_types.py b/internals/data_types.py new file mode 100644 index 00000000..bb62b535 --- /dev/null +++ b/internals/data_types.py @@ -0,0 +1,312 @@ +# -*- coding: utf-8 -*- +# Copyright 2023 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License") +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Import needed to reference a class within its own class method. +# https://stackoverflow.com/a/33533514 +from __future__ import annotations + +from typing import TypedDict + +from internals.core_enums import * + + +# JSON representation of Stage entity data. +class StageDict(TypedDict): + id: int + feature_id: int + stage_type: int + intent_stage: int + intent_thread_url: str | None + + # Dev trial specific fields. + ready_for_trial_url: str | None + announcement_url: str | None + + + # Origin trial specific fields. + experiment_goals: str | None + experiment_risks: str | None + extensions: list[StageDict] # type: ignore + origin_trial_feedback_url: str | None + + # Trial extension specific fields. + ot_stage_id: int | None + experiment_extension_reason: str | None + + # Ship specific fields + finch_url: str | None + + # Enterprise specific fields + rollout_details: str | None + rollout_impact: int | None + rollout_milestone: int | None + rollout_platforms: list[str] + enterprise_policies: list[str] + + # Email information + pm_emails: list[str] + tl_emails: list[str] + ux_emails: list[str] + te_emails: list[str] + + # Milestone fields + desktop_first: int | None + android_first: int | None + ios_first: int | None + webview_first: int | None + desktop_last: int | None + android_last: int | None + ios_last: int | None + webview_last: int | None + + # Legacy fields or fields that now live on stage entities. + # TODO(danielrsmith): stop representing these on the feature dict + # and references them direct from stage entities. + intent_to_implement_url: str | None + intent_to_experiment_url: str | None + intent_to_extend_experiment_url: str | None + intent_to_ship_url: str | None + + # Legacy milestone fields that now live on stage entities. + dt_milestone_desktop_start: int | None + dt_milestone_android_start: int | None + dt_milestone_ios_start: int | None + dt_milestone_webview_start: int | None + ot_milestone_desktop_start: int | None + ot_milestone_android_start: int | None + ot_milestone_webview_start: int | None + ot_milestone_desktop_end: int | None + ot_milestone_android_end: int | None + ot_milestone_webview_end: int | None + shipped_milestone: int | None + shipped_android_milestone: int | None + shipped_ios_milestone: int | None + shipped_webview_milestone: int | None + + +############################# +## FeatureDict definitions ## +############################# +# Nested JSON type definitions. +class FeatureDictInnerResourceInfo(TypedDict): + samples: list[str] + docs: list[str] + + +class FeatureDictInnerStandardsInfo(TypedDict): + spec: str | None + maturity: FeatureDictInnerMaturityInfo + + +class FeatureDictInnerMaturityInfo(TypedDict): + text: str | None + short_text: str | None + val: int + + +class FeatureDictInnerBrowserStatus(TypedDict): + text: str | None + val: str | None + milestone_str: str | None + + +class FeatureDictInnerViewInfo(TypedDict): + text: str | None + val: int | None + url: str | None + notes: str | None + + +class FeatureDictInnerChromeBrowserInfo(TypedDict): + bug: str | None + blink_components: list[str] | None + devrel: list[str] | None + owners: list[str] | None + origintrial: bool | None + intervention: bool | None + prefixed: bool | None + flag: bool | None + status: FeatureDictInnerBrowserStatus + desktop: int | None + android: int | None + webview: int | None + ios: int | None + + +class FeatureDictInnerSingleBrowserInfo(TypedDict): + view: FeatureDictInnerViewInfo | None + + +class FeatureBrowsersInfo(TypedDict): + chrome: FeatureDictInnerChromeBrowserInfo + ff: FeatureDictInnerSingleBrowserInfo + safari: FeatureDictInnerSingleBrowserInfo + webdev: FeatureDictInnerSingleBrowserInfo + other: FeatureDictInnerSingleBrowserInfo + + +# Basic user info displayed for create/update attributes in +# FeatureEntry edit information. +class FeatureDictInnerUserEditInfo(TypedDict): + by: str | None + when: str | None + + +# JSON representation of FeatureEntry entity. Created from +# converters.feature_entry_to_json_verbose(). +class VerboseFeatureDict(TypedDict): + + # Metadata: Creation and updates. + id: int + created: FeatureDictInnerUserEditInfo + updated: FeatureDictInnerUserEditInfo + accurate_as_of: str | None + creator_email: str | None + updater_email: str | None + + # Metadata: Access controls + owner_emails: list[str] + editor_emails: list[str] + cc_emails: list[str] + spec_mentor_emails: list[str] + unlisted: bool + deleted: bool + + # Renamed metadata fields + editors: list[str] + cc_recipients: list[str] + spec_mentors: list[str] + creator: str | None + + # Descriptive info. + name: str + summary: str + category: str + category_int: int + blink_components: list[str] + star_count: int + search_tags: list[str] + feature_notes: str | None + enterprise_feature_categories: list[str] + + # Metadata: Process information + feature_type: str + feature_type_int: int + intent_stage: str + intent_stage_int: int + active_stage_id: int | None + bug_url: str | None + launch_bug_url: str | None + breaking_change: bool + + # Implementation in Chrome + flag_name: str | None + ongoing_constraints: str | None + + # Topic: Adoption + motivation: str | None + devtrial_instructions: str | None + activation_risks: str | None + measurement: str | None + availability_expectation: str | None + adoption_expectation: str | None + adoption_plan: str | None + + # Gate: Standardization and Interop + initial_public_proposal_url: str | None + explainer_links: list[str] + requires_embedder_support: bool + spec_link: str | None + api_spec: str | None + prefixed: bool | None + interop_compat_risks: str | None + all_platforms: bool | None + all_platforms_descr: bool | None + tag_review: str | None + non_oss_deps: str | None + anticipated_spec_changes: str | None + + # Gate: Security & Privacy + security_risks: str | None + tags: list[str] + tag_review_status: str + tag_review_status_int: int | None + security_review_status: str + security_review_status_int: int | None + privacy_review_status: str + privacy_review_status_int: int | None + + # Gate: Testing / Regressions + ergonomics_risks: str | None + wpt: bool | None + wpt_descr: str | None + webview_risks: str | None + + # Gate: Devrel & Docs + devrel_emails: list[str] + debuggability: str | None + doc_links: list[str] + sample_links: list[str] + + stages: list[StageDict] + + # Legacy fields or fields that now live on stage entities. + # TODO(danielrsmith): stop representing these on the feature dict + # and references them direct from stage entities. + intent_to_implement_url: str | None + intent_to_experiment_url: str | None + intent_to_extend_experiment_url: str | None + intent_to_ship_url: str | None + ready_for_trial_url: str | None + origin_trial_feeback_url: str | None + experiment_goals: str | None + experiment_risks: str | None + announcement_url: str | None + experiment_extension_reason: str | None + + rollout_details: str | None + rollout_impact: int | None + rollout_milestone: int | None + + experiment_timeline: str | None + resources: FeatureDictInnerResourceInfo + comments: str | None # feature_notes + + # Repeated in 'browsers' section. TODO(danielrsmith): delete these? + ff_views: int + safari_views: int + web_dev_views: int + + browsers: FeatureBrowsersInfo + + # Legacy milestone fields that now live on stage entities. + dt_milestone_desktop_start: int | None + dt_milestone_android_start: int | None + dt_milestone_ios_start: int | None + dt_milestone_webview_start: int | None + ot_milestone_desktop_start: int | None + ot_milestone_android_start: int | None + ot_milestone_webview_start: int | None + ot_milestone_desktop_end: int | None + ot_milestone_android_end: int | None + ot_milestone_webview_end: int | None + + finch_url: str | None + + standards: FeatureDictInnerStandardsInfo + is_released: bool + is_enterprise_feature: bool + updated_display: str | None + new_crbug_url: str | None diff --git a/pages/testdata/intentpreview_test/test_html_rendering.html b/pages/testdata/intentpreview_test/test_html_rendering.html index e8f97dd9..42919fff 100644 --- a/pages/testdata/intentpreview_test/test_html_rendering.html +++ b/pages/testdata/intentpreview_test/test_html_rendering.html @@ -167,7 +167,7 @@ limitations under the License.

Specification

- + None @@ -181,16 +181,16 @@ limitations under the License.

Motivation

-

+

None



Initial public proposal

- + None

TAG review

- + None

TAG review status

@@ -201,7 +201,7 @@ limitations under the License.

Risks



Interoperability and Compatibility

-

+

None



Gecko: No signal @@ -229,14 +229,14 @@ limitations under the License. Does this intent deprecate or change behavior of existing APIs, such that it has potentially high risk for Android WebView-based applications?

-

+

None



Debuggability

-

+

None

@@ -247,7 +247,7 @@ No

Flag name

- +None

Requires code in //chrome?

False