Skip to content

Commit 0f41df2

Browse files
authored
feat: add fetch submission files (openedx#110)
chore: remove cache busting
1 parent 91fbb89 commit 0f41df2

File tree

5 files changed

+94
-48
lines changed

5 files changed

+94
-48
lines changed

src/data/redux/thunkActions/download.js

+24-16
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import FileSaver from 'file-saver';
44
import { RequestKeys } from 'data/constants/requests';
55
import { selectors } from 'data/redux';
66
import { locationId } from 'data/constants/app';
7-
import { stringifyUrl } from 'data/services/lms/utils';
7+
import api from 'data/services/lms/api';
88

99
import { networkRequest } from './requests';
1010
import * as module from './download';
@@ -14,6 +14,10 @@ export const DownloadException = (files) => ({
1414
name: 'DownloadException',
1515
});
1616

17+
export const FetchSubmissionFilesException = () => ({
18+
name: 'FetchSubmissionFilesException',
19+
});
20+
1721
/**
1822
* Generate a manifest file content based on files object
1923
* @param {obj[]} files - list of file entries with downloadUrl, name, description, and size
@@ -47,24 +51,13 @@ export const zipFiles = async (files, blobs, username) => {
4751
FileSaver.saveAs(zipFile, zipName);
4852
};
4953

50-
/**
51-
* generate url with additional timestamp for cache busting.
52-
* This is implemented for fixing issue with the browser not
53-
* allowing the user to fetch the same url as the image tag.
54-
* @param {string} url
55-
* @returns {string}
56-
*/
57-
export const getTimeStampUrl = (url) => stringifyUrl(url, {
58-
ora_grading_download_timestamp: new Date().getTime(),
59-
});
60-
6154
/**
6255
* Download a file and return its blob is successful, or null if not.
6356
* @param {obj} file - file entry with downloadUrl
6457
* @return {Promise} - file blob or null
6558
*/
6659
export const downloadFile = (file) => fetch(
67-
module.getTimeStampUrl(file.downloadUrl),
60+
file.downloadUrl,
6861
).then((response) => {
6962
if (!response.ok) {
7063
// This is necessary because some of the error such as 404 does not throw.
@@ -95,19 +88,34 @@ export const downloadBlobs = async (files) => {
9588
if (errors.length) {
9689
throw DownloadException(errors);
9790
}
98-
return blobs;
91+
return ({ blobs, files });
92+
};
93+
94+
/**
95+
* @param {string} submissionUUID
96+
* @returns Promise
97+
*/
98+
export const getSubmissionFiles = async (submissionUUID) => {
99+
try {
100+
const { files } = await api.fetchSubmissionFiles(submissionUUID);
101+
return files;
102+
} catch {
103+
throw FetchSubmissionFilesException();
104+
}
99105
};
100106

101107
/**
102108
* Download all files for the selected submission as a zip file.
103109
* Throw error and do not download zip if any of the files fail to fetch.
104110
*/
105111
export const downloadFiles = () => (dispatch, getState) => {
106-
const { files } = selectors.grading.selected.response(getState());
112+
const submissionUUID = selectors.grading.selected.submissionUUID(getState());
107113
const username = selectors.grading.selected.username(getState());
108114
dispatch(networkRequest({
109115
requestKey: RequestKeys.downloadFiles,
110-
promise: module.downloadBlobs(files).then(blobs => module.zipFiles(files, blobs, username)),
116+
promise: module.getSubmissionFiles(submissionUUID)
117+
.then(module.downloadBlobs)
118+
.then(({ blobs, files }) => module.zipFiles(files, blobs, username)),
111119
}));
112120
};
113121

src/data/redux/thunkActions/download.test.js

+37-28
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import FileSaver from 'file-saver';
33

44
import { selectors } from 'data/redux';
55
import { RequestKeys } from 'data/constants/requests';
6+
import api from 'data/services/lms/api';
67
import * as download from './download';
78

89
const mockBlobWriter = jest.fn().mockName('BlobWriter');
@@ -38,7 +39,7 @@ jest.mock('data/redux', () => ({
3839
selectors: {
3940
grading: {
4041
selected: {
41-
response: jest.fn(),
42+
submissionUUID: jest.fn(),
4243
username: jest.fn(),
4344
},
4445
},
@@ -57,13 +58,13 @@ describe('download thunkActions', () => {
5758
});
5859
const files = [mockFile('test-file1.jpg'), mockFile('test-file2.pdf')];
5960
const blobs = ['blob1', 'blob2'];
60-
const response = { files };
61+
const submissionUUID = 'submission-uuid';
6162
const username = 'student-name';
6263
let dispatch;
6364
const getState = () => testState;
6465
describe('genManifest', () => {
6566
test('returns a list of strings with filename and description for each file', () => {
66-
expect(download.genManifest(response.files)).toEqual(
67+
expect(download.genManifest(files)).toEqual(
6768
[
6869
`Filename: ${files[0].name}\nDescription: ${files[0].description}\nSize: ${files[0].size}`,
6970
`Filename: ${files[1].name}\nDescription: ${files[1].description}\nSize: ${files[0].size}`,
@@ -86,43 +87,25 @@ describe('download thunkActions', () => {
8687
});
8788
});
8889
});
89-
90-
describe('getTimeStampUrl', () => {
91-
it('generate different url every milisecond for cache busting', () => {
92-
const testUrl = 'test/url?param1=true';
93-
const firstGen = download.getTimeStampUrl(testUrl);
94-
// fast forward for 1 milisecond
95-
jest.advanceTimersByTime(1);
96-
const secondGen = download.getTimeStampUrl(testUrl);
97-
expect(firstGen).not.toEqual(secondGen);
98-
});
99-
});
100-
10190
describe('downloadFile', () => {
10291
let fetch;
103-
let getTimeStampUrl;
10492
const blob = 'test-blob';
10593
const file = files[0];
10694
beforeEach(() => {
10795
fetch = window.fetch;
10896
window.fetch = jest.fn();
109-
getTimeStampUrl = download.getTimeStampUrl;
110-
download.getTimeStampUrl = jest.fn();
11197
});
11298
afterEach(() => {
11399
window.fetch = fetch;
114-
download.getTimeStampUrl = getTimeStampUrl;
115100
});
116101
it('returns blob output if successful', () => {
117102
window.fetch.mockReturnValue(Promise.resolve({ ok: true, blob: () => blob }));
118103
expect(download.downloadFile(file)).resolves.toEqual(blob);
119-
expect(download.getTimeStampUrl).toBeCalledWith(file.downloadUrl);
120104
});
121105
it('throw if not successful', () => {
122106
const failFetchStatusText = 'failed to fetch';
123107
window.fetch.mockReturnValue(Promise.resolve({ ok: false, statusText: failFetchStatusText }));
124108
expect(() => download.downloadFile(file)).rejects.toThrow(failFetchStatusText);
125-
expect(download.getTimeStampUrl).toBeCalledWith(file.downloadUrl);
126109
});
127110
});
128111

@@ -135,10 +118,10 @@ describe('download thunkActions', () => {
135118
afterEach(() => { download.downloadFile = downloadFile; });
136119

137120
it('returns a mapping of all files to download action', async () => {
138-
const downloadedBlobs = await download.downloadBlobs(files);
121+
const downloadBlobs = await download.downloadBlobs(files);
139122
expect(download.downloadFile).toHaveBeenCalledTimes(files.length);
140-
expect(downloadedBlobs.length).toEqual(files.length);
141-
expect(downloadedBlobs).toEqual(files.map(file => file.name));
123+
expect(downloadBlobs.blobs.length).toEqual(files.length);
124+
expect(downloadBlobs.blobs).toEqual(files.map(file => file.name));
142125
});
143126

144127
it('returns a mapping of errors from download action', () => {
@@ -148,25 +131,51 @@ describe('download thunkActions', () => {
148131
});
149132
});
150133

134+
describe('getSubmissionFiles', () => {
135+
let fetchSubmissionFiles;
136+
beforeEach(() => {
137+
fetchSubmissionFiles = api.fetchSubmissionFiles;
138+
api.fetchSubmissionFiles = jest.fn();
139+
});
140+
afterEach(() => { api.fetchSubmissionFiles = fetchSubmissionFiles; });
141+
it('return api.fetchSubmissionFiles on success', async () => {
142+
api.fetchSubmissionFiles = () => Promise.resolve({ files });
143+
const data = await download.getSubmissionFiles();
144+
expect(data).toEqual(files);
145+
});
146+
147+
it('throw FetchSubmissionFilesException on fetch failure', () => {
148+
api.fetchSubmissionFiles = () => Promise.reject();
149+
expect(() => download.getSubmissionFiles()).rejects.toEqual(download.FetchSubmissionFilesException());
150+
});
151+
});
152+
151153
describe('downloadFiles', () => {
152154
let downloadBlobs;
155+
let getSubmissionFiles;
153156
beforeEach(() => {
154157
dispatch = jest.fn();
155-
selectors.grading.selected.response = () => ({ files });
158+
selectors.grading.selected.submissionUUID = () => submissionUUID;
156159
selectors.grading.selected.username = () => username;
157160
download.zipFiles = jest.fn();
158161

159162
downloadBlobs = download.downloadBlobs;
160-
download.downloadBlobs = () => Promise.resolve(blobs);
163+
download.downloadBlobs = () => Promise.resolve({ blobs, files });
164+
165+
getSubmissionFiles = download.getSubmissionFiles;
166+
download.getSubmissionFiles = () => Promise.resolve(files);
167+
});
168+
afterEach(() => {
169+
download.downloadBlobs = downloadBlobs;
170+
download.getSubmissionFiles = getSubmissionFiles;
161171
});
162-
afterEach(() => { download.downloadBlobs = downloadBlobs; });
163172
it('dispatches network request with downloadFiles key', () => {
164173
download.downloadFiles()(dispatch, getState);
165174
const { networkRequest } = dispatch.mock.calls[0][0];
166175
expect(networkRequest.requestKey).toEqual(RequestKeys.downloadFiles);
167176
});
168177
it('dispatches network request for downloadFiles, zipping output of downloadBlobs', async () => {
169-
download.downloadBlobs = () => Promise.resolve(blobs);
178+
download.downloadBlobs = () => Promise.resolve({ blobs, files });
170179
download.downloadFiles()(dispatch, getState);
171180
const { networkRequest } = dispatch.mock.calls[0][0];
172181
await networkRequest.promise;

src/data/services/lms/api.js

+18-4
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
*********************************************************************************/
1515

1616
/**
17-
* get('/api/initialize', { ora_location, course_id? })
17+
* get('/api/initialize', { oraLocation })
1818
* @return {
1919
* oraMetadata: { name, prompt, type ('individual' vs 'team'), rubricConfig, fileUploadResponseConfig },
2020
* courseMetadata: { courseOrg, courseName, courseNumber, courseId },
@@ -38,7 +38,7 @@ const initializeApp = () => get(
3838
).then(response => response.data);
3939

4040
/**
41-
* get('/api/submission', { submissionUUID })
41+
* get('/api/submission', { oraLocation, submissionUUID })
4242
* @return {
4343
* submision: {
4444
* gradeData,
@@ -54,9 +54,22 @@ const fetchSubmission = (submissionUUID) => get(
5454
}),
5555
).then(response => response.data);
5656

57+
/**
58+
* get('/api/submission/files', { oraLocation, submissionUUID })
59+
* @return {
60+
* response: { files: [{}], text: <html> },
61+
* }
62+
*/
63+
const fetchSubmissionFiles = (submissionUUID) => get(
64+
stringifyUrl(urls.fetchSubmissionFilesUrl, {
65+
[paramKeys.oraLocation]: locationId,
66+
[paramKeys.submissionUUID]: submissionUUID,
67+
}),
68+
).then(response => response.data);
69+
5770
/**
5871
* fetches the current grade, gradeStatus, and rubricResponse data for the given submission
59-
* get('/api/submissionStatus', { submissionUUID })
72+
* get('/api/submissionStatus', { oraLocation, submissionUUID })
6073
* @return {obj} submissionStatus object
6174
* {
6275
* gradeData,
@@ -72,7 +85,7 @@ const fetchSubmissionStatus = (submissionUUID) => get(
7285
).then(response => response.data);
7386

7487
/**
75-
* post('api/lock', { ora_location, submissionUUID });
88+
* post('api/lock', { oraLocation, submissionUUID });
7689
* @param {string} submissionUUID
7790
*/
7891
const lockSubmission = (submissionUUID) => post(
@@ -120,6 +133,7 @@ const updateGrade = (submissionUUID, gradeData) => post(
120133
export default StrictDict({
121134
initializeApp,
122135
fetchSubmission,
136+
fetchSubmissionFiles,
123137
fetchSubmissionStatus,
124138
lockSubmission,
125139
updateGrade,

src/data/services/lms/api.test.js

+13
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,19 @@ describe('lms service api methods', () => {
7171
},
7272
});
7373
});
74+
describe('fetchSubmissionFiles', () => {
75+
testAPI({
76+
promise: api.fetchSubmissionFiles(submissionUUID),
77+
method: methodKeys.get,
78+
expected: {
79+
urlKey: urlKeys.fetchSubmissionFilesUrl,
80+
urlParams: {
81+
[paramKeys.oraLocation]: locationId,
82+
[paramKeys.submissionUUID]: submissionUUID,
83+
},
84+
},
85+
});
86+
});
7487
describe('fetchSubmissionStatus', () => {
7588
testAPI({
7689
promise: api.fetchSubmissionStatus(submissionUUID),

src/data/services/lms/urls.js

+2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const baseEsgUrl = `${api}ora_staff_grader/`;
88

99
const oraInitializeUrl = `${baseEsgUrl}initialize`;
1010
const fetchSubmissionUrl = `${baseEsgUrl}submission`;
11+
const fetchSubmissionFilesUrl = `${baseEsgUrl}submission/files`;
1112
const fetchSubmissionStatusUrl = `${baseEsgUrl}submission/status`;
1213
const fetchSubmissionLockUrl = `${baseEsgUrl}submission/lock`;
1314
const batchUnlockSubmissionsUrl = `${baseEsgUrl}submission/batch/unlock`;
@@ -24,6 +25,7 @@ export default StrictDict({
2425
api,
2526
oraInitializeUrl,
2627
fetchSubmissionUrl,
28+
fetchSubmissionFilesUrl,
2729
fetchSubmissionStatusUrl,
2830
fetchSubmissionLockUrl,
2931
batchUnlockSubmissionsUrl,

0 commit comments

Comments
 (0)