@openstax/ts-utils 1.37.2 → 1.38.0

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,24 +1,34 @@
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
- export declare const matchAttemptCompleted: (attempt: UXapiStatement) => (statement: UXapiStatement) => boolean;
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;
20
- export declare const oldestStatement: (statements: UXapiStatement[]) => UXapiStatement | undefined;
21
- export declare const mostRecentStatement: (statements: UXapiStatement[]) => UXapiStatement | undefined;
24
+ export declare const resolveScoredForAttempt: (statements: UXapiStatement[], attempt: UXapiStatement, activityIRI?: string) => UXapiStatement | undefined;
25
+ export declare const getStatementTimeString: (statement: UXapiStatement) => string;
26
+ export declare const getStatementTime: (statement: UXapiStatement) => Date;
27
+ export declare const oldestStatement: (items: UXapiStatement[]) => UXapiStatement | undefined;
28
+ export declare const mostRecentStatement: (items: UXapiStatement[]) => UXapiStatement | undefined;
29
+ export declare const oldestEntry: (items: AttemptEntry[]) => AttemptEntry | undefined;
30
+ export declare const mostRecentEntry: (items: AttemptEntry[]) => AttemptEntry | undefined;
31
+ export declare const bestEntry: (items: AttemptEntry[]) => AttemptEntry | undefined;
22
32
  export declare const resolveAttemptInfo: (statements: UXapiStatement[], options?: {
23
33
  activityIRI?: string;
24
34
  currentAttempt?: string;
@@ -70,3 +80,7 @@ export declare const createAttemptActivityStatement: (attemptStatement: UXapiSta
70
80
  export declare const putAttemptActivityStatement: (gateway: LrsGateway, attemptStatement: UXapiStatement, verb: UXapiStatement["verb"], result?: UXapiStatement["result"]) => Promise<import(".").EagerXapiStatement>;
71
81
  export declare const createCompletedStatement: (attemptStatement: UXapiStatement, result?: UXapiStatement["result"]) => Pick<UXapiStatement, "object" | "verb" | "context" | "result">;
72
82
  export declare const putCompletedStatement: (gateway: LrsGateway, attemptStatement: UXapiStatement, result: UXapiStatement["result"]) => Promise<import(".").EagerXapiStatement>;
83
+ export declare const createCompletedPendingScoringStatement: (attemptStatement: UXapiStatement) => Pick<UXapiStatement, "object" | "verb" | "context" | "result">;
84
+ export declare const putCompletedPendingScoringStatement: (gateway: LrsGateway, attemptStatement: UXapiStatement) => Promise<import(".").EagerXapiStatement>;
85
+ export declare const createScoredStatement: (attemptStatement: UXapiStatement, result: UXapiStatement["result"]) => Pick<UXapiStatement, "object" | "verb" | "context" | "result">;
86
+ export declare const putScoredStatement: (gateway: LrsGateway, attemptStatement: UXapiStatement, result: UXapiStatement["result"]) => Promise<import(".").EagerXapiStatement>;
@@ -15,11 +15,20 @@ 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;
22
- return statement.verb.id === Verb.Completed
24
+ return statement.verb.id === Verb.Completed && attempt.id
25
+ && statement.context !== undefined
26
+ && ((_a = statement.context.statement) === null || _a === void 0 ? void 0 : _a.id) === attempt.id
27
+ && statement.context.registration === ((_b = attempt.context) === null || _b === void 0 ? void 0 : _b.registration);
28
+ };
29
+ export const matchAttemptScored = (attempt) => (statement) => {
30
+ var _a, _b;
31
+ return statement.verb.id === Verb.Scored && attempt.id
23
32
  && statement.context !== undefined
24
33
  && ((_a = statement.context.statement) === null || _a === void 0 ? void 0 : _a.id) === attempt.id
25
34
  && statement.context.registration === ((_b = attempt.context) === null || _b === void 0 ? void 0 : _b.registration);
@@ -32,78 +41,91 @@ 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));
35
- 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
- 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
- export const resolveAttemptInfo = (statements, options) => {
38
- // TODO optimize. i'm 100% that this could all be done in one iteration but i'm not messing around with that for now.
39
- const attempts = resolveAttempts(statements, options);
40
- /* attempts that have a completed statement */
41
- const resolveAttemptsWithCompleted = (statements, attempts, activityIRI) => attempts.reduce((acc, attempt) => {
42
- const completed = resolveCompletedForAttempt(statements, attempt, activityIRI);
43
- if (completed)
44
- acc.push({ attempt, completed });
45
- return acc;
46
- }, []);
47
- const completedPairs = resolveAttemptsWithCompleted(statements, attempts, options === null || options === void 0 ? void 0 : options.activityIRI);
48
- const completedAttempts = completedPairs.map(p => p.attempt);
49
- /* the last attempt sorted by timestamp */
50
- const currentAttempt = (options === null || options === void 0 ? void 0 : options.currentAttempt)
51
- ? attempts.find(attempt => attempt.id === options.currentAttempt)
52
- : (options === null || options === void 0 ? void 0 : options.currentPreference) === 'oldest'
53
- ? oldestStatement(attempts)
54
- : mostRecentStatement(attempts);
55
- /* all statements for the current attempt (doesn't include the attempt or completed statements) */
56
- const currentAttemptStatements = currentAttempt ? statements.filter(statement => {
57
- var _a;
58
- return (!(options === null || options === void 0 ? void 0 : options.activityIRI) || statement.object.id === options.activityIRI)
59
- && ((_a = statement.context) === null || _a === void 0 ? void 0 : _a.registration) === currentAttempt.id;
60
- }) : [];
61
- const currentAttemptCompleted = currentAttempt && resolveCompletedForAttempt(statements, currentAttempt, options === null || options === void 0 ? void 0 : options.activityIRI);
62
- const mostRecentPair = completedPairs.reduce((cur, pair) => {
63
- if (!cur)
64
- return pair;
65
- return isAfter(parseISO(cur.attempt.timestamp), parseISO(pair.attempt.timestamp)) ? cur : pair;
66
- }, undefined);
67
- /*
68
- * the structure allows for the possibility of multiple incomplete attempts.
69
- * the implementation can choose at its discretion to ignore the currentAttempt
70
- * and instead make a new one, for instance if the implementation desires
71
- * an attempt timeout feature
72
- */
73
- const hasScaledScore = (p) => {
44
+ export const resolveScoredForAttempt = (statements, attempt, activityIRI) => {
45
+ // scored or completed statements that contain a score in their
46
+ // result are 'scoring statements' for our purposes.
47
+ const scoringStatements = statements.filter(s => {
74
48
  var _a;
75
- return p.completed.result !== undefined &&
76
- typeof ((_a = p.completed.result.score) === null || _a === void 0 ? void 0 : _a.scaled) === 'number';
77
- };
78
- // Find best scored pair
79
- const bestScoredPair = completedPairs
80
- .filter(hasScaledScore)
81
- .reduce((best, pair) => {
49
+ return (matchAttemptScored(attempt)(s) || matchAttemptCompleted(attempt)(s))
50
+ && ((_a = s.result) === null || _a === void 0 ? void 0 : _a.score)
51
+ && (!activityIRI || s.object.id === activityIRI);
52
+ });
53
+ return mostRecentStatement(scoringStatements);
54
+ };
55
+ const resolveAttemptEntries = (statements, attempts, activityIRI) => attempts.reduce((acc, attempt) => {
56
+ const completed = resolveCompletedForAttempt(statements, attempt, activityIRI);
57
+ const scored = resolveScoredForAttempt(statements, attempt, activityIRI);
58
+ acc.push({ attempt, completed, scored });
59
+ return acc;
60
+ }, []);
61
+ export const getStatementTimeString = (statement) => 'stored' in statement && statement.stored ? statement.stored : statement.timestamp;
62
+ export const getStatementTime = (statement) => parseISO(getStatementTimeString(statement));
63
+ const findByComparator = (comparator, extractor) => (items) => {
64
+ return items.reduce((best, item) => {
82
65
  if (!best)
83
- return pair;
84
- return pair.completed.result.score.scaled > best.completed.result.score.scaled ? pair : best;
66
+ return item;
67
+ return comparator(extractor(best), extractor(item)) ? best : item;
85
68
  }, undefined);
86
- // if no scored pair get newest completed without a score
87
- const bestDefaultPair = !bestScoredPair && completedPairs.length > 0
88
- ? completedPairs.reduce((best, pair) => {
89
- if (!best)
90
- return pair;
91
- return isAfter(parseISO(pair.attempt.timestamp), parseISO(best.attempt.timestamp))
92
- ? pair
93
- : best;
94
- }, undefined)
95
- : undefined;
96
- const bestPair = bestScoredPair !== null && bestScoredPair !== void 0 ? bestScoredPair : bestDefaultPair;
69
+ };
70
+ export const oldestStatement = findByComparator(isBefore, getStatementTime);
71
+ export const mostRecentStatement = findByComparator(isAfter, getStatementTime);
72
+ export const oldestEntry = findByComparator(isBefore, (entry) => getStatementTime(entry.attempt));
73
+ export const mostRecentEntry = findByComparator(isAfter, (entry) => getStatementTime(entry.attempt));
74
+ const scaledScoreForEntry = (entry) => {
75
+ var _a;
76
+ const scoringStatement = entry.scored;
77
+ const score = (_a = scoringStatement === null || scoringStatement === void 0 ? void 0 : scoringStatement.result) === null || _a === void 0 ? void 0 : _a.score;
78
+ if (!score)
79
+ return { score: undefined, entry };
80
+ // Prefer scaled, otherwise normalize raw/max if available
81
+ if (score.scaled !== undefined)
82
+ return { score: score.scaled, entry };
83
+ if (score.raw !== undefined && score.max !== undefined && score.max !== 0) {
84
+ return { score: score.raw / score.max, entry };
85
+ }
86
+ return { score: undefined, entry };
87
+ };
88
+ /*
89
+ * the 'best' entry is the one with the highest scaled score.
90
+ * if two entries have no score, the most recent is considered best.
91
+ */
92
+ export const bestEntry = findByComparator((a, b) => {
93
+ // if neither has a score, use the most recent
94
+ if (a.score === undefined && b.score === undefined) {
95
+ return isAfter(getStatementTime(a.entry.attempt), getStatementTime(b.entry.attempt));
96
+ }
97
+ if (a.score === undefined)
98
+ return false;
99
+ if (b.score === undefined)
100
+ return true;
101
+ return a.score > b.score;
102
+ }, scaledScoreForEntry);
103
+ export const resolveAttemptInfo = (statements, options) => {
104
+ // TODO optimize. i'm 100% that this could all be done in one iteration but i'm not messing around with that for now.
105
+ const attempts = resolveAttempts(statements, options);
106
+ const attemptEntries = resolveAttemptEntries(statements, attempts, options === null || options === void 0 ? void 0 : options.activityIRI);
107
+ const completedEntries = attemptEntries.filter((entry) => !!entry.completed);
108
+ const current = (options === null || options === void 0 ? void 0 : options.currentAttempt)
109
+ ? attemptEntries.find(({ attempt }) => attempt.id === options.currentAttempt)
110
+ : (options === null || options === void 0 ? void 0 : options.currentPreference) === 'oldest' /* the attempt sorted by timestamp */
111
+ ? oldestEntry(attemptEntries)
112
+ : mostRecentEntry(attemptEntries);
113
+ const currentAttemptStatements = current
114
+ ? statements.filter(statement => {
115
+ var _a;
116
+ return (!(options === null || options === void 0 ? void 0 : options.activityIRI) || statement.object.id === options.activityIRI) &&
117
+ ((_a = statement.context) === null || _a === void 0 ? void 0 : _a.registration) === current.attempt.id;
118
+ })
119
+ : [];
120
+ const mostRecentWithCompleted = mostRecentEntry(completedEntries);
121
+ const best = bestEntry(completedEntries);
97
122
  return {
98
123
  attempts: attempts.length,
99
- bestCompletedAttempt: bestPair === null || bestPair === void 0 ? void 0 : bestPair.attempt,
100
- bestCompletedAttemptCompleted: bestPair === null || bestPair === void 0 ? void 0 : bestPair.completed,
101
- completedAttempts: completedAttempts.length,
102
- currentAttempt,
103
- currentAttemptCompleted,
124
+ completedAttempts: completedEntries.length,
104
125
  currentAttemptStatements,
105
- mostRecentAttemptWithCompleted: mostRecentPair === null || mostRecentPair === void 0 ? void 0 : mostRecentPair.attempt,
106
- mostRecentAttemptWithCompletedCompleted: mostRecentPair === null || mostRecentPair === void 0 ? void 0 : mostRecentPair.completed,
126
+ current,
127
+ best,
128
+ mostRecentWithCompleted,
107
129
  };
108
130
  };
109
131
  /*
@@ -269,3 +291,42 @@ export const createCompletedStatement = (attemptStatement, result) => {
269
291
  export const putCompletedStatement = async (gateway, attemptStatement, result) => {
270
292
  return (await gateway.putXapiStatements([createCompletedStatement(attemptStatement, result)]))[0];
271
293
  };
294
+ // pending score statement for when the open written response has been graded.
295
+ export const createCompletedPendingScoringStatement = (attemptStatement) => createCompletedStatement(attemptStatement, {
296
+ extensions: { [EXT_PENDING_SCORING]: 'true' },
297
+ });
298
+ export const putCompletedPendingScoringStatement = async (gateway, attemptStatement) => {
299
+ return (await gateway.putXapiStatements([
300
+ createCompletedPendingScoringStatement(attemptStatement),
301
+ ]))[0];
302
+ };
303
+ // scored statement for when the open written response has been graded.
304
+ export const createScoredStatement = (attemptStatement, result) => {
305
+ var _a, _b;
306
+ return {
307
+ context: {
308
+ ...(((_a = attemptStatement.context) === null || _a === void 0 ? void 0 : _a.contextActivities) ? {
309
+ contextActivities: attemptStatement.context.contextActivities,
310
+ } : {}),
311
+ ...(((_b = attemptStatement.context) === null || _b === void 0 ? void 0 : _b.registration) ? {
312
+ registration: attemptStatement.context.registration,
313
+ } : {}),
314
+ statement: {
315
+ objectType: 'StatementRef',
316
+ id: attemptStatement.id,
317
+ }
318
+ },
319
+ object: attemptStatement.object,
320
+ verb: {
321
+ display: { 'en-US': 'Scored' },
322
+ id: Verb.Scored,
323
+ },
324
+ result: {
325
+ // no duration here, we treat Scored as a pure grading event
326
+ ...result,
327
+ }
328
+ };
329
+ };
330
+ export const putScoredStatement = async (gateway, attemptStatement, result) => {
331
+ return (await gateway.putXapiStatements([createScoredStatement(attemptStatement, result)]))[0];
332
+ };
@@ -1,6 +1,6 @@
1
1
  import { AccountsGateway, MappedUserInfo } from '../accountsGateway';
2
2
  import { AuthProvider } from '../authProvider';
3
- import { ActivityState } from './attempt-utils';
3
+ import { ActivityState, AttemptEntry } from './attempt-utils';
4
4
  import { LrsGateway } from '.';
5
5
  export interface Grade {
6
6
  activityProgress: 'Initialized' | 'Started' | 'inProgress' | 'Submitted' | 'Completed';
@@ -23,15 +23,8 @@ export declare const getRegistrationAttemptInfo: (lrs: LrsGateway, registration:
23
23
  }) => Promise<{
24
24
  [key: string]: ActivityState;
25
25
  }>;
26
- export declare const getScoreGrade: (score: {
27
- scaled?: number;
28
- raw?: number;
29
- min?: number;
30
- max?: number;
31
- }, options: {
26
+ export declare const getScoreGrade: (entry: Partial<AttemptEntry>, options: {
32
27
  maxScore?: number;
33
- startedAt?: string;
34
- submittedAt?: string;
35
28
  userId?: string;
36
29
  }) => Grade;
37
30
  export type Progress = {
@@ -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, getStatementTimeString, 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 });
@@ -23,59 +23,47 @@ export const getRegistrationAttemptInfo = async (lrs, registration, options) =>
23
23
  // lti: http://www.imsglobal.org/spec/lti-ags/v2p0#score-publish-service
24
24
  // ltijs: https://cvmcosta.me/ltijs/#/grading
25
25
  // Note: "min" is currently completely ignored
26
- export const getScoreGrade = (score, options) => {
27
- const { raw, scaled, max } = score;
28
- const { maxScore, startedAt, submittedAt, userId } = options;
26
+ export const getScoreGrade = (entry, options) => {
27
+ var _a, _b, _c, _d, _e;
28
+ const { raw, scaled, max } = ((_b = (_a = entry.scored) === null || _a === void 0 ? void 0 : _a.result) === null || _b === void 0 ? void 0 : _b.score) || {};
29
+ const { maxScore, userId } = options;
29
30
  const scoreMaximum = maxScore !== null && maxScore !== void 0 ? maxScore : 100;
30
31
  const scoreGiven = raw && max
31
32
  ? scoreMaximum / max * raw
32
33
  : scaled
33
34
  ? scaled * scoreMaximum
34
35
  : 0;
35
- const submission = {};
36
- if (startedAt) {
37
- submission.startedAt = startedAt;
38
- }
39
- if (submittedAt) {
40
- submission.submittedAt = submittedAt;
41
- }
36
+ const submission = {
37
+ startedAt: entry.attempt ? getStatementTimeString(entry.attempt) : undefined,
38
+ submittedAt: entry.completed ? getStatementTimeString(entry.completed) : undefined,
39
+ };
40
+ const pendingManual = entry.scored === undefined
41
+ && ((_e = (_d = (_c = entry.completed) === null || _c === void 0 ? void 0 : _c.result) === null || _d === void 0 ? void 0 : _d.extensions) === null || _e === void 0 ? void 0 : _e[EXT_PENDING_SCORING]) === 'true';
42
42
  return {
43
43
  userId,
44
- activityProgress: submittedAt ? 'Completed' : 'Started',
44
+ activityProgress: entry.completed ? 'Completed' : 'Started',
45
45
  // canvas assumes that anything that isn't 'FullyGraded' requires manual grading and displays a "needs grading" icon.
46
46
  // if you warp your mind you can consider the portion of the assignment which is completed to be fully graded.
47
- gradingProgress: 'FullyGraded',
47
+ gradingProgress: pendingManual ? 'PendingManual' : 'FullyGraded',
48
48
  scoreMaximum,
49
49
  scoreGiven: roundToPrecision(scoreGiven, -2),
50
50
  submission,
51
51
  };
52
52
  };
53
53
  // These methods assign 0's to incomplete activities
54
- const pickAttemptAndCompleted = (state, gradePreference) => {
55
- if (gradePreference === 'best' && state.bestCompletedAttempt) {
56
- return {
57
- attempt: state.bestCompletedAttempt,
58
- completed: state.bestCompletedAttemptCompleted,
59
- };
60
- }
61
- // return current attempt if gradePreference is current
62
- return {
63
- attempt: state.currentAttempt,
64
- completed: state.currentAttemptCompleted,
65
- };
54
+ const getGradingAttemptEntry = (state, gradePreference) => {
55
+ return (gradePreference === 'best' ? state.best : state.current) || {};
66
56
  };
67
57
  const getCompletedActivityStateGradeAndProgress = ({ name, scoreMaximum, state, userId, gradePreference = 'current' }) => {
68
- var _a;
69
- const { attempt, completed } = pickAttemptAndCompleted(state, gradePreference);
58
+ const entry = getGradingAttemptEntry(state, gradePreference);
59
+ const grade = getScoreGrade(entry, {
60
+ maxScore: scoreMaximum,
61
+ userId,
62
+ });
70
63
  return {
71
- grade: getScoreGrade(((_a = completed === null || completed === void 0 ? void 0 : completed.result) === null || _a === void 0 ? void 0 : _a.score) || {}, {
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
- }),
64
+ grade,
77
65
  progress: {
78
- scaled: completed ? 1 : 0,
66
+ scaled: entry.completed ? 1 : 0,
79
67
  },
80
68
  name,
81
69
  };
@@ -88,7 +76,7 @@ export const getCompletedUserInfosGradeAndProgress = (infos, scoreMaximum, grade
88
76
  gradePreference,
89
77
  }));
90
78
  export const getCurrentGrade = async (services, registration, options) => {
91
- var _a;
79
+ var _a, _b;
92
80
  const user = await services.ltiAuthProvider.getUser();
93
81
  if (!user) {
94
82
  return null;
@@ -106,7 +94,7 @@ export const getCurrentGrade = async (services, registration, options) => {
106
94
  gradePreference,
107
95
  });
108
96
  }
109
- if (userInfo.currentAttemptCompleted || !incompleteAttemptCallback) {
97
+ if (((_b = userInfo.current) === null || _b === void 0 ? void 0 : _b.completed) || !incompleteAttemptCallback) {
110
98
  return getCompletedActivityStateGradeAndProgress({
111
99
  name,
112
100
  scoreMaximum,
@@ -127,7 +115,7 @@ export const getAssignmentGrades = async (services, assignmentIRI, registration,
127
115
  }
128
116
  // Partition based on whether the chosen preference has a completed attempt
129
117
  const [incompleteInfo, completedInfo] = partition((info) => {
130
- const { completed } = pickAttemptAndCompleted(info.data, gradePreference);
118
+ const { completed } = getGradingAttemptEntry(info.data, gradePreference);
131
119
  return completed === undefined;
132
120
  })(mappedInfo);
133
121
  return getCompletedUserInfosGradeAndProgress(completedInfo, scoreMaximum, gradePreference).concat(await incompleteAttemptsCallback(incompleteInfo));