Skip to content

Commit f32b6d7

Browse files
committed
Merge drag-and-drop image upload commits
2 parents c087690 + 34c8136 commit f32b6d7

File tree

9 files changed

+2411
-897
lines changed

9 files changed

+2411
-897
lines changed

curate/models.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -403,7 +403,6 @@ def make_thumbnail(self):
403403
return False
404404

405405
image.thumbnail(settings.THUMB_SIZE, Image.ANTIALIAS)
406-
fh.close()
407406

408407
# Path to save to, name, and extension
409408
thumb_name, thumb_extension = os.path.splitext(self.image.name)
@@ -426,6 +425,8 @@ def make_thumbnail(self):
426425
image.save(temp_thumb, FTYPE)
427426
temp_thumb.seek(0)
428427

428+
fh.close()
429+
429430
# Load a ContentFile into the thumbnail field so it gets saved
430431
self.thumbnail.save(thumb_filename, ContentFile(temp_thumb.read()), save=False)
431432
temp_thumb.close()

curate/views_api.py

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -512,18 +512,25 @@ class ImageUploadView(APIView):
512512
def put(self, request, **kwargs):
513513
if 'file' not in request.data:
514514
raise ParseError("Empty content")
515-
f = request.data['file']
516-
article = get_object_or_404(Article, id=self.kwargs['article_pk'])
517-
518-
try:
519-
img = Image.open(f)
520-
img.verify()
521-
except:
522-
raise ParseError("File is not in a supported image format")
523-
kf = KeyFigure()
524-
kf.article = article
525-
kf.image.save(f.name, f, save=True)
526-
serializer=KeyFigureSerializer(instance=kf)
515+
516+
files = request.data.getlist('file')
517+
article = get_object_or_404(Article.objects.prefetch_related('key_figures'), id=self.kwargs['article_pk'])
518+
519+
original_figures = list(article.key_figures.all())
520+
521+
new_figures = []
522+
523+
for file in files:
524+
try:
525+
img = Image.open(file)
526+
img.verify()
527+
except:
528+
raise ParseError("File is not in a supported image format")
529+
new_figures.append(KeyFigure.objects.create(article=article, image=file))
530+
531+
all_figures = original_figures + new_figures
532+
serializer = KeyFigureSerializer(all_figures, many=True)
533+
527534
return Response(serializer.data, status=status.HTTP_201_CREATED)
528535

529536
# Autocomplete views

dist/js/bundle.js

Lines changed: 292 additions & 195 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,10 @@
2323
"path": "^0.12.7",
2424
"query-string": "^6.2.0",
2525
"react": "^16.9.0",
26-
"react-cookie": "^3.0.4",
26+
"react-cookie": "^4.0.1",
2727
"react-debounce-input": "^3.2.0",
2828
"react-dom": "^16.9.0",
29+
"react-dropzone": "^10.1.10",
2930
"react-image-lightbox": "^5.1.0",
3031
"react-responsive-carousel": "^3.1.43",
3132
"react-router": "^5.0.1",

src/components/ArticleContent.jsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,9 @@ const styles = theme => ({
8787
additionalLink: {
8888
marginRight: theme.spacing(1)
8989
},
90+
figureList: {
91+
paddingTop: theme.spacing(1),
92+
}
9093
});
9194

9295
class ArticleContent extends React.PureComponent {
@@ -257,7 +260,9 @@ class ArticleContent extends React.PureComponent {
257260
}
258261
</Typography>
259262
<ArticleKeywords keywords={article.keywords} />
260-
<FigureList figures={show_figures} loading={loading} onFigureClick={this.handle_figure_click} />
263+
<div className={classes.figureList}>
264+
<FigureList figures={show_figures} loading={loading} onFigureClick={this.handle_figure_click} />
265+
</div>
261266
<div hidden={this.empty(article.author_contributions)}>
262267
<Typography component="span" variant="body2">
263268
<span className={classes.grayedTitle}>Author contributions:</span>

src/components/ArticleEditor.jsx

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -35,16 +35,19 @@ import {
3535
Tooltip,
3636
} from '@material-ui/core';
3737
import ChipInput from 'material-ui-chip-input'
38-
import C from '../constants/constants';
39-
import TransparencyIcon from '../components/shared/TransparencyIcon.jsx';
40-
import FigureSelector from './FigureSelector.jsx';
41-
import LabeledBox from '../components/shared/LabeledBox.jsx';
42-
import Loader from '../components/shared/Loader.jsx';
38+
4339
import { makeStyles } from '@material-ui/styles';
4440
import { withStyles } from '@material-ui/core/styles';
4541
import { clone, debounce, find, get, includes, set, truncate } from 'lodash'
4642
import {json_api_req, simple_api_req, unspecified, summarize_api_errors} from '../util/util.jsx'
43+
4744
import {retrieve_authors,retrieve_title, retrieve_abstract} from '../components/curateform/DOILookup.jsx'
45+
import C from '../constants/constants';
46+
import KeyFigureUploader from './KeyFigureUploader.jsx';
47+
import LabeledBox from '../components/shared/LabeledBox.jsx';
48+
import Loader from '../components/shared/Loader.jsx';
49+
import TransparencyIcon from '../components/shared/TransparencyIcon.jsx';
50+
4851
const Transition = React.forwardRef((props, ref) => {
4952
return <Slide direction="up" {...props} ref={ref}/>;
5053
})
@@ -538,6 +541,7 @@ function initialFormState() {
538541
commentaries: [],
539542
media_coverage: [],
540543
transparency_urls: [],
544+
dragging_files: false,
541545
}
542546
}
543547

@@ -1242,7 +1246,7 @@ class ArticleEditor extends React.Component {
12421246

12431247
render() {
12441248
let {classes, article_id, open} = this.props
1245-
let { doi_loading, form, snack_message, loading } = this.state
1249+
let { doi_loading, dragging_files, form, snack_message, loading } = this.state
12461250
let content = <Loader />
12471251

12481252
const replication = form.article_type === 'REPLICATION'
@@ -1256,7 +1260,14 @@ class ArticleEditor extends React.Component {
12561260
const rep_std_details = find(C.REPORTING_STANDARDS_TYPES, {value: form.reporting_standards_type})
12571261

12581262
if (!loading && article_id != null) content = (
1259-
<Grid container spacing={3}>
1263+
<Grid
1264+
container
1265+
spacing={3}
1266+
onDragEnter={() => this.setState({ dragging_files: true })}
1267+
onDragEnd={ () => this.setState({ dragging_files: false }) }
1268+
onDragExit={ () => this.setState({ dragging_files: false }) }
1269+
onDrop={ () => this.setState({ dragging_files: false }) }
1270+
>
12601271
<Grid item className="ArticleEditorHalf">
12611272
<Grid container>
12621273
<Grid item xs={6}>
@@ -1641,9 +1652,12 @@ Completing all 7 earns you "Basic 7 (retroactive)" compliance
16411652
<Typography variant="overline">Key Figures</Typography>
16421653
<Grid container>
16431654
<Grid item xs={12}>
1644-
<FigureSelector article_id={article_id}
1645-
onChange={this.update_figures}
1646-
figures={form.key_figures} />
1655+
<KeyFigureUploader
1656+
dragging_files={dragging_files}
1657+
onChange={this.update_figures}
1658+
article_id={article_id}
1659+
figures={form.key_figures}
1660+
/>
16471661
</Grid>
16481662
</Grid>
16491663
</Grid>
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import React, { useCallback, useState } from 'react'
2+
import {useDropzone} from 'react-dropzone'
3+
import { useCookies } from 'react-cookie';
4+
5+
import { concat } from 'lodash'
6+
import { makeStyles } from '@material-ui/styles';
7+
import { Button, Icon, Typography } from '@material-ui/core';
8+
9+
import FigureList from './shared/FigureList.jsx';
10+
import Loader from './shared/Loader.jsx';
11+
12+
13+
const useStyles = makeStyles(theme => ({
14+
figurelist: {
15+
paddingTop: theme.spacing(1),
16+
},
17+
}))
18+
19+
export default function KeyFigureUploader({ article_id, dragging_files, figures, onChange }) {
20+
const classes = useStyles()
21+
const [cookies] = useCookies()
22+
const [number_images_loading, set_number_images_loading] = useState(0)
23+
24+
const onDrop = useCallback(acceptedFiles => {
25+
let formData = new FormData();
26+
acceptedFiles.forEach(file => formData.append('file', file))
27+
28+
set_number_images_loading(acceptedFiles.length)
29+
30+
fetch(`/api/articles/${article_id}/key_figures/upload/`, {
31+
method: 'PUT',
32+
headers: {
33+
'X-CSRFToken': cookies.csrftoken,
34+
},
35+
body: formData
36+
}).then(response => {
37+
if (response.ok) {
38+
return response.json()
39+
} else {
40+
set_number_images_loading(0)
41+
throw new Error('Error uploading images')
42+
}
43+
}).then(data => {
44+
onChange(data)
45+
set_number_images_loading(0)
46+
})
47+
}, [])
48+
const {getRootProps, getInputProps, isDragActive} = useDropzone({onDrop})
49+
50+
return (
51+
<div>
52+
<div
53+
{...getRootProps()}
54+
style={
55+
{
56+
backgroundColor: (isDragActive || dragging_files) ? 'rgba(0,255,0,0.05)' : null,
57+
width: '100%',
58+
height: '8rem',
59+
borderRadius: 8,
60+
border: 'dashed 2px',
61+
display: 'flex',
62+
alignItems: 'center',
63+
justifyContent: 'center',
64+
}
65+
}
66+
>
67+
<input {...getInputProps()} accept=".png,.jpg,.gif,.pdf"/>
68+
{
69+
isDragActive ?
70+
<Typography variant="h4">Drop the files here</Typography> :
71+
<div style={{display: 'flex', width: '50%', justifyContent: 'space-evenly'}}>
72+
<div style={{ display: 'flex', alignItems: 'center' }}>
73+
<Icon style={{ fontSize: '3rem' }} color="disabled">add</Icon>
74+
<Icon style={{ fontSize: '3rem' }} color="disabled">photo_library</Icon>
75+
</div>
76+
<div style={{ display: 'flex', flexDirection: 'column' }}>
77+
<Typography>
78+
Drag and drop images here&nbsp;
79+
<Typography color="textSecondary" component="span">
80+
(.png, .jpg, .gif or .pdf)
81+
</Typography>
82+
</Typography>
83+
<Typography align="center" color="textSecondary">or</Typography>
84+
<Button variant="outlined">
85+
Browse to select image files
86+
</Button>
87+
</div>
88+
</div>
89+
}
90+
</div>
91+
<div className={classes.figurelist}>
92+
<FigureList
93+
figures={figures}
94+
show_delete={true}
95+
number_images_loading={number_images_loading}
96+
onChange={onChange}
97+
/>
98+
</div>
99+
</div>
100+
)
101+
}

src/components/shared/FigureList.jsx

Lines changed: 44 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,16 @@ import React from 'react';
22
import PropTypes from 'prop-types';
33

44
import Loader from './Loader.jsx';
5+
import { withCookies, Cookies } from 'react-cookie';
56

7+
import { times } from 'lodash'
68
import {TextField, Button, Icon, Typography, Menu, Grid, InputLabel,
79
FormControl, Select, OutlinedInput, InputBase, Tooltip} from '@material-ui/core';
810

911
import { withStyles } from '@material-ui/core/styles';
1012

13+
import { simple_api_req } from '../../util/util.jsx'
14+
1115
const styles = {
1216
container: {
1317
display: 'flex',
@@ -43,14 +47,26 @@ class FigureList extends React.Component {
4347
this.handle_add = this.handle_add.bind(this)
4448
}
4549

46-
delete_figure = idx => {
47-
if (this.props.onDelete != null) this.props.onDelete(idx)
50+
delete_figure(idx) {
51+
let {figures, cookies} = this.props
52+
let pk = figures[idx].id
53+
this.setState({loading: true}, () => {
54+
simple_api_req('DELETE', `/api/key_figures/${pk}/delete/`, null, cookies.get('csrftoken'), (res) => {
55+
figures.splice(idx, 1)
56+
this.setState({loading: false}, () => {
57+
if (this.props.onChange != null) this.props.onChange(figures)
58+
})
59+
}, (err) => {
60+
this.setState({loading: false})
61+
console.error(err)
62+
})
63+
})
4864
}
4965

5066
figure_click = (idx, event) => {
51-
let {figures, showDelete} = this.props
67+
let {figures, show_delete} = this.props
5268
console.log(`figure_click ${idx}`)
53-
if (showDelete) this.delete_figure(idx)
69+
if (show_delete) this.delete_figure(idx)
5470
else this.props.onFigureClick(figures, idx)
5571
}
5672

@@ -59,49 +75,57 @@ class FigureList extends React.Component {
5975
}
6076

6177
render_thumbnail(kf, i) {
62-
let {classes, showDelete} = this.props
78+
let {classes, show_delete} = this.props
6379
let kind = kf.is_table ? 'Table' : 'Figure'
6480
let img = (
6581
<img key={i}
6682
className={classes.thumbnail}
6783
style={{backgroundImage: `url(${kf.image})`}} />
6884
)
69-
let tooltip = showDelete ? "Delete figure" : "Enlarge figure"
85+
let tooltip = show_delete ? "Delete figure" : "Enlarge figure"
7086
return <Tooltip title={tooltip} key={i}><a key={i} href="#" onClick={this.figure_click.bind(this, i)}>{ img }</a></Tooltip>
7187
}
7288

7389
render() {
74-
let {classes, figures, showAdd, loading} = this.props
75-
let addButton, spinner
90+
let {classes, figures, showAdd, loading, number_images_loading} = this.props
7691
if (figures == null) figures = []
77-
if (showAdd) addButton = <a href="#" onClick={this.handle_add} className={classes.thumbnail}>
78-
<span className={classes.add}>
79-
<Icon fontSize='large'>add</Icon> <Typography variant="body2">Add</Typography>
80-
</span>
81-
</a>
92+
93+
let spinner
8294
if (loading) spinner = <Loader size="25" />
95+
96+
let loading_images = []
97+
times(number_images_loading, (i) => {
98+
loading_images.push(
99+
<div className={classes.thumbnail} key={`uploading_image_${i}`} style={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
100+
<Loader/>
101+
</div>
102+
)
103+
})
104+
83105
return (
84106
<div className={classes.container}>
85107
{ figures.map(this.render_thumbnail) }
86-
{ addButton }
87108
{ spinner }
109+
{ loading_images }
88110
</div>
89111
)
90112
}
91113
}
92114

93115
FigureList.propTypes = {
94-
onDelete: PropTypes.func,
95-
showDelete: PropTypes.bool,
116+
onChange: PropTypes.func,
117+
show_delete: PropTypes.bool,
96118
showAdd: PropTypes.bool,
97-
loading: PropTypes.bool
119+
loading: PropTypes.bool,
120+
number_images_loading: PropTypes.number,
98121
}
99122

100123
FigureList.defaultProps = {
101124
figures: [],
102-
showDelete: false,
125+
show_delete: false,
103126
showAdd: false,
104-
loading: false
127+
loading: false,
128+
number_images_loading: 0,
105129
};
106130

107-
export default withStyles(styles)(FigureList);
131+
export default withCookies(withStyles(styles)(FigureList));

0 commit comments

Comments
 (0)