@pie-element/ebsr 6.5.0 → 6.5.1-beta.587
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +463 -0
- package/configure/CHANGELOG.md +447 -0
- package/configure/lib/defaults.js +34 -11
- package/configure/lib/defaults.js.map +1 -1
- package/configure/lib/index.js +30 -23
- package/configure/lib/index.js.map +1 -1
- package/configure/lib/main.js +109 -72
- package/configure/lib/main.js.map +1 -1
- package/configure/package.json +3 -3
- package/configure/src/__tests__/__snapshots__/index.test.js.snap +12 -2
- package/configure/src/__tests__/index.test.js +164 -211
- package/configure/src/defaults.js +49 -27
- package/configure/src/index.js +19 -39
- package/configure/src/main.jsx +165 -149
- package/controller/CHANGELOG.md +67 -0
- package/controller/lib/defaults.js.map +1 -1
- package/controller/lib/index.js +123 -29
- package/controller/lib/index.js.map +1 -1
- package/controller/lib/utils.js.map +1 -1
- package/controller/package.json +2 -2
- package/controller/src/__tests__/index.test.js +113 -141
- package/controller/src/__tests__/utils.test.js +6 -9
- package/controller/src/defaults.js +1 -1
- package/controller/src/index.js +109 -70
- package/controller/src/utils.js +6 -4
- package/docs/config-schema.json +376 -0
- package/docs/config-schema.json.md +289 -1
- package/docs/demo/config.js +2 -2
- package/docs/demo/generate.js +3 -3
- package/docs/pie-schema.json +177 -0
- package/docs/pie-schema.json.md +138 -0
- package/lib/index.js +12 -9
- package/lib/index.js.map +1 -1
- package/lib/print.js +243 -0
- package/lib/print.js.map +1 -0
- package/package.json +8 -4
- package/src/__tests__/index.test.js +2 -7
- package/src/index.js +7 -5
- package/src/print.js +179 -0
package/controller/src/index.js
CHANGED
|
@@ -1,19 +1,16 @@
|
|
|
1
1
|
import defaults from './defaults';
|
|
2
|
-
import { lockChoices, getShuffledChoices } from '@pie-lib/controller-utils';
|
|
2
|
+
import { lockChoices, getShuffledChoices, partialScoring } from '@pie-lib/controller-utils';
|
|
3
3
|
import { isResponseCorrect } from './utils';
|
|
4
4
|
import get from 'lodash/get';
|
|
5
5
|
import isEmpty from 'lodash/isEmpty';
|
|
6
6
|
|
|
7
|
-
const prepareChoice = (model, env, defaultFeedback) => choice => {
|
|
7
|
+
const prepareChoice = (model, env, defaultFeedback) => (choice) => {
|
|
8
8
|
const out = {
|
|
9
9
|
label: choice.label,
|
|
10
|
-
value: choice.value
|
|
10
|
+
value: choice.value,
|
|
11
11
|
};
|
|
12
12
|
|
|
13
|
-
if (
|
|
14
|
-
env.role === 'instructor' &&
|
|
15
|
-
(env.mode === 'view' || env.mode === 'evaluate')
|
|
16
|
-
) {
|
|
13
|
+
if (env.role === 'instructor' && (env.mode === 'view' || env.mode === 'evaluate')) {
|
|
17
14
|
out.rationale = model.rationaleEnabled ? choice.rationale : null;
|
|
18
15
|
} else {
|
|
19
16
|
out.rationale = null;
|
|
@@ -37,30 +34,22 @@ const prepareChoice = (model, env, defaultFeedback) => choice => {
|
|
|
37
34
|
};
|
|
38
35
|
|
|
39
36
|
const parsePart = (part, key, session, env) => {
|
|
40
|
-
const defaultFeedback = Object.assign(
|
|
41
|
-
{ correct: 'Correct', incorrect: 'Incorrect' },
|
|
42
|
-
part.defaultFeedback
|
|
43
|
-
);
|
|
37
|
+
const defaultFeedback = Object.assign({ correct: 'Correct', incorrect: 'Incorrect' }, part.defaultFeedback);
|
|
44
38
|
|
|
45
|
-
let choices = part.choices
|
|
46
|
-
? part.choices.map(prepareChoice(part, env, defaultFeedback))
|
|
47
|
-
: [];
|
|
39
|
+
let choices = part.choices ? part.choices.map(prepareChoice(part, env, defaultFeedback)) : [];
|
|
48
40
|
|
|
49
41
|
return {
|
|
50
42
|
...part,
|
|
51
43
|
choices,
|
|
52
44
|
disabled: env.mode !== 'gather',
|
|
53
45
|
complete: {
|
|
54
|
-
min: part.choices.filter(c => c.correct).length
|
|
46
|
+
min: part.choices.filter((c) => c.correct).length,
|
|
55
47
|
},
|
|
56
|
-
responseCorrect:
|
|
57
|
-
env.mode === 'evaluate'
|
|
58
|
-
? isResponseCorrect(part, key, session)
|
|
59
|
-
: undefined
|
|
48
|
+
responseCorrect: env.mode === 'evaluate' ? isResponseCorrect(part, key, session) : undefined,
|
|
60
49
|
};
|
|
61
50
|
};
|
|
62
51
|
|
|
63
|
-
export const normalize = question => ({
|
|
52
|
+
export const normalize = ({ partA = {}, partB = {}, ...question }) => ({
|
|
64
53
|
partLabels: true,
|
|
65
54
|
partLabelType: 'Letters',
|
|
66
55
|
...question,
|
|
@@ -71,7 +60,9 @@ export const normalize = question => ({
|
|
|
71
60
|
promptEnabled: true,
|
|
72
61
|
teacherInstructionsEnabled: true,
|
|
73
62
|
studentInstructionsEnabled: true,
|
|
74
|
-
|
|
63
|
+
gridColumns: '2',
|
|
64
|
+
...partA,
|
|
65
|
+
choicesLayout: partA.choicesLayout || (partA.verticalMode === false && 'horizontal') || 'vertical',
|
|
75
66
|
},
|
|
76
67
|
partB: {
|
|
77
68
|
...defaults.partB,
|
|
@@ -80,8 +71,10 @@ export const normalize = question => ({
|
|
|
80
71
|
feedbackEnabled: true,
|
|
81
72
|
teacherInstructionsEnabled: true,
|
|
82
73
|
studentInstructionsEnabled: true,
|
|
83
|
-
|
|
84
|
-
|
|
74
|
+
gridColumns: '2',
|
|
75
|
+
...partB,
|
|
76
|
+
choicesLayout: partB.choicesLayout || (partB.verticalMode === false && 'horizontal') || 'vertical',
|
|
77
|
+
},
|
|
85
78
|
});
|
|
86
79
|
|
|
87
80
|
/**
|
|
@@ -98,8 +91,8 @@ export async function model(question, session, env, updateSession) {
|
|
|
98
91
|
|
|
99
92
|
const shuffledValues = {};
|
|
100
93
|
|
|
101
|
-
const us = part => (id, element, update) =>
|
|
102
|
-
new Promise(resolve => {
|
|
94
|
+
const us = (part) => (id, element, update) =>
|
|
95
|
+
new Promise((resolve) => {
|
|
103
96
|
shuffledValues[part] = update.shuffledValues;
|
|
104
97
|
resolve();
|
|
105
98
|
});
|
|
@@ -115,7 +108,7 @@ export async function model(question, session, env, updateSession) {
|
|
|
115
108
|
partAChoices,
|
|
116
109
|
{ shuffledValues: (session.shuffledValues || {}).partA },
|
|
117
110
|
us('partA'),
|
|
118
|
-
'value'
|
|
111
|
+
'value',
|
|
119
112
|
);
|
|
120
113
|
}
|
|
121
114
|
|
|
@@ -127,40 +120,33 @@ export async function model(question, session, env, updateSession) {
|
|
|
127
120
|
partBChoices,
|
|
128
121
|
{ shuffledValues: (session.shuffledValues || {}).partB },
|
|
129
122
|
us('partB'),
|
|
130
|
-
'value'
|
|
123
|
+
'value',
|
|
131
124
|
);
|
|
132
125
|
}
|
|
133
126
|
|
|
134
127
|
if (!isEmpty(shuffledValues)) {
|
|
135
128
|
if (updateSession && typeof updateSession === 'function') {
|
|
136
129
|
updateSession(session.id, session.element, {
|
|
137
|
-
shuffledValues
|
|
138
|
-
}).catch(e => {
|
|
130
|
+
shuffledValues,
|
|
131
|
+
}).catch((e) => {
|
|
139
132
|
console.error('update session failed', e);
|
|
140
133
|
});
|
|
141
134
|
}
|
|
142
135
|
}
|
|
143
136
|
|
|
144
137
|
if (normalizedQuestion.partLabels) {
|
|
145
|
-
partA.partLabel =
|
|
146
|
-
|
|
147
|
-
partB.partLabel =
|
|
148
|
-
normalizedQuestion.partLabelType === 'Letters' ? 'Part B' : 'Part 2';
|
|
138
|
+
partA.partLabel = normalizedQuestion.partLabelType === 'Letters' ? 'Part A' : 'Part 1';
|
|
139
|
+
partB.partLabel = normalizedQuestion.partLabelType === 'Letters' ? 'Part B' : 'Part 2';
|
|
149
140
|
} else {
|
|
150
141
|
partA.partLabel = undefined;
|
|
151
142
|
partB.partLabel = undefined;
|
|
152
143
|
}
|
|
153
144
|
|
|
154
|
-
if (
|
|
155
|
-
|
|
156
|
-
(env.mode === 'view' || env.mode === 'evaluate')
|
|
157
|
-
) {
|
|
158
|
-
partA.teacherInstructions = normalizedQuestion.partA
|
|
159
|
-
.teacherInstructionsEnabled
|
|
145
|
+
if (env.role === 'instructor' && (env.mode === 'view' || env.mode === 'evaluate')) {
|
|
146
|
+
partA.teacherInstructions = normalizedQuestion.partA.teacherInstructionsEnabled
|
|
160
147
|
? normalizedQuestion.partA.teacherInstructions
|
|
161
148
|
: null;
|
|
162
|
-
partB.teacherInstructions = normalizedQuestion.partB
|
|
163
|
-
.teacherInstructionsEnabled
|
|
149
|
+
partB.teacherInstructions = normalizedQuestion.partB.teacherInstructionsEnabled
|
|
164
150
|
? normalizedQuestion.partB.teacherInstructions
|
|
165
151
|
: null;
|
|
166
152
|
} else {
|
|
@@ -168,41 +154,37 @@ export async function model(question, session, env, updateSession) {
|
|
|
168
154
|
partB.teacherInstructions = null;
|
|
169
155
|
}
|
|
170
156
|
|
|
171
|
-
partA.prompt = normalizedQuestion.partA.promptEnabled
|
|
172
|
-
|
|
173
|
-
: null;
|
|
174
|
-
partB.prompt = normalizedQuestion.partB.promptEnabled
|
|
175
|
-
? normalizedQuestion.partB.prompt
|
|
176
|
-
: null;
|
|
157
|
+
partA.prompt = normalizedQuestion.partA.promptEnabled ? normalizedQuestion.partA.prompt : null;
|
|
158
|
+
partB.prompt = normalizedQuestion.partB.promptEnabled ? normalizedQuestion.partB.prompt : null;
|
|
177
159
|
|
|
178
|
-
return new Promise(resolve => {
|
|
160
|
+
return new Promise((resolve) => {
|
|
179
161
|
resolve({
|
|
180
162
|
disabled: env.mode !== 'gather',
|
|
181
163
|
mode: env.mode,
|
|
182
164
|
partA,
|
|
183
|
-
partB
|
|
165
|
+
partB,
|
|
184
166
|
});
|
|
185
167
|
});
|
|
186
168
|
}
|
|
187
169
|
|
|
188
170
|
export const createDefaultModel = (model = {}) =>
|
|
189
|
-
new Promise(resolve => {
|
|
171
|
+
new Promise((resolve) => {
|
|
190
172
|
resolve({
|
|
191
173
|
...defaults,
|
|
192
|
-
...model
|
|
174
|
+
...model,
|
|
193
175
|
});
|
|
194
176
|
});
|
|
195
177
|
|
|
196
|
-
const isCorrect = c => c.correct === true;
|
|
178
|
+
const isCorrect = (c) => c.correct === true;
|
|
197
179
|
|
|
198
|
-
const getScore = (config, sessionPart, key) => {
|
|
180
|
+
const getScore = (config, sessionPart, key, partialScoringEnabled) => {
|
|
199
181
|
const { choices = [] } = (config && config[key]) || {};
|
|
200
182
|
const maxScore = choices.length;
|
|
201
183
|
const { value: sessionPartValue } = sessionPart || {};
|
|
202
184
|
|
|
203
|
-
const chosen = c => !!(sessionPartValue || []).find(v => v === c.value);
|
|
204
|
-
const correctAndNotChosen = c => isCorrect(c) && !chosen(c);
|
|
205
|
-
const incorrectAndChosen = c => !isCorrect(c) && chosen(c);
|
|
185
|
+
const chosen = (c) => !!(sessionPartValue || []).find((v) => v === c.value);
|
|
186
|
+
const correctAndNotChosen = (c) => isCorrect(c) && !chosen(c);
|
|
187
|
+
const incorrectAndChosen = (c) => !isCorrect(c) && chosen(c);
|
|
206
188
|
const correctChoices = choices.reduce((total, choice) => {
|
|
207
189
|
if (correctAndNotChosen(choice) || incorrectAndChosen(choice)) {
|
|
208
190
|
return total - 1;
|
|
@@ -212,15 +194,15 @@ const getScore = (config, sessionPart, key) => {
|
|
|
212
194
|
}, choices.length);
|
|
213
195
|
|
|
214
196
|
// determine score for a part
|
|
215
|
-
if (!
|
|
197
|
+
if (!partialScoringEnabled && correctChoices < maxScore) {
|
|
216
198
|
return 0;
|
|
217
199
|
}
|
|
218
200
|
|
|
219
201
|
return parseFloat(maxScore ? (correctChoices / maxScore).toFixed(2) : 0);
|
|
220
202
|
};
|
|
221
203
|
|
|
222
|
-
export function outcome(config, session) {
|
|
223
|
-
return new Promise(resolve => {
|
|
204
|
+
export function outcome(config, session, env) {
|
|
205
|
+
return new Promise((resolve) => {
|
|
224
206
|
const { value } = session || {};
|
|
225
207
|
|
|
226
208
|
if (!session || !value) {
|
|
@@ -230,13 +212,15 @@ export function outcome(config, session) {
|
|
|
230
212
|
if (value) {
|
|
231
213
|
const { partA, partB } = value || {};
|
|
232
214
|
|
|
233
|
-
const
|
|
234
|
-
const scoreB = getScore(config, partB, 'partB');
|
|
215
|
+
const partialScoringEnabled = partialScoring.enabled(config, env);
|
|
235
216
|
|
|
236
|
-
|
|
217
|
+
const scoreA = getScore(config, partA, 'partA', partialScoringEnabled);
|
|
218
|
+
const scoreB = getScore(config, partB, 'partB', partialScoringEnabled);
|
|
219
|
+
|
|
220
|
+
if (!partialScoringEnabled) {
|
|
237
221
|
// The EBSR item is worth 1 point
|
|
238
222
|
// That point is awarded if and only if both parts are fully correct, otherwise no points are awarded
|
|
239
|
-
resolve({ score:
|
|
223
|
+
resolve({ score: scoreA === 1 && scoreB === 1 ? 1 : 0, scoreA, scoreB, max: 1 });
|
|
240
224
|
} else {
|
|
241
225
|
// The EBSR item is worth 2 points
|
|
242
226
|
if (scoreA === 1) {
|
|
@@ -256,10 +240,10 @@ export function outcome(config, session) {
|
|
|
256
240
|
});
|
|
257
241
|
}
|
|
258
242
|
|
|
259
|
-
const returnPartCorrect = choices => {
|
|
243
|
+
const returnPartCorrect = (choices) => {
|
|
260
244
|
let answers = [];
|
|
261
245
|
|
|
262
|
-
choices.forEach(i => {
|
|
246
|
+
choices.forEach((i) => {
|
|
263
247
|
const { correct, value } = i;
|
|
264
248
|
if (correct) {
|
|
265
249
|
answers.push(value);
|
|
@@ -269,7 +253,7 @@ const returnPartCorrect = choices => {
|
|
|
269
253
|
};
|
|
270
254
|
|
|
271
255
|
export const createCorrectResponseSession = (question, env) => {
|
|
272
|
-
return new Promise(resolve => {
|
|
256
|
+
return new Promise((resolve) => {
|
|
273
257
|
if (env.mode !== 'evaluate' && env.role === 'instructor') {
|
|
274
258
|
const { partA, partB } = question;
|
|
275
259
|
|
|
@@ -280,17 +264,72 @@ export const createCorrectResponseSession = (question, env) => {
|
|
|
280
264
|
value: {
|
|
281
265
|
partA: {
|
|
282
266
|
id: 'partA',
|
|
283
|
-
value: partACorrect
|
|
267
|
+
value: partACorrect,
|
|
284
268
|
},
|
|
285
269
|
partB: {
|
|
286
270
|
id: 'partB',
|
|
287
|
-
value: partBCorrect
|
|
288
|
-
}
|
|
271
|
+
value: partBCorrect,
|
|
272
|
+
},
|
|
289
273
|
},
|
|
290
|
-
id: '1'
|
|
274
|
+
id: '1',
|
|
291
275
|
});
|
|
292
276
|
} else {
|
|
293
277
|
resolve(null);
|
|
294
278
|
}
|
|
295
279
|
});
|
|
296
280
|
};
|
|
281
|
+
|
|
282
|
+
const validatePart = (model = {}, config = {}) => {
|
|
283
|
+
const { choices } = model;
|
|
284
|
+
const { minAnswerChoices = 2, maxAnswerChoices } = config;
|
|
285
|
+
const reversedChoices = [...(choices || [])].reverse();
|
|
286
|
+
const choicesErrors = {};
|
|
287
|
+
let hasCorrectResponse = false;
|
|
288
|
+
|
|
289
|
+
reversedChoices.forEach((choice, index) => {
|
|
290
|
+
const { correct, value, label } = choice;
|
|
291
|
+
|
|
292
|
+
if (correct) {
|
|
293
|
+
hasCorrectResponse = true;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (label === '' || label === '<div></div>') {
|
|
297
|
+
choicesErrors[value] = 'Content should not be empty.';
|
|
298
|
+
} else {
|
|
299
|
+
const identicalAnswer = reversedChoices.slice(index + 1).some((c) => c.label === label);
|
|
300
|
+
|
|
301
|
+
if (identicalAnswer) {
|
|
302
|
+
choicesErrors[value] = 'Content should be unique.';
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
const errors = {};
|
|
308
|
+
const nbOfChoices = (choices || []).length;
|
|
309
|
+
|
|
310
|
+
if (nbOfChoices < minAnswerChoices) {
|
|
311
|
+
errors.answerChoicesError = `There should be at least ${minAnswerChoices} choices defined.`;
|
|
312
|
+
} else if (nbOfChoices > maxAnswerChoices) {
|
|
313
|
+
errors.answerChoicesError = `No more than ${maxAnswerChoices} choices should be defined.`;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (!hasCorrectResponse) {
|
|
317
|
+
errors.correctResponseError = 'No correct response defined.';
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (!isEmpty(choicesErrors)) {
|
|
321
|
+
errors.choicesErrors = choicesErrors;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return errors;
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
export const validate = (model = {}, config = {}) => {
|
|
328
|
+
const { partA, partB } = model || {};
|
|
329
|
+
const { partA: partAConfig, partB: partBConfig } = config || {};
|
|
330
|
+
|
|
331
|
+
const partAErrors = validatePart(partA, partAConfig);
|
|
332
|
+
const partBErrors = validatePart(partB, partBConfig);
|
|
333
|
+
|
|
334
|
+
return { partA: partAErrors, partB: partBErrors };
|
|
335
|
+
};
|
package/controller/src/utils.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import isEqual from 'lodash/isEqual';
|
|
2
2
|
|
|
3
|
-
export const getCorrectResponse = (choices) =>
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
export const getCorrectResponse = (choices) =>
|
|
4
|
+
choices &&
|
|
5
|
+
choices
|
|
6
|
+
.filter((c) => c.correct)
|
|
7
|
+
.map((c) => c.value)
|
|
8
|
+
.sort();
|
|
7
9
|
|
|
8
10
|
export const isResponseCorrect = (question, key, session) => {
|
|
9
11
|
let correctResponse = getCorrectResponse(question.choices);
|