Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Support for Uploading Files #175

Merged
merged 22 commits into from
Nov 7, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ deployment/ansible/roles/azavea.*
# Django
/src/django/static/
/src/django/data/
/src/django/media/
/.venv

# JS
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Handle boundaries with no shape [#173](https://github.com/azavea/iow-boundary-tool/pull/173)
- Add annotation support [#176](https://github.com/azavea/iow-boundary-tool/pull/176)
- Add "Needs Revision" test submission [#177](https://github.com/azavea/iow-boundary-tool/pull/177)
- Add support for uploading files [#175](https://github.com/azavea/iow-boundary-tool/pull/175)

### Changed

Expand All @@ -71,6 +72,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Return user information from login endpoint [#136](https://github.com/azavea/iow-boundary-tool/pull/136)
- Limit boundary list by contributor's selected utility [#148](https://github.com/azavea/iow-boundary-tool/pull/148)
- Guard Draw Page Actions [#156](https://github.com/azavea/iow-boundary-tool/pull/156)
- Only redirect to /welcome for new boundaries [#175](https://github.com/azavea/iow-boundary-tool/pull/175)

### Fixed

Expand Down
27 changes: 27 additions & 0 deletions deployment/terraform/storage.tf
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,30 @@ resource "aws_s3_bucket" "logs" {
Environment = var.environment
}
}

resource "aws_s3_bucket" "data" {
bucket = "${lower(replace(var.project, " ", ""))}-${lower(var.environment)}-data-${var.aws_region}"
acl = "private"

cors_rule {
allowed_headers = ["*"]
allowed_methods = ["HEAD", "GET", "POST"]
allowed_origins = lower(var.environment) == "staging" ? ["http://localhost:4545", "http://localhost:8181", "https://${var.r53_public_hosted_zone}"] : ["https://${var.r53_public_hosted_zone}"]
expose_headers = ["ETag"]
max_age_seconds = 300
}

server_side_encryption_configuration {
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "AES256"
}
}
}

tags = {
Name = "${lower(replace(var.project, " ", ""))}-${lower(var.environment)}-data-${var.aws_region}"
Project = var.project
Environment = var.environment
}
}
11 changes: 1 addition & 10 deletions src/app/src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,16 +71,7 @@ function PrivateRoutes() {
<Route path='/submissions/*' element={<Submissions />} />
<Route
path='*'
element={
<Navigate
to={
hasWelcomePageAccess
? '/welcome'
: '/submissions'
}
replace
/>
}
element={<Navigate to={'/submissions'} replace />}
/>
</Routes>
</>
Expand Down
56 changes: 54 additions & 2 deletions src/app/src/api/boundaries.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { setHasZoomedToShape } from '../store/mapSlice';
import api from './api';
import TAGS, {
getListTagProvider,
Expand Down Expand Up @@ -49,7 +50,12 @@ const boundaryApi = api.injectEndpoints({
);
}

formData.append('reference_images_meta', referenceImagesMeta);
if (referenceImagesMeta.length > 0) {
formData.append(
'reference_images_meta',
referenceImagesMeta
);
}

if (shape) {
formData.append('shape', shape, shape.name);
Expand All @@ -75,10 +81,55 @@ const boundaryApi = api.injectEndpoints({
query: ({ id, shape }) => ({
url: `/boundaries/${id}/shape/`,
method: 'PUT',
data: shape,
data: { shape },
}),
}),

replaceBoundaryShape: build.mutation({
query: ({ id, file }) => {
const data = new FormData();
data.append('file', file, file.name);

return {
url: `/boundaries/${id}/shape/`,
method: 'PUT',
headers: {
'Content-Type': 'multipart/form-data',
},
data,
};
},
onQueryStarted: async ({ id }, { dispatch, queryFulfilled }) => {
const patchResult = dispatch(
api.util.updateQueryData(
'getBoundaryDetails',
id,
draftDetails => {
draftDetails.submission.shape = null;
mstone121 marked this conversation as resolved.
Show resolved Hide resolved
}
)
);

dispatch(setHasZoomedToShape(false));

try {
const { data: shape } = await queryFulfilled;
dispatch(
api.util.updateQueryData(
'getBoundaryDetails',
id,
draftDetails => {
draftDetails.submission.shape = shape;
}
)
);
} catch {
patchResult.undo();
dispatch(setHasZoomedToShape(false));
}
},
}),

deleteBoundaryShape: build.mutation({
query: id => ({
url: `/boundaries/${id}/shape/`,
Expand Down Expand Up @@ -116,6 +167,7 @@ export const {
useGetBoundaryDetailsQuery,
useStartNewBoundaryMutation,
useUpdateBoundaryShapeMutation,
useReplaceBoundaryShapeMutation,
useDeleteBoundaryShapeMutation,
useSubmitBoundaryMutation,
} = boundaryApi;
25 changes: 19 additions & 6 deletions src/app/src/api/referenceImages.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,24 @@ import api from './api';
const referenceImagesApi = api.injectEndpoints({
endpoints: build => ({
uploadReferenceImage: build.mutation({
query: ({ boundaryId, ...details }) => ({
url: `/boundaries/${boundaryId}/reference-images/`,
method: 'POST',
data: details,
}),
query: ({ boundaryId, file, ...details }) => {
const data = new FormData();

data.append('file', file, file.name);

for (const [key, value] of Object.entries(details)) {
data.append(key, value);
}

return {
url: `/boundaries/${boundaryId}/reference-images/`,
method: 'POST',
headers: {
'Content-Type': 'multipart/form-data',
},
data,
};
},
onQueryStarted: async (
{ boundaryId },
{ dispatch, queryFulfilled }
Expand All @@ -33,7 +46,7 @@ const referenceImagesApi = api.injectEndpoints({
},
}),
updateReferenceImage: build.mutation({
query: ({ boundaryId, id, ...details }) => ({
query: ({ boundaryId, id, file, ...details }) => ({
url: `/boundaries/${boundaryId}/reference-images/${id}/`,
method: 'PUT',
data: details,
Expand Down
38 changes: 20 additions & 18 deletions src/app/src/components/DrawTools/EditPolygonModal.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { useState, useEffect } from 'react';
import {
Button,
Flex,
Expand All @@ -14,14 +13,22 @@ import {
} from '@chakra-ui/react';
import { CloudUploadIcon } from '@heroicons/react/outline';

export default function EditPolygonModal({ isOpen, defaultLabel, onClose }) {
const [label, setLabel] = useState(defaultLabel);
import { ACCEPT_SHAPES } from '../../constants';
import { useBoundaryId, useFilePicker } from '../../hooks';
import { useReplaceBoundaryShapeMutation } from '../../api/boundaries';

useEffect(() => setLabel(defaultLabel), [defaultLabel]);
export default function EditPolygonModal({ isOpen, label, onClose }) {
const [replaceShape, { isLoading }] = useReplaceBoundaryShapeMutation();
const id = useBoundaryId();

const renamePolygon = () => {
// TODO implement this
};
const uploadShape = file =>
replaceShape({ id, file }).unwrap().then(onClose);

const openFileDialog = useFilePicker({
onChange: files => uploadShape(files[0]),
multiple: false,
accept: ACCEPT_SHAPES,
});

return (
<Modal size='sm' isOpen={isOpen} onClose={onClose} isCentered>
Expand All @@ -31,30 +38,25 @@ export default function EditPolygonModal({ isOpen, defaultLabel, onClose }) {
Edit polygon
</ModalHeader>
<ModalBody mx={4}>
<Input
value={label}
onChange={({ target: { value } }) =>
setLabel(value)
}
/>
<Input value={label} disabled />
</ModalBody>
<ModalFooter>
<Flex w='100%' m={4} mt={2}>
<Button
variant='secondary'
leftIcon={<Icon as={CloudUploadIcon} />}
onClick={openFileDialog}
isLoading={isLoading}
>
Upload File
</Button>
<Spacer />
<Button
variant='cta'
onClick={() => {
renamePolygon(label);
onClose();
}}
onClick={onClose}
disabled={isLoading}
>
Save changes
Cancel
</Button>
</Flex>
</ModalFooter>
Expand Down
19 changes: 10 additions & 9 deletions src/app/src/components/DrawTools/useEditingPolygon.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react';
import { useEffect } from 'react';
import { useMap } from 'react-leaflet';
import L from 'leaflet';
import 'leaflet-draw';
Expand All @@ -12,6 +12,7 @@ import {
} from '../../constants';
import { useUpdateBoundaryShapeMutation } from '../../api/boundaries';
import { useBoundaryId, useTrailingDebounceCallback } from '../../hooks';
import { setHasZoomedToShape } from '../../store/mapSlice';
import { useDrawBoundary, useDrawPermissions } from '../DrawContext';
import api from '../../api/api';

Expand Down Expand Up @@ -56,9 +57,8 @@ export default function useEditingPolygon() {

const shape = useDrawBoundary().submission?.shape;
const { canWrite } = useDrawPermissions();
const { editMode, basemapType, polygonIsVisible } = useSelector(
state => state.map
);
const { editMode, basemapType, polygonIsVisible, hasZoomedToShape } =
useSelector(state => state.map);

const [updateShape] = useUpdateBoundaryShapeMutation();

Expand Down Expand Up @@ -126,13 +126,14 @@ export default function useEditingPolygon() {
updatePolygonFromDrawEvent,
]);

const [hasZoomedToShape, setHasZoomedToShape] = useState(false);

// Fit map bounds to shape exactly once after loading
useEffect(() => {
if (shape && !hasZoomedToShape) {
map.fitBounds(featureGroup.getBounds());
setHasZoomedToShape(true);
// This can fail if fired before the reference images are loaded
try {
map.fitBounds(featureGroup.getBounds());
dispatch(setHasZoomedToShape(true));
} catch {}
}
}, [shape, map, hasZoomedToShape, setHasZoomedToShape]);
}, [dispatch, shape, map, hasZoomedToShape]);
}
18 changes: 18 additions & 0 deletions src/app/src/components/Layers/L.DistortableImage.Edit.fix.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,4 +66,22 @@ L.DistortableImage.Edit.prototype._refresh = function () {
this._overlay.fire('refresh');
};

// Prevent unbinding listeners on layers that have already been removed.
// Same as https://github.com/publiclab/Leaflet.DistortableImage/blob/2b743c747dcdfe2c3de51b50283084aa327348b6/src/edit/handles/EditHandle.js#L58-L68
// except handles nulls more gracefully.
L.EditHandle.prototype._unbindListeners = function () {
this?.off(
{
contextmenu: L.DomEvent.stop,
dragstart: this._onHandleDragStart,
drag: this._onHandleDrag,
dragend: this._onHandleDragEnd,
},
this
);

this?._handled?._map?.off('zoomend', this.updateHandle, this);
this?._handled?.off('update', this.updateHandle, this);
};

export default L;
10 changes: 6 additions & 4 deletions src/app/src/components/Layers/ReferenceImageLayer.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,8 @@ export default function ReferenceImageLayer() {
);

const createLayer = useCallback(
({ id, corners, mode }) => {
// TODO use reference image url here
const layer = new L.distortableImageOverlay('', {
({ id, corners, mode, file }) => {
const layer = new L.distortableImageOverlay(file, {
actions: [
L.DragAction,
L.ScaleAction,
Expand Down Expand Up @@ -112,12 +111,15 @@ export default function ReferenceImageLayer() {
const imageShouldBeAdded = id => !(id in referenceImageLayers.current);
const imageShouldBeHidden = id => !(id in visibleImages);

for (const { id, distortion, mode } of Object.values(visibleImages)) {
for (const { id, distortion, mode, file } of Object.values(
visibleImages
)) {
if (imageShouldBeAdded(id)) {
referenceImageLayers.current[id] = createLayer({
id,
corners: distortion,
mode,
file,
});

map.addLayer(referenceImageLayers.current[id]);
Expand Down
Loading