Skip to content

Commit

Permalink
Merge pull request #145 from azavea/ms/wire-up-create-boundary
Browse files Browse the repository at this point in the history
Wire up create boundary
  • Loading branch information
mstone121 authored Oct 27, 2022
2 parents 34cefc5 + 88ae60c commit 3db0dea
Show file tree
Hide file tree
Showing 17 changed files with 266 additions and 43 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Wire up boundary details page [#143](https://github.com/azavea/iow-boundary-tool/pull/143) [#152](https://github.com/azavea/iow-boundary-tool/pull/152)
- Add check for missing migrations [#153](https://github.com/azavea/iow-boundary-tool/pull/153)
- Save reference image metadata on draw page [#144](https://github.com/azavea/iow-boundary-tool/pull/144)
- Wire up create boundary [#145](https://github.com/azavea/iow-boundary-tool/pull/145)

### Changed

Expand Down
9 changes: 2 additions & 7 deletions src/app/src/api/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,8 @@ import { BASE_API_URL } from '../constants';

import TAGS from './tags';

const axiosBaseQuery = ({ url, method, data, params }) =>
apiClient({
url: `${BASE_API_URL}${url}`,
method,
data,
params,
})
const axiosBaseQuery = ({ url, ...axiosParams }) =>
apiClient({ url: `${BASE_API_URL}${url}`, ...axiosParams })
.then(result => ({ data: result.data }))
.catch(error => ({
error: {
Expand Down
46 changes: 41 additions & 5 deletions src/app/src/api/boundaries.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,47 @@ const boundaryApi = api.injectEndpoints({
}),

startNewBoundary: build.mutation({
query: newBoundary => ({
url: '/boundaries/',
method: 'POST',
data: newBoundary,
}),
query: ({ reference_images, shape, ...details }) => {
const formData = new FormData();
const referenceImagesMeta = [];

for (const reference_image of reference_images) {
formData.append(
'reference_images[]',
reference_image,
reference_image.name
);

referenceImagesMeta.push(
JSON.stringify({
filename: reference_image.name,
visible: true,
distortion: null,
mode: 'distort',
opacity: 100,
})
);
}

formData.append('reference_images_meta', referenceImagesMeta);

if (shape) {
formData.append('shape', shape, shape.name);
}

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

return {
url: '/boundaries/',
method: 'POST',
headers: {
'Content-Type': 'multipart/form-data',
},
data: formData,
};
},
invalidatesTags: getNewItemTagInvalidator(TAGS.BOUNDARY),
}),

Expand Down
2 changes: 1 addition & 1 deletion src/app/src/components/DrawTools/DrawTools.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ function DrawTools({ mode, details }) {

// Add the polygon indicated by `details` to the state
useEffect(() => {
if (details) {
if (details?.submission?.shape) {
dispatch(
setPolygon({
points: details.submission.shape.coordinates[0],
Expand Down
57 changes: 45 additions & 12 deletions src/app/src/components/ModalSections/FileUpload.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useEffect, useState } from 'react';
import { useSelector } from 'react-redux';
import {
Box,
Button,
Expand All @@ -8,27 +9,61 @@ import {
List,
ListItem,
Text,
useToast,
} from '@chakra-ui/react';
import { useNavigate } from 'react-router-dom';
import { CloudUploadIcon } from '@heroicons/react/outline';

import { convertIndexedObjectToArray } from '../../utils';
import ModalSection from './ModalSection';
import { useFilePicker } from '../../hooks';
import {
convertIndexedObjectToArray,
fileIsImageFile,
fileIsShapeFile,
} from '../../utils';
import { useEndpointToastError, useFilePicker } from '../../hooks';
import { useStartNewBoundaryMutation } from '../../api/boundaries';

export default function FileUpload({ PreviousButton }) {
const toast = useToast();
const navigate = useNavigate();
const utilityId = useSelector(state => state.auth.utility.id);

const [startNewBoundary, { error }] = useStartNewBoundaryMutation();
useEndpointToastError(error);

const [files, setFiles] = useState([]);
const addFiles = newFiles => setFiles(files => [...files, ...newFiles]);

const handleContinue = () => {
const referenceImages = files.filter(fileIsImageFile);
const shapeFiles = files.filter(fileIsShapeFile);

if (shapeFiles.length > 1) {
toast({
title: 'Only one shapefile may be uploaded at a time.',
status: 'error',
isClosable: true,
duration: 5000,
});
return;
}

// TODO figure out reference image upload from this page
const addFiles = newFiles => newFiles.forEach(() => {});
startNewBoundary({
utility_id: utilityId,
reference_images: referenceImages,
shape: shapeFiles?.[0],
})
.unwrap()
.then(id => navigate(`/draw/${id}`));
};

return (
<ModalSection
preHeading='Optional'
heading='Would you like to add your current map?'
prevButton={PreviousButton}
nextButton={
<Button variant='cta' onClick={() => navigate('/draw/3')}>
<Button variant='cta' onClick={handleContinue}>
Continue
</Button>
}
Expand All @@ -41,7 +76,7 @@ export default function FileUpload({ PreviousButton }) {

<Flex mt={4} w='100%' grow>
<UploadBox addFiles={addFiles} />
<FilesBox />
<FilesBox files={files} />
</Flex>
</ModalSection>
);
Expand Down Expand Up @@ -183,19 +218,17 @@ function Bold({ children }) {
);
}

function FilesBox() {
const imageEntries = []; // TODO: get images

if (imageEntries.length === 0) return null;
function FilesBox({ files }) {
if (files.length === 0) return null;

return (
<Box w='50%' pl={4}>
<Heading pb={4} size='small'>
Uploaded Files
</Heading>
<List>
{imageEntries.map(([url, { name }]) => (
<ListItem key={url} mb={6}>
{files.map(({ name }) => (
<ListItem key={name} mb={6}>
<Text mb={2} p={2} color='gray.700' bg='gray.50'>
{name}
</Text>
Expand Down
10 changes: 10 additions & 0 deletions src/app/src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,13 @@ export const ROLES = {
VALIDATOR: 'V',
ADMINISTRATOR: 'A',
};

export const REFERENCE_IMAGE_MIME_TYPES = ['image/png', 'image/jpeg'];
export const REFERENCE_IMAGE_FILE_EXTENSIONS = ['.png', '.jpg', '.jpeg'];
export const SHAPE_FILE_EXTENSIONS = ['.shp'];

export const FILE_UPLOAD_ACCEPT_STRING = [
...REFERENCE_IMAGE_MIME_TYPES,
...REFERENCE_IMAGE_FILE_EXTENSIONS,
...SHAPE_FILE_EXTENSIONS,
].join(', ');
5 changes: 3 additions & 2 deletions src/app/src/hooks.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { useParams } from 'react-router';
import { useMap } from 'react-leaflet';
import { useSelector } from 'react-redux';
import { useToast } from '@chakra-ui/react';

import { convertIndexedObjectToArray } from './utils';
import { useParams } from 'react-router';
import { FILE_UPLOAD_ACCEPT_STRING } from './constants';

export function useDialogController(initialState = false) {
const [isOpen, setIsOpen] = useState(initialState);
Expand Down Expand Up @@ -86,7 +87,7 @@ export function useFilePicker(onChange) {
input.type = 'file';
input.multiple = true;
input.onchange = handlePickFiles;
input.accept = 'image/png, image/jpeg, .png, .jpg, .jpeg';
input.accept = FILE_UPLOAD_ACCEPT_STRING;

input.click();
};
Expand Down
17 changes: 17 additions & 0 deletions src/app/src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import {
NC_NORTH,
NC_SOUTH,
NC_WEST,
REFERENCE_IMAGE_FILE_EXTENSIONS,
REFERENCE_IMAGE_MIME_TYPES,
SHAPE_FILE_EXTENSIONS,
} from './constants';

export function heroToChakraIcon(icon) {
Expand Down Expand Up @@ -106,3 +109,17 @@ export function getBoundaryShapeFilename(boundary) {
boundary.utility.name.replaceAll(/\s+/g, '_'),
].join('_')}.geojson`;
}

export function fileIsImageFile(file) {
return (
REFERENCE_IMAGE_FILE_EXTENSIONS.some(extension =>
file.name.endsWith(extension)
) || REFERENCE_IMAGE_MIME_TYPES.includes(file.type)
);
}

export function fileIsShapeFile(file) {
return SHAPE_FILE_EXTENSIONS.some(extension =>
file.name.endsWith(extension)
);
}
6 changes: 0 additions & 6 deletions src/django/api/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,6 @@
from rest_framework.exceptions import APIException


class ForbiddenException(APIException):
status_code = 403
default_detail = 'You are not allowed to perform this action.'
default_code = 'forbidden'


class BadRequestException(APIException):
status_code = 400
default_detail = 'There was a problem with your request.'
Expand Down
9 changes: 9 additions & 0 deletions src/django/api/fields.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from rest_framework.fields import FileField

from django.contrib.gis.geos import Polygon


class ShapefileField(FileField):
def to_internal_value(self, data):
# shapefile = super().to_internal_value(data)
return Polygon()
20 changes: 20 additions & 0 deletions src/django/api/migrations/0024_make_submission_shape_nullable.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Generated by Django 3.2.13 on 2022-10-24 22:27

import django.contrib.gis.db.models.fields
from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
('api', '0023_referenceimage_opacity'),
]

operations = [
migrations.AlterField(
model_name='submission',
name='shape',
field=django.contrib.gis.db.models.fields.PolygonField(
geography=True, null=True, srid=4326),
),
]
2 changes: 1 addition & 1 deletion src/django/api/models/submission.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class Submission(models.Model):
boundary = models.ForeignKey(
Boundary, on_delete=models.PROTECT, related_name='submissions'
)
shape = gis_models.PolygonField(geography=True)
shape = gis_models.PolygonField(geography=True, null=True)
created_at = models.DateTimeField(auto_now_add=True)
created_by = models.ForeignKey(
User, on_delete=models.PROTECT, related_name="creator"
Expand Down
20 changes: 20 additions & 0 deletions src/django/api/parsers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import json
from rest_framework.parsers import MultiPartParser


class NewBoundaryParser(MultiPartParser):
def parse(self, stream, media_type=None, parser_context=None):
form_data = super().parse(stream, media_type, parser_context)

if 'reference_images_meta' in form_data.data:
# TODO Figure out why brackets are removed
new_reference_images_meta = json.loads(
'[' + form_data.data['reference_images_meta'] + ']'
)

form_data.data = {
k: v if k != 'reference_images_meta' else new_reference_images_meta
for (k, v) in form_data.data.items()
}

return form_data
7 changes: 6 additions & 1 deletion src/django/api/serializers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,10 @@
from .user import UserSerializer
from .utility import UtilitySerializer
from .state import StateIDSerializer
from .boundary import BoundaryListSerializer, BoundaryDetailSerializer
from .boundary import (
BoundaryListSerializer,
BoundaryDetailSerializer,
NewBoundarySerializer,
)
from .shape import ShapeSerializer
from .reference_image import ReferenceImageSerializer
Loading

0 comments on commit 3db0dea

Please sign in to comment.