@openstax/ts-utils 1.37.1 → 1.38.0-beta

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.
@@ -1,22 +1,27 @@
1
1
  import { LrsGateway, UXapiStatement } from '.';
2
+ export declare const EXT_PENDING_SCORING = "https://openstax.org/xapi/extensions/pending-scoring";
3
+ export type AttemptEntry = {
4
+ attempt: UXapiStatement;
5
+ completed?: UXapiStatement;
6
+ scored?: UXapiStatement;
7
+ };
2
8
  export type ActivityState = {
3
9
  attempts: number;
4
- bestCompletedAttempt?: UXapiStatement;
5
- bestCompletedAttemptCompleted?: UXapiStatement;
6
10
  completedAttempts: number;
7
- currentAttempt?: UXapiStatement;
8
- currentAttemptCompleted?: UXapiStatement;
9
11
  currentAttemptStatements: UXapiStatement[];
10
- mostRecentAttemptWithCompleted?: UXapiStatement;
11
- mostRecentAttemptWithCompletedCompleted?: UXapiStatement;
12
+ current?: AttemptEntry;
13
+ best?: AttemptEntry;
14
+ mostRecentWithCompleted?: AttemptEntry;
12
15
  };
13
16
  export declare const matchAttempt: (statement: UXapiStatement) => boolean;
14
17
  export declare const matchAttemptCompleted: (attempt: UXapiStatement) => (statement: UXapiStatement) => boolean;
18
+ export declare const matchAttemptScored: (attempt: UXapiStatement) => (statement: UXapiStatement) => boolean;
15
19
  export declare const resolveAttempts: (statements: UXapiStatement[], options?: {
16
20
  activityIRI?: string;
17
21
  parentActivityAttempt?: string;
18
22
  }) => UXapiStatement[];
19
23
  export declare const resolveCompletedForAttempt: (statements: UXapiStatement[], attempt: UXapiStatement, activityIRI?: string) => UXapiStatement | undefined;
24
+ export declare const resolveScoredForAttempt: (statements: UXapiStatement[], attempt: UXapiStatement, activityIRI?: string) => UXapiStatement | undefined;
20
25
  export declare const oldestStatement: (statements: UXapiStatement[]) => UXapiStatement | undefined;
21
26
  export declare const mostRecentStatement: (statements: UXapiStatement[]) => UXapiStatement | undefined;
22
27
  export declare const resolveAttemptInfo: (statements: UXapiStatement[], options?: {
@@ -70,3 +75,7 @@ export declare const createAttemptActivityStatement: (attemptStatement: UXapiSta
70
75
  export declare const putAttemptActivityStatement: (gateway: LrsGateway, attemptStatement: UXapiStatement, verb: UXapiStatement["verb"], result?: UXapiStatement["result"]) => Promise<import(".").EagerXapiStatement>;
71
76
  export declare const createCompletedStatement: (attemptStatement: UXapiStatement, result?: UXapiStatement["result"]) => Pick<UXapiStatement, "object" | "verb" | "context" | "result">;
72
77
  export declare const putCompletedStatement: (gateway: LrsGateway, attemptStatement: UXapiStatement, result: UXapiStatement["result"]) => Promise<import(".").EagerXapiStatement>;
78
+ export declare const createCompletedPendingScoringStatement: (attemptStatement: UXapiStatement) => Pick<UXapiStatement, "object" | "verb" | "context" | "result">;
79
+ export declare const putCompletedPendingScoringStatement: (gateway: LrsGateway, attemptStatement: UXapiStatement) => Promise<import(".").EagerXapiStatement>;
80
+ export declare const createScoredStatement: (attemptStatement: UXapiStatement, result: UXapiStatement["result"]) => Pick<UXapiStatement, "object" | "verb" | "context" | "result">;
81
+ export declare const putScoredStatement: (gateway: LrsGateway, attemptStatement: UXapiStatement, result: UXapiStatement["result"]) => Promise<import(".").EagerXapiStatement>;
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.putCompletedStatement = exports.createCompletedStatement = exports.putAttemptActivityStatement = exports.createAttemptActivityStatement = exports.putAttemptStatement = exports.createAttemptStatement = exports.createStatement = exports.loadActivityAttemptInfo = exports.loadStatementsForActivityAndFirstChildren = exports.resolveAttemptInfo = exports.mostRecentStatement = exports.oldestStatement = exports.resolveCompletedForAttempt = exports.resolveAttempts = exports.matchAttemptCompleted = exports.matchAttempt = void 0;
6
+ exports.putScoredStatement = exports.createScoredStatement = exports.putCompletedPendingScoringStatement = exports.createCompletedPendingScoringStatement = exports.putCompletedStatement = exports.createCompletedStatement = exports.putAttemptActivityStatement = exports.createAttemptActivityStatement = exports.putAttemptStatement = exports.createAttemptStatement = exports.createStatement = exports.loadActivityAttemptInfo = exports.loadStatementsForActivityAndFirstChildren = exports.resolveAttemptInfo = exports.mostRecentStatement = exports.oldestStatement = exports.resolveScoredForAttempt = exports.resolveCompletedForAttempt = exports.resolveAttempts = exports.matchAttemptScored = exports.matchAttemptCompleted = exports.matchAttempt = exports.EXT_PENDING_SCORING = void 0;
7
7
  /*
8
8
  * the structure of xapi statements for handling multiple attempts of an activity
9
9
  * including the option of a parent activity/attempt such that a new attempt
@@ -21,7 +21,9 @@ var Verb;
21
21
  (function (Verb) {
22
22
  Verb["Attempted"] = "http://adlnet.gov/expapi/verbs/attempted";
23
23
  Verb["Completed"] = "http://adlnet.gov/expapi/verbs/completed";
24
+ Verb["Scored"] = "http://adlnet.gov/expapi/verbs/scored";
24
25
  })(Verb || (Verb = {}));
26
+ exports.EXT_PENDING_SCORING = 'https://openstax.org/xapi/extensions/pending-scoring';
25
27
  const matchAttempt = (statement) => statement.verb.id === Verb.Attempted;
26
28
  exports.matchAttempt = matchAttempt;
27
29
  const matchAttemptCompleted = (attempt) => (statement) => {
@@ -32,6 +34,14 @@ const matchAttemptCompleted = (attempt) => (statement) => {
32
34
  && statement.context.registration === ((_b = attempt.context) === null || _b === void 0 ? void 0 : _b.registration);
33
35
  };
34
36
  exports.matchAttemptCompleted = matchAttemptCompleted;
37
+ const matchAttemptScored = (attempt) => (statement) => {
38
+ var _a, _b;
39
+ return statement.verb.id === Verb.Scored
40
+ && statement.context !== undefined
41
+ && ((_a = statement.context.statement) === null || _a === void 0 ? void 0 : _a.id) === attempt.id
42
+ && statement.context.registration === ((_b = attempt.context) === null || _b === void 0 ? void 0 : _b.registration);
43
+ };
44
+ exports.matchAttemptScored = matchAttemptScored;
35
45
  const resolveAttempts = (statements, options) => statements.filter(statement => {
36
46
  var _a;
37
47
  return (0, exports.matchAttempt)(statement)
@@ -42,6 +52,19 @@ exports.resolveAttempts = resolveAttempts;
42
52
  const resolveCompletedForAttempt = (statements, attempt, activityIRI) => statements.find(statement => (0, exports.matchAttemptCompleted)(attempt)(statement)
43
53
  && (!activityIRI || statement.object.id === activityIRI));
44
54
  exports.resolveCompletedForAttempt = resolveCompletedForAttempt;
55
+ const resolveScoredForAttempt = (statements, attempt, activityIRI) => {
56
+ // Prefer the most recent Scored
57
+ const scored = (0, exports.mostRecentStatement)(statements.filter(s => (0, exports.matchAttemptScored)(attempt)(s) && (!activityIRI || s.object.id === activityIRI)));
58
+ if (scored)
59
+ return scored;
60
+ // if no scored found, return most recent Completed with a score for the attempt.
61
+ const completedWithScore = (0, exports.mostRecentStatement)(statements.filter(s => (0, exports.matchAttemptCompleted)(attempt)(s) &&
62
+ (!activityIRI || s.object.id === activityIRI) && s.result &&
63
+ s.result.score &&
64
+ (s.result.score.scaled !== undefined)));
65
+ return completedWithScore;
66
+ };
67
+ exports.resolveScoredForAttempt = resolveScoredForAttempt;
45
68
  const oldestStatement = (statements) => statements.reduce((result, statement) => result && (0, isBefore_1.default)((0, parseISO_1.default)('stored' in result && result.stored ? result.stored : result.timestamp), (0, parseISO_1.default)(statement.timestamp)) ? result : statement, statements[0]);
46
69
  exports.oldestStatement = oldestStatement;
47
70
  const mostRecentStatement = (statements) => statements.reduce((result, statement) => result && (0, isAfter_1.default)((0, parseISO_1.default)('stored' in result && result.stored ? result.stored : result.timestamp), (0, parseISO_1.default)(statement.timestamp)) ? result : statement, statements[0]);
@@ -57,55 +80,81 @@ const resolveAttemptInfo = (statements, options) => {
57
80
  return acc;
58
81
  }, []);
59
82
  const completedPairs = resolveAttemptsWithCompleted(statements, attempts, options === null || options === void 0 ? void 0 : options.activityIRI);
60
- const completedAttempts = completedPairs.map(p => p.attempt);
83
+ const completedAttempts = completedPairs.length;
61
84
  /* the last attempt sorted by timestamp */
62
85
  const currentAttempt = (options === null || options === void 0 ? void 0 : options.currentAttempt)
63
86
  ? attempts.find(attempt => attempt.id === options.currentAttempt)
64
87
  : (options === null || options === void 0 ? void 0 : options.currentPreference) === 'oldest'
65
88
  ? (0, exports.oldestStatement)(attempts)
66
89
  : (0, exports.mostRecentStatement)(attempts);
67
- /* all statements for the current attempt (doesn't include the attempt or completed statements) */
68
- const currentAttemptStatements = currentAttempt ? statements.filter(statement => {
69
- var _a;
70
- return (!(options === null || options === void 0 ? void 0 : options.activityIRI) || statement.object.id === options.activityIRI)
71
- && ((_a = statement.context) === null || _a === void 0 ? void 0 : _a.registration) === currentAttempt.id;
72
- }) : [];
73
- const currentAttemptCompleted = currentAttempt && (0, exports.resolveCompletedForAttempt)(statements, currentAttempt, options === null || options === void 0 ? void 0 : options.activityIRI);
90
+ const current = currentAttempt
91
+ ? {
92
+ attempt: currentAttempt,
93
+ completed: (0, exports.resolveCompletedForAttempt)(statements, currentAttempt, options === null || options === void 0 ? void 0 : options.activityIRI),
94
+ scored: (0, exports.resolveScoredForAttempt)(statements, currentAttempt, options === null || options === void 0 ? void 0 : options.activityIRI),
95
+ }
96
+ : undefined;
97
+ const currentAttemptStatements = currentAttempt
98
+ ? statements.filter(statement => {
99
+ var _a;
100
+ return (!(options === null || options === void 0 ? void 0 : options.activityIRI) || statement.object.id === options.activityIRI) &&
101
+ ((_a = statement.context) === null || _a === void 0 ? void 0 : _a.registration) === currentAttempt.id;
102
+ })
103
+ : [];
74
104
  const mostRecentPair = completedPairs.reduce((cur, pair) => {
75
105
  if (!cur)
76
106
  return pair;
77
107
  return (0, isAfter_1.default)((0, parseISO_1.default)(cur.attempt.timestamp), (0, parseISO_1.default)(pair.attempt.timestamp)) ? cur : pair;
78
108
  }, undefined);
79
- /*
80
- * the structure allows for the possibility of multiple incomplete attempts.
81
- * the implementation can choose at its discretion to ignore the currentAttempt
82
- * and instead make a new one, for instance if the implementation desires
83
- * an attempt timeout feature
84
- */
85
- const hasScaledScore = (p) => {
109
+ const mostRecentWithCompleted = mostRecentPair
110
+ ? {
111
+ attempt: mostRecentPair.attempt,
112
+ completed: (0, exports.resolveCompletedForAttempt)(statements, mostRecentPair.attempt, options === null || options === void 0 ? void 0 : options.activityIRI),
113
+ scored: (0, exports.resolveScoredForAttempt)(statements, mostRecentPair.attempt, options === null || options === void 0 ? void 0 : options.activityIRI),
114
+ }
115
+ : undefined;
116
+ const scaledForAttempt = (attempt) => {
86
117
  var _a;
87
- return p.completed.result !== undefined &&
88
- typeof ((_a = p.completed.result.score) === null || _a === void 0 ? void 0 : _a.scaled) === 'number';
118
+ // resolveScoredForAttempt may return a Scored, or fallback to a Completed that has a score
119
+ const picked = (0, exports.resolveScoredForAttempt)(statements, attempt, options === null || options === void 0 ? void 0 : options.activityIRI);
120
+ const score = (_a = picked === null || picked === void 0 ? void 0 : picked.result) === null || _a === void 0 ? void 0 : _a.score;
121
+ if (!score)
122
+ return undefined;
123
+ // Prefer scaled, otherwise normalize raw/max if available
124
+ if (score.scaled !== undefined)
125
+ return score.scaled;
126
+ if (score.raw !== undefined && score.max !== undefined && score.max !== 0) {
127
+ return score.raw / score.max;
128
+ }
129
+ return undefined;
89
130
  };
90
- // Filter to only scored pairs with scaled.
91
- const scoredPairs = completedPairs.filter(hasScaledScore);
92
- const bestPair = scoredPairs.reduce((best, pair) => {
93
- if (!best)
94
- return pair;
95
- return pair.completed.result.score.scaled > best.completed.result.score.scaled
96
- ? pair
97
- : best;
131
+ let bestAttempt = completedPairs.reduce((best, { attempt }) => {
132
+ const bScaled = best !== undefined ? scaledForAttempt(best) : undefined;
133
+ const aScaled = scaledForAttempt(attempt);
134
+ if (bScaled === undefined)
135
+ return aScaled === undefined ? best : attempt;
136
+ if (aScaled === undefined)
137
+ return best;
138
+ return aScaled > bScaled ? attempt : best;
98
139
  }, undefined);
140
+ // Fallback if no attempt had a score, use newest completed as best
141
+ if (!bestAttempt && mostRecentPair) {
142
+ bestAttempt = mostRecentPair.attempt;
143
+ }
144
+ const best = bestAttempt
145
+ ? {
146
+ attempt: bestAttempt,
147
+ completed: (0, exports.resolveCompletedForAttempt)(statements, bestAttempt, options && options.activityIRI),
148
+ scored: (0, exports.resolveScoredForAttempt)(statements, bestAttempt, options && options.activityIRI),
149
+ }
150
+ : undefined;
99
151
  return {
100
152
  attempts: attempts.length,
101
- bestCompletedAttempt: bestPair === null || bestPair === void 0 ? void 0 : bestPair.attempt,
102
- bestCompletedAttemptCompleted: bestPair === null || bestPair === void 0 ? void 0 : bestPair.completed,
103
- completedAttempts: completedAttempts.length,
104
- currentAttempt,
105
- currentAttemptCompleted,
153
+ completedAttempts,
106
154
  currentAttemptStatements,
107
- mostRecentAttemptWithCompleted: mostRecentPair === null || mostRecentPair === void 0 ? void 0 : mostRecentPair.attempt,
108
- mostRecentAttemptWithCompletedCompleted: mostRecentPair === null || mostRecentPair === void 0 ? void 0 : mostRecentPair.completed,
155
+ current,
156
+ best,
157
+ mostRecentWithCompleted,
109
158
  };
110
159
  };
111
160
  exports.resolveAttemptInfo = resolveAttemptInfo;
@@ -281,3 +330,46 @@ const putCompletedStatement = async (gateway, attemptStatement, result) => {
281
330
  return (await gateway.putXapiStatements([(0, exports.createCompletedStatement)(attemptStatement, result)]))[0];
282
331
  };
283
332
  exports.putCompletedStatement = putCompletedStatement;
333
+ // pending score statement for when the open written response has been graded.
334
+ const createCompletedPendingScoringStatement = (attemptStatement) => (0, exports.createCompletedStatement)(attemptStatement, {
335
+ extensions: { [exports.EXT_PENDING_SCORING]: 'true' },
336
+ });
337
+ exports.createCompletedPendingScoringStatement = createCompletedPendingScoringStatement;
338
+ const putCompletedPendingScoringStatement = async (gateway, attemptStatement) => {
339
+ return (await gateway.putXapiStatements([
340
+ (0, exports.createCompletedPendingScoringStatement)(attemptStatement),
341
+ ]))[0];
342
+ };
343
+ exports.putCompletedPendingScoringStatement = putCompletedPendingScoringStatement;
344
+ // scored statement for when the open written response has been graded.
345
+ const createScoredStatement = (attemptStatement, result) => {
346
+ var _a, _b;
347
+ return {
348
+ context: {
349
+ ...(((_a = attemptStatement.context) === null || _a === void 0 ? void 0 : _a.contextActivities) ? {
350
+ contextActivities: attemptStatement.context.contextActivities,
351
+ } : {}),
352
+ ...(((_b = attemptStatement.context) === null || _b === void 0 ? void 0 : _b.registration) ? {
353
+ registration: attemptStatement.context.registration,
354
+ } : {}),
355
+ statement: {
356
+ objectType: 'StatementRef',
357
+ id: attemptStatement.id,
358
+ }
359
+ },
360
+ object: attemptStatement.object,
361
+ verb: {
362
+ display: { 'en-US': 'Scored' },
363
+ id: Verb.Scored,
364
+ },
365
+ result: {
366
+ // no duration here, we treat Scored as a pure grading event
367
+ ...result,
368
+ }
369
+ };
370
+ };
371
+ exports.createScoredStatement = createScoredStatement;
372
+ const putScoredStatement = async (gateway, attemptStatement, result) => {
373
+ return (await gateway.putXapiStatements([(0, exports.createScoredStatement)(attemptStatement, result)]))[0];
374
+ };
375
+ exports.putScoredStatement = putScoredStatement;
@@ -59,31 +59,32 @@ const getScoreGrade = (score, options) => {
59
59
  };
60
60
  exports.getScoreGrade = getScoreGrade;
61
61
  // These methods assign 0's to incomplete activities
62
- const pickAttemptAndCompleted = (state, gradePreference) => {
63
- if (gradePreference === 'best' && state.bestCompletedAttempt) {
64
- return {
65
- attempt: state.bestCompletedAttempt,
66
- completed: state.bestCompletedAttemptCompleted,
67
- };
68
- }
69
- // return current attempt if gradePreference is current
62
+ const pickAttemptAndCompletedOrScored = (state, gradePreference) => {
63
+ var _a;
64
+ const entry = (gradePreference === 'best' ? state.best : state.current);
70
65
  return {
71
- attempt: state.currentAttempt,
72
- completed: state.currentAttemptCompleted,
66
+ attempt: entry === null || entry === void 0 ? void 0 : entry.attempt,
67
+ completedOrScored: (_a = entry === null || entry === void 0 ? void 0 : entry.scored) !== null && _a !== void 0 ? _a : entry === null || entry === void 0 ? void 0 : entry.completed,
68
+ completed: entry === null || entry === void 0 ? void 0 : entry.completed,
73
69
  };
74
70
  };
75
71
  const getCompletedActivityStateGradeAndProgress = ({ name, scoreMaximum, state, userId, gradePreference = 'current' }) => {
76
- var _a;
77
- const { attempt, completed } = pickAttemptAndCompleted(state, gradePreference);
72
+ var _a, _b, _c;
73
+ const { attempt, completedOrScored, completed } = pickAttemptAndCompletedOrScored(state, gradePreference);
74
+ const grade = (0, exports.getScoreGrade)(((_a = completedOrScored === null || completedOrScored === void 0 ? void 0 : completedOrScored.result) === null || _a === void 0 ? void 0 : _a.score) || {}, {
75
+ maxScore: scoreMaximum,
76
+ startedAt: attempt === null || attempt === void 0 ? void 0 : attempt.timestamp,
77
+ submittedAt: completed === null || completed === void 0 ? void 0 : completed.timestamp,
78
+ userId,
79
+ });
80
+ // If this Completed is explicitly "pending scoring", surface that in the grade.
81
+ if (((_c = (_b = completedOrScored === null || completedOrScored === void 0 ? void 0 : completedOrScored.result) === null || _b === void 0 ? void 0 : _b.extensions) === null || _c === void 0 ? void 0 : _c[attempt_utils_1.EXT_PENDING_SCORING]) === 'true') {
82
+ grade.gradingProgress = 'PendingManual';
83
+ }
78
84
  return {
79
- grade: (0, exports.getScoreGrade)(((_a = completed === null || completed === void 0 ? void 0 : completed.result) === null || _a === void 0 ? void 0 : _a.score) || {}, {
80
- maxScore: scoreMaximum,
81
- startedAt: attempt === null || attempt === void 0 ? void 0 : attempt.timestamp,
82
- submittedAt: completed === null || completed === void 0 ? void 0 : completed.timestamp,
83
- userId,
84
- }),
85
+ grade,
85
86
  progress: {
86
- scaled: completed ? 1 : 0,
87
+ scaled: completedOrScored ? 1 : 0,
87
88
  },
88
89
  name,
89
90
  };
@@ -115,7 +116,7 @@ const getCurrentGrade = async (services, registration, options) => {
115
116
  gradePreference,
116
117
  });
117
118
  }
118
- if (userInfo.currentAttemptCompleted || !incompleteAttemptCallback) {
119
+ if (userInfo.current && userInfo.current.completed || !incompleteAttemptCallback) {
119
120
  return getCompletedActivityStateGradeAndProgress({
120
121
  name,
121
122
  scoreMaximum,
@@ -137,8 +138,8 @@ const getAssignmentGrades = async (services, assignmentIRI, registration, option
137
138
  }
138
139
  // Partition based on whether the chosen preference has a completed attempt
139
140
  const [incompleteInfo, completedInfo] = (0, partition_1.default)((info) => {
140
- const { completed } = pickAttemptAndCompleted(info.data, gradePreference);
141
- return completed === undefined;
141
+ const { completedOrScored } = pickAttemptAndCompletedOrScored(info.data, gradePreference);
142
+ return completedOrScored === undefined;
142
143
  })(mappedInfo);
143
144
  return (0, exports.getCompletedUserInfosGradeAndProgress)(completedInfo, scoreMaximum, gradePreference).concat(await incompleteAttemptsCallback(incompleteInfo));
144
145
  };