@openstax/ts-utils 1.30.3 → 1.31.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.
- package/dist/cjs/services/apiGateway/index.d.ts +1 -1
- package/dist/cjs/services/documentStore/fileSystemAssert.d.ts +1 -0
- package/dist/cjs/services/documentStore/fileSystemAssert.js +14 -0
- package/dist/cjs/services/documentStore/unversioned/file-system.js +15 -2
- package/dist/cjs/services/documentStore/versioned/file-system.js +5 -0
- package/dist/cjs/services/fileServer/index.d.ts +0 -11
- package/dist/cjs/services/fileServer/localFileServer.js +1 -48
- package/dist/cjs/services/fileServer/s3FileServer.js +0 -70
- package/dist/cjs/services/lrsGateway/xapiUtils.d.ts +13 -4
- package/dist/cjs/services/lrsGateway/xapiUtils.js +19 -4
- package/dist/cjs/tsconfig.without-specs.cjs.tsbuildinfo +1 -1
- package/dist/esm/services/apiGateway/index.d.ts +1 -1
- package/dist/esm/services/documentStore/fileSystemAssert.d.ts +1 -0
- package/dist/esm/services/documentStore/fileSystemAssert.js +10 -0
- package/dist/esm/services/documentStore/unversioned/file-system.js +15 -2
- package/dist/esm/services/documentStore/versioned/file-system.js +5 -0
- package/dist/esm/services/fileServer/index.d.ts +0 -11
- package/dist/esm/services/fileServer/localFileServer.js +2 -49
- package/dist/esm/services/fileServer/s3FileServer.js +1 -68
- package/dist/esm/services/lrsGateway/xapiUtils.d.ts +13 -4
- package/dist/esm/services/lrsGateway/xapiUtils.js +19 -4
- package/dist/esm/tsconfig.without-specs.esm.tsbuildinfo +1 -1
- package/package.json +5 -6
- package/script/bin/.init-params-script.bash.swp +0 -0
|
@@ -38,7 +38,7 @@ export type ApiClientResponse<Ro> = Ro extends any ? {
|
|
|
38
38
|
} : never;
|
|
39
39
|
export type ExpandRoute<T> = T extends ((...args: infer A) => infer R) & {
|
|
40
40
|
renderUrl: (...args: infer Ar) => Promise<string>;
|
|
41
|
-
} ? (
|
|
41
|
+
} ? (...args: A) => R & {
|
|
42
42
|
renderUrl: (...args: Ar) => Promise<string>;
|
|
43
43
|
} : never;
|
|
44
44
|
export type MapRoutesToClient<Ru> = [Ru] extends [AnyRoute<Ru>] ? {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const assertNoUndefined: (obj: any, path?: string[]) => void;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export const assertNoUndefined = (obj, path = []) => {
|
|
2
|
+
if (obj === undefined) {
|
|
3
|
+
throw new Error(`unknown attribute type ${typeof obj} with value ${obj} at ${path.join('.') || 'root'}.`);
|
|
4
|
+
}
|
|
5
|
+
if (obj && typeof obj === 'object') {
|
|
6
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
7
|
+
assertNoUndefined(value, [...path, key]);
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
};
|
|
@@ -4,6 +4,7 @@ import { hashValue } from '../../..';
|
|
|
4
4
|
import { resolveConfigValue } from '../../../config';
|
|
5
5
|
import { ConflictError, NotFoundError } from '../../../errors';
|
|
6
6
|
import { ifDefined, isDefined } from '../../../guards';
|
|
7
|
+
import { assertNoUndefined } from '../fileSystemAssert';
|
|
7
8
|
export const fileSystemUnversionedDocumentStore = (initializer) => () => (configProvider) => (_, hashKey, options) => {
|
|
8
9
|
const tableName = resolveConfigValue(configProvider[initializer.configSpace || 'fileSystem'].tableName);
|
|
9
10
|
const tablePath = tableName.then((table) => path.join(initializer.dataDir, table));
|
|
@@ -42,6 +43,7 @@ export const fileSystemUnversionedDocumentStore = (initializer) => () => (config
|
|
|
42
43
|
return {
|
|
43
44
|
loadAllDocumentsTheBadWay,
|
|
44
45
|
getItemsByField: async (key, value, pageKey) => {
|
|
46
|
+
assertNoUndefined(value, [key]);
|
|
45
47
|
const pageSize = 10;
|
|
46
48
|
const items = await loadAllDocumentsTheBadWay();
|
|
47
49
|
const filteredItems = items.filter((item) => item[key] === value);
|
|
@@ -53,11 +55,18 @@ export const fileSystemUnversionedDocumentStore = (initializer) => () => (config
|
|
|
53
55
|
return { items: paginatedItems, nextPageToken };
|
|
54
56
|
},
|
|
55
57
|
batchGetItem: async (ids) => {
|
|
56
|
-
const items = await Promise.all(ids.map((id) =>
|
|
58
|
+
const items = await Promise.all(ids.map((id) => {
|
|
59
|
+
assertNoUndefined(id, ['id']);
|
|
60
|
+
return load(hashFilename(id));
|
|
61
|
+
}));
|
|
57
62
|
return items.filter(isDefined);
|
|
58
63
|
},
|
|
59
|
-
getItem: (id) =>
|
|
64
|
+
getItem: (id) => {
|
|
65
|
+
assertNoUndefined(id, ['id']);
|
|
66
|
+
return load(hashFilename(id));
|
|
67
|
+
},
|
|
60
68
|
incrementItemAttribute: async (id, attribute) => {
|
|
69
|
+
assertNoUndefined(id, ['id']);
|
|
61
70
|
const filename = hashFilename(id);
|
|
62
71
|
const path = await filePath(filename);
|
|
63
72
|
await mkTableDir;
|
|
@@ -88,11 +97,13 @@ export const fileSystemUnversionedDocumentStore = (initializer) => () => (config
|
|
|
88
97
|
throw new NotFoundError(`Item with ${hashKey.toString()} "${id}" does not exist`);
|
|
89
98
|
}
|
|
90
99
|
const newItem = { ...data, ...item };
|
|
100
|
+
assertNoUndefined(newItem);
|
|
91
101
|
return new Promise((resolve, reject) => {
|
|
92
102
|
writeFile(path, JSON.stringify(newItem, null, 2), (err) => err ? reject(err) : resolve(newItem));
|
|
93
103
|
});
|
|
94
104
|
},
|
|
95
105
|
putItem: async (item) => {
|
|
106
|
+
assertNoUndefined(item);
|
|
96
107
|
const path = await filePath(hashFilename(item[hashKey]));
|
|
97
108
|
await mkTableDir;
|
|
98
109
|
return new Promise((resolve, reject) => {
|
|
@@ -100,6 +111,7 @@ export const fileSystemUnversionedDocumentStore = (initializer) => () => (config
|
|
|
100
111
|
});
|
|
101
112
|
},
|
|
102
113
|
createItem: async (item) => {
|
|
114
|
+
assertNoUndefined(item);
|
|
103
115
|
const hashed = hashFilename(item[hashKey]);
|
|
104
116
|
const existingItem = await load(hashed);
|
|
105
117
|
if (existingItem) {
|
|
@@ -120,6 +132,7 @@ export const fileSystemUnversionedDocumentStore = (initializer) => () => (config
|
|
|
120
132
|
// Process items sequentially to ensure consistent conflict detection
|
|
121
133
|
// Note: concurrency parameter is ignored for filesystem to avoid race conditions
|
|
122
134
|
for (const item of items) {
|
|
135
|
+
assertNoUndefined(item);
|
|
123
136
|
try {
|
|
124
137
|
const hashed = hashFilename(item[hashKey]);
|
|
125
138
|
const existingItem = await load(hashed);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { assertNoUndefined } from '../fileSystemAssert';
|
|
1
2
|
import { fileSystemUnversionedDocumentStore } from '../unversioned/file-system';
|
|
2
3
|
const PAGE_LIMIT = 5;
|
|
3
4
|
export const fileSystemVersionedDocumentStore = (initializer) => () => (configProvider) => (_, hashKey, options) => {
|
|
@@ -9,6 +10,7 @@ export const fileSystemVersionedDocumentStore = (initializer) => () => (configPr
|
|
|
9
10
|
}));
|
|
10
11
|
},
|
|
11
12
|
getVersions: async (id, startVersion) => {
|
|
13
|
+
assertNoUndefined(id, ['id']);
|
|
12
14
|
const item = await unversionedDocuments.getItem(id);
|
|
13
15
|
const versions = item === null || item === void 0 ? void 0 : item.items.reverse();
|
|
14
16
|
if (!versions) {
|
|
@@ -23,6 +25,7 @@ export const fileSystemVersionedDocumentStore = (initializer) => () => (configPr
|
|
|
23
25
|
};
|
|
24
26
|
},
|
|
25
27
|
getItem: async (id, timestamp) => {
|
|
28
|
+
assertNoUndefined(id, ['id']);
|
|
26
29
|
const item = await unversionedDocuments.getItem(id);
|
|
27
30
|
if (timestamp) {
|
|
28
31
|
return item === null || item === void 0 ? void 0 : item.items.find(version => version.timestamp === timestamp);
|
|
@@ -38,6 +41,7 @@ export const fileSystemVersionedDocumentStore = (initializer) => () => (configPr
|
|
|
38
41
|
save: async (changes) => {
|
|
39
42
|
var _a;
|
|
40
43
|
const document = { ...item, ...changes, timestamp, author };
|
|
44
|
+
assertNoUndefined(document);
|
|
41
45
|
const container = (_a = await unversionedDocuments.getItem(document[hashKey])) !== null && _a !== void 0 ? _a : { id: document[hashKey], items: [] };
|
|
42
46
|
const updated = { ...container, items: [...container.items, document] };
|
|
43
47
|
await unversionedDocuments.putItem(updated);
|
|
@@ -49,6 +53,7 @@ export const fileSystemVersionedDocumentStore = (initializer) => () => (configPr
|
|
|
49
53
|
var _a;
|
|
50
54
|
const author = (options === null || options === void 0 ? void 0 : options.getAuthor) ? await options.getAuthor(...authorArgs) : authorArgs[0];
|
|
51
55
|
const document = { ...item, timestamp: new Date().getTime(), author };
|
|
56
|
+
assertNoUndefined(document);
|
|
52
57
|
const container = (_a = await unversionedDocuments.getItem(document[hashKey])) !== null && _a !== void 0 ? _a : { id: document[hashKey], items: [] };
|
|
53
58
|
const updated = { ...container, items: [...container.items, document] };
|
|
54
59
|
await unversionedDocuments.putItem(updated);
|
|
@@ -14,16 +14,5 @@ export interface FileServerAdapter {
|
|
|
14
14
|
putFileContent: (source: FileValue, content: string) => Promise<FileValue>;
|
|
15
15
|
getSignedViewerUrl: (source: FileValue) => Promise<string>;
|
|
16
16
|
getFileContent: (source: FileValue) => Promise<Buffer>;
|
|
17
|
-
getSignedFileUploadConfig: () => Promise<{
|
|
18
|
-
url: string;
|
|
19
|
-
payload: {
|
|
20
|
-
[key: string]: string;
|
|
21
|
-
};
|
|
22
|
-
}>;
|
|
23
|
-
copyFileTo: (source: FileValue, destinationPath: string) => Promise<FileValue>;
|
|
24
|
-
copyFileToDirectory: (source: FileValue, destinationDirectory: string) => Promise<FileValue>;
|
|
25
|
-
isTemporaryUpload: (source: FileValue) => boolean;
|
|
26
|
-
getFileChecksum: (source: FileValue) => Promise<string>;
|
|
27
|
-
filesEqual: (sourceA: FileValue, sourceB: FileValue) => Promise<boolean>;
|
|
28
17
|
}
|
|
29
18
|
export declare const isFileOrFolder: (thing: any) => thing is FileValue | FolderValue;
|
|
@@ -1,18 +1,16 @@
|
|
|
1
1
|
/* cspell:ignore originalname */
|
|
2
|
-
import crypto from 'crypto';
|
|
3
2
|
import fs from 'fs';
|
|
4
3
|
import https from 'https';
|
|
5
4
|
import path from 'path';
|
|
6
5
|
import cors from 'cors';
|
|
7
6
|
import express from 'express';
|
|
8
7
|
import multer from 'multer';
|
|
9
|
-
import { v4 as uuid } from 'uuid';
|
|
10
8
|
import { assertString } from '../../assertions';
|
|
11
9
|
import { resolveConfigValue } from '../../config';
|
|
12
10
|
import { ifDefined } from '../../guards';
|
|
13
|
-
import {
|
|
11
|
+
import { once } from '../../misc/helpers';
|
|
14
12
|
/* istanbul ignore next */
|
|
15
|
-
const startServer =
|
|
13
|
+
const startServer = once((port, uploadDir) => {
|
|
16
14
|
// TODO - re-evaluate the `preservePath` behavior to match whatever s3 does
|
|
17
15
|
const upload = multer({ dest: uploadDir, preservePath: true });
|
|
18
16
|
const fileServerApp = express();
|
|
@@ -68,54 +66,9 @@ export const localFileServer = (initializer) => (configProvider) => {
|
|
|
68
66
|
await fs.promises.writeFile(filePath, content);
|
|
69
67
|
return source;
|
|
70
68
|
};
|
|
71
|
-
const getSignedFileUploadConfig = async () => {
|
|
72
|
-
const prefix = 'uploads/' + uuid();
|
|
73
|
-
return {
|
|
74
|
-
url: `https://${await host}:${await port}/`,
|
|
75
|
-
payload: {
|
|
76
|
-
key: prefix + '/${filename}',
|
|
77
|
-
}
|
|
78
|
-
};
|
|
79
|
-
};
|
|
80
|
-
const copyFileTo = async (source, destinationPath) => {
|
|
81
|
-
const sourcePath = path.join(await fileDir, source.path);
|
|
82
|
-
const destPath = path.join(await fileDir, destinationPath);
|
|
83
|
-
const destDirectory = path.dirname(destPath);
|
|
84
|
-
await fs.promises.mkdir(destDirectory, { recursive: true });
|
|
85
|
-
await fs.promises.copyFile(sourcePath, destPath);
|
|
86
|
-
return {
|
|
87
|
-
...source,
|
|
88
|
-
path: destinationPath
|
|
89
|
-
};
|
|
90
|
-
};
|
|
91
|
-
const copyFileToDirectory = async (source, destination) => {
|
|
92
|
-
const destinationPath = path.join(destination, source.label);
|
|
93
|
-
return copyFileTo(source, destinationPath);
|
|
94
|
-
};
|
|
95
|
-
const isTemporaryUpload = (source) => {
|
|
96
|
-
return source.path.indexOf('uploads/') === 0;
|
|
97
|
-
};
|
|
98
|
-
const getFileChecksum = async (source) => {
|
|
99
|
-
const filePath = path.join(await fileDir, source.path);
|
|
100
|
-
const fileContent = await fs.promises.readFile(filePath);
|
|
101
|
-
return crypto.createHash('md5').update(fileContent).digest('hex');
|
|
102
|
-
};
|
|
103
|
-
const filesEqual = async (sourceA, sourceB) => {
|
|
104
|
-
const [aSum, bSum] = await Promise.all([
|
|
105
|
-
getFileChecksum(sourceA),
|
|
106
|
-
getFileChecksum(sourceB)
|
|
107
|
-
]);
|
|
108
|
-
return aSum === bSum;
|
|
109
|
-
};
|
|
110
69
|
return {
|
|
111
70
|
getSignedViewerUrl,
|
|
112
71
|
getFileContent,
|
|
113
72
|
putFileContent,
|
|
114
|
-
getSignedFileUploadConfig,
|
|
115
|
-
copyFileTo,
|
|
116
|
-
copyFileToDirectory,
|
|
117
|
-
isTemporaryUpload,
|
|
118
|
-
getFileChecksum,
|
|
119
|
-
filesEqual,
|
|
120
73
|
};
|
|
121
74
|
};
|
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
/* cspell:ignore presigner */
|
|
2
|
-
import {
|
|
3
|
-
import { createPresignedPost } from '@aws-sdk/s3-presigned-post';
|
|
2
|
+
import { GetObjectCommand, PutObjectCommand, S3Client } from '@aws-sdk/client-s3';
|
|
4
3
|
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
|
5
|
-
import path from 'path';
|
|
6
|
-
import { v4 as uuid } from 'uuid';
|
|
7
4
|
import { once } from '../..';
|
|
8
5
|
import { assertDefined } from '../../assertions';
|
|
9
6
|
import { resolveConfigValue } from '../../config';
|
|
@@ -44,73 +41,9 @@ export const s3FileServer = (initializer) => (configProvider) => {
|
|
|
44
41
|
await (await s3Service()).send(command);
|
|
45
42
|
return source;
|
|
46
43
|
};
|
|
47
|
-
/*
|
|
48
|
-
* https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/modules/_aws_sdk_s3_presigned_post.html
|
|
49
|
-
* https://docs.aws.amazon.com/AmazonS3/latest/userguide/HTTPPOSTExamples.html
|
|
50
|
-
* https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-post-example.html
|
|
51
|
-
*/
|
|
52
|
-
const getSignedFileUploadConfig = async () => {
|
|
53
|
-
const prefix = 'uploads/' + uuid();
|
|
54
|
-
const bucket = (await bucketName());
|
|
55
|
-
const Conditions = [
|
|
56
|
-
{ acl: 'private' },
|
|
57
|
-
{ bucket },
|
|
58
|
-
['starts-with', '$key', prefix]
|
|
59
|
-
];
|
|
60
|
-
const defaultFields = {
|
|
61
|
-
acl: 'private',
|
|
62
|
-
};
|
|
63
|
-
const { url, fields } = await createPresignedPost(await s3Service(), {
|
|
64
|
-
Bucket: bucket,
|
|
65
|
-
Key: prefix + '/${filename}',
|
|
66
|
-
Conditions,
|
|
67
|
-
Fields: defaultFields,
|
|
68
|
-
Expires: 3600, // 1 hour
|
|
69
|
-
});
|
|
70
|
-
return {
|
|
71
|
-
url, payload: fields
|
|
72
|
-
};
|
|
73
|
-
};
|
|
74
|
-
const copyFileTo = async (source, destinationPath) => {
|
|
75
|
-
const bucket = (await bucketName());
|
|
76
|
-
const destinationPathWithoutLeadingSlash = destinationPath.replace(/^\//, '');
|
|
77
|
-
const command = new CopyObjectCommand({
|
|
78
|
-
Bucket: bucket,
|
|
79
|
-
Key: destinationPathWithoutLeadingSlash,
|
|
80
|
-
CopySource: path.join(bucket, source.path),
|
|
81
|
-
});
|
|
82
|
-
await (await s3Service()).send(command);
|
|
83
|
-
return {
|
|
84
|
-
...source,
|
|
85
|
-
path: destinationPathWithoutLeadingSlash
|
|
86
|
-
};
|
|
87
|
-
};
|
|
88
|
-
const copyFileToDirectory = async (source, destination) => {
|
|
89
|
-
const destinationPath = path.join(destination, source.label);
|
|
90
|
-
return copyFileTo(source, destinationPath);
|
|
91
|
-
};
|
|
92
|
-
const isTemporaryUpload = (source) => {
|
|
93
|
-
return source.path.indexOf('uploads/') === 0;
|
|
94
|
-
};
|
|
95
|
-
const getFileChecksum = async (source) => {
|
|
96
|
-
const bucket = (await bucketName());
|
|
97
|
-
const command = new HeadObjectCommand({ Bucket: bucket, Key: source.path });
|
|
98
|
-
const response = await (await s3Service()).send(command);
|
|
99
|
-
return assertDefined(response.ETag);
|
|
100
|
-
};
|
|
101
|
-
const filesEqual = async (sourceA, sourceB) => {
|
|
102
|
-
const [aSum, bSum] = await Promise.all([getFileChecksum(sourceA), getFileChecksum(sourceB)]);
|
|
103
|
-
return aSum === bSum;
|
|
104
|
-
};
|
|
105
44
|
return {
|
|
106
45
|
getFileContent,
|
|
107
46
|
putFileContent,
|
|
108
47
|
getSignedViewerUrl,
|
|
109
|
-
getSignedFileUploadConfig,
|
|
110
|
-
copyFileTo,
|
|
111
|
-
copyFileToDirectory,
|
|
112
|
-
isTemporaryUpload,
|
|
113
|
-
getFileChecksum,
|
|
114
|
-
filesEqual,
|
|
115
48
|
};
|
|
116
49
|
};
|
|
@@ -3,11 +3,15 @@ import { AuthProvider } from '../authProvider';
|
|
|
3
3
|
import { ActivityState } from './attempt-utils';
|
|
4
4
|
import { LrsGateway } from '.';
|
|
5
5
|
export interface Grade {
|
|
6
|
-
scoreGiven: number;
|
|
7
|
-
scoreMaximum: number;
|
|
8
|
-
comment?: string;
|
|
9
6
|
activityProgress: 'Initialized' | 'Started' | 'inProgress' | 'Submitted' | 'Completed';
|
|
7
|
+
comment?: string;
|
|
10
8
|
gradingProgress: 'FullyGraded' | 'Pending' | 'PendingManual' | 'Failed' | 'NotReady';
|
|
9
|
+
scoreGiven: number;
|
|
10
|
+
scoreMaximum: number;
|
|
11
|
+
submission?: {
|
|
12
|
+
startedAt?: string;
|
|
13
|
+
submittedAt?: string;
|
|
14
|
+
};
|
|
11
15
|
userId?: string;
|
|
12
16
|
}
|
|
13
17
|
export declare const getRegistrationAttemptInfo: (lrs: LrsGateway, registration: string, options?: {
|
|
@@ -24,7 +28,12 @@ export declare const getScoreGrade: (score: {
|
|
|
24
28
|
raw?: number;
|
|
25
29
|
min?: number;
|
|
26
30
|
max?: number;
|
|
27
|
-
},
|
|
31
|
+
}, options: {
|
|
32
|
+
maxScore?: number;
|
|
33
|
+
startedAt?: string;
|
|
34
|
+
submittedAt?: string;
|
|
35
|
+
userId?: string;
|
|
36
|
+
}) => Grade;
|
|
28
37
|
export type Progress = {
|
|
29
38
|
scaled: number;
|
|
30
39
|
max?: number;
|
|
@@ -22,29 +22,44 @@ export const getRegistrationAttemptInfo = async (lrs, registration, options) =>
|
|
|
22
22
|
// generates a payload that can be sent to the LMS, documentation here:
|
|
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) => {
|
|
26
27
|
const { raw, scaled, max } = score;
|
|
28
|
+
const { maxScore, startedAt, submittedAt, userId } = options;
|
|
27
29
|
const scoreMaximum = maxScore !== null && maxScore !== void 0 ? maxScore : 100;
|
|
28
30
|
const scoreGiven = raw && max
|
|
29
31
|
? scoreMaximum / max * raw
|
|
30
32
|
: scaled
|
|
31
33
|
? scaled * scoreMaximum
|
|
32
34
|
: 0;
|
|
35
|
+
const submission = {};
|
|
36
|
+
if (startedAt) {
|
|
37
|
+
submission.startedAt = startedAt;
|
|
38
|
+
}
|
|
39
|
+
if (submittedAt) {
|
|
40
|
+
submission.submittedAt = submittedAt;
|
|
41
|
+
}
|
|
33
42
|
return {
|
|
34
43
|
userId,
|
|
35
|
-
activityProgress:
|
|
44
|
+
activityProgress: submittedAt ? 'Completed' : 'Started',
|
|
36
45
|
// canvas assumes that anything that isn't 'FullyGraded' requires manual grading and displays a "needs grading" icon.
|
|
37
46
|
// if you warp your mind you can consider the portion of the assignment which is completed to be fully graded.
|
|
38
47
|
gradingProgress: 'FullyGraded',
|
|
39
48
|
scoreMaximum,
|
|
40
49
|
scoreGiven: roundToPrecision(scoreGiven, -2),
|
|
50
|
+
submission,
|
|
41
51
|
};
|
|
42
52
|
};
|
|
43
53
|
// These methods assign 0's to incomplete activities
|
|
44
54
|
const getCompletedActivityStateGradeAndProgress = ({ name, scoreMaximum, state, userId }) => {
|
|
45
|
-
var _a, _b;
|
|
55
|
+
var _a, _b, _c, _d;
|
|
46
56
|
return ({
|
|
47
|
-
grade: getScoreGrade(((_b = (_a = state.currentAttemptCompleted) === null || _a === void 0 ? void 0 : _a.result) === null || _b === void 0 ? void 0 : _b.score) || {},
|
|
57
|
+
grade: getScoreGrade(((_b = (_a = state.currentAttemptCompleted) === null || _a === void 0 ? void 0 : _a.result) === null || _b === void 0 ? void 0 : _b.score) || {}, {
|
|
58
|
+
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,
|
|
61
|
+
userId,
|
|
62
|
+
}),
|
|
48
63
|
progress: {
|
|
49
64
|
scaled: state.currentAttemptCompleted ? 1 : 0,
|
|
50
65
|
},
|