Skip to content

Commit 847b570

Browse files
tcnichollongshuicyddey2
authored
787 enable uploading multiple files at once (#802)
* endpoint does not work, but new endpoint for multiple files added. tests also added. note - i will consolidate these endpoints once it is working * iterate through files, upload all of them * tests sort of work, need to be cleaned up New Menu has multiple file uploads, does not work yet * multiple property now in, new views, does not upload yet * multiple select selects shows multiple files when they are selected together it does not when they are selected one after the other * logging files when we click finish * codegen, adding multiple files endpoint * getting a 422 for uploading multiple files * does not work * adding print statement back in * adding print statement back in * some changes to how formdata is handled error: "WARNING:multipart.multipart:Did not find CR at end of boundary (40)" * clean up * 734 view and modify list of metadata definitions in UI (#758) * add endpoint and test * codegen and black * add action * add search and get endpoint * add search and get endpoint to action * add to reducer * basic page for metadata definition ready * add delete confirmation * add create metadata modal * ordering in decending order * update message * black * fix the pytest * codegen * fix codegen * stretched icon when extractor description long (#789) * disable ripple effect * fix the stretch --------- Co-authored-by: Chen Wang <[email protected]> * 792 clear previous log before switching extraction logs (#793) * immediately update before the time interval refresh * add proper reset * Pagination for files & folder page under dataset (#797) * Implement wordcloud visualization (#786) * Implement wordcloud visualization * Changing the name * message if no datasets exist, button link to create (#767) * button to create datasets if none exist * new message * center, no previous and next if we have no datasets * Updated the labels for Share (#798) * Updated the labels for Share * Fixed the message * 778 page to display each metadata definition (#801) * new page of metadata definition entry * initial page for metadata definition page * basic metadata defintion page * 788 duplicated extractor registration when extractor version updated (#791) * we now replace the old extractor * created timestamp is from original extractor * 701 improve file version selection (#743) * placeholder for where new version select will go * not clickable, new imports * select no longer in version chip new dropdown on filepage problem - current version not selected on file load * does not look good select and file details end up at bottom * clean up the logic * fix bug * make it look nicer * add snackbar * change to half width * change history to details * remove width --------- Co-authored-by: Chen Wang <[email protected]> * add swagger to traefik (#805) * add swagger to traefik * add to deployment * add to ingress rule * Change hostname to edu (#813) * updated * typo * missed more spots * basic message for no metadata definitions (#766) * endpoint logic is wrong; also fix pytest * write better pytest * rewrite the structure of select file and construct the form in the function * back to bad request again * everything working except redirect * go to the first files route * formatting * codegen * fixing delete of files in tests * fixing package lock --------- Co-authored-by: Chen Wang <[email protected]> Co-authored-by: Dipannita <[email protected]>
1 parent 6f8de5c commit 847b570

File tree

11 files changed

+503
-15
lines changed

11 files changed

+503
-15
lines changed

backend/app/routers/datasets.py

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,22 +9,21 @@
99
from typing import List, Optional
1010

1111
from beanie import PydanticObjectId
12-
from beanie.operators import Or
1312
from beanie.odm.operators.update.general import Inc
13+
from beanie.operators import Or
1414
from bson import ObjectId
1515
from bson import json_util
1616
from elasticsearch import Elasticsearch
1717
from fastapi import (
1818
APIRouter,
1919
HTTPException,
2020
Depends,
21-
Security,
2221
File,
2322
UploadFile,
2423
Request,
2524
)
2625
from fastapi.responses import StreamingResponse
27-
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
26+
from fastapi.security import HTTPBearer
2827
from minio import Minio
2928
from pika.adapters.blocking_connection import BlockingChannel
3029
from rocrate.model.person import Person
@@ -51,8 +50,8 @@
5150
from app.models.folders import FolderOut, FolderIn, FolderDB, FolderDBViewList
5251
from app.models.metadata import MetadataDB
5352
from app.models.pyobjectid import PyObjectId
54-
from app.models.users import UserOut
5553
from app.models.thumbnails import ThumbnailDB
54+
from app.models.users import UserOut
5655
from app.rabbitmq.listeners import submit_dataset_job
5756
from app.routers.files import add_file_entry, remove_file_entry
5857
from app.search.connect import (
@@ -462,6 +461,54 @@ async def save_file(
462461
raise HTTPException(status_code=404, detail=f"Dataset {dataset_id} not found")
463462

464463

464+
@router.post("/{dataset_id}/filesMultiple", response_model=List[FileOut])
465+
async def save_files(
466+
dataset_id: str,
467+
files: List[UploadFile],
468+
folder_id: Optional[str] = None,
469+
user=Depends(get_current_user),
470+
fs: Minio = Depends(dependencies.get_fs),
471+
es=Depends(dependencies.get_elasticsearchclient),
472+
rabbitmq_client: BlockingChannel = Depends(dependencies.get_rabbitmq),
473+
allow: bool = Depends(Authorization("uploader")),
474+
):
475+
if (dataset := await DatasetDB.get(PydanticObjectId(dataset_id))) is not None:
476+
files_added = []
477+
for file in files:
478+
if user is None:
479+
raise HTTPException(
480+
status_code=401,
481+
detail=f"User not found. Session might have expired.",
482+
)
483+
484+
new_file = FileDB(name=file.filename, creator=user, dataset_id=dataset.id)
485+
486+
if folder_id is not None:
487+
if (
488+
folder := await FolderDB.get(PydanticObjectId(folder_id))
489+
) is not None:
490+
new_file.folder_id = folder.id
491+
else:
492+
raise HTTPException(
493+
status_code=404, detail=f"Folder {folder_id} not found"
494+
)
495+
496+
await add_file_entry(
497+
new_file,
498+
user,
499+
fs,
500+
es,
501+
rabbitmq_client,
502+
file.file,
503+
content_type=file.content_type,
504+
)
505+
files_added.append(new_file.dict())
506+
return files_added
507+
508+
else:
509+
raise HTTPException(status_code=404, detail=f"Dataset {dataset_id} not found")
510+
511+
465512
@router.post("/createFromZip", response_model=DatasetOut)
466513
async def create_dataset_from_zip(
467514
user=Depends(get_current_user),

backend/app/tests/test_files.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,26 @@
33
from fastapi.testclient import TestClient
44

55
from app.config import settings
6-
from app.tests.utils import create_dataset, upload_file, generate_png
6+
from app.tests.utils import create_dataset, upload_file, upload_files, generate_png
77

88

99
def test_create_and_delete(client: TestClient, headers: dict):
1010
dataset_id = create_dataset(client, headers).get("id")
1111
response = upload_file(client, headers, dataset_id)
12-
file = response
1312
file_id = response["id"]
1413
# DELETE FILE
1514
response = client.delete(f"{settings.API_V2_STR}/files/{file_id}", headers=headers)
1615
assert response.status_code == 200
1716

17+
response_multiple = upload_files(client, headers, dataset_id)
18+
file_ids = [f["id"] for f in response_multiple]
19+
# DELETE MULTIPLE FILES
20+
for file_id in file_ids:
21+
response = client.delete(
22+
f"{settings.API_V2_STR}/files/{file_id}", headers=headers
23+
)
24+
assert response.status_code == 200
25+
1826

1927
def test_get_one(client: TestClient, headers: dict):
2028
temp_name = "testing file.txt"

backend/app/tests/utils.py

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,11 @@
3333
"description": "a dataset is a container of files and metadata",
3434
}
3535

36-
filename_example = "test_upload.csv"
37-
file_content_example = "year,location,count\n2023,Atlanta,4"
36+
filename_example_1 = "test_upload1.csv"
37+
file_content_example_1 = "year,location,count\n2023,Atlanta,4"
38+
39+
filename_example_2 = "test_upload2.csv"
40+
file_content_example_2 = "year,location,count\n2022,Seattle,2"
3841

3942
listener_v2_example = {
4043
"name": "test.listener_v2_example",
@@ -143,8 +146,8 @@ def upload_file(
143146
client: TestClient,
144147
headers: dict,
145148
dataset_id: str,
146-
filename=filename_example,
147-
content=file_content_example,
149+
filename=filename_example_1,
150+
content=file_content_example_1,
148151
):
149152
"""Uploads a dummy file (optionally with custom name/content) to a dataset and returns the JSON."""
150153
with open(filename, "w") as tempf:
@@ -161,6 +164,36 @@ def upload_file(
161164
return response.json()
162165

163166

167+
def upload_files(
168+
client: TestClient,
169+
headers: dict,
170+
dataset_id: str,
171+
filenames=[filename_example_1, filename_example_2],
172+
file_contents=[file_content_example_1, file_content_example_2],
173+
):
174+
"""Uploads a dummy file (optionally with custom name/content) to a dataset and returns the JSON."""
175+
upload_files = []
176+
for i in range(0, len(filenames)):
177+
with open(filenames[i], "w") as tempf:
178+
tempf.write(file_contents[i])
179+
upload_files.append(filenames[i])
180+
files = [
181+
("files", open(filename_example_1, "rb")),
182+
("files", open(filename_example_2, "rb")),
183+
]
184+
response = client.post(
185+
f"{settings.API_V2_STR}/datasets/{dataset_id}/filesMultiple",
186+
headers=headers,
187+
files=files,
188+
)
189+
for f in upload_files:
190+
os.remove(f)
191+
assert response.status_code == 200
192+
json_response = response.json()
193+
assert len(json_response) == 2
194+
return response.json()
195+
196+
164197
def create_folder(
165198
client: TestClient,
166199
headers: dict,

frontend/src/actions/file.js

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,42 @@ export function createFile(selectedDatasetId, folderId, selectedFile) {
146146
};
147147
}
148148

149+
export const CREATE_FILES = "CREATE_FILES";
150+
151+
export function createFiles(selectedDatasetId, selectedFiles, folderId) {
152+
return (dispatch) => {
153+
let formData = new FormData();
154+
let tmp = [];
155+
if (selectedFiles.length > 0) {
156+
for (let i = 0; i < selectedFiles.length; i++) {
157+
tmp.push(selectedFiles[i]);
158+
}
159+
}
160+
formData["files"] = tmp;
161+
162+
return V2.DatasetsService.saveFilesApiV2DatasetsDatasetIdFilesMultiplePost(
163+
selectedDatasetId,
164+
formData,
165+
folderId
166+
)
167+
.then((files) => {
168+
dispatch({
169+
type: CREATE_FILES,
170+
files: files,
171+
receivedAt: Date.now(),
172+
});
173+
})
174+
.catch((reason) => {
175+
dispatch(
176+
handleErrors(
177+
reason,
178+
createFiles(selectedDatasetId, selectedFiles, folderId)
179+
)
180+
);
181+
});
182+
};
183+
}
184+
149185
export const RESET_CREATE_FILE = "RESET_CREATE_FILE";
150186

151187
export function resetFileCreated() {
@@ -157,6 +193,17 @@ export function resetFileCreated() {
157193
};
158194
}
159195

196+
export const RESET_CREATE_FILES = "RESET_CREATE_FILES";
197+
198+
export function resetFilesCreated() {
199+
return (dispatch) => {
200+
dispatch({
201+
type: RESET_CREATE_FILES,
202+
receivedAt: Date.now(),
203+
});
204+
};
205+
}
206+
160207
export const UPDATE_FILE = "UPDATE_FILE";
161208

162209
export function updateFile(selectedFile, fileId) {

frontend/src/components/datasets/NewMenu.tsx

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import React from "react";
1313
import { useSelector } from "react-redux";
1414
import { RootState } from "../../types/data";
1515
import { UploadFile } from "../files/UploadFile";
16+
import { UploadFileMultiple } from "../files/UploadFileMultiple";
1617
import UploadIcon from "@mui/icons-material/Upload";
1718
import { Folder } from "@material-ui/icons";
1819

@@ -29,6 +30,8 @@ export const NewMenu = (props: ActionsMenuProps): JSX.Element => {
2930

3031
const [anchorEl, setAnchorEl] = React.useState<Element | null>(null);
3132
const [createFileOpen, setCreateFileOpen] = React.useState<boolean>(false);
33+
const [createMultipleFileOpen, setCreateMultipleFileOpen] =
34+
React.useState<boolean>(false);
3235
const [newFolder, setNewFolder] = React.useState<boolean>(false);
3336

3437
const handleCloseNewFolder = () => {
@@ -58,6 +61,22 @@ export const NewMenu = (props: ActionsMenuProps): JSX.Element => {
5861
folderId={folderId}
5962
/>
6063
</Dialog>
64+
<Dialog
65+
open={createMultipleFileOpen}
66+
onClose={() => {
67+
setCreateMultipleFileOpen(false);
68+
}}
69+
fullWidth={true}
70+
maxWidth="lg"
71+
aria-labelledby="form-dialog"
72+
>
73+
<UploadFileMultiple
74+
selectedDatasetId={datasetId}
75+
selectedDatasetName={about.name}
76+
setCreateMultipleFileOpen={setCreateMultipleFileOpen}
77+
folderId={folderId}
78+
/>
79+
</Dialog>
6180

6281
<CreateFolder
6382
datasetId={datasetId}
@@ -92,6 +111,17 @@ export const NewMenu = (props: ActionsMenuProps): JSX.Element => {
92111
</ListItemIcon>
93112
<ListItemText>Upload File</ListItemText>
94113
</MenuItem>
114+
<MenuItem
115+
onClick={() => {
116+
setCreateMultipleFileOpen(true);
117+
handleOptionClose();
118+
}}
119+
>
120+
<ListItemIcon>
121+
<UploadIcon fontSize="small" />
122+
</ListItemIcon>
123+
<ListItemText>Upload Multiple Files</ListItemText>
124+
</MenuItem>
95125
<MenuItem
96126
onClick={() => {
97127
setNewFolder(true);
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import React from "react";
2+
3+
import { Box, Input } from "@mui/material";
4+
5+
type UploadFileMultipleModalProps = {
6+
setSelectedFiles: any;
7+
};
8+
9+
// https://stackoverflow.com/questions/68213700/react-js-upload-multiple-files
10+
export const UploadFileInputMultiple: React.FC<UploadFileMultipleModalProps> = (
11+
props: UploadFileMultipleModalProps
12+
) => {
13+
const { setSelectedFiles } = props;
14+
15+
const handleMultipleFileChange = (event) => {
16+
// let tempFormData = new FormData();
17+
// for (let i = 0; i < event.target.files.length; i++) {
18+
// tempFormData.append("files", event.target.files[i]);
19+
// }
20+
setSelectedFiles(event.target.files);
21+
};
22+
23+
return (
24+
<Box sx={{ width: "100%", padding: "1em 0em" }}>
25+
<Input
26+
id="file-input-multiple"
27+
type="file"
28+
inputProps={{ multiple: true }}
29+
onChange={handleMultipleFileChange}
30+
sx={{ width: "100%" }}
31+
disableUnderline={true}
32+
/>
33+
</Box>
34+
);
35+
};

0 commit comments

Comments
 (0)