Skip to content

Commit

Permalink
Add option to edit project metadata (asreview#584)
Browse files Browse the repository at this point in the history
terrymyc authored Jun 14, 2021
1 parent 76d8d6f commit 8e5216a
Showing 10 changed files with 201 additions and 82 deletions.
38 changes: 7 additions & 31 deletions asreview/webapp/api.py
Original file line number Diff line number Diff line change
@@ -65,6 +65,7 @@
from asreview.webapp.utils.project import get_paper_data
from asreview.webapp.utils.project import get_statistics
from asreview.webapp.utils.project import init_project
from asreview.webapp.utils.project import update_project_info
from asreview.webapp.utils.project import label_instance
from asreview.webapp.utils.project import read_data
from asreview.webapp.utils.project import move_label_from_labeled_to_pool
@@ -227,38 +228,13 @@ def api_update_project_info(project_id): # noqa: F401
project_description = request.form['description']
project_authors = request.form['authors']

project_id_new = re.sub('[^A-Za-z0-9]+', '-', project_name).lower()

try:

# read the file with project info
with open(get_project_file_path(project_id), "r") as fp:
project_info = json.load(fp)

project_info["id"] = project_id_new
project_info["name"] = project_name
project_info["authors"] = project_authors
project_info["description"] = project_description

# # backwards support <0.10
# if "projectInitReady" not in project_info:
# project_info["projectInitReady"] = True

# update the file with project info
with open(get_project_file_path(project_id), "w") as fp:
json.dump(project_info, fp)

# rename the folder
get_project_path(project_id) \
.rename(Path(asreview_path(), project_id_new))

except Exception as err:
logging.error(err)
response = jsonify(message="project-update-failure")

return response, 500
project_id_new = update_project_info(
project_id,
project_name=project_name,
project_description=project_description,
project_authors=project_authors)

return api_get_project_info(project_id_new)
return jsonify(id=project_id_new)


@bp.route('/datasets', methods=["GET"])
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from "react";
import React, { useState, useEffect } from "react";
import { makeStyles } from "@material-ui/core/styles";

import {
@@ -63,23 +63,29 @@ function mapDispatchToProps(dispatch) {
};
}

const ProjectInit = (props) => {
const ProjectInfo = (props) => {
const classes = useStyles();

// const [open, setOpen] = React.useState(props.open)

// the state of the form data
const [info, setInfo] = React.useState({
const [info, setInfo] = useState({
authors: "",
name: "",
description: "",
});
const [error, setError] = React.useState({
const [error, setError] = useState({
code: null,
message: null,
});

const onChange = (evt) => {
if (error.code) {
setError({
code: null,
message: null,
});
}
setInfo({
...info,
[evt.target.name]: evt.target.value,
@@ -94,12 +100,22 @@ const ProjectInit = (props) => {
bodyFormData.set("authors", info.authors);
bodyFormData.set("description", info.description);

ProjectAPI.init(bodyFormData)
(props.edit
? ProjectAPI.info(props.project_id, true, bodyFormData)
: ProjectAPI.init(bodyFormData)
)
.then((result) => {
// set the project_id in the redux store
props.setProjectId(result.data["id"]);

props.handleAppState("project-page");
// switch to project page if init
// reload project info if edit
if (!props.edit) {
props.onClose(); // set newProject state to false
props.handleAppState("project-page");
} else {
props.onClose(); // set editing state to false
props.reloadProjectInfo();
}
})
.catch((error) => {
setError({
@@ -109,9 +125,22 @@ const ProjectInit = (props) => {
});
};

useEffect(() => {
// pre-fill project info in edit mode
if (props.edit) {
setInfo({
name: props.name,
authors: props.authors,
description: props.description,
});
}
}, [props.edit, props.name, props.authors, props.description]);

return (
<Dialog open={props.open} onClose={props.onClose} fullWidth={true}>
<DialogTitle>Create a new project</DialogTitle>
<DialogTitle>
{props.edit ? "Edit project info" : "Create a new project"}
</DialogTitle>

{error.code === 503 && (
<DialogContent dividers={true}>
@@ -182,12 +211,12 @@ const ProjectInit = (props) => {
color="primary"
disabled={info.name.length < 3}
>
Create
{props.edit ? "Update" : "Create"}
</Button>
</DialogActions>
)}
</Dialog>
);
};

export default connect(mapStateToProps, mapDispatchToProps)(ProjectInit);
export default connect(mapStateToProps, mapDispatchToProps)(ProjectInfo);
100 changes: 74 additions & 26 deletions asreview/webapp/src/PreReviewComponents/ProjectPage.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState, useRef, useEffect } from "react";
import React, { useState, useRef, useEffect, useCallback } from "react";
import { makeStyles } from "@material-ui/core/styles";

import {
@@ -11,7 +11,11 @@ import {
IconButton,
Tooltip,
} from "@material-ui/core";
import { StartReview, PreReviewZone } from "../PreReviewComponents";
import {
StartReview,
PreReviewZone,
ProjectInfo,
} from "../PreReviewComponents";

import ErrorHandler from "../ErrorHandler";
import DangerZone from "../DangerZone.js";
@@ -21,6 +25,7 @@ import { ProjectAPI } from "../api/index.js";

import KeyboardVoiceIcon from "@material-ui/icons/KeyboardVoice";
import GetAppIcon from "@material-ui/icons/GetApp";
import EditOutlinedIcon from "@material-ui/icons/EditOutlined";

import Finished from "../images/Finished.svg";
import InReview from "../images/InReview.svg";
@@ -80,6 +85,7 @@ const ProjectPage = (props) => {
const [state, setState] = useState({
// info-header
infoLoading: true,
infoEditing: false,
info: null,

// stage
@@ -94,6 +100,29 @@ const ProjectPage = (props) => {
message: null,
});

const editProjectInfo = () => {
setState({
...state,
infoEditing: true,
});
};

const finishEditProjectInfo = () => {
setState({
...state,
infoEditing: false,
});
};

const reloadProjectInfo = () => {
setState((s) => {
return {
...s,
infoLoading: true,
};
});
};

const finishProjectSetup = () => {
setState({
...state,
@@ -105,8 +134,8 @@ const ProjectPage = (props) => {
const finishProjectFirstTraining = () => {
setState({
...state,
info: { ...state.info, projectInitReady: true },
training: false,
info: { ...state.info, projectInitReady: true },
});
};

@@ -149,6 +178,27 @@ const ProjectPage = (props) => {
}
};

const fetchProjectInfo = useCallback(async () => {
ProjectAPI.info(props.project_id)
.then((result) => {
// update the state with the fetched data
setState((s) => {
return {
...s,
infoLoading: false,
info: result.data,
finished: result.data.reviewFinished,
};
});
})
.catch((error) => {
setError({
code: error.code,
message: error.message,
});
});
}, [props.project_id]);

const scrollToTop = () => {
EndRef.current.scrollIntoView({ behavior: "smooth" });
};
@@ -160,29 +210,10 @@ const ProjectPage = (props) => {
}, [state.setup, state.finished, state.infoLoading]);

useEffect(() => {
const fetchProjectInfo = async () => {
ProjectAPI.info(props.project_id)
.then((result) => {
// update the state with the fetched data
setState((s) => {
return {
...s,
infoLoading: false,
info: result.data,
finished: result.data.reviewFinished,
};
});
})
.catch((error) => {
setError({
code: error.code,
message: error.message,
});
});
};

fetchProjectInfo();
}, [props.project_id, state.finished, error.message]);
if (state.infoLoading) {
fetchProjectInfo();
}
}, [fetchProjectInfo, state.infoLoading, error.message]);

return (
<Box>
@@ -210,6 +241,11 @@ const ProjectPage = (props) => {
className={classes.title}
>
{state.info.name}
<Tooltip title="Edit info">
<IconButton size="small" onClick={editProjectInfo}>
<EditOutlinedIcon />
</IconButton>
</Tooltip>
</Typography>
<Typography color="primary" variant="h5">
{state.info.description}
@@ -326,6 +362,18 @@ const ProjectPage = (props) => {
</Container>
</Box>
)}
{/* Edit project info*/}
{error.message === null && !state.infoLoading && (
<ProjectInfo
edit={state.infoEditing}
open={state.infoEditing}
onClose={finishEditProjectInfo}
reloadProjectInfo={reloadProjectInfo}
name={state.info.name}
authors={state.info.authors}
description={state.info.description}
/>
)}
</Box>
);
};
6 changes: 3 additions & 3 deletions asreview/webapp/src/PreReviewComponents/ProjectUpload.js
Original file line number Diff line number Diff line change
@@ -439,9 +439,9 @@ const ProjectUpload = ({
From file/URL:
<Typography variant="body2" gutterBottom>
Select a file from your computer or fill in a link to a file
from the Internet. The accepted file formats are CSV, Excel, TSV, and
RIS. The selected dataset should contain the title and abstract
of each record. Read more about
from the Internet. The accepted file formats are CSV, Excel,
TSV, and RIS. The selected dataset should contain the title and
abstract of each record. Read more about
<Link
className={classes.link}
href="https://asreview.readthedocs.io/en/latest/intro/datasets.html"
2 changes: 1 addition & 1 deletion asreview/webapp/src/PreReviewComponents/index.js
Original file line number Diff line number Diff line change
@@ -5,7 +5,7 @@ export { default as PriorKnowledgeRandom } from "../PreReviewComponents/PriorKno
export { default as ListItemPaper } from "../PreReviewComponents/ListItemPaper";
export { default as SearchResult } from "../PreReviewComponents/SearchResult";
export { default as PaperCard } from "../PreReviewComponents/PaperCard";
export { default as ProjectInit } from "../PreReviewComponents/ProjectInit";
export { default as ProjectInfo } from "../PreReviewComponents/ProjectInfo";
export { default as ProjectUpload } from "../PreReviewComponents/ProjectUpload";
export { default as ProjectUploadFile } from "../PreReviewComponents/ProjectUploadFile";
export { default as ProjectUploadBenchmarkDatasets } from "../PreReviewComponents/ProjectUploadBenchmarkDatasets";
4 changes: 2 additions & 2 deletions asreview/webapp/src/Projects.js
Original file line number Diff line number Diff line change
@@ -9,7 +9,7 @@ import ProjectCard from "./ProjectCard";

import { ImportDialog, QuickTourDialog } from "./Components";

import { ProjectInit } from "./PreReviewComponents";
import { ProjectInfo } from "./PreReviewComponents";

import { ProjectAPI } from "./api/index.js";

@@ -161,7 +161,7 @@ const Projects = (props) => {
</Container>

{open.newProject && (
<ProjectInit
<ProjectInfo
handleAppState={props.handleAppState}
open={open.newProject}
onClose={handleCloseNewProject}
Loading

0 comments on commit 8e5216a

Please sign in to comment.