View and edit stage-specific data on edit_all page (#2562)

* display and edit stages based on stage ID

* fix test

* defensive coding with intent stages

* new routes

* save active_stage_id in guide.py

This change is set to land in a separate PR.

* display active stage on detail page correctly

* Save to specific stage IDs when using edit_all

* remove log

* remove "SHIPPED" stage types

* fix processes and tests

* add shipped form field to shipping form field

* Show matching process stages on process overview

* fix tests

* remove active_stage_id reference

* remove active_stage_id from test

* typo

* update url

* fix web test

* Consolidate "prepare to ship" and "ship"

* add small comment

* organize guide.py

* one process stage per stage entity

* style fix

* form_fields value given as a string

* remove duplicated function

* clear up logic for writing to stages

* backwards-compatible urls and api

* Update tests

* fix merge conflict block removal

* changes suggested by @jrobbins
This commit is contained in:
Daniel Smith 2022-12-22 11:02:45 -08:00 коммит произвёл GitHub
Родитель 6b44a3b84f
Коммит 8470308481
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
7 изменённых файлов: 333 добавлений и 118 удалений

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

@ -2,12 +2,14 @@ import {LitElement, html, nothing} from 'lit';
import {ALL_FIELDS} from './form-field-specs';
import {ref} from 'lit/directives/ref.js';
import './chromedash-textarea';
import {STAGE_SPECIFIC_FIELDS} from './form-field-enums';
import {showToastMessage} from './utils.js';
export class ChromedashFormField extends LitElement {
static get properties() {
return {
name: {type: String},
stageId: {type: Number},
value: {type: String},
disabled: {type: Boolean},
loading: {type: Boolean},
@ -19,6 +21,7 @@ export class ChromedashFormField extends LitElement {
constructor() {
super();
this.name = '';
this.stageId = 0;
this.value = '';
this.disabled = false;
this.loading = false;
@ -71,8 +74,10 @@ export class ChromedashFormField extends LitElement {
this.fieldProps.initial : this.value;
// form field name can be specified in form-field-spec to match DB field name
const fieldName = this.fieldProps.name || this.name;
let fieldName = this.fieldProps.name || this.name;
if (STAGE_SPECIFIC_FIELDS.has(fieldName) && this.stageId) {
fieldName = `${fieldName}__${this.stageId}`;
}
// choices can be specified in form-field-spec or fetched from API
const choices = this.fieldProps.choices || this.componentChoices;

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

@ -1,13 +1,12 @@
import {LitElement, css, html} from 'lit';
import {LitElement, css, html, nothing} from 'lit';
import {ref} from 'lit/directives/ref.js';
import {showToastMessage} from './utils.js';
import {showToastMessage, findProcessStage} from './utils.js';
import './chromedash-form-table';
import './chromedash-form-field';
import {
formatFeatureForEdit,
FLAT_FORMS_BY_FEATURE_TYPE,
FLAT_ENTERPRISE_PREPARE_TO_SHIP_NAME,
FLAT_ENTERPRISE_PREPARE_TO_SHIP} from './form-definition';
METADATA_FORM_FIELDS,
FLAT_FORMS_BY_INTENT_TYPE} from './form-definition';
import {SHARED_STYLES} from '../sass/shared-css.js';
import {FORM_STYLES} from '../sass/forms-css.js';
@ -25,6 +24,7 @@ export class ChromedashGuideEditallPage extends LitElement {
return {
featureId: {type: Number},
feature: {type: Object},
process: {type: Object},
loading: {type: Boolean},
appTitle: {type: String},
nextPage: {type: String},
@ -35,7 +35,6 @@ export class ChromedashGuideEditallPage extends LitElement {
super();
this.featureId = 0;
this.feature = {};
this.featureForEdit = {};
this.loading = true;
this.appTitle = '';
this.nextPage = '';
@ -48,9 +47,12 @@ export class ChromedashGuideEditallPage extends LitElement {
fetchData() {
this.loading = true;
window.csClient.getFeature(this.featureId).then((feature) => {
Promise.all([
window.csClient.getFeature(this.featureId),
window.csClient.getFeatureProcess(this.featureId),
]).then(([feature, process]) => {
this.feature = feature;
this.featureForEdit = formatFeatureForEdit(feature);
this.process = process;
if (this.feature.name) {
document.title = `${this.feature.name} - ${this.appTitle}`;
}
@ -91,30 +93,6 @@ export class ChromedashGuideEditallPage extends LitElement {
window.location.href = `/guide/edit/${this.featureId}`;
}
getForms() {
const forms = JSON.parse(JSON.stringify(
FLAT_FORMS_BY_FEATURE_TYPE[this.featureForEdit.feature_type]));
// Ensures the rollout field is shown for breaking changes.
if (this.featureForEdit.breaking_change &&
!forms.some(([name]) => name === FLAT_ENTERPRISE_PREPARE_TO_SHIP_NAME)) {
forms.splice(
forms.length - 1,
0,
[FLAT_ENTERPRISE_PREPARE_TO_SHIP_NAME, FLAT_ENTERPRISE_PREPARE_TO_SHIP]);
}
return forms;
}
// get a comma-spearated list of field names
getFormFields() {
let fields = [];
this.getForms().map((form) => {
fields = [...fields, ...form[1]];
});
return fields.join();
}
renderSkeletons() {
return html`
<h3><sl-skeleton effect="sheen"></sl-skeleton></h3>
@ -157,24 +135,67 @@ export class ChromedashGuideEditallPage extends LitElement {
`;
}
getStageFormFields(processStage) {
return FLAT_FORMS_BY_INTENT_TYPE[processStage.outgoing_stage] || [];
}
renderStageFormFields(formattedFeature, processStage, feStage, formFields) {
if (!formFields) return nothing;
return html`
<h3>${processStage.name}</h3>
<section class="flat_form">
${formFields.map((field) => html`
<chromedash-form-field
name=${field}
stageId=${feStage.stage_id}
value=${formattedFeature[field]}>
</chromedash-form-field>
`)}
</section>
`;
}
getForms(formattedFeature, feStages) {
const formsToRender = [html`
<h3>Feature metadata</h3>
<section class="flat_form">
${METADATA_FORM_FIELDS.map((field) => html`
<chromedash-form-field
name=${field}
value=${formattedFeature[field]}>
</chromedash-form-field>
`)}
</section>
`];
let allFormFields = [...METADATA_FORM_FIELDS];
for (const feStage of feStages) {
const processStage = findProcessStage(feStage, this.process);
if (!processStage) {
continue;
}
const formFields = this.getStageFormFields(processStage);
formsToRender.push(this.renderStageFormFields(
formattedFeature, processStage, feStage, formFields));
allFormFields = [...allFormFields, ...formFields];
}
return [allFormFields, formsToRender];
}
renderForm() {
const formattedFeature = formatFeatureForEdit(this.feature);
const stages = this.feature.stages.map(stage => stage.stage_id);
const [allFormFields, formsToRender] = this.getForms(formattedFeature, this.feature.stages);
return html`
<form name="feature_form" method="POST" action="/guide/editall/${this.featureId}">
<input type="hidden" name="stages" value="${stages}">
<input type="hidden" name="token">
<input type="hidden" name="nextPage" value=${this.getNextPage()} >
<input type="hidden" name="form_fields" value=${this.getFormFields(this.featureForEdit.feature_type)}>
<input type="hidden" name="form_fields" value=${allFormFields.join(',')}>
<chromedash-form-table ${ref(this.registerFormSubmitHandler)}>
${this.getForms().map(([sectionName, flatFormFields]) => html`
<h3>${sectionName}</h3>
<section class="flat_form">
${flatFormFields.map((field) => html`
<chromedash-form-field
name=${field}
value=${this.featureForEdit[field]}>
</chromedash-form-field>
`)}
</section>
`)}
${formsToRender}
</chromedash-form-table>
<section class="final_buttons">

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

@ -6,6 +6,17 @@ import '../js-src/cs-client';
import sinon from 'sinon';
describe('chromedash-guide-editall-page', () => {
const process = {
stages: [{
name: 'stage one',
description: 'a description',
progress_items: [],
outgoing_stage: 1,
actions: [],
stage_type: 110,
}],
};
const validFeaturePromise = Promise.resolve({
id: 123456,
name: 'feature one',
@ -15,6 +26,18 @@ describe('chromedash-guide-editall-page', () => {
feature_type_int: 0,
intent_stage: 'fake intent stage',
new_crbug_url: 'fake crbug link',
stages: [
{
stage_id: 1,
stage_type: 110,
intent_stage: 1,
},
{
stage_id: 2,
stage_type: 120,
intent_stage: 2,
},
],
browsers: {
chrome: {
blink_components: ['Blink'],
@ -51,12 +74,15 @@ describe('chromedash-guide-editall-page', () => {
await fixture(html`<chromedash-toast></chromedash-toast>`);
window.csClient = new ChromeStatusClient('fake_token', 1);
sinon.stub(window.csClient, 'getFeature');
sinon.stub(window.csClient, 'getFeatureProcess');
sinon.stub(window.csClient, 'getBlinkComponents');
window.csClient.getFeatureProcess.returns(process);
window.csClient.getBlinkComponents.returns(Promise.resolve({}));
});
afterEach(() => {
window.csClient.getFeature.restore();
window.csClient.getFeatureProcess.restore();
window.csClient.getBlinkComponents.restore();
});

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

@ -144,7 +144,7 @@ const FLAT_IMPLEMENT_FIELDS = [
];
const FLAT_DEV_TRAIL_FIELDS = [
const FLAT_DEV_TRIAL_FIELDS = [
// Standardizaton
'devtrial_instructions', 'doc_links',
'interop_compat_risks',
@ -193,6 +193,8 @@ const FLAT_PREPARE_TO_SHIP_FIELDS = [
// Implementation
'measurement',
'non_oss_deps',
'shipped_milestone', 'shipped_android_milestone',
'shipped_ios_milestone', 'shipped_webview_milestone',
];
@ -216,27 +218,26 @@ export const FLAT_FORMS = [
['Feature metadata', FLAT_METADATA_FIELDS],
['Identify the need', FLAT_IDENTIFY_FIELDS],
['Prototype a solution', FLAT_IMPLEMENT_FIELDS],
['Dev trial', FLAT_DEV_TRAIL_FIELDS],
['Dev trial', FLAT_DEV_TRIAL_FIELDS],
['Origin trial', FLAT_ORIGIN_TRIAL_FIELDS],
['Prepare to ship', FLAT_PREPARE_TO_SHIP_FIELDS],
['Ship', FLAT_SHIP_FIELDS],
];
export const FLAT_FORMS_BY_FEATURE_TYPE = {
[FEATURE_TYPES.FEATURE_TYPE_INCUBATE_ID[0]]: FLAT_FORMS,
[FEATURE_TYPES.FEATURE_TYPE_EXISTING_ID[0]]: FLAT_FORMS,
[FEATURE_TYPES.FEATURE_TYPE_CODE_CHANGE_ID[0]]: [
['Feature metadata', FLAT_METADATA_FIELDS],
['Identify the need', FLAT_IDENTIFY_FIELDS],
['Dev trial', FLAT_DEV_TRAIL_FIELDS],
['Dev trial', FLAT_DEV_TRIAL_FIELDS],
['Prepare to ship', FLAT_PREPARE_TO_SHIP_FIELDS],
['Ship', FLAT_SHIP_FIELDS],
],
[FEATURE_TYPES.FEATURE_TYPE_DEPRECATION_ID[0]]: [
['Feature metadata', FLAT_METADATA_FIELDS],
['Identify the need', FLAT_IDENTIFY_FIELDS],
['Dev trial', FLAT_DEV_TRAIL_FIELDS],
['Dev trial', FLAT_DEV_TRIAL_FIELDS],
['Origin trial', FLAT_ORIGIN_TRIAL_FIELDS],
['Prepare to ship', FLAT_PREPARE_TO_SHIP_FIELDS],
['Ship', FLAT_SHIP_FIELDS],
@ -447,3 +448,15 @@ export const IMPL_STATUS_FORMS = {
[INTENT_STAGES.INTENT_REMOVED[0]]:
[IMPLEMENTATION_STATUS.REMOVED[0], IMPLSTATUS_ALLMILESTONES],
};
export const FLAT_FORMS_BY_INTENT_TYPE = {
[INTENT_STAGES.INTENT_INCUBATE[0]]: NEWFEATURE_INCUBATE,
[INTENT_STAGES.INTENT_IMPLEMENT[0]]: FLAT_IMPLEMENT_FIELDS,
[INTENT_STAGES.INTENT_EXPERIMENT[0]]: FLAT_DEV_TRIAL_FIELDS,
[INTENT_STAGES.INTENT_EXTEND_TRIAL[0]]: FLAT_ORIGIN_TRIAL_FIELDS,
[INTENT_STAGES.INTENT_IMPLEMENT_SHIP[0]]: NEWFEATURE_EVALREADINESSTOSHIP,
[INTENT_STAGES.INTENT_SHIP[0]]: FLAT_PREPARE_TO_SHIP_FIELDS,
[INTENT_STAGES.INTENT_ROLLOUT[0]]: ENTERPRISE_PREPARE_TO_SHIP,
[INTENT_STAGES.INTENT_SHIPPED[0]]: FLAT_SHIP_FIELDS,
[INTENT_STAGES.INTENT_REMOVED[0]]: DEPRECATION_REMOVED,
};

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

@ -82,6 +82,46 @@ export const INTENT_STAGES = {
INTENT_ROLLOUT: [10, 'Rollout'],
};
// Every mutable field that exists on the Stage entity and every key
// in MilestoneSet.MILESTONE_FIELD_MAPPING should be listed here.
export const STAGE_SPECIFIC_FIELDS = new Set([
// Milestone fields.
'shipped_milestone',
'shipped_android_milestone',
'shipped_ios_milestone',
'shipped_webview_milestone',
'ot_milestone_desktop_start',
'ot_milestone_desktop_end',
'ot_milestone_android_start',
'ot_milestone_android_end',
'ot_milestone_ios_start',
'ot_milestone_ios_end',
'ot_milestone_webview_start',
'ot_milestone_webview_end',
'dt_milestone_desktop_start',
'dt_milestone_android_start',
'dt_milestone_ios_start',
'dt_milestone_webview_start',
// Intent fields.
'intent_to_implement_url',
'intent_to_ship_url',
'intent_to_experiment_url',
'intent_to_extend_experiment_url',
// Misc fields.
'origin_trial_feedback_url',
'finch_url',
'experiment_goals',
'experiment_risks',
'experiment_extension_reason',
'rollout_milestone',
'rollout_platforms',
'rollout_details',
'enterprise_policies',
'ready_for_trial_url',
]);
export const IMPLEMENTATION_STATUS = {
NO_ACTIVE_DEV: [1, 'No active development'],
PROPOSED: [2, 'Proposed'],

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

@ -198,7 +198,11 @@ class FeatureEditHandler(basehandlers.FlaskHandler):
'devrel': 'devrel_emails',
'spec_mentors': 'spec_mentor_emails',
'comments': 'feature_notes',
'ready_for_trial_url': 'announcement_url'}
'ready_for_trial_url': 'announcement_url',
'intent_to_implement_url': 'intent_thread_url',
'intent_to_ship_url': 'intent_thread_url',
'intent_to_experiment_url': 'intent_thread_url',
'intent_to_extend_experiment_url': 'intent_thread_url'}
# Field name, data type
STAGE_FIELDS: list[tuple[str, str]] = [
@ -218,6 +222,38 @@ class FeatureEditHandler(basehandlers.FlaskHandler):
('enterprise_policies', 'split_str'),
]
INTENT_FIELDS: list[str] = [
'intent_to_implement_url',
'intent_to_experiment_url',
'intent_to_extend_experiment_url',
'intent_to_ship_url'
]
DEV_TRIAL_MILESTONE_FIELDS: list[tuple[str, str]] = [
('dt_milestone_desktop_start', 'desktop_first'),
('dt_milestone_android_start', 'android_first'),
('dt_milestone_ios_start', 'ios_first'),
('dt_milestone_webview_start', 'webview_first')
]
OT_MILESTONE_FIELDS: list[tuple[str, str]] = [
('ot_milestone_desktop_start', 'desktop_first'),
('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'),
]
SHIPPING_MILESTONE_FIELDS: list[tuple[str, str]] = [
('shipped_milestone', 'desktop_first'),
('shipped_android_milestone', 'android_first'),
('shipped_ios_milestone', 'ios_first'),
('shipped_webview_milestone', 'webview_first'),
]
CHECKBOX_FIELDS: frozenset[str] = frozenset([
'accurate_as_of', 'unlisted', 'api_spec', 'all_platforms',
'wpt', 'requires_embedder_support', 'prefixed', 'breaking_change'])
@ -347,18 +383,33 @@ class FeatureEditHandler(basehandlers.FlaskHandler):
setattr(fe, 'active_stage_id', active_stage_id)
setattr(fe, 'intent_stage', intent_stage_val)
for field, field_type in self.STAGE_FIELDS:
if self.touched(field):
field_val = self._get_field_val(field, field_type)
setattr(feature, field, field_val)
stage_update_items.append((field, field_val))
# List of stage IDs will be present if the request comes from edit_all page.
stage_ids = self.form.get('stages')
if stage_ids:
stage_ids_list = [int(id) for id in stage_ids.split(',')]
self.update_stages_editall(
feature, fe.feature_type, stage_ids_list, changed_fields)
else:
for field, field_type in self.STAGE_FIELDS:
if self.touched(field):
field_val = self._get_field_val(field, field_type)
setattr(feature, field, field_val)
stage_update_items.append((field, field_val))
for field in MilestoneSet.MILESTONE_FIELD_MAPPING.keys():
if self.touched(field):
# TODO(jrobbins): Consider supporting milestones that are not ints.
field_val = self._get_field_val(field, 'int')
setattr(feature, field, field_val)
stage_update_items.append((field, field_val))
for field in MilestoneSet.MILESTONE_FIELD_MAPPING.keys():
if self.touched(field):
# TODO(jrobbins): Consider supporting milestones that are not ints.
field_val = self._get_field_val(field, 'int')
setattr(feature, field, field_val)
stage_update_items.append((field, field_val))
# If a stage_id is supplied, we make changes to only that specific stage.
if stage_update_items and stage_id:
self.update_single_stage(stage_id, stage_update_items, changed_fields)
# Otherwise, we find the associated stages and make changes (edit-all).
elif stage_update_items:
self.update_multiple_stages(feature_id, feature.feature_type,
stage_update_items, changed_fields)
# Update metadata fields.
now = datetime.now()
@ -375,14 +426,6 @@ class FeatureEditHandler(basehandlers.FlaskHandler):
key: ndb.Key = fe.put()
feature.put()
# If a stage_id is supplied, we make changes to only that specific stage.
if stage_update_items and stage_id:
self.update_single_stage(stage_id, stage_update_items, changed_fields)
# Otherwise, we find the associated stages and make changes (edit-all).
elif stage_update_items:
self.update_multiple_stages(feature_id, feature.feature_type,
stage_update_items, changed_fields)
notifier_helpers.notify_subscribers_and_save_amendments(
fe, changed_fields, notify=True)
# Remove all feature-related cache.
@ -402,40 +445,10 @@ class FeatureEditHandler(basehandlers.FlaskHandler):
redirect_url = '/guide/edit/' + str(key.integer_id())
return self.redirect(redirect_url)
def update_single_stage(self, stage_id: int,
update_items: list[tuple[str, Any]],
changed_fields: list[tuple[str, Any, Any]]) -> None:
"""Make given changes to a specified stage."""
stage_to_update = Stage.get_by_id(stage_id)
if stage_to_update is None:
self.abort(404, msg=f'Stage {stage_id} not found.')
for field, new_val in update_items:
# Update the field's name if it has been renamed.
old_field_name = field
field = self.RENAMED_FIELD_MAPPING.get(field, field)
old_val = None
if field in MilestoneSet.MILESTONE_FIELD_MAPPING:
milestone_field = MilestoneSet.MILESTONE_FIELD_MAPPING[field]
if stage_to_update.milestones is None:
stage_to_update.milestones = MilestoneSet()
old_val = getattr(stage_to_update.milestones, milestone_field)
setattr(stage_to_update.milestones, milestone_field, new_val)
elif field.startswith('intent_'):
old_val = getattr(stage_to_update, 'intent_thread_url')
setattr(stage_to_update, 'intent_thread_url', new_val)
else:
old_val = getattr(stage_to_update, field)
setattr(stage_to_update, field, new_val)
if old_val != new_val:
changed_fields.append((old_field_name, old_val, new_val))
stage_to_update.put()
def update_multiple_stages(self, feature_id: int, feature_type: int,
update_items: list[tuple[str, Any]],
changed_fields: list[tuple[str, Any, Any]]) -> None:
"""Handle updating stages when IDs have not been specified."""
# Get all existing stages associated with the feature.
stages = stage_helpers.get_feature_stages(feature_id)
@ -486,3 +499,103 @@ class FeatureEditHandler(basehandlers.FlaskHandler):
for stages_by_type in stages.values():
for stage in stages_by_type:
stage.put()
def update_single_stage(self, stage_id: int,
update_items: list[tuple[str, Any]],
changed_fields: list[tuple[str, Any, Any]]) -> None:
"""Update the fields of the stage of a given ID."""
stage_to_update = Stage.get_by_id(stage_id)
if stage_to_update is None:
self.abort(404, msg=f'Stage {stage_id} not found.')
# Determine if 'intent_thread_url' field needs to be changed.
intent_thread_val = None
changed_field = None
for field in self.INTENT_FIELDS:
field_val = self._get_field_val(field, 'link')
if field_val is not None:
intent_thread_val = field_val
changed_field = field
break
if changed_field is not None:
changed_fields.append(
(changed_field, stage_to_update.intent_thread_url, intent_thread_val))
setattr(stage_to_update, 'intent_thread_url', intent_thread_val)
for field, new_val in update_items:
# Update the field's name if it has been renamed.
old_field_name = field
field = self.RENAMED_FIELD_MAPPING.get(field, field)
old_val = None
if field in MilestoneSet.MILESTONE_FIELD_MAPPING:
milestone_field = MilestoneSet.MILESTONE_FIELD_MAPPING[field]
if stage_to_update.milestones is None:
stage_to_update.milestones = MilestoneSet()
old_val = getattr(stage_to_update.milestones, milestone_field)
setattr(stage_to_update.milestones, milestone_field, new_val)
else:
old_val = getattr(stage_to_update, field)
setattr(stage_to_update, field, new_val)
if old_val != new_val:
changed_fields.append((old_field_name, old_val, new_val))
stage_to_update.put()
def update_stages_editall(self, feature: Feature, feature_type: int,
stage_ids: list[int], changed_fields: list[tuple[str, Any, Any]]) -> None:
"""Handle the updates for stages on the edit-all page."""
for id in stage_ids:
stage = Stage.get_by_id(id)
if not stage:
self.abort(404, msg=f'No stage {id} found')
# Update the stage-specific fields.
for field, field_type in self.STAGE_FIELDS:
# To differentiate stages that have the same fields, the stage ID
# is appended to the field name with 2 underscores.
field_with_id = f'{field}__{id}'
new_field_name = self.RENAMED_FIELD_MAPPING.get(field, field)
old_val = getattr(stage, new_field_name)
new_val = self._get_field_val(field_with_id, field_type)
setattr(stage, new_field_name, new_val)
changed_fields.append((field, old_val, new_val))
# Determine if 'intent_thread_url' field needs to be changed.
intent_thread_val = None
changed_field = None
for field in self.INTENT_FIELDS:
field_val = self._get_field_val(f'{field}__{id}', 'link')
if field_val is not None:
intent_thread_val = field_val
changed_field = field
break
if changed_field is not None:
changed_fields.append(
(changed_field, stage.intent_thread_url, intent_thread_val))
setattr(stage, 'intent_thread_url', intent_thread_val)
milestone_fields = []
# Determine if the stage type is one with specific milestone fields.
if stage.stage_type == core_enums.STAGE_TYPES_DEV_TRIAL[feature_type]:
milestone_fields = self.DEV_TRIAL_MILESTONE_FIELDS
if stage.stage_type == core_enums.STAGE_TYPES_ORIGIN_TRIAL[feature_type]:
milestone_fields = self.OT_MILESTONE_FIELDS
if stage.stage_type == core_enums.STAGE_TYPES_SHIPPING[feature_type]:
milestone_fields = self.SHIPPING_MILESTONE_FIELDS
for field, milestone_field in milestone_fields:
field_with_id = f'{field}__{id}'
old_val = None
new_val = self._get_field_val(field_with_id, 'int')
milestoneset_entity = stage.milestones
setattr(feature, field, new_val)
milestoneset_entity = getattr(stage, 'milestones')
if milestoneset_entity is None:
milestoneset_entity = MilestoneSet()
else:
old_val = getattr(milestoneset_entity, milestone_field)
setattr(milestoneset_entity, milestone_field, new_val)
stage.milestones = milestoneset_entity
changed_fields.append((field, old_val, new_val))
stage.put()

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

@ -126,10 +126,12 @@ class FeatureEditHandlerTest(testing_config.CustomTestCase):
core_enums.STAGE_BLINK_DEV_TRIAL,
core_enums.STAGE_BLINK_EVAL_READINESS,
core_enums.STAGE_BLINK_ORIGIN_TRIAL,
core_enums.STAGE_BLINK_EXTEND_ORIGIN_TRIAL]
core_enums.STAGE_BLINK_SHIPPING]
stage_id = 10
for stage_type in stage_types:
stage = Stage(feature_id=feature_id, stage_type=stage_type,
stage = Stage(id=stage_id, feature_id=feature_id, stage_type=stage_type,
milestones=MilestoneSet())
stage_id += 10
stage.put()
# OT stage will be used to edit a single stage.
if stage_type == 150:
@ -211,7 +213,7 @@ class FeatureEditHandlerTest(testing_config.CustomTestCase):
self.handler.process_post_data(
feature_id=self.feature_1.key.integer_id(), stage_id=self.stage_id)
def test_post__normal_valid_multiple_stages(self):
def test_post__normal_valid_editall(self):
"""Allowed user can edit a feature."""
testing_config.sign_in('user1@google.com', 1234567890)
@ -224,23 +226,22 @@ class FeatureEditHandlerTest(testing_config.CustomTestCase):
new_ready_for_trial_url = 'https://example.com/trial'
new_intent_to_experiment_url = 'https://example.com/intent'
new_experiment_risks = 'Some pretty risky business'
new_experiment_extension_reason = 'It would be fun'
new_origin_trial_feedback_url = 'https://example.com/ot_intent'
new_intent_to_ship_url = 'https://example.com/shipping'
with test_app.test_request_context(
self.request_path, data={
'stages': '30,50,60',
'form_fields': form_fields,
'category': '2',
'name': 'Revised feature name',
'summary': 'Revised feature summary',
'shipped_milestone': new_shipped_milestone,
'ready_for_trial_url': new_ready_for_trial_url,
'intent_to_experiment_url': new_intent_to_experiment_url,
'experiment_risks': new_experiment_risks,
'experiment_extension_reason': new_experiment_extension_reason,
'origin_trial_feedback_url': new_origin_trial_feedback_url,
'intent_to_ship_url': new_intent_to_ship_url,
'shipped_milestone__60': new_shipped_milestone,
'ready_for_trial_url__30': new_ready_for_trial_url,
'intent_to_experiment_url__50': new_intent_to_experiment_url,
'experiment_risks__50': new_experiment_risks,
'origin_trial_feedback_url__50': new_origin_trial_feedback_url,
'intent_to_ship_url__60': new_intent_to_ship_url,
'feature_type': '1'
}):
actual_response = self.handler.process_post_data(
@ -267,15 +268,13 @@ class FeatureEditHandlerTest(testing_config.CustomTestCase):
# Ensure changes were also made to Stage entities
stages = stage_helpers.get_feature_stages(
self.feature_1.key.integer_id())
self.assertEqual(len(stages.keys()), 7)
self.assertEqual(len(stages.keys()), 6)
dev_trial_stage = stages.get(130)
origin_trial_stages = stages.get(150)
ot_extension_stages = stages.get(151)
# Stage for shipping should have been created.
shipping_stages = stages.get(160)
self.assertIsNotNone(origin_trial_stages)
self.assertIsNotNone(shipping_stages)
self.assertIsNotNone(ot_extension_stages)
# Check that correct stage fields were changed.
self.assertEqual(dev_trial_stage[0].announcement_url,
new_ready_for_trial_url)
@ -285,8 +284,6 @@ class FeatureEditHandlerTest(testing_config.CustomTestCase):
new_intent_to_experiment_url)
self.assertEqual(origin_trial_stages[0].origin_trial_feedback_url,
new_origin_trial_feedback_url)
self.assertEqual(ot_extension_stages[0].experiment_extension_reason,
new_experiment_extension_reason)
self.assertEqual(shipping_stages[0].milestones.desktop_first,
int(new_shipped_milestone))
self.assertEqual(shipping_stages[0].intent_thread_url,