Skip to content
Merged
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
3 changes: 0 additions & 3 deletions packages/categorize/configure/src/defaults.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
import { multiplePlacements } from './utils';

export default {
model: {
allowAlternateEnabled: true,
allowMaxChoicesPerCategory: false,
allowMultiplePlacementsEnabled: multiplePlacements.disabled,
alternates: [],
categories: [],
categoriesPerRow: 2,
Expand Down
15 changes: 12 additions & 3 deletions packages/categorize/configure/src/design/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -538,10 +538,19 @@ export class Design extends React.Component {
model.correctResponse || [],
);

const resolvedAllowMultiplePlacements = (() => {
if (model.allowMultiplePlacementsEnabled != null) {
return model.allowMultiplePlacementsEnabled;
}
const allChoiceIds = (model.correctResponse || []).flatMap((cr) => cr.choices || []);
const isExclusive = allChoiceIds.length === new Set(allChoiceIds).size;
return isExclusive ? multiplePlacements.disabled : multiplePlacements.enabled;
})();

const choices = model.choices.map((c) => {
c.correctResponseCount = this.countChoiceInCorrectResponse(c);
// ensure categoryCount is set even though updatedModel hasn't been called
c.categoryCount = this.checkAllowMultiplePlacements(model.allowMultiplePlacementsEnabled, c);
c.categoryCount = this.checkAllowMultiplePlacements(resolvedAllowMultiplePlacements, c);
return c;
});

Expand Down Expand Up @@ -612,7 +621,7 @@ export class Design extends React.Component {
hideSettings={settingsPanelDisabled}
settings={
<Panel
model={model}
model={{ ...model, allowMultiplePlacementsEnabled: resolvedAllowMultiplePlacements }}
onChangeModel={this.updateModel}
configuration={configuration}
onChangeConfiguration={onConfigurationChanged}
Expand Down Expand Up @@ -692,7 +701,7 @@ export class Design extends React.Component {
imageSupport={imageSupport}
uploadSoundSupport={uploadSoundSupport}
choices={choices}
model={model}
model={{ ...model, allowMultiplePlacementsEnabled: resolvedAllowMultiplePlacements }}
onModelChanged={this.updateModel}
toolbarOpts={toolbarOpts}
spellCheck={spellCheckEnabled}
Expand Down
67 changes: 67 additions & 0 deletions packages/categorize/controller/src/__tests__/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,73 @@ describe('controller', () => {
});
});

describe('allowMultiplePlacementsEnabled / categoryCount', () => {
const exclusiveCorrectResponse = [
{ category: '1', choices: ['1', '2'] },
{ category: '2', choices: ['3', '4'] },
];
const nonExclusiveCorrectResponse = [
{ category: '1', choices: ['1', '2'] },
{ category: '2', choices: ['2', '3'] }, // '2' appears in both → reuse required
];

it('explicit "Yes" → categoryCount 0 for all choices', async () => {
const q = makeQuestion({ allowMultiplePlacementsEnabled: 'Yes', correctResponse: exclusiveCorrectResponse });
const result = await model(q, {}, { mode: 'gather' }, jest.fn());
expect(result.choices.every((c) => c.categoryCount === 0)).toBe(true);
});

it('explicit "No" → categoryCount 1 for all choices', async () => {
const q = makeQuestion({ allowMultiplePlacementsEnabled: 'No', correctResponse: nonExclusiveCorrectResponse });
const result = await model(q, {}, { mode: 'gather' }, jest.fn());
expect(result.choices.every((c) => c.categoryCount === 1)).toBe(true);
});

it('no property + exclusive correct response → derived "No" → categoryCount 1', async () => {
const q = makeQuestion({ correctResponse: exclusiveCorrectResponse });
const result = await model(q, {}, { mode: 'gather' }, jest.fn());
expect(result.choices.every((c) => c.categoryCount === 1)).toBe(true);
});

it('no property + non-exclusive correct response → derived "Yes" → categoryCount 0', async () => {
const q = makeQuestion({ correctResponse: nonExclusiveCorrectResponse });
const result = await model(q, {}, { mode: 'gather' }, jest.fn());
expect(result.choices.every((c) => c.categoryCount === 0)).toBe(true);
});

it('no property + exclusive → sets allowMultiplePlacementsEnabled on out', async () => {
const q = makeQuestion({ correctResponse: exclusiveCorrectResponse });
const result = await model(q, {}, { mode: 'gather' }, jest.fn());
expect(result.allowMultiplePlacementsEnabled).toBe('No');
});

it('no property + non-exclusive → sets allowMultiplePlacementsEnabled on out', async () => {
const q = makeQuestion({ correctResponse: nonExclusiveCorrectResponse });
const result = await model(q, {}, { mode: 'gather' }, jest.fn());
expect(result.allowMultiplePlacementsEnabled).toBe('Yes');
});

it('explicit property → does not set allowMultiplePlacementsEnabled on out', async () => {
const q = makeQuestion({ allowMultiplePlacementsEnabled: 'No', correctResponse: exclusiveCorrectResponse });
const result = await model(q, {}, { mode: 'gather' }, jest.fn());
expect(result.allowMultiplePlacementsEnabled).toBeUndefined();
});

it('perChoice → uses categoryCount from each choice', async () => {
const q = makeQuestion({
allowMultiplePlacementsEnabled: 'Set Per Choice',
choices: [
{ id: '1', content: 'Foo', categoryCount: 0 },
{ id: '2', content: 'Bar', categoryCount: 1 },
],
correctResponse: exclusiveCorrectResponse,
});
const result = await model(q, {}, { mode: 'gather' }, jest.fn());
expect(result.choices.find((c) => c.id === '1').categoryCount).toBe(0);
expect(result.choices.find((c) => c.id === '2').categoryCount).toBe(1);
});
});

describe('correct response', () => {
it('returns correct response if env is correct', async () => {
const sess = await createCorrectResponseSession(question, {
Expand Down
3 changes: 0 additions & 3 deletions packages/categorize/controller/src/defaults.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
import { multiplePlacements } from './utils';

export default {
allowMultiplePlacementsEnabled: multiplePlacements.disabled,
alternates: [],
categories: [],
categoriesPerRow: 2,
Expand Down
18 changes: 15 additions & 3 deletions packages/categorize/controller/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -154,13 +154,24 @@ export const model = async (question, session, env, updateSession) => {
choices = await getShuffledChoices(choices, session, updateSession, 'id');
}

const resolvedAllowMultiple = (() => {
if (question.allowMultiplePlacementsEnabled != null) {
return question.allowMultiplePlacementsEnabled;
}
// Derive from correct response: if any choice id appears in more than one category, reuse is required
const allChoiceIds = (correctResponse || []).flatMap((cr) => cr.choices || []);
const isExclusive = allChoiceIds.length === new Set(allChoiceIds).size;
return isExclusive ? multiplePlacements.disabled : multiplePlacements.enabled;
})();

choices = (choices || []).map((c) => {
let categoryCount;
if (normalizedQuestion.allowMultiplePlacementsEnabled === multiplePlacements.enabled) {
if (resolvedAllowMultiple === multiplePlacements.enabled) {
categoryCount = 0;
} else if (normalizedQuestion.allowMultiplePlacementsEnabled === multiplePlacements.disabled) {
} else if (resolvedAllowMultiple === multiplePlacements.disabled) {
categoryCount = 1;
} else {
// perChoice — use the value set on each choice individually
categoryCount = c.categoryCount || 0;
}
return { ...c, categoryCount };
Expand Down Expand Up @@ -203,6 +214,7 @@ export const model = async (question, session, env, updateSession) => {
possibleResponses,
responseAreasToBeFilled,
hasUnplacedChoices,
...(question.allowMultiplePlacementsEnabled == null && { allowMultiplePlacementsEnabled: resolvedAllowMultiple }),
};

if (role === 'instructor' && (mode === 'view' || mode === 'evaluate')) {
Expand Down Expand Up @@ -276,7 +288,7 @@ export const getLogTrace = (model, session, env) => {
}

if (hasAlternates) {
traceLog.push(`Alternate response combinations are accepted for this question.`);
traceLog.push('Alternate response combinations are accepted for this question.');
}

if (hasAlternates) {
Expand Down
Loading