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:
Родитель
6b44a3b84f
Коммит
8470308481
|
@ -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'],
|
||||
|
|
215
pages/guide.py
215
pages/guide.py
|
@ -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,
|
||||
|
|
Загрузка…
Ссылка в новой задаче