@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.
@@ -68,8 +68,7 @@ const s3FileServer = (initializer) => (configProvider) => {
68
68
  const Conditions = [
69
69
  { acl: 'private' },
70
70
  { bucket },
71
- ['starts-with', '$key', prefix],
72
- ['starts-with', '$Content-Type', ''],
71
+ ['starts-with', '$key', prefix]
73
72
  ];
74
73
  const defaultFields = {
75
74
  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;
@@ -50,7 +50,14 @@ const resolveAttemptInfo = (statements, options) => {
50
50
  // TODO optimize. i'm 100% that this could all be done in one iteration but i'm not messing around with that for now.
51
51
  const attempts = (0, exports.resolveAttempts)(statements, options);
52
52
  /* attempts that have a completed statement */
53
- const completedAttempts = attempts.filter(attempt => !!(0, exports.resolveCompletedForAttempt)(statements, attempt, options === null || options === void 0 ? void 0 : options.activityIRI));
53
+ const resolveAttemptsWithCompleted = (statements, attempts, activityIRI) => attempts.reduce((acc, attempt) => {
54
+ const completed = (0, exports.resolveCompletedForAttempt)(statements, attempt, activityIRI);
55
+ if (completed)
56
+ acc.push({ attempt, completed });
57
+ return acc;
58
+ }, []);
59
+ const completedPairs = resolveAttemptsWithCompleted(statements, attempts, options === null || options === void 0 ? void 0 : options.activityIRI);
60
+ const completedAttempts = completedPairs.map(p => p.attempt);
54
61
  /* the last attempt sorted by timestamp */
55
62
  const currentAttempt = (options === null || options === void 0 ? void 0 : options.currentAttempt)
56
63
  ? attempts.find(attempt => attempt.id === options.currentAttempt)
@@ -64,23 +71,41 @@ const resolveAttemptInfo = (statements, options) => {
64
71
  && ((_a = statement.context) === null || _a === void 0 ? void 0 : _a.registration) === currentAttempt.id;
65
72
  }) : [];
66
73
  const currentAttemptCompleted = currentAttempt && (0, exports.resolveCompletedForAttempt)(statements, currentAttempt, options === null || options === void 0 ? void 0 : options.activityIRI);
67
- const mostRecentAttemptWithCompleted = completedAttempts.reduce((current, attempt) => current && (0, isAfter_1.default)((0, parseISO_1.default)(current.timestamp), (0, parseISO_1.default)(attempt.timestamp)) ? current : attempt, completedAttempts[0]);
68
- const mostRecentAttemptWithCompletedCompleted = mostRecentAttemptWithCompleted
69
- && (0, exports.resolveCompletedForAttempt)(statements, mostRecentAttemptWithCompleted, options === null || options === void 0 ? void 0 : options.activityIRI);
74
+ const mostRecentPair = completedPairs.reduce((cur, pair) => {
75
+ if (!cur)
76
+ return pair;
77
+ return (0, isAfter_1.default)((0, parseISO_1.default)(cur.attempt.timestamp), (0, parseISO_1.default)(pair.attempt.timestamp)) ? cur : pair;
78
+ }, undefined);
70
79
  /*
71
80
  * the structure allows for the possibility of multiple incomplete attempts.
72
81
  * the implementation can choose at its discretion to ignore the currentAttempt
73
82
  * and instead make a new one, for instance if the implementation desires
74
83
  * an attempt timeout feature
75
84
  */
85
+ const hasScaledScore = (p) => {
86
+ var _a;
87
+ return p.completed.result !== undefined &&
88
+ typeof ((_a = p.completed.result.score) === null || _a === void 0 ? void 0 : _a.scaled) === 'number';
89
+ };
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;
98
+ }, undefined);
76
99
  return {
77
100
  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,
78
103
  completedAttempts: completedAttempts.length,
79
104
  currentAttempt,
80
- currentAttemptCompleted: currentAttemptCompleted,
105
+ currentAttemptCompleted,
81
106
  currentAttemptStatements,
82
- mostRecentAttemptWithCompleted,
83
- mostRecentAttemptWithCompletedCompleted,
107
+ mostRecentAttemptWithCompleted: mostRecentPair === null || mostRecentPair === void 0 ? void 0 : mostRecentPair.attempt,
108
+ mostRecentAttemptWithCompletedCompleted: mostRecentPair === null || mostRecentPair === void 0 ? void 0 : mostRecentPair.completed,
84
109
  };
85
110
  };
86
111
  exports.resolveAttemptInfo = resolveAttemptInfo;
@@ -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[]>;
@@ -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.getAssignmentGrades = exports.getCurrentGrade = exports.getScoreGrade = exports.getRegistrationAttemptInfo = void 0;
6
+ exports.getAssignmentGrades = exports.getCurrentGrade = exports.getCompletedUserInfosGradeAndProgress = exports.getScoreGrade = exports.getRegistrationAttemptInfo = void 0;
7
7
  const partition_1 = __importDefault(require("lodash/fp/partition"));
8
8
  const __1 = require("../..");
9
9
  const attempt_utils_1 = require("./attempt-utils");
@@ -59,24 +59,43 @@ const getScoreGrade = (score, options) => {
59
59
  };
60
60
  exports.getScoreGrade = getScoreGrade;
61
61
  // These methods assign 0's to incomplete activities
62
- const getCompletedActivityStateGradeAndProgress = ({ name, scoreMaximum, state, userId }) => {
63
- var _a, _b, _c, _d;
64
- return ({
65
- grade: (0, exports.getScoreGrade)(((_b = (_a = state.currentAttemptCompleted) === null || _a === void 0 ? void 0 : _a.result) === null || _b === void 0 ? void 0 : _b.score) || {}, {
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
70
+ return {
71
+ attempt: state.currentAttempt,
72
+ completed: state.currentAttemptCompleted,
73
+ };
74
+ };
75
+ const getCompletedActivityStateGradeAndProgress = ({ name, scoreMaximum, state, userId, gradePreference = 'current' }) => {
76
+ var _a;
77
+ const { attempt, completed } = pickAttemptAndCompleted(state, gradePreference);
78
+ return {
79
+ grade: (0, exports.getScoreGrade)(((_a = completed === null || completed === void 0 ? void 0 : completed.result) === null || _a === void 0 ? void 0 : _a.score) || {}, {
66
80
  maxScore: scoreMaximum,
67
- startedAt: (_c = state.currentAttempt) === null || _c === void 0 ? void 0 : _c.timestamp,
68
- submittedAt: (_d = state.currentAttemptCompleted) === null || _d === void 0 ? void 0 : _d.timestamp,
81
+ startedAt: attempt === null || attempt === void 0 ? void 0 : attempt.timestamp,
82
+ submittedAt: completed === null || completed === void 0 ? void 0 : completed.timestamp,
69
83
  userId,
70
84
  }),
71
85
  progress: {
72
- scaled: state.currentAttemptCompleted ? 1 : 0,
86
+ scaled: completed ? 1 : 0,
73
87
  },
74
88
  name,
75
- });
89
+ };
76
90
  };
77
- const getCompletedUserInfosGradeAndProgress = (infos, scoreMaximum) => infos.map(({ data, fullName, platformUserId }) => getCompletedActivityStateGradeAndProgress({
78
- name: fullName, scoreMaximum, userId: platformUserId, state: data
91
+ const getCompletedUserInfosGradeAndProgress = (infos, scoreMaximum, gradePreference) => infos.map(({ data, fullName, platformUserId }) => getCompletedActivityStateGradeAndProgress({
92
+ name: fullName,
93
+ scoreMaximum,
94
+ userId: platformUserId,
95
+ state: data,
96
+ gradePreference,
79
97
  }));
98
+ exports.getCompletedUserInfosGradeAndProgress = getCompletedUserInfosGradeAndProgress;
80
99
  const getCurrentGrade = async (services, registration, options) => {
81
100
  var _a;
82
101
  const user = await services.ltiAuthProvider.getUser();
@@ -84,26 +103,43 @@ const getCurrentGrade = async (services, registration, options) => {
84
103
  return null;
85
104
  }
86
105
  const userId = (_a = options === null || options === void 0 ? void 0 : options.userId) !== null && _a !== void 0 ? _a : user.uuid;
87
- const { currentPreference, incompleteAttemptCallback, name, scoreMaximum } = options !== null && options !== void 0 ? options : {};
106
+ const { currentPreference, incompleteAttemptCallback, name, scoreMaximum, gradePreference, } = options !== null && options !== void 0 ? options : {};
88
107
  const infoPerUser = await (0, exports.getRegistrationAttemptInfo)(services.lrs, registration, { currentPreference });
89
108
  const userInfo = infoPerUser[user.uuid];
90
109
  if (!userInfo) {
91
- return getCompletedActivityStateGradeAndProgress({ name, scoreMaximum, state: (0, attempt_utils_1.resolveAttemptInfo)([]), userId });
110
+ return getCompletedActivityStateGradeAndProgress({
111
+ name,
112
+ scoreMaximum,
113
+ state: (0, attempt_utils_1.resolveAttemptInfo)([]),
114
+ userId,
115
+ gradePreference,
116
+ });
92
117
  }
93
118
  if (userInfo.currentAttemptCompleted || !incompleteAttemptCallback) {
94
- return getCompletedActivityStateGradeAndProgress({ name, scoreMaximum, state: userInfo, userId });
119
+ return getCompletedActivityStateGradeAndProgress({
120
+ name,
121
+ scoreMaximum,
122
+ state: userInfo,
123
+ userId,
124
+ gradePreference,
125
+ });
95
126
  }
96
127
  return incompleteAttemptCallback(userInfo);
97
128
  };
98
129
  exports.getCurrentGrade = getCurrentGrade;
99
130
  const getAssignmentGrades = async (services, assignmentIRI, registration, options) => {
100
- const { anyUser, currentPreference, incompleteAttemptsCallback, platformId, scoreMaximum, user } = options !== null && options !== void 0 ? options : {};
131
+ const { anyUser, currentPreference, incompleteAttemptsCallback, platformId, scoreMaximum, user, gradePreference = 'current', } = options !== null && options !== void 0 ? options : {};
101
132
  const infoPerUserUuid = await (0, exports.getRegistrationAttemptInfo)(services.lrs, registration, { activity: assignmentIRI, anyUser, currentPreference, user });
102
133
  const mappedInfo = await services.accountsGateway.mapUserUuids(infoPerUserUuid, platformId);
134
+ // If no custom callback, just return graded results based on preference
103
135
  if (!incompleteAttemptsCallback) {
104
- return getCompletedUserInfosGradeAndProgress(mappedInfo, scoreMaximum);
136
+ return (0, exports.getCompletedUserInfosGradeAndProgress)(mappedInfo, scoreMaximum, gradePreference);
105
137
  }
106
- const [incompleteInfo, completedInfo] = (0, partition_1.default)((info) => info.data.currentAttemptCompleted === undefined)(mappedInfo);
107
- return getCompletedUserInfosGradeAndProgress(completedInfo, scoreMaximum).concat(await incompleteAttemptsCallback(incompleteInfo));
138
+ // Partition based on whether the chosen preference has a completed attempt
139
+ const [incompleteInfo, completedInfo] = (0, partition_1.default)((info) => {
140
+ const { completed } = pickAttemptAndCompleted(info.data, gradePreference);
141
+ return completed === undefined;
142
+ })(mappedInfo);
143
+ return (0, exports.getCompletedUserInfosGradeAndProgress)(completedInfo, scoreMaximum, gradePreference).concat(await incompleteAttemptsCallback(incompleteInfo));
108
144
  };
109
145
  exports.getAssignmentGrades = getAssignmentGrades;