@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.
- package/dist/cjs/services/lrsGateway/attempt-utils.d.ts +15 -6
- package/dist/cjs/services/lrsGateway/attempt-utils.js +125 -33
- package/dist/cjs/services/lrsGateway/xapiUtils.js +23 -22
- package/dist/cjs/tsconfig.without-specs.cjs.tsbuildinfo +1 -1
- package/dist/esm/services/lrsGateway/attempt-utils.d.ts +15 -6
- package/dist/esm/services/lrsGateway/attempt-utils.js +118 -32
- package/dist/esm/services/lrsGateway/xapiUtils.js +24 -23
- package/dist/esm/tsconfig.without-specs.esm.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/script/bin/.init-params-script.bash.swp +0 -0
|
@@ -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
|
-
|
|
11
|
-
|
|
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>;
|
|
@@ -15,7 +15,9 @@ var Verb;
|
|
|
15
15
|
(function (Verb) {
|
|
16
16
|
Verb["Attempted"] = "http://adlnet.gov/expapi/verbs/attempted";
|
|
17
17
|
Verb["Completed"] = "http://adlnet.gov/expapi/verbs/completed";
|
|
18
|
+
Verb["Scored"] = "http://adlnet.gov/expapi/verbs/scored";
|
|
18
19
|
})(Verb || (Verb = {}));
|
|
20
|
+
export const EXT_PENDING_SCORING = 'https://openstax.org/xapi/extensions/pending-scoring';
|
|
19
21
|
export const matchAttempt = (statement) => statement.verb.id === Verb.Attempted;
|
|
20
22
|
export const matchAttemptCompleted = (attempt) => (statement) => {
|
|
21
23
|
var _a, _b;
|
|
@@ -24,6 +26,13 @@ export const matchAttemptCompleted = (attempt) => (statement) => {
|
|
|
24
26
|
&& ((_a = statement.context.statement) === null || _a === void 0 ? void 0 : _a.id) === attempt.id
|
|
25
27
|
&& statement.context.registration === ((_b = attempt.context) === null || _b === void 0 ? void 0 : _b.registration);
|
|
26
28
|
};
|
|
29
|
+
export const matchAttemptScored = (attempt) => (statement) => {
|
|
30
|
+
var _a, _b;
|
|
31
|
+
return statement.verb.id === Verb.Scored
|
|
32
|
+
&& statement.context !== undefined
|
|
33
|
+
&& ((_a = statement.context.statement) === null || _a === void 0 ? void 0 : _a.id) === attempt.id
|
|
34
|
+
&& statement.context.registration === ((_b = attempt.context) === null || _b === void 0 ? void 0 : _b.registration);
|
|
35
|
+
};
|
|
27
36
|
export const resolveAttempts = (statements, options) => statements.filter(statement => {
|
|
28
37
|
var _a;
|
|
29
38
|
return matchAttempt(statement)
|
|
@@ -32,6 +41,18 @@ export const resolveAttempts = (statements, options) => statements.filter(statem
|
|
|
32
41
|
});
|
|
33
42
|
export const resolveCompletedForAttempt = (statements, attempt, activityIRI) => statements.find(statement => matchAttemptCompleted(attempt)(statement)
|
|
34
43
|
&& (!activityIRI || statement.object.id === activityIRI));
|
|
44
|
+
export const resolveScoredForAttempt = (statements, attempt, activityIRI) => {
|
|
45
|
+
// Prefer the most recent Scored
|
|
46
|
+
const scored = mostRecentStatement(statements.filter(s => matchAttemptScored(attempt)(s) && (!activityIRI || s.object.id === activityIRI)));
|
|
47
|
+
if (scored)
|
|
48
|
+
return scored;
|
|
49
|
+
// if no scored found, return most recent Completed with a score for the attempt.
|
|
50
|
+
const completedWithScore = mostRecentStatement(statements.filter(s => matchAttemptCompleted(attempt)(s) &&
|
|
51
|
+
(!activityIRI || s.object.id === activityIRI) && s.result &&
|
|
52
|
+
s.result.score &&
|
|
53
|
+
(s.result.score.scaled !== undefined)));
|
|
54
|
+
return completedWithScore;
|
|
55
|
+
};
|
|
35
56
|
export const oldestStatement = (statements) => statements.reduce((result, statement) => result && isBefore(parseISO('stored' in result && result.stored ? result.stored : result.timestamp), parseISO(statement.timestamp)) ? result : statement, statements[0]);
|
|
36
57
|
export const mostRecentStatement = (statements) => statements.reduce((result, statement) => result && isAfter(parseISO('stored' in result && result.stored ? result.stored : result.timestamp), parseISO(statement.timestamp)) ? result : statement, statements[0]);
|
|
37
58
|
export const resolveAttemptInfo = (statements, options) => {
|
|
@@ -45,55 +66,81 @@ export const resolveAttemptInfo = (statements, options) => {
|
|
|
45
66
|
return acc;
|
|
46
67
|
}, []);
|
|
47
68
|
const completedPairs = resolveAttemptsWithCompleted(statements, attempts, options === null || options === void 0 ? void 0 : options.activityIRI);
|
|
48
|
-
const completedAttempts = completedPairs.
|
|
69
|
+
const completedAttempts = completedPairs.length;
|
|
49
70
|
/* the last attempt sorted by timestamp */
|
|
50
71
|
const currentAttempt = (options === null || options === void 0 ? void 0 : options.currentAttempt)
|
|
51
72
|
? attempts.find(attempt => attempt.id === options.currentAttempt)
|
|
52
73
|
: (options === null || options === void 0 ? void 0 : options.currentPreference) === 'oldest'
|
|
53
74
|
? oldestStatement(attempts)
|
|
54
75
|
: mostRecentStatement(attempts);
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
76
|
+
const current = currentAttempt
|
|
77
|
+
? {
|
|
78
|
+
attempt: currentAttempt,
|
|
79
|
+
completed: resolveCompletedForAttempt(statements, currentAttempt, options === null || options === void 0 ? void 0 : options.activityIRI),
|
|
80
|
+
scored: resolveScoredForAttempt(statements, currentAttempt, options === null || options === void 0 ? void 0 : options.activityIRI),
|
|
81
|
+
}
|
|
82
|
+
: undefined;
|
|
83
|
+
const currentAttemptStatements = currentAttempt
|
|
84
|
+
? statements.filter(statement => {
|
|
85
|
+
var _a;
|
|
86
|
+
return (!(options === null || options === void 0 ? void 0 : options.activityIRI) || statement.object.id === options.activityIRI) &&
|
|
87
|
+
((_a = statement.context) === null || _a === void 0 ? void 0 : _a.registration) === currentAttempt.id;
|
|
88
|
+
})
|
|
89
|
+
: [];
|
|
62
90
|
const mostRecentPair = completedPairs.reduce((cur, pair) => {
|
|
63
91
|
if (!cur)
|
|
64
92
|
return pair;
|
|
65
93
|
return isAfter(parseISO(cur.attempt.timestamp), parseISO(pair.attempt.timestamp)) ? cur : pair;
|
|
66
94
|
}, undefined);
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
95
|
+
const mostRecentWithCompleted = mostRecentPair
|
|
96
|
+
? {
|
|
97
|
+
attempt: mostRecentPair.attempt,
|
|
98
|
+
completed: resolveCompletedForAttempt(statements, mostRecentPair.attempt, options === null || options === void 0 ? void 0 : options.activityIRI),
|
|
99
|
+
scored: resolveScoredForAttempt(statements, mostRecentPair.attempt, options === null || options === void 0 ? void 0 : options.activityIRI),
|
|
100
|
+
}
|
|
101
|
+
: undefined;
|
|
102
|
+
const scaledForAttempt = (attempt) => {
|
|
74
103
|
var _a;
|
|
75
|
-
return
|
|
76
|
-
|
|
104
|
+
// resolveScoredForAttempt may return a Scored, or fallback to a Completed that has a score
|
|
105
|
+
const picked = resolveScoredForAttempt(statements, attempt, options === null || options === void 0 ? void 0 : options.activityIRI);
|
|
106
|
+
const score = (_a = picked === null || picked === void 0 ? void 0 : picked.result) === null || _a === void 0 ? void 0 : _a.score;
|
|
107
|
+
if (!score)
|
|
108
|
+
return undefined;
|
|
109
|
+
// Prefer scaled, otherwise normalize raw/max if available
|
|
110
|
+
if (score.scaled !== undefined)
|
|
111
|
+
return score.scaled;
|
|
112
|
+
if (score.raw !== undefined && score.max !== undefined && score.max !== 0) {
|
|
113
|
+
return score.raw / score.max;
|
|
114
|
+
}
|
|
115
|
+
return undefined;
|
|
77
116
|
};
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
if (
|
|
82
|
-
return
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
117
|
+
let bestAttempt = completedPairs.reduce((best, { attempt }) => {
|
|
118
|
+
const bScaled = best !== undefined ? scaledForAttempt(best) : undefined;
|
|
119
|
+
const aScaled = scaledForAttempt(attempt);
|
|
120
|
+
if (bScaled === undefined)
|
|
121
|
+
return aScaled === undefined ? best : attempt;
|
|
122
|
+
if (aScaled === undefined)
|
|
123
|
+
return best;
|
|
124
|
+
return aScaled > bScaled ? attempt : best;
|
|
86
125
|
}, undefined);
|
|
126
|
+
// Fallback if no attempt had a score, use newest completed as best
|
|
127
|
+
if (!bestAttempt && mostRecentPair) {
|
|
128
|
+
bestAttempt = mostRecentPair.attempt;
|
|
129
|
+
}
|
|
130
|
+
const best = bestAttempt
|
|
131
|
+
? {
|
|
132
|
+
attempt: bestAttempt,
|
|
133
|
+
completed: resolveCompletedForAttempt(statements, bestAttempt, options && options.activityIRI),
|
|
134
|
+
scored: resolveScoredForAttempt(statements, bestAttempt, options && options.activityIRI),
|
|
135
|
+
}
|
|
136
|
+
: undefined;
|
|
87
137
|
return {
|
|
88
138
|
attempts: attempts.length,
|
|
89
|
-
|
|
90
|
-
bestCompletedAttemptCompleted: bestPair === null || bestPair === void 0 ? void 0 : bestPair.completed,
|
|
91
|
-
completedAttempts: completedAttempts.length,
|
|
92
|
-
currentAttempt,
|
|
93
|
-
currentAttemptCompleted,
|
|
139
|
+
completedAttempts,
|
|
94
140
|
currentAttemptStatements,
|
|
95
|
-
|
|
96
|
-
|
|
141
|
+
current,
|
|
142
|
+
best,
|
|
143
|
+
mostRecentWithCompleted,
|
|
97
144
|
};
|
|
98
145
|
};
|
|
99
146
|
/*
|
|
@@ -259,3 +306,42 @@ export const createCompletedStatement = (attemptStatement, result) => {
|
|
|
259
306
|
export const putCompletedStatement = async (gateway, attemptStatement, result) => {
|
|
260
307
|
return (await gateway.putXapiStatements([createCompletedStatement(attemptStatement, result)]))[0];
|
|
261
308
|
};
|
|
309
|
+
// pending score statement for when the open written response has been graded.
|
|
310
|
+
export const createCompletedPendingScoringStatement = (attemptStatement) => createCompletedStatement(attemptStatement, {
|
|
311
|
+
extensions: { [EXT_PENDING_SCORING]: 'true' },
|
|
312
|
+
});
|
|
313
|
+
export const putCompletedPendingScoringStatement = async (gateway, attemptStatement) => {
|
|
314
|
+
return (await gateway.putXapiStatements([
|
|
315
|
+
createCompletedPendingScoringStatement(attemptStatement),
|
|
316
|
+
]))[0];
|
|
317
|
+
};
|
|
318
|
+
// scored statement for when the open written response has been graded.
|
|
319
|
+
export const createScoredStatement = (attemptStatement, result) => {
|
|
320
|
+
var _a, _b;
|
|
321
|
+
return {
|
|
322
|
+
context: {
|
|
323
|
+
...(((_a = attemptStatement.context) === null || _a === void 0 ? void 0 : _a.contextActivities) ? {
|
|
324
|
+
contextActivities: attemptStatement.context.contextActivities,
|
|
325
|
+
} : {}),
|
|
326
|
+
...(((_b = attemptStatement.context) === null || _b === void 0 ? void 0 : _b.registration) ? {
|
|
327
|
+
registration: attemptStatement.context.registration,
|
|
328
|
+
} : {}),
|
|
329
|
+
statement: {
|
|
330
|
+
objectType: 'StatementRef',
|
|
331
|
+
id: attemptStatement.id,
|
|
332
|
+
}
|
|
333
|
+
},
|
|
334
|
+
object: attemptStatement.object,
|
|
335
|
+
verb: {
|
|
336
|
+
display: { 'en-US': 'Scored' },
|
|
337
|
+
id: Verb.Scored,
|
|
338
|
+
},
|
|
339
|
+
result: {
|
|
340
|
+
// no duration here, we treat Scored as a pure grading event
|
|
341
|
+
...result,
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
};
|
|
345
|
+
export const putScoredStatement = async (gateway, attemptStatement, result) => {
|
|
346
|
+
return (await gateway.putXapiStatements([createScoredStatement(attemptStatement, result)]))[0];
|
|
347
|
+
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import partition from 'lodash/fp/partition';
|
|
2
2
|
import { roundToPrecision } from '../..';
|
|
3
|
-
import { resolveAttemptInfo } from './attempt-utils';
|
|
3
|
+
import { EXT_PENDING_SCORING, resolveAttemptInfo } from './attempt-utils';
|
|
4
4
|
export const getRegistrationAttemptInfo = async (lrs, registration, options) => {
|
|
5
5
|
const { currentPreference, ...xapiOptions } = options !== null && options !== void 0 ? options : {};
|
|
6
6
|
const allStatements = await lrs.getAllXapiStatements({ ...xapiOptions, registration });
|
|
@@ -51,31 +51,32 @@ export const getScoreGrade = (score, options) => {
|
|
|
51
51
|
};
|
|
52
52
|
};
|
|
53
53
|
// These methods assign 0's to incomplete activities
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
attempt: state.bestCompletedAttempt,
|
|
58
|
-
completed: state.bestCompletedAttemptCompleted,
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
// return current attempt if gradePreference is current
|
|
54
|
+
const pickAttemptAndCompletedOrScored = (state, gradePreference) => {
|
|
55
|
+
var _a;
|
|
56
|
+
const entry = (gradePreference === 'best' ? state.best : state.current);
|
|
62
57
|
return {
|
|
63
|
-
attempt:
|
|
64
|
-
|
|
58
|
+
attempt: entry === null || entry === void 0 ? void 0 : entry.attempt,
|
|
59
|
+
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,
|
|
60
|
+
completed: entry === null || entry === void 0 ? void 0 : entry.completed,
|
|
65
61
|
};
|
|
66
62
|
};
|
|
67
63
|
const getCompletedActivityStateGradeAndProgress = ({ name, scoreMaximum, state, userId, gradePreference = 'current' }) => {
|
|
68
|
-
var _a;
|
|
69
|
-
const { attempt, completed } =
|
|
64
|
+
var _a, _b, _c;
|
|
65
|
+
const { attempt, completedOrScored, completed } = pickAttemptAndCompletedOrScored(state, gradePreference);
|
|
66
|
+
const grade = getScoreGrade(((_a = completedOrScored === null || completedOrScored === void 0 ? void 0 : completedOrScored.result) === null || _a === void 0 ? void 0 : _a.score) || {}, {
|
|
67
|
+
maxScore: scoreMaximum,
|
|
68
|
+
startedAt: attempt === null || attempt === void 0 ? void 0 : attempt.timestamp,
|
|
69
|
+
submittedAt: completed === null || completed === void 0 ? void 0 : completed.timestamp,
|
|
70
|
+
userId,
|
|
71
|
+
});
|
|
72
|
+
// If this Completed is explicitly "pending scoring", surface that in the grade.
|
|
73
|
+
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[EXT_PENDING_SCORING]) === 'true') {
|
|
74
|
+
grade.gradingProgress = 'PendingManual';
|
|
75
|
+
}
|
|
70
76
|
return {
|
|
71
|
-
grade
|
|
72
|
-
maxScore: scoreMaximum,
|
|
73
|
-
startedAt: attempt === null || attempt === void 0 ? void 0 : attempt.timestamp,
|
|
74
|
-
submittedAt: completed === null || completed === void 0 ? void 0 : completed.timestamp,
|
|
75
|
-
userId,
|
|
76
|
-
}),
|
|
77
|
+
grade,
|
|
77
78
|
progress: {
|
|
78
|
-
scaled:
|
|
79
|
+
scaled: completedOrScored ? 1 : 0,
|
|
79
80
|
},
|
|
80
81
|
name,
|
|
81
82
|
};
|
|
@@ -106,7 +107,7 @@ export const getCurrentGrade = async (services, registration, options) => {
|
|
|
106
107
|
gradePreference,
|
|
107
108
|
});
|
|
108
109
|
}
|
|
109
|
-
if (userInfo.
|
|
110
|
+
if (userInfo.current && userInfo.current.completed || !incompleteAttemptCallback) {
|
|
110
111
|
return getCompletedActivityStateGradeAndProgress({
|
|
111
112
|
name,
|
|
112
113
|
scoreMaximum,
|
|
@@ -127,8 +128,8 @@ export const getAssignmentGrades = async (services, assignmentIRI, registration,
|
|
|
127
128
|
}
|
|
128
129
|
// Partition based on whether the chosen preference has a completed attempt
|
|
129
130
|
const [incompleteInfo, completedInfo] = partition((info) => {
|
|
130
|
-
const {
|
|
131
|
-
return
|
|
131
|
+
const { completedOrScored } = pickAttemptAndCompletedOrScored(info.data, gradePreference);
|
|
132
|
+
return completedOrScored === undefined;
|
|
132
133
|
})(mappedInfo);
|
|
133
134
|
return getCompletedUserInfosGradeAndProgress(completedInfo, scoreMaximum, gradePreference).concat(await incompleteAttemptsCallback(incompleteInfo));
|
|
134
135
|
};
|