@openstax/ts-utils 1.33.1 → 1.34.1

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.
@@ -62,8 +62,7 @@ export const s3FileServer = (initializer) => (configProvider) => {
62
62
  const Conditions = [
63
63
  { acl: 'private' },
64
64
  { bucket },
65
- ['starts-with', '$key', prefix],
66
- ['starts-with', '$Content-Type', ''],
65
+ ['starts-with', '$key', prefix]
67
66
  ];
68
67
  const defaultFields = {
69
68
  acl: 'private',
@@ -1,6 +1,8 @@
1
1
  import { LrsGateway, UXapiStatement } from '.';
2
2
  export type ActivityState = {
3
3
  attempts: number;
4
+ bestCompletedAttempt?: UXapiStatement;
5
+ bestCompletedAttemptCompleted?: UXapiStatement;
4
6
  completedAttempts: number;
5
7
  currentAttempt?: UXapiStatement;
6
8
  currentAttemptCompleted?: UXapiStatement;
@@ -38,7 +38,14 @@ export const resolveAttemptInfo = (statements, options) => {
38
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
39
  const attempts = resolveAttempts(statements, options);
40
40
  /* attempts that have a completed statement */
41
- const completedAttempts = attempts.filter(attempt => !!resolveCompletedForAttempt(statements, attempt, options === null || options === void 0 ? void 0 : options.activityIRI));
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);
42
49
  /* the last attempt sorted by timestamp */
43
50
  const currentAttempt = (options === null || options === void 0 ? void 0 : options.currentAttempt)
44
51
  ? attempts.find(attempt => attempt.id === options.currentAttempt)
@@ -52,23 +59,41 @@ export const resolveAttemptInfo = (statements, options) => {
52
59
  && ((_a = statement.context) === null || _a === void 0 ? void 0 : _a.registration) === currentAttempt.id;
53
60
  }) : [];
54
61
  const currentAttemptCompleted = currentAttempt && resolveCompletedForAttempt(statements, currentAttempt, options === null || options === void 0 ? void 0 : options.activityIRI);
55
- const mostRecentAttemptWithCompleted = completedAttempts.reduce((current, attempt) => current && isAfter(parseISO(current.timestamp), parseISO(attempt.timestamp)) ? current : attempt, completedAttempts[0]);
56
- const mostRecentAttemptWithCompletedCompleted = mostRecentAttemptWithCompleted
57
- && resolveCompletedForAttempt(statements, mostRecentAttemptWithCompleted, 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);
58
67
  /*
59
68
  * the structure allows for the possibility of multiple incomplete attempts.
60
69
  * the implementation can choose at its discretion to ignore the currentAttempt
61
70
  * and instead make a new one, for instance if the implementation desires
62
71
  * an attempt timeout feature
63
72
  */
73
+ const hasScaledScore = (p) => {
74
+ 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
+ // Filter to only scored pairs with scaled.
79
+ const scoredPairs = completedPairs.filter(hasScaledScore);
80
+ const bestPair = scoredPairs.reduce((best, pair) => {
81
+ if (!best)
82
+ return pair;
83
+ return pair.completed.result.score.scaled > best.completed.result.score.scaled
84
+ ? pair
85
+ : best;
86
+ }, undefined);
64
87
  return {
65
88
  attempts: attempts.length,
89
+ bestCompletedAttempt: bestPair === null || bestPair === void 0 ? void 0 : bestPair.attempt,
90
+ bestCompletedAttemptCompleted: bestPair === null || bestPair === void 0 ? void 0 : bestPair.completed,
66
91
  completedAttempts: completedAttempts.length,
67
92
  currentAttempt,
68
- currentAttemptCompleted: currentAttemptCompleted,
93
+ currentAttemptCompleted,
69
94
  currentAttemptStatements,
70
- mostRecentAttemptWithCompleted,
71
- mostRecentAttemptWithCompletedCompleted,
95
+ mostRecentAttemptWithCompleted: mostRecentPair === null || mostRecentPair === void 0 ? void 0 : mostRecentPair.attempt,
96
+ mostRecentAttemptWithCompletedCompleted: mostRecentPair === null || mostRecentPair === void 0 ? void 0 : mostRecentPair.completed,
72
97
  };
73
98
  };
74
99
  /*
@@ -44,6 +44,8 @@ export type GradeAndProgress = {
44
44
  progress: Progress;
45
45
  name?: string;
46
46
  };
47
+ export type UserActivityInfo = MappedUserInfo<ActivityState>;
48
+ export declare const getCompletedUserInfosGradeAndProgress: (infos: UserActivityInfo[], scoreMaximum?: number, gradePreference?: "current" | "best") => GradeAndProgress[];
47
49
  export declare const getCurrentGrade: (services: {
48
50
  lrs: LrsGateway;
49
51
  ltiAuthProvider: AuthProvider;
@@ -53,8 +55,8 @@ export declare const getCurrentGrade: (services: {
53
55
  name?: string;
54
56
  scoreMaximum?: number;
55
57
  userId?: string;
58
+ gradePreference?: "current" | "best";
56
59
  }) => Promise<GradeAndProgress | null>;
57
- export type UserActivityInfo = MappedUserInfo<ActivityState>;
58
60
  export declare const getAssignmentGrades: (services: {
59
61
  accountsGateway: AccountsGateway;
60
62
  lrs: LrsGateway;
@@ -65,4 +67,5 @@ export declare const getAssignmentGrades: (services: {
65
67
  platformId?: string;
66
68
  scoreMaximum?: number;
67
69
  user?: string;
70
+ gradePreference?: "current" | "best";
68
71
  }) => Promise<GradeAndProgress[]>;
@@ -51,23 +51,41 @@ export const getScoreGrade = (score, options) => {
51
51
  };
52
52
  };
53
53
  // These methods assign 0's to incomplete activities
54
- const getCompletedActivityStateGradeAndProgress = ({ name, scoreMaximum, state, userId }) => {
55
- var _a, _b, _c, _d;
56
- return ({
57
- grade: getScoreGrade(((_b = (_a = state.currentAttemptCompleted) === null || _a === void 0 ? void 0 : _a.result) === null || _b === void 0 ? void 0 : _b.score) || {}, {
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
+ };
66
+ };
67
+ const getCompletedActivityStateGradeAndProgress = ({ name, scoreMaximum, state, userId, gradePreference = 'current' }) => {
68
+ var _a;
69
+ const { attempt, completed } = pickAttemptAndCompleted(state, gradePreference);
70
+ return {
71
+ grade: getScoreGrade(((_a = completed === null || completed === void 0 ? void 0 : completed.result) === null || _a === void 0 ? void 0 : _a.score) || {}, {
58
72
  maxScore: scoreMaximum,
59
- startedAt: (_c = state.currentAttempt) === null || _c === void 0 ? void 0 : _c.timestamp,
60
- submittedAt: (_d = state.currentAttemptCompleted) === null || _d === void 0 ? void 0 : _d.timestamp,
73
+ startedAt: attempt === null || attempt === void 0 ? void 0 : attempt.timestamp,
74
+ submittedAt: completed === null || completed === void 0 ? void 0 : completed.timestamp,
61
75
  userId,
62
76
  }),
63
77
  progress: {
64
- scaled: state.currentAttemptCompleted ? 1 : 0,
78
+ scaled: completed ? 1 : 0,
65
79
  },
66
80
  name,
67
- });
81
+ };
68
82
  };
69
- const getCompletedUserInfosGradeAndProgress = (infos, scoreMaximum) => infos.map(({ data, fullName, platformUserId }) => getCompletedActivityStateGradeAndProgress({
70
- name: fullName, scoreMaximum, userId: platformUserId, state: data
83
+ export const getCompletedUserInfosGradeAndProgress = (infos, scoreMaximum, gradePreference) => infos.map(({ data, fullName, platformUserId }) => getCompletedActivityStateGradeAndProgress({
84
+ name: fullName,
85
+ scoreMaximum,
86
+ userId: platformUserId,
87
+ state: data,
88
+ gradePreference,
71
89
  }));
72
90
  export const getCurrentGrade = async (services, registration, options) => {
73
91
  var _a;
@@ -76,24 +94,41 @@ export const getCurrentGrade = async (services, registration, options) => {
76
94
  return null;
77
95
  }
78
96
  const userId = (_a = options === null || options === void 0 ? void 0 : options.userId) !== null && _a !== void 0 ? _a : user.uuid;
79
- const { currentPreference, incompleteAttemptCallback, name, scoreMaximum } = options !== null && options !== void 0 ? options : {};
97
+ const { currentPreference, incompleteAttemptCallback, name, scoreMaximum, gradePreference, } = options !== null && options !== void 0 ? options : {};
80
98
  const infoPerUser = await getRegistrationAttemptInfo(services.lrs, registration, { currentPreference });
81
99
  const userInfo = infoPerUser[user.uuid];
82
100
  if (!userInfo) {
83
- return getCompletedActivityStateGradeAndProgress({ name, scoreMaximum, state: resolveAttemptInfo([]), userId });
101
+ return getCompletedActivityStateGradeAndProgress({
102
+ name,
103
+ scoreMaximum,
104
+ state: resolveAttemptInfo([]),
105
+ userId,
106
+ gradePreference,
107
+ });
84
108
  }
85
109
  if (userInfo.currentAttemptCompleted || !incompleteAttemptCallback) {
86
- return getCompletedActivityStateGradeAndProgress({ name, scoreMaximum, state: userInfo, userId });
110
+ return getCompletedActivityStateGradeAndProgress({
111
+ name,
112
+ scoreMaximum,
113
+ state: userInfo,
114
+ userId,
115
+ gradePreference,
116
+ });
87
117
  }
88
118
  return incompleteAttemptCallback(userInfo);
89
119
  };
90
120
  export const getAssignmentGrades = async (services, assignmentIRI, registration, options) => {
91
- const { anyUser, currentPreference, incompleteAttemptsCallback, platformId, scoreMaximum, user } = options !== null && options !== void 0 ? options : {};
121
+ const { anyUser, currentPreference, incompleteAttemptsCallback, platformId, scoreMaximum, user, gradePreference = 'current', } = options !== null && options !== void 0 ? options : {};
92
122
  const infoPerUserUuid = await getRegistrationAttemptInfo(services.lrs, registration, { activity: assignmentIRI, anyUser, currentPreference, user });
93
123
  const mappedInfo = await services.accountsGateway.mapUserUuids(infoPerUserUuid, platformId);
124
+ // If no custom callback, just return graded results based on preference
94
125
  if (!incompleteAttemptsCallback) {
95
- return getCompletedUserInfosGradeAndProgress(mappedInfo, scoreMaximum);
126
+ return getCompletedUserInfosGradeAndProgress(mappedInfo, scoreMaximum, gradePreference);
96
127
  }
97
- const [incompleteInfo, completedInfo] = partition((info) => info.data.currentAttemptCompleted === undefined)(mappedInfo);
98
- return getCompletedUserInfosGradeAndProgress(completedInfo, scoreMaximum).concat(await incompleteAttemptsCallback(incompleteInfo));
128
+ // Partition based on whether the chosen preference has a completed attempt
129
+ const [incompleteInfo, completedInfo] = partition((info) => {
130
+ const { completed } = pickAttemptAndCompleted(info.data, gradePreference);
131
+ return completed === undefined;
132
+ })(mappedInfo);
133
+ return getCompletedUserInfosGradeAndProgress(completedInfo, scoreMaximum, gradePreference).concat(await incompleteAttemptsCallback(incompleteInfo));
99
134
  };