diff --git a/.vscode/settings.json b/.vscode/settings.json index db2b68f4590ffec707c286474aa1004c29a5709e..b02ef900e7a5db6b3da7e7593e38600b00b950bf 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -40,5 +40,6 @@ }, "search.exclude": { "**/env": true - } + }, + "python.pythonPath": "server\\env\\Scripts\\python.exe" } diff --git a/client/src/actions/types.ts b/client/src/actions/types.ts index 189bf16ca5f3baa63bdf8d0ad5d5ae0fa6805d10..5932deb0b24a027b5fc329113f5513253a82ff2a 100644 --- a/client/src/actions/types.ts +++ b/client/src/actions/types.ts @@ -29,5 +29,9 @@ export default { SET_CITIES_TOTAL: 'SET_CITIES_TOTAL', SET_CITIES_COUNT: 'SET_CITIES_COUNT', 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', } diff --git a/client/src/enum/ComponentTypes.ts b/client/src/enum/ComponentTypes.ts index 8acb2fd91da2eba39dd935ddce1f0a8af9a59198..8567e1c82bba8eb42e0d8a705bb6179ce56ba118 100644 --- a/client/src/enum/ComponentTypes.ts +++ b/client/src/enum/ComponentTypes.ts @@ -1,5 +1,5 @@ export enum ComponentTypes { Text = 1, - Checkbox, Image, + Checkbox, } diff --git a/client/src/interfaces/ApiModels.ts b/client/src/interfaces/ApiModels.ts index 650814f11fe4bd135822dcdb97855c430dd750ca..347fdbfa514b3f2adb71c4d0c47b19a08fa99376 100644 --- a/client/src/interfaces/ApiModels.ts +++ b/client/src/interfaces/ApiModels.ts @@ -84,6 +84,7 @@ export interface Component { export interface ImageComponent extends Component { data: { media_id: number + filename: string } } diff --git a/client/src/interfaces/ApiRichModels.ts b/client/src/interfaces/ApiRichModels.ts index ebc2f885fb15251dee86cc1c2d0c33e86166af8c..2388f8308ba8bfa38d4d5376040f937244f4cbd5 100644 --- a/client/src/interfaces/ApiRichModels.ts +++ b/client/src/interfaces/ApiRichModels.ts @@ -1,4 +1,4 @@ -import { Component, QuestionAlternative, QuestionAnswer, QuestionType } from './ApiModels' +import { Component, Media, QuestionAlternative, QuestionAnswer, QuestionType } from './ApiModels' export interface RichCompetition { name: string @@ -17,6 +17,7 @@ export interface RichSlide { competition_id: number components: Component[] questions: RichQuestion[] + medias: Media[] } export interface RichTeam { diff --git a/client/src/pages/admin/AdminPage.test.tsx b/client/src/pages/admin/AdminPage.test.tsx index ab694cee23501ef51a644c1df30f36662d9d36f6..445373a032207faa3f546a0c480fb03a093b1529 100644 --- a/client/src/pages/admin/AdminPage.test.tsx +++ b/client/src/pages/admin/AdminPage.test.tsx @@ -40,7 +40,7 @@ it('renders admin view', () => { }, } ;(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) }) render( diff --git a/client/src/pages/admin/competitions/CompetitionManager.test.tsx b/client/src/pages/admin/competitions/CompetitionManager.test.tsx index f47d8045d4486bc3d33dac2d28a65ffdc3eb5298..7af04abb66bba7ded7655398f288cba1c62bf78b 100644 --- a/client/src/pages/admin/competitions/CompetitionManager.test.tsx +++ b/client/src/pages/admin/competitions/CompetitionManager.test.tsx @@ -47,7 +47,7 @@ it('renders competition manager', () => { } ;(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) }) render( diff --git a/client/src/pages/presentationEditor/components/CompetitionSettings.test.tsx b/client/src/pages/presentationEditor/components/CompetitionSettings.test.tsx deleted file mode 100644 index a655a30f0763524c61e81185065ab4974a15ae91..0000000000000000000000000000000000000000 --- a/client/src/pages/presentationEditor/components/CompetitionSettings.test.tsx +++ /dev/null @@ -1,10 +0,0 @@ -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 }} /> - ) -}) diff --git a/client/src/pages/presentationEditor/components/ImageComponentDisplay.test.tsx b/client/src/pages/presentationEditor/components/ImageComponentDisplay.test.tsx index b726023b8e6079e7a631c2be396857e9ca0ef4c1..9e78f8e13fd94cf36434ef63ed7504695208de1c 100644 --- a/client/src/pages/presentationEditor/components/ImageComponentDisplay.test.tsx +++ b/client/src/pages/presentationEditor/components/ImageComponentDisplay.test.tsx @@ -3,5 +3,11 @@ import React from 'react' import ImageComponentDisplay from './ImageComponentDisplay' 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} + /> + ) }) diff --git a/client/src/pages/presentationEditor/components/ImageComponentDisplay.tsx b/client/src/pages/presentationEditor/components/ImageComponentDisplay.tsx index cffba9e59afa4288f980b9b4d3833494a02ec7d9..7886a9b15899dfe4b790be9d3673591b4d1940d7 100644 --- a/client/src/pages/presentationEditor/components/ImageComponentDisplay.tsx +++ b/client/src/pages/presentationEditor/components/ImageComponentDisplay.tsx @@ -1,43 +1,20 @@ -import React, { useState } from 'react' -import { Rnd } from 'react-rnd' +import React from 'react' import { ImageComponent } from '../../../interfaces/ApiModels' -import { Position, Size } from '../../../interfaces/Components' type ImageComponentProps = { component: ImageComponent + width: number + height: number } -const ImageComponentDisplay = ({ component }: ImageComponentProps) => { - const [currentPos, setCurrentPos] = useState<Position>({ x: component.x, y: component.y }) - const [currentSize, setCurrentSize] = useState<Size>({ w: component.w, h: component.h }) +const ImageComponentDisplay = ({ component, width, height }: ImageComponentProps) => { return ( - <Rnd - minWidth={50} - minHeight={50} - bounds="parent" - onDragStop={(e, d) => { - 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> + <img + src={`http://localhost:5000/static/images/${component.data.filename}`} + height={height} + width={width} + draggable={false} + /> ) } diff --git a/client/src/pages/presentationEditor/components/RndComponent.tsx b/client/src/pages/presentationEditor/components/RndComponent.tsx index 6302d3fcf50c68fcb232693aa0a3590999cc660f..ed8170ff17718ad5cd76307648194cf396526724 100644 --- a/client/src/pages/presentationEditor/components/RndComponent.tsx +++ b/client/src/pages/presentationEditor/components/RndComponent.tsx @@ -44,7 +44,14 @@ const RndComponent = ({ component }: ImageComponentProps) => { /> ) 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: break } diff --git a/client/src/pages/presentationEditor/components/SettingsPanel.test.tsx b/client/src/pages/presentationEditor/components/SettingsPanel.test.tsx index a7a71e25921deb276cbaca2089d2ecbf58c8493c..a17569ec7b6e9e3ed665a294cc7b2b6f1da0be58 100644 --- a/client/src/pages/presentationEditor/components/SettingsPanel.test.tsx +++ b/client/src/pages/presentationEditor/components/SettingsPanel.test.tsx @@ -6,6 +6,7 @@ import { BrowserRouter } from 'react-router-dom' import store from '../../../store' import CompetitionSettings from './CompetitionSettings' import SettingsPanel from './SettingsPanel' +import SlideSettings from './SlideSettings' it('renders settings panel', () => { render( @@ -28,5 +29,5 @@ it('renders slide settings tab', () => { const tabs = wrapper.find('.MuiTabs-flexContainer') expect(wrapper.find(CompetitionSettings).length).toEqual(1) 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) }) diff --git a/client/src/pages/presentationEditor/components/SlideSettings.tsx b/client/src/pages/presentationEditor/components/SlideSettings.tsx index 85fe3cc08419333f9bb94fc4b9fbc1094c1ec7f7..ffa95333cbce25364f5fb0bd3abd80adeb298a9f 100644 --- a/client/src/pages/presentationEditor/components/SlideSettings.tsx +++ b/client/src/pages/presentationEditor/components/SlideSettings.tsx @@ -22,11 +22,11 @@ import { green, grey } from '@material-ui/core/colors' import { createStyles, makeStyles, Theme, withStyles } from '@material-ui/core/styles' import CloseIcon from '@material-ui/icons/Close' import axios from 'axios' +import { ImageComponent, TextComponent, QuestionAlternative, Media } from '../../../interfaces/ApiModels' import React, { useEffect, useState } from 'react' import { useParams } from 'react-router-dom' import { getEditorCompetition } from '../../../actions/editor' import { useAppDispatch, useAppSelector } from '../../../hooks' -import { QuestionAlternative, TextComponent } from '../../../interfaces/ApiModels' import { HiddenInput, TextCard } from './styled' import TextComponentEdit from './TextComponentEdit' @@ -101,7 +101,7 @@ const SlideSettings: React.FC = () => { if (activeSlide && activeSlide.questions[0]) { await axios .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(() => { dispatch(getEditorCompetition(id)) @@ -117,14 +117,28 @@ const SlideSettings: React.FC = () => { ?.components.filter((component) => component.type_id === 1) as TextComponent[] ) - const pictureList = [ - { id: 'picture1', name: 'Picture1.jpeg' }, - { id: 'picture2', name: 'Picture2.jpeg' }, - ] - const handleClosePictureClick = (id: string) => { - setPictures(pictures.filter((item) => item.id !== id)) //Will not be done like this when api is used + const images = useAppSelector( + (state) => + state.editor.competition.slides + .find((slide) => slide.id === state.editor.activeSlideId) + ?.components.filter((component) => component.type_id === 2) as ImageComponent[] + ) + + 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 () => { closeSlideTypeDialog() @@ -181,7 +195,7 @@ const SlideSettings: React.FC = () => { console.log('newValue: ' + newValue) await axios .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 } ) .then(() => { @@ -195,7 +209,7 @@ const SlideSettings: React.FC = () => { if (activeSlide && activeSlide.questions[0]) { await axios .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 } ) .then(() => { @@ -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]) { const files = Array.from(e.target.files) const file = files[0] - const reader = new FileReader() - reader.readAsDataURL(file) - reader.onload = function () { - console.log(reader.result) - // TODO: Send image to back-end (remove console.log) - } - reader.onerror = function (error) { - console.log('Error: ', error) + const formData = new FormData() + formData.append('image', file) + const response = await uploadFile(formData) + + if (response) { + const newComponent = createImageComponent(response) } } } @@ -411,19 +454,19 @@ const SlideSettings: React.FC = () => { <ListItem divider> <ListItemText className={classes.textCenter} primary="Bilder" /> </ListItem> - {pictures.map((picture) => ( - <div key={picture.id}> - <ListItem divider button> - <img - id="temp source, todo: add image source to elements of pictureList" - src="https://i1.wp.com/stickoutmedia.se/wp-content/uploads/2021/01/placeholder-3.png?ssl=1" - className={classes.importedImage} - /> - <ListItemText className={classes.textCenter} primary={picture.name} /> - <CloseIcon onClick={() => handleClosePictureClick(picture.id)} /> - </ListItem> - </div> - ))} + {images && + images.map((image) => ( + <div key={image.id}> + <ListItem divider button> + <img + src={`http://localhost:5000/static/images/thumbnail_${image.data.filename}`} + className={classes.importedImage} + /> + <ListItemText className={classes.textCenter} primary={image.data.filename} /> + <CloseIcon onClick={() => handleCloseimageClick(image)} /> + </ListItem> + </div> + ))} <ListItem className={classes.center} button> <HiddenInput accept="image/*" id="contained-button-file" multiple type="file" onChange={handleFileSelected} /> @@ -435,7 +478,7 @@ const SlideSettings: React.FC = () => { <ListItem button> <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" className={classes.importedImage} /> diff --git a/client/src/reducers/allReducers.ts b/client/src/reducers/allReducers.ts index c23384c2a2a21dfba86c60550154985a5c879782..398ec0a71669a6eab2aaf851ac0ea0f10b226b91 100644 --- a/client/src/reducers/allReducers.ts +++ b/client/src/reducers/allReducers.ts @@ -4,6 +4,7 @@ import { combineReducers } from 'redux' import citiesReducer from './citiesReducer' import competitionsReducer from './competitionsReducer' import editorReducer from './editorReducer' +import mediaReducer from './mediaReducer' import presentationReducer from './presentationReducer' import rolesReducer from './rolesReducer' import searchUserReducer from './searchUserReducer' @@ -23,6 +24,7 @@ const allReducers = combineReducers({ roles: rolesReducer, searchUsers: searchUserReducer, types: typesReducer, + media: mediaReducer, statistics: statisticsReducer, }) export default allReducers diff --git a/client/src/reducers/mediaReducer.ts b/client/src/reducers/mediaReducer.ts new file mode 100644 index 0000000000000000000000000000000000000000..ad5f3b46547f2e9a20135537ff814b196ca22331 --- /dev/null +++ b/client/src/reducers/mediaReducer.ts @@ -0,0 +1,39 @@ +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 + } +} diff --git a/server/app/database/controller/add.py b/server/app/database/controller/add.py index 9ab17ce950184980a89c35b7cfe10a7182e36e2b..1f57f7d06c74d9a0fd9999c99a5108b1d59ad25f 100644 --- a/server/app/database/controller/add.py +++ b/server/app/database/controller/add.py @@ -2,10 +2,11 @@ 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 from app.core import db -from app.database.controller import utils +from app.database.controller import get, search, utils from app.database.models import ( Blacklist, City, @@ -25,8 +26,12 @@ from app.database.models import ( User, ViewType, ) +from flask.globals import current_app from flask_restx import abort +from PIL import Image from sqlalchemy import exc +from sqlalchemy.orm import relation +from sqlalchemy.orm.session import sessionmaker def db_add(item): @@ -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 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)) diff --git a/server/populate.py b/server/populate.py index 9ca3c95e7f683decfac5e9f697d0cfdbff958e1e..703e96c2009dcddcf8cc5fc2865854ed106f621a 100644 --- a/server/populate.py +++ b/server/populate.py @@ -100,6 +100,7 @@ if __name__ == "__main__": app, _ = create_app("configmodule.DevelopmentConfig") with app.app_context(): + db.drop_all() db.create_all() _add_items()