Skip to content
Snippets Groups Projects
Commit 193aad5c authored by Josef Olsson's avatar Josef Olsson
Browse files

Resolve "Upload pictures"

parent 6c015dd1
No related branches found
No related tags found
1 merge request!91Resolve "Upload pictures"
Pipeline #42211 passed with warnings
Showing
with 180 additions and 87 deletions
...@@ -40,5 +40,6 @@ ...@@ -40,5 +40,6 @@
}, },
"search.exclude": { "search.exclude": {
"**/env": true "**/env": true
} },
"python.pythonPath": "server\\env\\Scripts\\python.exe"
} }
...@@ -29,5 +29,9 @@ export default { ...@@ -29,5 +29,9 @@ export default {
SET_CITIES_TOTAL: 'SET_CITIES_TOTAL', SET_CITIES_TOTAL: 'SET_CITIES_TOTAL',
SET_CITIES_COUNT: 'SET_CITIES_COUNT', SET_CITIES_COUNT: 'SET_CITIES_COUNT',
SET_TYPES: 'SET_TYPES', SET_TYPES: 'SET_TYPES',
SET_MEDIA_ID: 'SET_MEDIA_ID',
SET_MEDIA_FILENAME: 'SET_MEDIA_ID',
SET_MEDIA_TYPE_ID: 'SET_MEDIA_TYPE_ID',
SET_MEDIA_USER_ID: 'SET_MEDIA_USER_ID',
SET_STATISTICS: 'SET_STATISTICS', SET_STATISTICS: 'SET_STATISTICS',
} }
export enum ComponentTypes { export enum ComponentTypes {
Text = 1, Text = 1,
Checkbox,
Image, Image,
Checkbox,
} }
...@@ -84,6 +84,7 @@ export interface Component { ...@@ -84,6 +84,7 @@ export interface Component {
export interface ImageComponent extends Component { export interface ImageComponent extends Component {
data: { data: {
media_id: number media_id: number
filename: string
} }
} }
......
import { Component, QuestionAlternative, QuestionAnswer, QuestionType } from './ApiModels' import { Component, Media, QuestionAlternative, QuestionAnswer, QuestionType } from './ApiModels'
export interface RichCompetition { export interface RichCompetition {
name: string name: string
...@@ -17,6 +17,7 @@ export interface RichSlide { ...@@ -17,6 +17,7 @@ export interface RichSlide {
competition_id: number competition_id: number
components: Component[] components: Component[]
questions: RichQuestion[] questions: RichQuestion[]
medias: Media[]
} }
export interface RichTeam { export interface RichTeam {
......
...@@ -40,7 +40,7 @@ it('renders admin view', () => { ...@@ -40,7 +40,7 @@ it('renders admin view', () => {
}, },
} }
;(mockedAxios.get as jest.Mock).mockImplementation((path: string, params?: any) => { ;(mockedAxios.get as jest.Mock).mockImplementation((path: string, params?: any) => {
if (path === '/misc/cities') return Promise.resolve(cityRes) if (path === '/api/misc/cities') return Promise.resolve(cityRes)
else return Promise.resolve(rolesRes) else return Promise.resolve(rolesRes)
}) })
render( render(
......
...@@ -47,7 +47,7 @@ it('renders competition manager', () => { ...@@ -47,7 +47,7 @@ it('renders competition manager', () => {
} }
;(mockedAxios.get as jest.Mock).mockImplementation((path: string, params?: any) => { ;(mockedAxios.get as jest.Mock).mockImplementation((path: string, params?: any) => {
if (path === '/competitions/search') return Promise.resolve(compRes) if (path === '/api/competitions/search') return Promise.resolve(compRes)
else return Promise.resolve(cityRes) else return Promise.resolve(cityRes)
}) })
render( render(
......
import { render } from '@testing-library/react'
import React from 'react'
import { BrowserRouter } from 'react-router-dom'
import ImageComponentDisplay from './ImageComponentDisplay'
it('renders image component display', () => {
render(
<ImageComponentDisplay component={{ id: 0, x: 0, y: 0, w: 0, h: 0, type: 0, media_id: 0 }} />
)
})
...@@ -3,5 +3,11 @@ import React from 'react' ...@@ -3,5 +3,11 @@ import React from 'react'
import ImageComponentDisplay from './ImageComponentDisplay' import ImageComponentDisplay from './ImageComponentDisplay'
it('renders competition settings', () => { it('renders competition settings', () => {
render(<ImageComponentDisplay component={{ id: 0, x: 0, y: 0, w: 0, h: 0, media_id: 0, type: 2 }} />) render(
<ImageComponentDisplay
component={{ id: 0, x: 0, y: 0, w: 0, h: 0, data: { media_id: 0, filename: '' }, type_id: 2 }}
width={0}
height={0}
/>
)
}) })
import React, { useState } from 'react' import React from 'react'
import { Rnd } from 'react-rnd'
import { ImageComponent } from '../../../interfaces/ApiModels' import { ImageComponent } from '../../../interfaces/ApiModels'
import { Position, Size } from '../../../interfaces/Components'
type ImageComponentProps = { type ImageComponentProps = {
component: ImageComponent component: ImageComponent
width: number
height: number
} }
const ImageComponentDisplay = ({ component }: ImageComponentProps) => { const ImageComponentDisplay = ({ component, width, height }: ImageComponentProps) => {
const [currentPos, setCurrentPos] = useState<Position>({ x: component.x, y: component.y })
const [currentSize, setCurrentSize] = useState<Size>({ w: component.w, h: component.h })
return ( return (
<Rnd <img
minWidth={50} src={`http://localhost:5000/static/images/${component.data.filename}`}
minHeight={50} height={height}
bounds="parent" width={width}
onDragStop={(e, d) => { draggable={false}
setCurrentPos({ x: d.x, y: d.y }) />
}}
size={{ width: currentSize.w, height: currentSize.h }}
position={{ x: currentPos.x, y: currentPos.y }}
onResize={(e, direction, ref, delta, position) => {
setCurrentSize({
w: ref.offsetWidth,
h: ref.offsetHeight,
})
setCurrentPos(position)
}}
onResizeStop={() => {
console.log('Skicka data till server')
}}
>
<img
src="https://365psd.com/images/previews/c61/cartoon-cow-52394.png"
height={currentSize.h}
width={currentSize.w}
draggable={false}
/>
</Rnd>
) )
} }
......
...@@ -44,7 +44,14 @@ const RndComponent = ({ component }: ImageComponentProps) => { ...@@ -44,7 +44,14 @@ const RndComponent = ({ component }: ImageComponentProps) => {
/> />
) )
case ComponentTypes.Image: case ComponentTypes.Image:
return <ImageComponentDisplay key={component.id} component={component as ImageComponent} /> return (
<ImageComponentDisplay
key={component.id}
component={component as ImageComponent}
width={currentSize.w}
height={currentSize.h}
/>
)
default: default:
break break
} }
......
...@@ -6,6 +6,7 @@ import { BrowserRouter } from 'react-router-dom' ...@@ -6,6 +6,7 @@ import { BrowserRouter } from 'react-router-dom'
import store from '../../../store' import store from '../../../store'
import CompetitionSettings from './CompetitionSettings' import CompetitionSettings from './CompetitionSettings'
import SettingsPanel from './SettingsPanel' import SettingsPanel from './SettingsPanel'
import SlideSettings from './SlideSettings'
it('renders settings panel', () => { it('renders settings panel', () => {
render( render(
...@@ -28,5 +29,5 @@ it('renders slide settings tab', () => { ...@@ -28,5 +29,5 @@ it('renders slide settings tab', () => {
const tabs = wrapper.find('.MuiTabs-flexContainer') const tabs = wrapper.find('.MuiTabs-flexContainer')
expect(wrapper.find(CompetitionSettings).length).toEqual(1) expect(wrapper.find(CompetitionSettings).length).toEqual(1)
tabs.children().at(1).simulate('click') tabs.children().at(1).simulate('click')
expect(wrapper.text().includes('2')).toBe(true) //TODO: check that SlideSettings exists expect(wrapper.find(SlideSettings).length).toEqual(1)
}) })
...@@ -22,11 +22,11 @@ import { green, grey } from '@material-ui/core/colors' ...@@ -22,11 +22,11 @@ import { green, grey } from '@material-ui/core/colors'
import { createStyles, makeStyles, Theme, withStyles } from '@material-ui/core/styles' import { createStyles, makeStyles, Theme, withStyles } from '@material-ui/core/styles'
import CloseIcon from '@material-ui/icons/Close' import CloseIcon from '@material-ui/icons/Close'
import axios from 'axios' import axios from 'axios'
import { ImageComponent, TextComponent, QuestionAlternative, Media } from '../../../interfaces/ApiModels'
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import { useParams } from 'react-router-dom' import { useParams } from 'react-router-dom'
import { getEditorCompetition } from '../../../actions/editor' import { getEditorCompetition } from '../../../actions/editor'
import { useAppDispatch, useAppSelector } from '../../../hooks' import { useAppDispatch, useAppSelector } from '../../../hooks'
import { QuestionAlternative, TextComponent } from '../../../interfaces/ApiModels'
import { HiddenInput, TextCard } from './styled' import { HiddenInput, TextCard } from './styled'
import TextComponentEdit from './TextComponentEdit' import TextComponentEdit from './TextComponentEdit'
...@@ -101,7 +101,7 @@ const SlideSettings: React.FC = () => { ...@@ -101,7 +101,7 @@ const SlideSettings: React.FC = () => {
if (activeSlide && activeSlide.questions[0]) { if (activeSlide && activeSlide.questions[0]) {
await axios await axios
.delete( .delete(
`/competitions/${id}/slides/${activeSlideId}/questions/${activeSlide?.questions[0].id}/alternatives/${alternative_id}` `/api/competitions/${id}/slides/${activeSlideId}/questions/${activeSlide?.questions[0].id}/alternatives/${alternative_id}`
) )
.then(() => { .then(() => {
dispatch(getEditorCompetition(id)) dispatch(getEditorCompetition(id))
...@@ -117,14 +117,28 @@ const SlideSettings: React.FC = () => { ...@@ -117,14 +117,28 @@ const SlideSettings: React.FC = () => {
?.components.filter((component) => component.type_id === 1) as TextComponent[] ?.components.filter((component) => component.type_id === 1) as TextComponent[]
) )
const pictureList = [ const images = useAppSelector(
{ id: 'picture1', name: 'Picture1.jpeg' }, (state) =>
{ id: 'picture2', name: 'Picture2.jpeg' }, state.editor.competition.slides
] .find((slide) => slide.id === state.editor.activeSlideId)
const handleClosePictureClick = (id: string) => { ?.components.filter((component) => component.type_id === 2) as ImageComponent[]
setPictures(pictures.filter((item) => item.id !== id)) //Will not be done like this when api is used )
const handleCloseimageClick = async (image: ImageComponent) => {
await axios
.delete(`/api/media/images/${image.data.media_id}`)
.then(() => {
dispatch(getEditorCompetition(id))
})
.catch(console.log)
await axios
.delete(`/api/competitions/${id}/slides/${activeSlide?.id}/components/${image.id}`)
.then(() => {
dispatch(getEditorCompetition(id))
})
.catch(console.log)
} }
const [pictures, setPictures] = useState(pictureList)
const updateSlideType = async () => { const updateSlideType = async () => {
closeSlideTypeDialog() closeSlideTypeDialog()
...@@ -181,7 +195,7 @@ const SlideSettings: React.FC = () => { ...@@ -181,7 +195,7 @@ const SlideSettings: React.FC = () => {
console.log('newValue: ' + newValue) console.log('newValue: ' + newValue)
await axios await axios
.put( .put(
`/competitions/${id}/slides/${activeSlide?.id}/questions/${activeSlide?.questions[0].id}/alternatives/${alternative.id}`, `/api/competitions/${id}/slides/${activeSlide?.id}/questions/${activeSlide?.questions[0].id}/alternatives/${alternative.id}`,
{ value: newValue } { value: newValue }
) )
.then(() => { .then(() => {
...@@ -195,7 +209,7 @@ const SlideSettings: React.FC = () => { ...@@ -195,7 +209,7 @@ const SlideSettings: React.FC = () => {
if (activeSlide && activeSlide.questions[0]) { if (activeSlide && activeSlide.questions[0]) {
await axios await axios
.put( .put(
`/competitions/${id}/slides/${activeSlide?.id}/questions/${activeSlide?.questions[0].id}/alternatives/${alternative_id}`, `/api/competitions/${id}/slides/${activeSlide?.id}/questions/${activeSlide?.questions[0].id}/alternatives/${alternative_id}`,
{ text: newText } { text: newText }
) )
.then(() => { .then(() => {
...@@ -222,18 +236,47 @@ const SlideSettings: React.FC = () => { ...@@ -222,18 +236,47 @@ const SlideSettings: React.FC = () => {
} }
} }
const handleFileSelected = (e: React.ChangeEvent<HTMLInputElement>): void => { const uploadFile = async (formData: FormData) => {
// Uploads the file to the server and creates a Media object in database
// Returns media id
return await axios
.post(`/api/media/images`, formData)
.then((response) => {
dispatch(getEditorCompetition(id))
return response.data as Media
})
.catch(console.log)
}
const createImageComponent = async (media: Media) => {
const imageData = {
x: 0,
y: 0,
data: {
media_id: media.id,
filename: media.filename,
},
type_id: 2,
}
await axios
.post(`/api/competitions/${id}/slides/${activeSlide?.id}/components`, imageData)
.then(() => {
dispatch(getEditorCompetition(id))
})
.catch(console.log)
}
const handleFileSelected = async (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files !== null && e.target.files[0]) { if (e.target.files !== null && e.target.files[0]) {
const files = Array.from(e.target.files) const files = Array.from(e.target.files)
const file = files[0] const file = files[0]
const reader = new FileReader() const formData = new FormData()
reader.readAsDataURL(file) formData.append('image', file)
reader.onload = function () { const response = await uploadFile(formData)
console.log(reader.result)
// TODO: Send image to back-end (remove console.log) if (response) {
} const newComponent = createImageComponent(response)
reader.onerror = function (error) {
console.log('Error: ', error)
} }
} }
} }
...@@ -411,19 +454,19 @@ const SlideSettings: React.FC = () => { ...@@ -411,19 +454,19 @@ const SlideSettings: React.FC = () => {
<ListItem divider> <ListItem divider>
<ListItemText className={classes.textCenter} primary="Bilder" /> <ListItemText className={classes.textCenter} primary="Bilder" />
</ListItem> </ListItem>
{pictures.map((picture) => ( {images &&
<div key={picture.id}> images.map((image) => (
<ListItem divider button> <div key={image.id}>
<img <ListItem divider button>
id="temp source, todo: add image source to elements of pictureList" <img
src="https://i1.wp.com/stickoutmedia.se/wp-content/uploads/2021/01/placeholder-3.png?ssl=1" src={`http://localhost:5000/static/images/thumbnail_${image.data.filename}`}
className={classes.importedImage} className={classes.importedImage}
/> />
<ListItemText className={classes.textCenter} primary={picture.name} /> <ListItemText className={classes.textCenter} primary={image.data.filename} />
<CloseIcon onClick={() => handleClosePictureClick(picture.id)} /> <CloseIcon onClick={() => handleCloseimageClick(image)} />
</ListItem> </ListItem>
</div> </div>
))} ))}
<ListItem className={classes.center} button> <ListItem className={classes.center} button>
<HiddenInput accept="image/*" id="contained-button-file" multiple type="file" onChange={handleFileSelected} /> <HiddenInput accept="image/*" id="contained-button-file" multiple type="file" onChange={handleFileSelected} />
...@@ -435,7 +478,7 @@ const SlideSettings: React.FC = () => { ...@@ -435,7 +478,7 @@ const SlideSettings: React.FC = () => {
<ListItem button> <ListItem button>
<img <img
id="temp source, todo: add image source to elements of pictureList" id="temp source, todo: add image source to elements of imageList"
src="https://i1.wp.com/stickoutmedia.se/wp-content/uploads/2021/01/placeholder-3.png?ssl=1" src="https://i1.wp.com/stickoutmedia.se/wp-content/uploads/2021/01/placeholder-3.png?ssl=1"
className={classes.importedImage} className={classes.importedImage}
/> />
......
...@@ -4,6 +4,7 @@ import { combineReducers } from 'redux' ...@@ -4,6 +4,7 @@ import { combineReducers } from 'redux'
import citiesReducer from './citiesReducer' import citiesReducer from './citiesReducer'
import competitionsReducer from './competitionsReducer' import competitionsReducer from './competitionsReducer'
import editorReducer from './editorReducer' import editorReducer from './editorReducer'
import mediaReducer from './mediaReducer'
import presentationReducer from './presentationReducer' import presentationReducer from './presentationReducer'
import rolesReducer from './rolesReducer' import rolesReducer from './rolesReducer'
import searchUserReducer from './searchUserReducer' import searchUserReducer from './searchUserReducer'
...@@ -23,6 +24,7 @@ const allReducers = combineReducers({ ...@@ -23,6 +24,7 @@ const allReducers = combineReducers({
roles: rolesReducer, roles: rolesReducer,
searchUsers: searchUserReducer, searchUsers: searchUserReducer,
types: typesReducer, types: typesReducer,
media: mediaReducer,
statistics: statisticsReducer, statistics: statisticsReducer,
}) })
export default allReducers export default allReducers
import { AnyAction } from 'redux'
import Types from '../actions/types'
interface MediaState {
id: number
filename: string
mediatype_id: number
user_id: number
}
const initialState: MediaState = {
id: 0,
filename: '',
mediatype_id: 1,
user_id: 0,
}
export default function (state = initialState, action: AnyAction) {
switch (action.type) {
case Types.SET_MEDIA_ID:
return { ...state, id: action.payload as number }
case Types.SET_MEDIA_FILENAME:
return {
...state,
filename: action.payload as string,
}
case Types.SET_MEDIA_TYPE_ID:
return {
...state,
mediatype_id: action.payload as number,
}
case Types.SET_MEDIA_USER_ID:
return {
...state,
user_id: action.payload as number,
}
default:
return state
}
}
...@@ -2,10 +2,11 @@ ...@@ -2,10 +2,11 @@
This file contains functionality to add data to the database. This file contains functionality to add data to the database.
""" """
from sqlalchemy.orm.session import sessionmaker import os
import app.core.http_codes as codes import app.core.http_codes as codes
from app.core import db from app.core import db
from app.database.controller import utils from app.database.controller import get, search, utils
from app.database.models import ( from app.database.models import (
Blacklist, Blacklist,
City, City,
...@@ -25,8 +26,12 @@ from app.database.models import ( ...@@ -25,8 +26,12 @@ from app.database.models import (
User, User,
ViewType, ViewType,
) )
from flask.globals import current_app
from flask_restx import abort from flask_restx import abort
from PIL import Image
from sqlalchemy import exc from sqlalchemy import exc
from sqlalchemy.orm import relation
from sqlalchemy.orm.session import sessionmaker
def db_add(item): def db_add(item):
...@@ -97,6 +102,21 @@ def component(type_id, slide_id, data, x=0, y=0, w=0, h=0): ...@@ -97,6 +102,21 @@ def component(type_id, slide_id, data, x=0, y=0, w=0, h=0):
Adds a component to the slide at the specified coordinates with the Adds a component to the slide at the specified coordinates with the
provided size and data . provided size and data .
""" """
from app.apis.media import PHOTO_PATH
if type_id == 2: # 2 is image
item_image = get.one(Media, data["media_id"])
filename = item_image.filename
path = os.path.join(PHOTO_PATH, filename)
with Image.open(path) as im:
h = im.height
w = im.width
largest = max(w, h)
if largest > 600:
ratio = 600 / largest
w *= ratio
h *= ratio
return db_add(Component(slide_id, type_id, data, x, y, w, h)) return db_add(Component(slide_id, type_id, data, x, y, w, h))
......
...@@ -100,6 +100,7 @@ if __name__ == "__main__": ...@@ -100,6 +100,7 @@ if __name__ == "__main__":
app, _ = create_app("configmodule.DevelopmentConfig") app, _ = create_app("configmodule.DevelopmentConfig")
with app.app_context(): with app.app_context():
db.drop_all() db.drop_all()
db.create_all() db.create_all()
_add_items() _add_items()
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment