Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions assets/blocks/quiz/data.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,49 @@ import categoryQuestionBlock from './category-question-block';
*/
import { mapKeys, mapValues, isObject, omit } from 'lodash';

/**
* Question attributes that are derived from the server and must not be sent
* back as editable structure.
*
* @type {string[]}
*/
export const READ_ONLY_ATTRIBUTES = [
'categories',
'shared',
'options.studentHelp',
'media',
'categoryName',
'lock',
];

/**
* Normalize a quiz REST response into the structure the editor tracks.
*
* Only the fields that make up the editor's structure are kept. Other plugins
* (e.g. Polylang) may inject extra top-level fields such as `lang` and
* `translations` into the REST response; including them here would make the
* structure comparison detect a permanent diff and trigger an infinite save
* loop.
*
* @param {Object} structure The quiz response.
*
* @return {Object} The normalized structure.
*/
export function normalizeServerStructure( structure ) {
if ( ! structure ) {
return {};
}

return {
lesson_status: structure.lesson_status,
lesson_title: structure.lesson_title,
options: structure.options,
questions: ( structure.questions || [] ).map( ( question ) =>
omit( question, READ_ONLY_ATTRIBUTES )
),
};
Comment thread
donnapep marked this conversation as resolved.
}

/**
* Quiz settings and questions data.
*
Expand Down
69 changes: 69 additions & 0 deletions assets/blocks/quiz/data.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/**
* Internal dependencies
*/
import { normalizeServerStructure } from './data';

describe( 'normalizeServerStructure', () => {
it( 'returns an empty object for a falsy response', () => {
expect( normalizeServerStructure( undefined ) ).toEqual( {} );
expect( normalizeServerStructure( null ) ).toEqual( {} );
} );

it( 'strips injected top-level fields (e.g. Polylang lang/translations)', () => {
const structure = {
lesson_status: 'publish',
lesson_title: 'Lesson',
options: { pass_required: true },
questions: [ { id: 1, title: 'Q1' } ],
lang: 'en',
translations: { en: 42 },
};

const normalized = normalizeServerStructure( structure );

expect( normalized ).not.toHaveProperty( 'lang' );
expect( normalized ).not.toHaveProperty( 'translations' );
expect( normalized ).toEqual( {
lesson_status: 'publish',
lesson_title: 'Lesson',
options: { pass_required: true },
questions: [ { id: 1, title: 'Q1' } ],
} );
} );

it( 'removes read-only attributes from questions', () => {
const structure = {
lesson_status: 'publish',
lesson_title: 'Lesson',
options: {},
questions: [
{
id: 1,
title: 'Q1',
shared: true,
categories: [ 5 ],
lock: true,
options: { grade: 1, studentHelp: 'help text' },
},
],
};

expect( normalizeServerStructure( structure ).questions[ 0 ] ).toEqual(
{
id: 1,
title: 'Q1',
options: { grade: 1 },
}
);
} );

it( 'defaults questions to an empty array when missing', () => {
const structure = {
lesson_status: 'publish',
lesson_title: 'Lesson',
options: {},
};

expect( normalizeServerStructure( structure ).questions ).toEqual( [] );
} );
} );
24 changes: 3 additions & 21 deletions assets/blocks/quiz/quiz-store.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,12 @@ import {
parseQuestionBlocks,
syncQuestionBlocks,
normalizeAttributes,
normalizeServerStructure,
READ_ONLY_ATTRIBUTES,
} from './data';

export const QUIZ_STORE = 'sensei/quiz-structure';

const READ_ONLY_ATTRIBUTES = [
'categories',
'shared',
'options.studentHelp',
'media',
'categoryName',
'lock',
];

/**
* Syncronize this block with quiz data.
*
Expand Down Expand Up @@ -204,16 +197,5 @@ registerStructureStore( {
*
* @return {Object} The modified response.
*/
setServerStructure( structure ) {
if ( ! structure ) {
return {};
}

return {
...structure,
questions: structure.questions.map( ( question ) =>
omit( question, READ_ONLY_ATTRIBUTES )
),
};
},
setServerStructure: normalizeServerStructure,
} );
4 changes: 4 additions & 0 deletions changelog/fix-quiz-save-loop-injected-rest-fields
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: patch
Type: fixed

Fix an infinite save loop when editing a lesson with a quiz block while a plugin (e.g. Polylang) injects extra fields into the REST response.
Loading