@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.
Files changed (39) hide show
  1. package/CHANGELOG.md +463 -0
  2. package/configure/CHANGELOG.md +447 -0
  3. package/configure/lib/defaults.js +34 -11
  4. package/configure/lib/defaults.js.map +1 -1
  5. package/configure/lib/index.js +30 -23
  6. package/configure/lib/index.js.map +1 -1
  7. package/configure/lib/main.js +109 -72
  8. package/configure/lib/main.js.map +1 -1
  9. package/configure/package.json +3 -3
  10. package/configure/src/__tests__/__snapshots__/index.test.js.snap +12 -2
  11. package/configure/src/__tests__/index.test.js +164 -211
  12. package/configure/src/defaults.js +49 -27
  13. package/configure/src/index.js +19 -39
  14. package/configure/src/main.jsx +165 -149
  15. package/controller/CHANGELOG.md +67 -0
  16. package/controller/lib/defaults.js.map +1 -1
  17. package/controller/lib/index.js +123 -29
  18. package/controller/lib/index.js.map +1 -1
  19. package/controller/lib/utils.js.map +1 -1
  20. package/controller/package.json +2 -2
  21. package/controller/src/__tests__/index.test.js +113 -141
  22. package/controller/src/__tests__/utils.test.js +6 -9
  23. package/controller/src/defaults.js +1 -1
  24. package/controller/src/index.js +109 -70
  25. package/controller/src/utils.js +6 -4
  26. package/docs/config-schema.json +376 -0
  27. package/docs/config-schema.json.md +289 -1
  28. package/docs/demo/config.js +2 -2
  29. package/docs/demo/generate.js +3 -3
  30. package/docs/pie-schema.json +177 -0
  31. package/docs/pie-schema.json.md +138 -0
  32. package/lib/index.js +12 -9
  33. package/lib/index.js.map +1 -1
  34. package/lib/print.js +243 -0
  35. package/lib/print.js.map +1 -0
  36. package/package.json +8 -4
  37. package/src/__tests__/index.test.js +2 -7
  38. package/src/index.js +7 -5
  39. package/src/print.js +179 -0
@@ -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
- ...question.partA
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
- ...question.partB
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
- normalizedQuestion.partLabelType === 'Letters' ? 'Part A' : 'Part 1';
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
- env.role === 'instructor' &&
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
- ? normalizedQuestion.partA.prompt
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 (!config.partialScoring && correctChoices < maxScore) {
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 scoreA = getScore(config, partA, 'partA');
234
- const scoreB = getScore(config, partB, 'partB');
215
+ const partialScoringEnabled = partialScoring.enabled(config, env);
235
216
 
236
- if (!config.partialScoring) {
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: (scoreA === 1 && scoreB === 1) ? 1 : 0, scoreA, scoreB, max: 1 });
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
+ };
@@ -1,9 +1,11 @@
1
1
  import isEqual from 'lodash/isEqual';
2
2
 
3
- export const getCorrectResponse = (choices) => choices && choices
4
- .filter(c => c.correct)
5
- .map(c => c.value)
6
- .sort();
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);