From d83d66daa4dfc513b1675f2261c0f5875dcf586a Mon Sep 17 00:00:00 2001 From: Albin Henriksson <albhe428@student.liu.se> Date: Thu, 8 Apr 2021 10:54:06 +0200 Subject: [PATCH 01/21] Add editor state in redux --- client/src/actions/editor.ts | 17 ++++++++++ client/src/actions/types.ts | 1 + .../PresentationEditorPage.tsx | 13 ++++++-- client/src/reducers/allReducers.ts | 2 ++ client/src/reducers/editorReducer.ts | 31 +++++++++++++++++++ 5 files changed, 61 insertions(+), 3 deletions(-) create mode 100644 client/src/actions/editor.ts create mode 100644 client/src/reducers/editorReducer.ts diff --git a/client/src/actions/editor.ts b/client/src/actions/editor.ts new file mode 100644 index 00000000..cf00921a --- /dev/null +++ b/client/src/actions/editor.ts @@ -0,0 +1,17 @@ +import axios from 'axios' +import { AppDispatch } from './../store' +import Types from './types' + +export const getEditorCompetition = (id: string) => async (dispatch: AppDispatch) => { + await axios + .get(`/competitions/${id}`) + .then((res) => { + dispatch({ + type: Types.SET_EDITOR_COMPETITION, + payload: res.data, + }) + }) + .catch((err) => { + console.log(err) + }) +} diff --git a/client/src/actions/types.ts b/client/src/actions/types.ts index 40e94303..bfa3032e 100644 --- a/client/src/actions/types.ts +++ b/client/src/actions/types.ts @@ -10,6 +10,7 @@ export default { SET_COMPETITIONS_FILTER_PARAMS: 'SET_COMPETITIONS_FILTER_PARAMS', SET_COMPETITIONS_TOTAL: 'SET_COMPETITIONS_TOTAL', SET_COMPETITIONS_COUNT: 'SET_COMPETITIONS_COUNT', + SET_EDITOR_COMPETITION: 'SET_EDITOR_COMPETITION', SET_CITIES: 'SET_CITIES', SET_CITIES_TOTAL: 'SET_CITIES_TOTAL', SET_CITIES_COUNT: 'SET_CITIES_COUNT', diff --git a/client/src/pages/presentationEditor/PresentationEditorPage.tsx b/client/src/pages/presentationEditor/PresentationEditorPage.tsx index 5a0dbc99..6e60aa40 100644 --- a/client/src/pages/presentationEditor/PresentationEditorPage.tsx +++ b/client/src/pages/presentationEditor/PresentationEditorPage.tsx @@ -5,8 +5,10 @@ import Drawer from '@material-ui/core/Drawer' import List from '@material-ui/core/List' import ListItemText from '@material-ui/core/ListItemText' import { createStyles, makeStyles, Theme } from '@material-ui/core/styles' -import React from 'react' +import React, { useEffect } from 'react' import { useParams } from 'react-router-dom' +import { getEditorCompetition } from '../../actions/editor' +import { useAppDispatch, useAppSelector } from '../../hooks' import SettingsPanel from './components/SettingsPanel' import { SlideListItem, ToolBarContainer, ViewButton, ViewButtonGroup } from './styled' @@ -69,14 +71,19 @@ interface CompetitionParams { const PresentationEditorPage: React.FC = () => { const classes = useStyles() - const params: CompetitionParams = useParams() + const { id }: CompetitionParams = useParams() + const dispatch = useAppDispatch() + const competition = useAppSelector((state) => state.editor.competition) + useEffect(() => { + dispatch(getEditorCompetition(id)) + }, []) return ( <div className={classes.root}> <CssBaseline /> <AppBar position="fixed" className={classes.appBar}> <ToolBarContainer> <Typography variant="h6" noWrap> - Tävling nr: {params.id} + Tävlingsnamn: {competition.name} </Typography> <ViewButtonGroup> <ViewButton variant="contained" color="secondary"> diff --git a/client/src/reducers/allReducers.ts b/client/src/reducers/allReducers.ts index b12b5346..732cff39 100644 --- a/client/src/reducers/allReducers.ts +++ b/client/src/reducers/allReducers.ts @@ -3,6 +3,7 @@ import { combineReducers } from 'redux' import citiesReducer from './citiesReducer' import competitionsReducer from './competitionsReducer' +import editorReducer from './editorReducer' import uiReducer from './uiReducer' import userReducer from './userReducer' @@ -12,5 +13,6 @@ const allReducers = combineReducers({ UI: uiReducer, competitions: competitionsReducer, cities: citiesReducer, + editor: editorReducer, }) export default allReducers diff --git a/client/src/reducers/editorReducer.ts b/client/src/reducers/editorReducer.ts new file mode 100644 index 00000000..a7a09367 --- /dev/null +++ b/client/src/reducers/editorReducer.ts @@ -0,0 +1,31 @@ +import { AnyAction } from 'redux' +import Types from '../actions/types' +import { Competition } from '../interfaces/Competition' + +interface EditorState { + competition: Competition +} + +const initialState: EditorState = { + competition: { + name: '', + id: 0, + year: 0, + city: { + id: 0, + name: '', + }, + }, +} + +export default function (state = initialState, action: AnyAction) { + switch (action.type) { + case Types.SET_EDITOR_COMPETITION: + return { + ...state, + competition: action.payload as Competition, + } + default: + return state + } +} -- GitLab From 1b2d265d13f150f3bc988768026eb327971359b4 Mon Sep 17 00:00:00 2001 From: Sebastian Karlsson <sebka991@student.liu.se> Date: Thu, 8 Apr 2021 14:20:25 +0200 Subject: [PATCH 02/21] Add getting slides from database in editor. --- client/src/interfaces/Competition.ts | 2 ++ client/src/interfaces/Slide.ts | 7 +++++++ .../presentationEditor/PresentationEditorPage.tsx | 15 +++------------ client/src/reducers/editorReducer.ts | 1 + 4 files changed, 13 insertions(+), 12 deletions(-) create mode 100644 client/src/interfaces/Slide.ts diff --git a/client/src/interfaces/Competition.ts b/client/src/interfaces/Competition.ts index 7a9c7032..aaf51951 100644 --- a/client/src/interfaces/Competition.ts +++ b/client/src/interfaces/Competition.ts @@ -1,8 +1,10 @@ import { City } from './City' +import { Slide } from './Slide' export interface Competition { name: string id: number city: City year: number + slides: Slide[] } diff --git a/client/src/interfaces/Slide.ts b/client/src/interfaces/Slide.ts new file mode 100644 index 00000000..3706ed7c --- /dev/null +++ b/client/src/interfaces/Slide.ts @@ -0,0 +1,7 @@ +export interface Slide { + id: number + competition_id: number + title: string + order: number + timer: number +} diff --git a/client/src/pages/presentationEditor/PresentationEditorPage.tsx b/client/src/pages/presentationEditor/PresentationEditorPage.tsx index 6e60aa40..abcfb399 100644 --- a/client/src/pages/presentationEditor/PresentationEditorPage.tsx +++ b/client/src/pages/presentationEditor/PresentationEditorPage.tsx @@ -16,15 +16,6 @@ function createSlide(name: string) { return { name } } -const slides = [ - createSlide('Sida 1'), - createSlide('Sida 2'), - createSlide('Sida 3'), - createSlide('Sida 4'), - createSlide('Sida 5'), - createSlide('Sida 6'), - createSlide('Sida 7'), -] const leftDrawerWidth = 150 const rightDrawerWidth = 390 @@ -109,9 +100,9 @@ const PresentationEditorPage: React.FC = () => { <div className={classes.toolbar} /> <Divider /> <List> - {slides.map((slide) => ( - <SlideListItem divider button key={slide.name}> - <ListItemText primary={slide.name} /> + {competition.slides.map((slide) => ( + <SlideListItem divider button key={slide.title}> + <ListItemText primary={slide.title} /> </SlideListItem> ))} </List> diff --git a/client/src/reducers/editorReducer.ts b/client/src/reducers/editorReducer.ts index a7a09367..eb238298 100644 --- a/client/src/reducers/editorReducer.ts +++ b/client/src/reducers/editorReducer.ts @@ -15,6 +15,7 @@ const initialState: EditorState = { id: 0, name: '', }, + slides: [], }, } -- GitLab From 2064c273573824c3f7ae05dbf8251755499adc6f Mon Sep 17 00:00:00 2001 From: Emil <Emil> Date: Thu, 8 Apr 2021 22:30:08 +0200 Subject: [PATCH 03/21] Feat: update competitions name in editor.competition --- client/src/actions/editor.ts | 15 ++++++ client/src/actions/types.ts | 1 + .../components/CompetitionSettings.tsx | 47 +++++++++++++++++-- client/src/reducers/editorReducer.ts | 8 ++++ 4 files changed, 68 insertions(+), 3 deletions(-) diff --git a/client/src/actions/editor.ts b/client/src/actions/editor.ts index cf00921a..e23cd084 100644 --- a/client/src/actions/editor.ts +++ b/client/src/actions/editor.ts @@ -15,3 +15,18 @@ export const getEditorCompetition = (id: string) => async (dispatch: AppDispatch console.log(err) }) } + +export const setCompetitionName = (id: string, name: string) => async (dispatch: AppDispatch) => { + await axios + .get(`/competitions/${id}`) + .then((res) => { + dispatch({ + type: Types.SET_COMPETITION_NAME, + payload: res.data, + name: name, + }) + }) + .catch((err) => { + console.log(err) + }) +} diff --git a/client/src/actions/types.ts b/client/src/actions/types.ts index bfa3032e..72441b10 100644 --- a/client/src/actions/types.ts +++ b/client/src/actions/types.ts @@ -11,6 +11,7 @@ export default { SET_COMPETITIONS_TOTAL: 'SET_COMPETITIONS_TOTAL', SET_COMPETITIONS_COUNT: 'SET_COMPETITIONS_COUNT', SET_EDITOR_COMPETITION: 'SET_EDITOR_COMPETITION', + SET_COMPETITION_NAME: 'SET COMPETITION NAME', SET_CITIES: 'SET_CITIES', SET_CITIES_TOTAL: 'SET_CITIES_TOTAL', SET_CITIES_COUNT: 'SET_CITIES_COUNT', diff --git a/client/src/pages/presentationEditor/components/CompetitionSettings.tsx b/client/src/pages/presentationEditor/components/CompetitionSettings.tsx index a9a1d767..ad7595d9 100644 --- a/client/src/pages/presentationEditor/components/CompetitionSettings.tsx +++ b/client/src/pages/presentationEditor/components/CompetitionSettings.tsx @@ -1,7 +1,10 @@ import { Button, Divider, List, ListItem, ListItemText, TextField } from '@material-ui/core' import { createStyles, makeStyles, Theme } from '@material-ui/core/styles' import CloseIcon from '@material-ui/icons/Close' -import React, { useState } from 'react' +import React, { useEffect, useState } from 'react' +import { useParams } from 'react-router-dom' +import { getEditorCompetition, setCompetitionName } from '../../../actions/editor' +import { useAppDispatch, useAppSelector } from '../../../hooks' const useStyles = makeStyles((theme: Theme) => createStyles({ @@ -34,8 +37,31 @@ const useStyles = makeStyles((theme: Theme) => }) ) +interface CompetitionParams { + id: string +} + const CompetitionSettings: React.FC = () => { const classes = useStyles() + const { id }: CompetitionParams = useParams() + const dispatch = useAppDispatch() + + const competition = useAppSelector((state) => state.editor.competition) + const competitionName = competition.name + useEffect(() => { + dispatch(getEditorCompetition(id)) + }, []) + + const updateCompetitionName = (event: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => { + if (timerHandle) { + clearTimeout(timerHandle) + setTimerHandle(undefined) + } + //Only updates filter and api 100ms after last input was made + setTimerHandle(window.setTimeout(() => dispatch(setCompetitionName(id, event.target.value)), 100)) + dispatch(setCompetitionName(id, competitionName)) + } + const initialList = [ { id: '1', name: 'Lag1' }, { id: '2', name: 'Lag2' }, @@ -44,13 +70,28 @@ const CompetitionSettings: React.FC = () => { const handleClick = (id: string) => { setTeams(teams.filter((item) => item.id !== id)) //Will not be done like this when api is used } + const [timerHandle, setTimerHandle] = React.useState<number | undefined>(undefined) const [teams, setTeams] = useState(initialList) + return ( <div className={classes.textInputContainer}> <form noValidate autoComplete="off"> - <TextField className={classes.textInput} id="outlined-basic" label="Tävlingsnamn" variant="outlined" /> + <TextField + className={classes.textInput} + id="outlined-basic" + label={'Tävlingsnamn'} + defaultValue={competitionName} + onChange={updateCompetitionName} + variant="outlined" + /> <Divider /> - <TextField className={classes.textInput} id="outlined-basic" label="Stad" variant="outlined" /> + <TextField + className={classes.textInput} + id="outlined-basic" + label="Stad" + defaultValue={competition.city.name} + variant="outlined" + /> </form> <List> diff --git a/client/src/reducers/editorReducer.ts b/client/src/reducers/editorReducer.ts index eb238298..80cf3a9b 100644 --- a/client/src/reducers/editorReducer.ts +++ b/client/src/reducers/editorReducer.ts @@ -26,6 +26,14 @@ export default function (state = initialState, action: AnyAction) { ...state, competition: action.payload as Competition, } + case Types.SET_COMPETITION_NAME: + return { + ...state, + competition: { + ...state.competition, + name: action.name, + } as Competition, + } default: return state } -- GitLab From d01725ca7fe7996ff0aed40f58fcbeeeb76cdac0 Mon Sep 17 00:00:00 2001 From: Emil <Emil> Date: Fri, 9 Apr 2021 00:56:32 +0200 Subject: [PATCH 04/21] removed editor state, added name update to competitions state --- client/src/actions/competitions.ts | 15 +++++++ client/src/actions/editor.ts | 32 --------------- .../admin/components/CompetitionManager.tsx | 3 +- .../PresentationEditorPage.tsx | 14 +++---- .../components/CompetitionSettings.tsx | 9 ++--- client/src/reducers/allReducers.ts | 2 - client/src/reducers/competitionsReducer.ts | 12 ++++++ client/src/reducers/editorReducer.ts | 40 ------------------- 8 files changed, 37 insertions(+), 90 deletions(-) delete mode 100644 client/src/actions/editor.ts delete mode 100644 client/src/reducers/editorReducer.ts diff --git a/client/src/actions/competitions.ts b/client/src/actions/competitions.ts index a758fd63..608060ae 100644 --- a/client/src/actions/competitions.ts +++ b/client/src/actions/competitions.ts @@ -37,3 +37,18 @@ export const getCompetitions = () => async (dispatch: AppDispatch, getState: () export const setFilterParams = (params: CompetitionFilterParams) => (dispatch: AppDispatch) => { dispatch({ type: Types.SET_COMPETITIONS_FILTER_PARAMS, payload: params }) } +export const setCompetitionName = (id: string, name: string) => async (dispatch: AppDispatch) => { + await axios + .get(`/competitions/search`) + .then((res) => { + dispatch({ + type: Types.SET_COMPETITION_NAME, + payload: res.data.items, + name: name, + id: +id - 1, + }) + }) + .catch((err) => { + console.log(err) + }) +} diff --git a/client/src/actions/editor.ts b/client/src/actions/editor.ts deleted file mode 100644 index e23cd084..00000000 --- a/client/src/actions/editor.ts +++ /dev/null @@ -1,32 +0,0 @@ -import axios from 'axios' -import { AppDispatch } from './../store' -import Types from './types' - -export const getEditorCompetition = (id: string) => async (dispatch: AppDispatch) => { - await axios - .get(`/competitions/${id}`) - .then((res) => { - dispatch({ - type: Types.SET_EDITOR_COMPETITION, - payload: res.data, - }) - }) - .catch((err) => { - console.log(err) - }) -} - -export const setCompetitionName = (id: string, name: string) => async (dispatch: AppDispatch) => { - await axios - .get(`/competitions/${id}`) - .then((res) => { - dispatch({ - type: Types.SET_COMPETITION_NAME, - payload: res.data, - name: name, - }) - }) - .catch((err) => { - console.log(err) - }) -} diff --git a/client/src/pages/admin/components/CompetitionManager.tsx b/client/src/pages/admin/components/CompetitionManager.tsx index ff73a994..c9a8b915 100644 --- a/client/src/pages/admin/components/CompetitionManager.tsx +++ b/client/src/pages/admin/components/CompetitionManager.tsx @@ -18,6 +18,7 @@ import { Link } from 'react-router-dom' import { getCities } from '../../../actions/cities' import { getCompetitions, setFilterParams } from '../../../actions/competitions' import { useAppDispatch, useAppSelector } from '../../../hooks' +import { Competition } from '../../../interfaces/Competition' import { CompetitionFilterParams } from '../../../interfaces/CompetitionFilterParams' import AddCompetition from './AddCompetition' import { FilterContainer, RemoveCompetition, TopBar, YearFilterTextField } from './styled' @@ -145,7 +146,7 @@ const CompetitionManager: React.FC = (props: any) => { </TableHead> <TableBody> {competitions && - competitions.map((row) => ( + competitions.map((row: Competition) => ( <TableRow key={row.name}> <TableCell scope="row"> <Button color="primary" component={Link} to={`/editor/competition-id=${row.id}`}> diff --git a/client/src/pages/presentationEditor/PresentationEditorPage.tsx b/client/src/pages/presentationEditor/PresentationEditorPage.tsx index abcfb399..19caef8f 100644 --- a/client/src/pages/presentationEditor/PresentationEditorPage.tsx +++ b/client/src/pages/presentationEditor/PresentationEditorPage.tsx @@ -5,10 +5,10 @@ import Drawer from '@material-ui/core/Drawer' import List from '@material-ui/core/List' import ListItemText from '@material-ui/core/ListItemText' import { createStyles, makeStyles, Theme } from '@material-ui/core/styles' -import React, { useEffect } from 'react' +import React from 'react' import { useParams } from 'react-router-dom' -import { getEditorCompetition } from '../../actions/editor' -import { useAppDispatch, useAppSelector } from '../../hooks' +import { useAppSelector } from '../../hooks' +import { Slide } from '../../interfaces/Slide' import SettingsPanel from './components/SettingsPanel' import { SlideListItem, ToolBarContainer, ViewButton, ViewButtonGroup } from './styled' @@ -63,11 +63,7 @@ interface CompetitionParams { const PresentationEditorPage: React.FC = () => { const classes = useStyles() const { id }: CompetitionParams = useParams() - const dispatch = useAppDispatch() - const competition = useAppSelector((state) => state.editor.competition) - useEffect(() => { - dispatch(getEditorCompetition(id)) - }, []) + const competition = useAppSelector((state) => state.competitions.competitions[+id - 1]) return ( <div className={classes.root}> <CssBaseline /> @@ -100,7 +96,7 @@ const PresentationEditorPage: React.FC = () => { <div className={classes.toolbar} /> <Divider /> <List> - {competition.slides.map((slide) => ( + {competition.slides.map((slide: Slide) => ( <SlideListItem divider button key={slide.title}> <ListItemText primary={slide.title} /> </SlideListItem> diff --git a/client/src/pages/presentationEditor/components/CompetitionSettings.tsx b/client/src/pages/presentationEditor/components/CompetitionSettings.tsx index ad7595d9..b668010b 100644 --- a/client/src/pages/presentationEditor/components/CompetitionSettings.tsx +++ b/client/src/pages/presentationEditor/components/CompetitionSettings.tsx @@ -1,9 +1,9 @@ import { Button, Divider, List, ListItem, ListItemText, TextField } from '@material-ui/core' import { createStyles, makeStyles, Theme } from '@material-ui/core/styles' import CloseIcon from '@material-ui/icons/Close' -import React, { useEffect, useState } from 'react' +import React, { useState } from 'react' import { useParams } from 'react-router-dom' -import { getEditorCompetition, setCompetitionName } from '../../../actions/editor' +import { setCompetitionName } from '../../../actions/competitions' import { useAppDispatch, useAppSelector } from '../../../hooks' const useStyles = makeStyles((theme: Theme) => @@ -46,11 +46,8 @@ const CompetitionSettings: React.FC = () => { const { id }: CompetitionParams = useParams() const dispatch = useAppDispatch() - const competition = useAppSelector((state) => state.editor.competition) + const competition = useAppSelector((state) => state.competitions.competitions[+id - 1]) const competitionName = competition.name - useEffect(() => { - dispatch(getEditorCompetition(id)) - }, []) const updateCompetitionName = (event: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => { if (timerHandle) { diff --git a/client/src/reducers/allReducers.ts b/client/src/reducers/allReducers.ts index 732cff39..b12b5346 100644 --- a/client/src/reducers/allReducers.ts +++ b/client/src/reducers/allReducers.ts @@ -3,7 +3,6 @@ import { combineReducers } from 'redux' import citiesReducer from './citiesReducer' import competitionsReducer from './competitionsReducer' -import editorReducer from './editorReducer' import uiReducer from './uiReducer' import userReducer from './userReducer' @@ -13,6 +12,5 @@ const allReducers = combineReducers({ UI: uiReducer, competitions: competitionsReducer, cities: citiesReducer, - editor: editorReducer, }) export default allReducers diff --git a/client/src/reducers/competitionsReducer.ts b/client/src/reducers/competitionsReducer.ts index b3003d4a..fabf4801 100644 --- a/client/src/reducers/competitionsReducer.ts +++ b/client/src/reducers/competitionsReducer.ts @@ -39,6 +39,18 @@ export default function (state = initialState, action: AnyAction) { ...state, count: action.payload as number, } + case Types.SET_COMPETITION_NAME: + return { + ...state, + competitions: state.competitions.map((competition, i) => + i === action.id + ? ({ + ...competition, + name: action.name as string, + } as Competition) + : (competition as Competition) + ) as Competition[], + } default: return state } diff --git a/client/src/reducers/editorReducer.ts b/client/src/reducers/editorReducer.ts deleted file mode 100644 index 80cf3a9b..00000000 --- a/client/src/reducers/editorReducer.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { AnyAction } from 'redux' -import Types from '../actions/types' -import { Competition } from '../interfaces/Competition' - -interface EditorState { - competition: Competition -} - -const initialState: EditorState = { - competition: { - name: '', - id: 0, - year: 0, - city: { - id: 0, - name: '', - }, - slides: [], - }, -} - -export default function (state = initialState, action: AnyAction) { - switch (action.type) { - case Types.SET_EDITOR_COMPETITION: - return { - ...state, - competition: action.payload as Competition, - } - case Types.SET_COMPETITION_NAME: - return { - ...state, - competition: { - ...state.competition, - name: action.name, - } as Competition, - } - default: - return state - } -} -- GitLab From 2c587eefd7d4080f664d6778f20a24a7af8688da Mon Sep 17 00:00:00 2001 From: Emil <Emil> Date: Fri, 9 Apr 2021 14:04:16 +0200 Subject: [PATCH 05/21] feat: update competition name --- client/src/actions/competitions.ts | 15 ------ client/src/actions/editor.ts | 41 ++++++++++++++++ .../admin/components/CompetitionManager.tsx | 3 +- .../PresentationEditorPage.tsx | 14 ++++-- .../components/CompetitionSettings.tsx | 17 ++++--- client/src/reducers/allReducers.ts | 2 + client/src/reducers/competitionsReducer.ts | 12 ----- client/src/reducers/editorReducer.ts | 48 +++++++++++++++++++ 8 files changed, 112 insertions(+), 40 deletions(-) create mode 100644 client/src/actions/editor.ts create mode 100644 client/src/reducers/editorReducer.ts diff --git a/client/src/actions/competitions.ts b/client/src/actions/competitions.ts index 608060ae..a758fd63 100644 --- a/client/src/actions/competitions.ts +++ b/client/src/actions/competitions.ts @@ -37,18 +37,3 @@ export const getCompetitions = () => async (dispatch: AppDispatch, getState: () export const setFilterParams = (params: CompetitionFilterParams) => (dispatch: AppDispatch) => { dispatch({ type: Types.SET_COMPETITIONS_FILTER_PARAMS, payload: params }) } -export const setCompetitionName = (id: string, name: string) => async (dispatch: AppDispatch) => { - await axios - .get(`/competitions/search`) - .then((res) => { - dispatch({ - type: Types.SET_COMPETITION_NAME, - payload: res.data.items, - name: name, - id: +id - 1, - }) - }) - .catch((err) => { - console.log(err) - }) -} diff --git a/client/src/actions/editor.ts b/client/src/actions/editor.ts new file mode 100644 index 00000000..683e716a --- /dev/null +++ b/client/src/actions/editor.ts @@ -0,0 +1,41 @@ +import axios from 'axios' +import { AppDispatch } from './../store' +import Types from './types' + +export const getEditorCompetition = (id: string) => async (dispatch: AppDispatch) => { + await axios + .get(`/competitions/${id}`) + .then((res) => { + dispatch({ + type: Types.SET_EDITOR_COMPETITION, + payload: res.data, + }) + }) + .catch((err) => { + console.log(err) + }) +} + +export const setCompetitionName = (id: string, name: string) => async (dispatch: AppDispatch) => { + await axios + .get(`/competitions/${id}`) + .then((res) => { + dispatch({ + type: Types.SET_COMPETITION_NAME, + payload: res.data, + name: name, + }) + }) + .catch((err) => { + console.log(err) + }) +} + +export const submitCompetitionName = (id: string, name: string) => async (dispatch: AppDispatch) => { + await axios + .put(`/competitions/${id}`, { name: name }) + .then(() => { + dispatch(getEditorCompetition(id)) + }) + .catch(console.log) +} diff --git a/client/src/pages/admin/components/CompetitionManager.tsx b/client/src/pages/admin/components/CompetitionManager.tsx index c9a8b915..ff73a994 100644 --- a/client/src/pages/admin/components/CompetitionManager.tsx +++ b/client/src/pages/admin/components/CompetitionManager.tsx @@ -18,7 +18,6 @@ import { Link } from 'react-router-dom' import { getCities } from '../../../actions/cities' import { getCompetitions, setFilterParams } from '../../../actions/competitions' import { useAppDispatch, useAppSelector } from '../../../hooks' -import { Competition } from '../../../interfaces/Competition' import { CompetitionFilterParams } from '../../../interfaces/CompetitionFilterParams' import AddCompetition from './AddCompetition' import { FilterContainer, RemoveCompetition, TopBar, YearFilterTextField } from './styled' @@ -146,7 +145,7 @@ const CompetitionManager: React.FC = (props: any) => { </TableHead> <TableBody> {competitions && - competitions.map((row: Competition) => ( + competitions.map((row) => ( <TableRow key={row.name}> <TableCell scope="row"> <Button color="primary" component={Link} to={`/editor/competition-id=${row.id}`}> diff --git a/client/src/pages/presentationEditor/PresentationEditorPage.tsx b/client/src/pages/presentationEditor/PresentationEditorPage.tsx index 19caef8f..abcfb399 100644 --- a/client/src/pages/presentationEditor/PresentationEditorPage.tsx +++ b/client/src/pages/presentationEditor/PresentationEditorPage.tsx @@ -5,10 +5,10 @@ import Drawer from '@material-ui/core/Drawer' import List from '@material-ui/core/List' import ListItemText from '@material-ui/core/ListItemText' import { createStyles, makeStyles, Theme } from '@material-ui/core/styles' -import React from 'react' +import React, { useEffect } from 'react' import { useParams } from 'react-router-dom' -import { useAppSelector } from '../../hooks' -import { Slide } from '../../interfaces/Slide' +import { getEditorCompetition } from '../../actions/editor' +import { useAppDispatch, useAppSelector } from '../../hooks' import SettingsPanel from './components/SettingsPanel' import { SlideListItem, ToolBarContainer, ViewButton, ViewButtonGroup } from './styled' @@ -63,7 +63,11 @@ interface CompetitionParams { const PresentationEditorPage: React.FC = () => { const classes = useStyles() const { id }: CompetitionParams = useParams() - const competition = useAppSelector((state) => state.competitions.competitions[+id - 1]) + const dispatch = useAppDispatch() + const competition = useAppSelector((state) => state.editor.competition) + useEffect(() => { + dispatch(getEditorCompetition(id)) + }, []) return ( <div className={classes.root}> <CssBaseline /> @@ -96,7 +100,7 @@ const PresentationEditorPage: React.FC = () => { <div className={classes.toolbar} /> <Divider /> <List> - {competition.slides.map((slide: Slide) => ( + {competition.slides.map((slide) => ( <SlideListItem divider button key={slide.title}> <ListItemText primary={slide.title} /> </SlideListItem> diff --git a/client/src/pages/presentationEditor/components/CompetitionSettings.tsx b/client/src/pages/presentationEditor/components/CompetitionSettings.tsx index b668010b..04e1d74e 100644 --- a/client/src/pages/presentationEditor/components/CompetitionSettings.tsx +++ b/client/src/pages/presentationEditor/components/CompetitionSettings.tsx @@ -1,9 +1,9 @@ import { Button, Divider, List, ListItem, ListItemText, TextField } from '@material-ui/core' import { createStyles, makeStyles, Theme } from '@material-ui/core/styles' import CloseIcon from '@material-ui/icons/Close' -import React, { useState } from 'react' +import React, { useEffect, useState } from 'react' import { useParams } from 'react-router-dom' -import { setCompetitionName } from '../../../actions/competitions' +import { getEditorCompetition, setCompetitionName, submitCompetitionName } from '../../../actions/editor' import { useAppDispatch, useAppSelector } from '../../../hooks' const useStyles = makeStyles((theme: Theme) => @@ -46,8 +46,10 @@ const CompetitionSettings: React.FC = () => { const { id }: CompetitionParams = useParams() const dispatch = useAppDispatch() - const competition = useAppSelector((state) => state.competitions.competitions[+id - 1]) - const competitionName = competition.name + const competition = useAppSelector((state) => state.editor.competition) + useEffect(() => { + dispatch(getEditorCompetition(id)) + }, []) const updateCompetitionName = (event: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => { if (timerHandle) { @@ -56,7 +58,10 @@ const CompetitionSettings: React.FC = () => { } //Only updates filter and api 100ms after last input was made setTimerHandle(window.setTimeout(() => dispatch(setCompetitionName(id, event.target.value)), 100)) - dispatch(setCompetitionName(id, competitionName)) + dispatch(setCompetitionName(id, competition.name)) + + setTimerHandle(window.setTimeout(() => dispatch(submitCompetitionName(id, event.target.value)), 100)) + dispatch(submitCompetitionName(id, competition.name)) } const initialList = [ @@ -77,7 +82,7 @@ const CompetitionSettings: React.FC = () => { className={classes.textInput} id="outlined-basic" label={'Tävlingsnamn'} - defaultValue={competitionName} + defaultValue={competition.name} onChange={updateCompetitionName} variant="outlined" /> diff --git a/client/src/reducers/allReducers.ts b/client/src/reducers/allReducers.ts index b12b5346..732cff39 100644 --- a/client/src/reducers/allReducers.ts +++ b/client/src/reducers/allReducers.ts @@ -3,6 +3,7 @@ import { combineReducers } from 'redux' import citiesReducer from './citiesReducer' import competitionsReducer from './competitionsReducer' +import editorReducer from './editorReducer' import uiReducer from './uiReducer' import userReducer from './userReducer' @@ -12,5 +13,6 @@ const allReducers = combineReducers({ UI: uiReducer, competitions: competitionsReducer, cities: citiesReducer, + editor: editorReducer, }) export default allReducers diff --git a/client/src/reducers/competitionsReducer.ts b/client/src/reducers/competitionsReducer.ts index fabf4801..b3003d4a 100644 --- a/client/src/reducers/competitionsReducer.ts +++ b/client/src/reducers/competitionsReducer.ts @@ -39,18 +39,6 @@ export default function (state = initialState, action: AnyAction) { ...state, count: action.payload as number, } - case Types.SET_COMPETITION_NAME: - return { - ...state, - competitions: state.competitions.map((competition, i) => - i === action.id - ? ({ - ...competition, - name: action.name as string, - } as Competition) - : (competition as Competition) - ) as Competition[], - } default: return state } diff --git a/client/src/reducers/editorReducer.ts b/client/src/reducers/editorReducer.ts new file mode 100644 index 00000000..760e6007 --- /dev/null +++ b/client/src/reducers/editorReducer.ts @@ -0,0 +1,48 @@ +import { AnyAction } from 'redux' +import Types from '../actions/types' +import { Competition } from '../interfaces/Competition' + +interface EditorState { + competition: Competition +} + +const initialState: EditorState = { + competition: { + name: '', + id: 0, + year: 0, + city: { + id: 0, + name: '', + }, + slides: [], + }, +} + +export default function (state = initialState, action: AnyAction) { + switch (action.type) { + case Types.SET_EDITOR_COMPETITION: + return { + ...state, + competition: action.payload as Competition, + } + case Types.SET_COMPETITION_NAME: + return { + ...state, + competition: { + ...state.competition, + name: action.name, + } as Competition, + } + case Types.SET_COMPETITION_NAME: + return { + ...state, + competition: { + ...state.competition, + name: action.name, + } as Competition, + } + default: + return state + } +} -- GitLab From 64ea931c9becc29563730ed01765f1982e7ed1c9 Mon Sep 17 00:00:00 2001 From: Sebastian Karlsson <sebka991@student.liu.se> Date: Mon, 12 Apr 2021 16:26:41 +0200 Subject: [PATCH 06/21] Add database connection for slides and teams to editor, update api models. --- client/src/actions/editor.ts | 44 ++++++++++++++++++- client/src/interfaces/ApiModels.ts | 20 +++++++-- client/src/interfaces/ApiRichModels.ts | 14 +++--- .../components/CompetitionSettings.tsx | 29 +++++------- client/src/reducers/editorReducer.ts | 5 +-- client/src/reducers/presentationReducer.ts | 5 +-- 6 files changed, 80 insertions(+), 37 deletions(-) diff --git a/client/src/actions/editor.ts b/client/src/actions/editor.ts index cf00921a..dcbcdf23 100644 --- a/client/src/actions/editor.ts +++ b/client/src/actions/editor.ts @@ -8,7 +8,49 @@ export const getEditorCompetition = (id: string) => async (dispatch: AppDispatch .then((res) => { dispatch({ type: Types.SET_EDITOR_COMPETITION, - payload: res.data, + //res.data, + payload: { + name: 'Nåt hårdkodat', + id: 1, + year: 1337, + city_id: 1, + slides: [ + { + competition_id: 1, + id: 1, + order: 1, + timer: 10, + title: 'Hej1', + questions: [], + body: 'Kropp', + settings: 'Inställning', + }, + { + competition_id: 1, + id: 2, + order: 2, + timer: 15, + title: 'Hej2', + questions: [], + body: 'Kropp nr 2', + settings: 'Inställning 2', + }, + ], + teams: [ + { + id: 1, + name: 'Örkelljunga IK', + question_answers: [], + competition_id: 1, + }, + { + id: 2, + name: 'Vadstena OK', + question_answers: [], + competition_id: 1, + }, + ], + }, }) }) .catch((err) => { diff --git a/client/src/interfaces/ApiModels.ts b/client/src/interfaces/ApiModels.ts index 194fb2e7..149c2d16 100644 --- a/client/src/interfaces/ApiModels.ts +++ b/client/src/interfaces/ApiModels.ts @@ -2,10 +2,18 @@ interface NameID { id: number name: string } -export interface City extends NameID {} -export interface Role extends NameID {} -export interface MediaType extends NameID {} -export interface QuestionType extends NameID {} +export interface City extends NameID { + users: User[] + competitions: Competition[] +} + +export interface Role extends NameID { + users: User[] +} + +export interface QuestionType extends NameID { + questions: Question[] +} export interface Media { id: number @@ -14,6 +22,10 @@ export interface Media { user_id: number } +export interface MediaType extends NameID { + media: Media[] +} + export interface User extends NameID { email: string role_id: number diff --git a/client/src/interfaces/ApiRichModels.ts b/client/src/interfaces/ApiRichModels.ts index 3f7e0124..779dbd8a 100644 --- a/client/src/interfaces/ApiRichModels.ts +++ b/client/src/interfaces/ApiRichModels.ts @@ -1,10 +1,10 @@ -import { City, Component, Media, QuestionAnswer, QuestionType } from './ApiModels' +import { QuestionAlternative, QuestionAnswer } from './ApiModels' export interface RichCompetition { name: string id: number year: number - city: City + city_id: number slides: RichSlide[] teams: RichTeam[] } @@ -15,9 +15,9 @@ export interface RichSlide { timer: number title: string competition_id: number - question: RichQuestion[] - components: Component[] - medias: Media[] + questions: RichQuestion[] + body: string + settings: string } export interface RichTeam { @@ -33,5 +33,7 @@ export interface RichQuestion { name: string title: string total_score: number - question_type: QuestionType + type_id: number + question_answers: QuestionAnswer[] + alternatives: QuestionAlternative[] } diff --git a/client/src/pages/presentationEditor/components/CompetitionSettings.tsx b/client/src/pages/presentationEditor/components/CompetitionSettings.tsx index c60aba98..065d1151 100644 --- a/client/src/pages/presentationEditor/components/CompetitionSettings.tsx +++ b/client/src/pages/presentationEditor/components/CompetitionSettings.tsx @@ -13,7 +13,7 @@ import { import { createStyles, makeStyles, Theme } from '@material-ui/core/styles' import CloseIcon from '@material-ui/icons/Close' import axios from 'axios' -import React, { useEffect, useState } from 'react' +import React, { useEffect } from 'react' import { useParams } from 'react-router-dom' import { getEditorCompetition } from '../../../actions/editor' import { useAppDispatch, useAppSelector } from '../../../hooks' @@ -78,38 +78,31 @@ const CompetitionSettings: React.FC = () => { .catch(console.log) } - const initialList = [ - { id: '1', name: 'Lag1' }, - { id: '2', name: 'Lag2' }, - { id: '3', name: 'Lag3' }, - ] - const handleClick = (id: string) => { - setTeams(teams.filter((item) => item.id !== id)) //Will not be done like this when api is used + const handleClick = async (tid: number) => { + await axios + .delete(`/competitions/${id}/teams/${tid}`, {}) + .then(() => { + dispatch(getEditorCompetition(id)) + }) + .catch(console.log) } - const [teams, setTeams] = useState(initialList) const cities = useAppSelector((state) => state.cities.cities) const updateCompetitionCity = async (city: City) => { - console.log(city) await axios - .put(`/competitions/${id}`, { city: city }) + .put(`/competitions/${id}`, { city_id: city.id }) .then(() => { dispatch(getEditorCompetition(id)) }) .catch(console.log) } const handleChange = (event: React.ChangeEvent<{ value: unknown }>) => { - updateCitySelected(event) cities.forEach((city) => { if (event.target.value === city.name) { updateCompetitionCity(city) } }) } - const [citySelected, selectCity] = React.useState(competition.city.name) - const updateCitySelected = (event: React.ChangeEvent<{ value: unknown }>) => { - selectCity(event.target.value as string) - } return ( <div className={classes.textInputContainer}> @@ -125,7 +118,7 @@ const CompetitionSettings: React.FC = () => { <Divider /> <FormControl variant="outlined" className={classes.dropDown}> <InputLabel id="region-selection-label">Region</InputLabel> - <Select value={citySelected} label="RegionSelect" onChange={handleChange}> + <Select value={cities[competition.city_id - 1]} label="RegionSelect" onChange={handleChange}> {cities.map((city) => ( <MenuItem value={city.name} key={city.name}> <Button>{city.name}</Button> @@ -139,7 +132,7 @@ const CompetitionSettings: React.FC = () => { <ListItem> <ListItemText className={classes.textCenter} primary="Lag" /> </ListItem> - {teams.map((team) => ( + {competition.teams.map((team) => ( <div key={team.id}> <ListItem divider button> <ListItemText primary={team.name} /> diff --git a/client/src/reducers/editorReducer.ts b/client/src/reducers/editorReducer.ts index 08fc2ae0..606f5d69 100644 --- a/client/src/reducers/editorReducer.ts +++ b/client/src/reducers/editorReducer.ts @@ -11,10 +11,7 @@ const initialState: EditorState = { name: '', id: 0, year: 0, - city: { - id: 0, - name: '', - }, + city_id: 1, slides: [], teams: [], }, diff --git a/client/src/reducers/presentationReducer.ts b/client/src/reducers/presentationReducer.ts index d71bcafb..b4325680 100644 --- a/client/src/reducers/presentationReducer.ts +++ b/client/src/reducers/presentationReducer.ts @@ -14,10 +14,7 @@ const initialState: PresentationState = { competition: { name: '', id: 0, - city: { - id: 0, - name: '', - }, + city_id: 0, slides: [], year: 0, teams: [], -- GitLab From 4e509e76d892dcd213f5ff5314176a036afd17d6 Mon Sep 17 00:00:00 2001 From: Emil <Emil> Date: Mon, 12 Apr 2021 22:01:56 +0200 Subject: [PATCH 07/21] =?UTF-8?q?Skapat=20fullst=C3=A4ndig,=20h=C3=A5rdkod?= =?UTF-8?q?ad=20t=C3=A4vling.=20Visar=20data=20fr=C3=A5n=20denna=20i=20edi?= =?UTF-8?q?torn?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/actions/editor.ts | 132 ++++++++++++++++-- client/src/interfaces/ApiModels.ts | 2 +- .../components/CompetitionSettings.tsx | 9 +- .../components/SlideSettings.tsx | 99 ++++++++++--- 4 files changed, 205 insertions(+), 37 deletions(-) diff --git a/client/src/actions/editor.ts b/client/src/actions/editor.ts index dcbcdf23..d5e6fede 100644 --- a/client/src/actions/editor.ts +++ b/client/src/actions/editor.ts @@ -10,7 +10,7 @@ export const getEditorCompetition = (id: string) => async (dispatch: AppDispatch type: Types.SET_EDITOR_COMPETITION, //res.data, payload: { - name: 'Nåt hårdkodat', + name: 'Tävling 1 (Hårdkodad)', id: 1, year: 1337, city_id: 1, @@ -20,33 +20,143 @@ export const getEditorCompetition = (id: string) => async (dispatch: AppDispatch id: 1, order: 1, timer: 10, - title: 'Hej1', - questions: [], - body: 'Kropp', - settings: 'Inställning', + title: 'Sida 1', + questions: [ + { + id: 1, + slide_id: 1, + name: 'Fråga 1 namn', + title: 'Fråga 1 titel', + total_score: 5, + type_id: 3, + question_answers: [ + { + id: 1, + question_id: 1, + team_id: 1, + data: 'question answer data 1', + score: 1, + }, + { + id: 2, + question_id: 1, + team_id: 2, + data: 'question answer data 2', + score: 3, + }, + ], + alternatives: [ + { + id: 1, + text: '1', + value: true, + question_id: 1, + }, + { + id: 2, + text: '0', + value: false, + question_id: 1, + }, + ], + }, + ], + body: 'Slide body 1', + settings: 'Slide settings 1', }, + { competition_id: 1, id: 2, order: 2, timer: 15, - title: 'Hej2', - questions: [], - body: 'Kropp nr 2', - settings: 'Inställning 2', + title: 'Sida 2', + questions: [ + { + id: 2, + slide_id: 2, + name: 'Fråga 2 namn', + title: 'Fråga 2 titel', + total_score: 6, + type_id: 3, + question_answers: [ + { + id: 3, + question_id: 2, + team_id: 1, + data: 'question answer data 1', + score: 1, + }, + { + id: 4, + question_id: 2, + team_id: 2, + data: 'question answer data 2', + score: 4, + }, + ], + alternatives: [ + { + id: 1, + text: '5', + value: true, + question_id: 2, + }, + { + id: 2, + text: 'abc', + value: false, + question_id: 2, + }, + ], + }, + ], + body: 'Slide body 2', + settings: 'Slide settings 2', }, ], + teams: [ { id: 1, name: 'Örkelljunga IK', - question_answers: [], + question_answers: [ + { + id: 1, + question_id: 1, + team_id: 1, + data: 'question answer data 1', + score: 1, + }, + { + id: 3, + question_id: 2, + team_id: 1, + data: 'question answer data 1', + score: 1, + }, + ], competition_id: 1, }, { id: 2, name: 'Vadstena OK', - question_answers: [], + question_answers: [ + { + id: 2, + question_id: 1, + team_id: 2, + data: 'question answer data 2', + score: 3, + }, + { + id: 4, + question_id: 2, + team_id: 2, + data: 'question answer data 2', + score: 4, + }, + ], competition_id: 1, }, ], diff --git a/client/src/interfaces/ApiModels.ts b/client/src/interfaces/ApiModels.ts index 149c2d16..a154a0c8 100644 --- a/client/src/interfaces/ApiModels.ts +++ b/client/src/interfaces/ApiModels.ts @@ -57,7 +57,7 @@ export interface QuestionAlternative { export interface QuestionAnswer { id: number question_id: number - team_id: string + team_id: number data: string score: number } diff --git a/client/src/pages/presentationEditor/components/CompetitionSettings.tsx b/client/src/pages/presentationEditor/components/CompetitionSettings.tsx index 065d1151..e8b0a4e0 100644 --- a/client/src/pages/presentationEditor/components/CompetitionSettings.tsx +++ b/client/src/pages/presentationEditor/components/CompetitionSettings.tsx @@ -15,6 +15,7 @@ import CloseIcon from '@material-ui/icons/Close' import axios from 'axios' import React, { useEffect } from 'react' import { useParams } from 'react-router-dom' +import { getCities } from '../../../actions/cities' import { getEditorCompetition } from '../../../actions/editor' import { useAppDispatch, useAppSelector } from '../../../hooks' import { City } from '../../../interfaces/ApiModels' @@ -63,10 +64,10 @@ const CompetitionSettings: React.FC = () => { const classes = useStyles() const { id }: CompetitionParams = useParams() const dispatch = useAppDispatch() - const competition = useAppSelector((state) => state.editor.competition) useEffect(() => { dispatch(getEditorCompetition(id)) + dispatch(getCities()) }, []) const updateCompetitionName = async (event: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => { @@ -80,7 +81,7 @@ const CompetitionSettings: React.FC = () => { const handleClick = async (tid: number) => { await axios - .delete(`/competitions/${id}/teams/${tid}`, {}) + .delete(`/competitions/${id}/teams/${tid}`) .then(() => { dispatch(getEditorCompetition(id)) }) @@ -96,6 +97,7 @@ const CompetitionSettings: React.FC = () => { }) .catch(console.log) } + const handleChange = (event: React.ChangeEvent<{ value: unknown }>) => { cities.forEach((city) => { if (event.target.value === city.name) { @@ -118,7 +120,8 @@ const CompetitionSettings: React.FC = () => { <Divider /> <FormControl variant="outlined" className={classes.dropDown}> <InputLabel id="region-selection-label">Region</InputLabel> - <Select value={cities[competition.city_id - 1]} label="RegionSelect" onChange={handleChange}> + {/*TODO: fixa så cities laddar in i statet likt i CompetitionManager*/} + <Select value={cities[competition.city_id - 1].name} label="RegionSelect" onChange={handleChange}> {cities.map((city) => ( <MenuItem value={city.name} key={city.name}> <Button>{city.name}</Button> diff --git a/client/src/pages/presentationEditor/components/SlideSettings.tsx b/client/src/pages/presentationEditor/components/SlideSettings.tsx index 936f3d8b..5023cfcb 100644 --- a/client/src/pages/presentationEditor/components/SlideSettings.tsx +++ b/client/src/pages/presentationEditor/components/SlideSettings.tsx @@ -10,10 +10,16 @@ import { Select, TextField, } from '@material-ui/core' -import { createStyles, makeStyles, Theme } from '@material-ui/core/styles' +import { CheckboxProps } from '@material-ui/core/Checkbox' +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 MoreHorizOutlinedIcon from '@material-ui/icons/MoreHorizOutlined' +import axios from 'axios' import React, { useState } from 'react' +import { useParams } from 'react-router-dom' +import { getEditorCompetition } from '../../../actions/editor' +import { useAppDispatch, useAppSelector } from '../../../hooks' const useStyles = makeStyles((theme: Theme) => createStyles({ @@ -58,22 +64,33 @@ const useStyles = makeStyles((theme: Theme) => }) ) +interface CompetitionParams { + id: string +} + const SlideSettings: React.FC = () => { const classes = useStyles() - - const [slideTypeSelected, selectSlideType] = React.useState('') - const handleChange = (event: React.ChangeEvent<{ value: unknown }>) => { - selectSlideType(event.target.value as string) + const { id }: CompetitionParams = useParams() + const dispatch = useAppDispatch() + const competition = useAppSelector((state) => state.editor.competition) + let currentSlide = competition.slides[0] + // Init currentSlide if slides are not in order + for (const slide of competition.slides) { + if (slide.order === 1) { + currentSlide = slide + break + } } - const answerList = [ - { id: 'answer1', name: 'Svar 1' }, - { id: 'answer2', name: 'Svar 2' }, - ] - const handleCloseAnswerClick = (id: string) => { - setAnswers(answers.filter((item) => item.id !== id)) //Will not be done like this when api is used + const handleCloseAnswerClick = async (alternative: number) => { + await axios + // TODO: implementera API för att kunnata bort svarsalternativ + .delete(`/competitions/${id}/slide/question/alternative/${alternative}`) + .then(() => { + dispatch(getEditorCompetition(id)) + }) + .catch(console.log) } - const [answers, setAnswers] = useState(answerList) const textList = [ { id: 'text1', name: 'Text 1' }, @@ -93,22 +110,53 @@ const SlideSettings: React.FC = () => { } const [pictures, setPictures] = useState(pictureList) + const updateSlideType = async (event: React.ChangeEvent<{ value: unknown }>) => { + await axios + // TODO: implementera API för att kunna ändra i questions->type_id + .put(`/competitions/${id}/slides/${currentSlide.id}`, { type_id: event.target.value }) + .then(() => { + dispatch(getEditorCompetition(id)) + }) + .catch(console.log) + } + + const updateAlternativeValue = async (event: React.ChangeEvent<HTMLInputElement>) => { + // Wheter the alternative is true or false + await axios + // TODO: implementera API för att kunna ändra i alternatives->value + .put(`/competitions/${id}/slides/${currentSlide.id}`, { value: event.target.value }) + .then(() => { + dispatch(getEditorCompetition(id)) + }) + .catch(console.log) + } + + const GreenCheckbox = withStyles({ + root: { + color: grey[900], + '&$checked': { + color: green[600], + }, + }, + checked: {}, + })((props: CheckboxProps) => <Checkbox color="default" {...props} />) + return ( <div className={classes.textInputContainer}> <div className={classes.whiteBackground}> <FormControl variant="outlined" className={classes.dropDown}> <InputLabel id="slide-type-selection-label">Sidtyp</InputLabel> - <Select value={slideTypeSelected} label="Sidtyp" defaultValue="informationSlide" onChange={handleChange}> - <MenuItem value="informationSlide"> + <Select value={currentSlide.questions[0].type_id} label="Sidtyp" onChange={updateSlideType}> + <MenuItem value={0}> <Button>Informationssida</Button> </MenuItem> - <MenuItem value="textQuestion"> + <MenuItem value={1}> <Button>Skriftlig fråga</Button> </MenuItem> - <MenuItem value="practicalQuestion"> + <MenuItem value={2}> <Button>Praktisk fråga</Button> </MenuItem> - <MenuItem value="multipleChoiceQuestion"> + <MenuItem value={3}> <Button>Flervalsfråga</Button> </MenuItem> </Select> @@ -123,6 +171,7 @@ const SlideSettings: React.FC = () => { helperText="Lämna blank för att inte använda timerfunktionen" label="Timer" type="number" + value={currentSlide.timer} /> </ListItem> @@ -134,12 +183,18 @@ const SlideSettings: React.FC = () => { secondary="(Fyll i rutan höger om textfältet för att markera korrekt svar)" /> </ListItem> - {answers.map((answer) => ( - <div key={answer.id}> + {currentSlide.questions[0].alternatives.map((alt) => ( + <div key={alt.id}> <ListItem divider> - <TextField className={classes.textInput} id="outlined-basic" label={answer.name} variant="outlined" /> - <Checkbox color="default" /> - <CloseIcon className={classes.clickableIcon} onClick={() => handleCloseAnswerClick(answer.id)} /> + <TextField + className={classes.textInput} + id="outlined-basic" + label={`Svar ${alt.id}`} + value={alt.text} + variant="outlined" + /> + <GreenCheckbox checked={alt.value} onChange={updateAlternativeValue} /> + <CloseIcon className={classes.clickableIcon} onClick={() => handleCloseAnswerClick(alt.id)} /> </ListItem> </div> ))} -- GitLab From a0e6de289863c430f8c083b149e2ecb3626e870a Mon Sep 17 00:00:00 2001 From: Albin Henriksson <albhe428@student.liu.se> Date: Tue, 13 Apr 2021 09:03:36 +0200 Subject: [PATCH 08/21] Add image input component --- .../presentationEditor/components/SlideSettings.tsx | 9 +++++++-- .../src/pages/presentationEditor/components/styled.tsx | 4 ++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/client/src/pages/presentationEditor/components/SlideSettings.tsx b/client/src/pages/presentationEditor/components/SlideSettings.tsx index 4b094021..f07105c8 100644 --- a/client/src/pages/presentationEditor/components/SlideSettings.tsx +++ b/client/src/pages/presentationEditor/components/SlideSettings.tsx @@ -8,12 +8,13 @@ import { ListItemText, MenuItem, Select, - TextField, + TextField } from '@material-ui/core' import { createStyles, makeStyles, Theme } from '@material-ui/core/styles' import CloseIcon from '@material-ui/icons/Close' import MoreHorizOutlinedIcon from '@material-ui/icons/MoreHorizOutlined' import React, { useState } from 'react' +import { HiddenInput } from './styled' const useStyles = makeStyles((theme: Theme) => createStyles({ @@ -184,7 +185,11 @@ const SlideSettings: React.FC = () => { </div> ))} <ListItem className={classes.center} button> - <Button>Lägg till bild</Button> + <HiddenInput accept="image/*" id="contained-button-file" multiple type="file" onChange={console.log} /> + + <label htmlFor="contained-button-file"> + <Button component="span">Lägg till bild</Button> + </label> </ListItem> </List> diff --git a/client/src/pages/presentationEditor/components/styled.tsx b/client/src/pages/presentationEditor/components/styled.tsx index 62239c5b..c4584f87 100644 --- a/client/src/pages/presentationEditor/components/styled.tsx +++ b/client/src/pages/presentationEditor/components/styled.tsx @@ -13,3 +13,7 @@ export const SlideEditorContainer = styled.div` width: 100%; height: 100%; ` + +export const HiddenInput = styled.input` + display: none; +` -- GitLab From 2a957ce2a253691f96e231fe65ecac2cd1b45deb Mon Sep 17 00:00:00 2001 From: Albin Henriksson <albhe428@student.liu.se> Date: Tue, 13 Apr 2021 09:43:14 +0200 Subject: [PATCH 09/21] Add image selector --- .../components/SlideSettings.tsx | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/client/src/pages/presentationEditor/components/SlideSettings.tsx b/client/src/pages/presentationEditor/components/SlideSettings.tsx index 4b225676..d5ab4912 100644 --- a/client/src/pages/presentationEditor/components/SlideSettings.tsx +++ b/client/src/pages/presentationEditor/components/SlideSettings.tsx @@ -132,6 +132,28 @@ const SlideSettings: React.FC = () => { .catch(console.log) } + const handleFileSelected = (e: React.ChangeEvent<HTMLInputElement>): void => { + 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 handleAddText = async () => { + console.log('Add text component') + // TODO: post the new text] + setTexts([...texts, { id: 'newText', name: 'New Text' }]) + } + const GreenCheckbox = withStyles({ root: { color: grey[900], @@ -217,7 +239,7 @@ const SlideSettings: React.FC = () => { </ListItem> </div> ))} - <ListItem className={classes.center} button> + <ListItem className={classes.center} button onClick={handleAddText}> <Button>Lägg till text</Button> </ListItem> </List> @@ -240,7 +262,7 @@ const SlideSettings: React.FC = () => { </div> ))} <ListItem className={classes.center} button> - <HiddenInput accept="image/*" id="contained-button-file" multiple type="file" onChange={console.log} /> + <HiddenInput accept="image/*" id="contained-button-file" multiple type="file" onChange={handleFileSelected} /> <label htmlFor="contained-button-file"> <Button component="span">Lägg till bild</Button> -- GitLab From 393a76ebc20ea9aac1f77bb68fef50d5ecfe235f Mon Sep 17 00:00:00 2001 From: robban64 <carl@schonfelder.se> Date: Wed, 14 Apr 2021 15:02:55 +0200 Subject: [PATCH 10/21] add: component --- server/app/apis/__init__.py | 3 ++ server/app/apis/components.py | 30 +++++++++++ server/app/core/dto.py | 6 +++ server/app/core/schemas.py | 14 ++++++ server/app/database/controller/add.py | 8 +++ server/app/database/controller/get.py | 7 ++- server/app/database/models.py | 72 +++++++++++++++++++++++++++ 7 files changed, 138 insertions(+), 2 deletions(-) diff --git a/server/app/apis/__init__.py b/server/app/apis/__init__.py index 3b2ed213..9e3bd034 100644 --- a/server/app/apis/__init__.py +++ b/server/app/apis/__init__.py @@ -44,6 +44,7 @@ from flask_restx import Api from .auth import api as auth_ns from .competitions import api as comp_ns +from .components import api as component_ns from .media import api as media_ns from .misc import api as misc_ns from .questions import api as question_ns @@ -58,6 +59,8 @@ flask_api.add_namespace(user_ns, path="/api/users") flask_api.add_namespace(auth_ns, path="/api/auth") flask_api.add_namespace(comp_ns, path="/api/competitions") flask_api.add_namespace(slide_ns, path="/api/competitions/<CID>/slides") +flask_api.add_namespace(component_ns, path="/api/competitions/<CID>/slides/<SID>/components/") + flask_api.add_namespace(team_ns, path="/api/competitions/<CID>/teams") flask_api.add_namespace(question_ns, path="/api/competitions/<CID>/questions") # flask_api.add_namespace(question_ns, path="/api/competitions/<CID>/slides/<SID>/question") diff --git a/server/app/apis/components.py b/server/app/apis/components.py index e69de29b..360463dc 100644 --- a/server/app/apis/components.py +++ b/server/app/apis/components.py @@ -0,0 +1,30 @@ +import app.database.controller as dbc +from app.apis import admin_required, item_response, list_response +from app.core.dto import ComponentDTO +from app.core.parsers import competition_parser, competition_search_parser +from app.database.models import Competition +from flask.globals import request +from flask_jwt_extended import jwt_required +from flask_restx import Resource + +api = ComponentDTO.api +schema = ComponentDTO.schema +list_schema = ComponentDTO.list_schema + + +@api.route("/<component_id>") +@api.param("CID, SID, component_id") +class ComponentByID(Resource): + @jwt_required + def get(self, CID, SID, component_id): + item = dbc.get.component(component_id) + return item_response(schema.dump(item)) + + +@api.route("/") +@api.param("CID, SID") +class ComponentList(Resource): + @jwt_required + def post(self, CID, SID): + item = dbc.add.component(**request.args) + return item_response(schema.dump(item)) diff --git a/server/app/core/dto.py b/server/app/core/dto.py index 99d467ba..3e6b0cda 100644 --- a/server/app/core/dto.py +++ b/server/app/core/dto.py @@ -5,6 +5,12 @@ from flask_restx import Namespace, fields from flask_uploads import IMAGES, UploadSet +class ComponentDTO: + api = Namespace("component") + schema = schemas.ComponentSchema(many=False) + list_schema = schemas.ComponentSchema(many=True) + + class MediaDTO: api = Namespace("media") image_set = UploadSet("photos", IMAGES) diff --git a/server/app/core/schemas.py b/server/app/core/schemas.py index 27440642..fca727b2 100644 --- a/server/app/core/schemas.py +++ b/server/app/core/schemas.py @@ -113,3 +113,17 @@ class CompetitionSchema(BaseSchema): name = ma.auto_field() year = ma.auto_field() city_id = ma.auto_field() + + +class ComponentSchema(BaseSchema): + class Meta(BaseSchema.Meta): + model = models.Component + + id = ma.auto_field() + x = ma.auto_field() + y = ma.auto_field() + w = ma.auto_field() + h = ma.auto_field() + slide_id = ma.auto_field() + text = ma.auto_field() + image_id = ma.auto_field() diff --git a/server/app/database/controller/add.py b/server/app/database/controller/add.py index f612cdd6..8d17cdb6 100644 --- a/server/app/database/controller/add.py +++ b/server/app/database/controller/add.py @@ -4,6 +4,8 @@ from app.database.models import ( Blacklist, City, Competition, + Component, + ImageComponent, Media, MediaType, Question, @@ -11,6 +13,7 @@ from app.database.models import ( Role, Slide, Team, + TextComponent, User, ) from flask_restx import abort @@ -31,6 +34,11 @@ def db_add(func): return wrapper +@db_add +def component(x, y, w, h, data, type_id, item_slide): + return Component(x, y, w, h, data, item_slide.id, type_id) + + @db_add def blacklist(jti): return Blacklist(jti) diff --git a/server/app/database/controller/get.py b/server/app/database/controller/get.py index a2b45fed..02e6df6a 100644 --- a/server/app/database/controller/get.py +++ b/server/app/database/controller/get.py @@ -1,11 +1,14 @@ -from app.database.models import Competition, Question, Slide, Team, User -from sqlalchemy.sql.expression import outerjoin +from app.database.models import Competition, Component, ImageComponent, Question, Slide, Team, TextComponent, User def user_exists(email): return User.query.filter(User.email == email).count() > 0 +def component(ID): + return Component.query.filter(Component.id == ID) + + def competition(CID, required=True, error_msg=None): return Competition.query.filter(Competition.id == CID).first_extended(required, error_msg) diff --git a/server/app/database/models.py b/server/app/database/models.py index 5a17fb0d..cc492222 100644 --- a/server/app/database/models.py +++ b/server/app/database/models.py @@ -130,6 +130,8 @@ class Slide(db.Model): background_image_id = db.Column(db.Integer, db.ForeignKey("media.id"), nullable=True) background_image = db.relationship("Media", uselist=False) + components = db.relationship("Component", backref="slide") + def __init__(self, order, competition_id): self.order = order self.competition_id = competition_id @@ -181,6 +183,76 @@ class QuestionAnswer(db.Model): self.team_id = team_id +class Component(db.Model): + # __mapper_args__ = {"polymorphic_on": type, "polymorphic_identity": "component"} + id = db.Column(db.Integer, primary_key=True) + x = db.Column(db.Integer, nullable=False, default=0) + y = db.Column(db.Integer, nullable=False, default=0) + w = db.Column(db.Integer, nullable=False, default=1) + h = db.Column(db.Integer, nullable=False, default=1) + data = db.Column(db.Text) + type_id = db.Column(db.Integer, db.ForeignKey("component_type.id"), nullable=False) + + slide_id = db.Column(db.Integer, db.ForeignKey("slide.id"), nullable=False) + + def __init__(self, x, y, w, h, data, slide_id, type_id): + self.x = x + self.y = y + self.w = w + self.h = h + self.data = data + self.slide_id = slide_id + self.type_id = type_id + + +""" +class ImageComponent(Component): + __mapper_args__ = {"polymorphic_identity": "image_component"} + + id = db.Column(db.Integer, db.ForeignKey("component.id"), primary_key=True) + # id = db.Column(db.Integer, primary_key=True) + image_id = db.Column(db.Integer, db.ForeignKey("media.id"), nullable=False) + image = db.relationship("Media", uselist=False) + + def __init__(self, id, image_id, x, y, w, h, slide_id, type): + super.__init__(x, y, w, h, slide_id, type) + self.id = id + self.image_id = image_id + + +class TextComponent(Component): + __mapper_args__ = {"polymorphic_identity": "text_component"} + id = db.Column(db.Integer, db.ForeignKey("component.id"), primary_key=True) + text = db.Column(db.Text, default="", nullable=False) + + def __init__(self, id, text, x, y, w, h, slide_id, type): + super.__init__(x, y, w, h, slide_id, type) + self.id = id + self.text = text +""" + + +class Code(db.Model): + table_args = (db.UniqueConstraint("pointer", "type"),) + id = db.Column(db.Integer, primary_key=True) + code = db.Column(db.Text, unique=True) + pointer = db.Column(db.Integer, nullable=False) + + view_type_id = db.Column(db.Integer, db.ForeignKey("view_type.id"), nullable=False) + + +class ViewType(db.Model): + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.Text) + codes = db.relationship("Code", backref="view_type") + + +class ComponentType(db.Model): + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.Text) + components = db.relationship("Component", backref="component_type") + + class MediaType(db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(STRING_SIZE), unique=True) -- GitLab From 039cfb63743e587f2ed68fd716814a813be64a1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20L=C3=B6fgren?= <viclo211@student.liu.se> Date: Wed, 14 Apr 2021 15:41:21 +0200 Subject: [PATCH 11/21] Add get component list, get by id and add api --- server/app/apis/components.py | 11 +++++++++-- server/app/core/parsers.py | 10 ++++++++++ server/app/core/schemas.py | 4 ++-- server/app/database/controller/add.py | 2 -- server/app/database/controller/get.py | 11 ++++++++--- server/app/database/models.py | 5 ++--- 6 files changed, 31 insertions(+), 12 deletions(-) diff --git a/server/app/apis/components.py b/server/app/apis/components.py index 360463dc..73e2a907 100644 --- a/server/app/apis/components.py +++ b/server/app/apis/components.py @@ -1,7 +1,7 @@ import app.database.controller as dbc from app.apis import admin_required, item_response, list_response from app.core.dto import ComponentDTO -from app.core.parsers import competition_parser, competition_search_parser +from app.core.parsers import component_parser from app.database.models import Competition from flask.globals import request from flask_jwt_extended import jwt_required @@ -24,7 +24,14 @@ class ComponentByID(Resource): @api.route("/") @api.param("CID, SID") class ComponentList(Resource): + @jwt_required + def get(self, CID, SID): + items = dbc.get.component_list(SID) + return list_response(list_schema.dump(items)) + @jwt_required def post(self, CID, SID): - item = dbc.add.component(**request.args) + args = component_parser.parse_args() + item_slide = dbc.get.slide(CID, SID) + item = dbc.add.component(item_slide=item_slide, **args) return item_response(schema.dump(item)) diff --git a/server/app/core/parsers.py b/server/app/core/parsers.py index 7a1c6089..a8136c9c 100644 --- a/server/app/core/parsers.py +++ b/server/app/core/parsers.py @@ -68,3 +68,13 @@ team_parser.add_argument("name", type=str, location="json") ###SEARCH_COMPETITION#### media_parser_search = search_parser.copy() media_parser_search.add_argument("filename", type=str, default=None, location="args") + + +###COMPONENT### +component_parser = reqparse.RequestParser() +component_parser.add_argument("x", type=str, default=None, location="json") +component_parser.add_argument("y", type=int, default=None, location="json") +component_parser.add_argument("w", type=int, default=None, location="json") +component_parser.add_argument("h", type=int, default=None, location="json") +component_parser.add_argument("data", type=dict, default=None, location="json") +component_parser.add_argument("type_id", type=int, default=None, location="json") diff --git a/server/app/core/schemas.py b/server/app/core/schemas.py index fca727b2..2cd14a31 100644 --- a/server/app/core/schemas.py +++ b/server/app/core/schemas.py @@ -124,6 +124,6 @@ class ComponentSchema(BaseSchema): y = ma.auto_field() w = ma.auto_field() h = ma.auto_field() + data = ma.auto_field() # TODO: Convert this to dict, or save as dict to begin with slide_id = ma.auto_field() - text = ma.auto_field() - image_id = ma.auto_field() + type_id = ma.auto_field() diff --git a/server/app/database/controller/add.py b/server/app/database/controller/add.py index 8d17cdb6..162bbd8c 100644 --- a/server/app/database/controller/add.py +++ b/server/app/database/controller/add.py @@ -5,7 +5,6 @@ from app.database.models import ( City, Competition, Component, - ImageComponent, Media, MediaType, Question, @@ -13,7 +12,6 @@ from app.database.models import ( Role, Slide, Team, - TextComponent, User, ) from flask_restx import abort diff --git a/server/app/database/controller/get.py b/server/app/database/controller/get.py index 02e6df6a..fa6914b2 100644 --- a/server/app/database/controller/get.py +++ b/server/app/database/controller/get.py @@ -1,12 +1,12 @@ -from app.database.models import Competition, Component, ImageComponent, Question, Slide, Team, TextComponent, User +from app.database.models import Competition, Component, Question, Slide, Team, User def user_exists(email): return User.query.filter(User.email == email).count() > 0 -def component(ID): - return Component.query.filter(Component.id == ID) +def component(ID, required=True, error_msg=None): + return Component.query.filter(Component.id == ID).first_extended(required, error_msg) def competition(CID, required=True, error_msg=None): @@ -55,5 +55,10 @@ def slide_list(CID): return Slide.query.filter(Slide.competition_id == CID).all() +def component_list(SID): + # TODO: Maybe take CID as argument and make sure that SID is in that competition? + return Component.query.filter(Component.slide_id == SID).all() + + def slide_count(CID): return Slide.query.filter(Slide.competition_id == CID).count() diff --git a/server/app/database/models.py b/server/app/database/models.py index cc492222..ec29860f 100644 --- a/server/app/database/models.py +++ b/server/app/database/models.py @@ -190,9 +190,8 @@ class Component(db.Model): y = db.Column(db.Integer, nullable=False, default=0) w = db.Column(db.Integer, nullable=False, default=1) h = db.Column(db.Integer, nullable=False, default=1) - data = db.Column(db.Text) + data = db.Column(db.Text) # TODO: Don't save this as text type_id = db.Column(db.Integer, db.ForeignKey("component_type.id"), nullable=False) - slide_id = db.Column(db.Integer, db.ForeignKey("slide.id"), nullable=False) def __init__(self, x, y, w, h, data, slide_id, type_id): @@ -200,7 +199,7 @@ class Component(db.Model): self.y = y self.w = w self.h = h - self.data = data + self.data = str(data) self.slide_id = slide_id self.type_id = type_id -- GitLab From 819d443d05a7ee10c6b026fe894c15d7dac4c47f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20L=C3=B6fgren?= <viclo211@student.liu.se> Date: Wed, 14 Apr 2021 16:28:42 +0200 Subject: [PATCH 12/21] Make data dict type --- server/app/core/schemas.py | 2 +- server/app/database/controller/add.py | 2 +- server/app/database/models.py | 25 ++++++++++++++++++++++--- 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/server/app/core/schemas.py b/server/app/core/schemas.py index 2cd14a31..058efed7 100644 --- a/server/app/core/schemas.py +++ b/server/app/core/schemas.py @@ -124,6 +124,6 @@ class ComponentSchema(BaseSchema): y = ma.auto_field() w = ma.auto_field() h = ma.auto_field() - data = ma.auto_field() # TODO: Convert this to dict, or save as dict to begin with + data = ma.auto_field() # TODO: Convert this to dict slide_id = ma.auto_field() type_id = ma.auto_field() diff --git a/server/app/database/controller/add.py b/server/app/database/controller/add.py index 162bbd8c..9e2a8e11 100644 --- a/server/app/database/controller/add.py +++ b/server/app/database/controller/add.py @@ -33,7 +33,7 @@ def db_add(func): @db_add -def component(x, y, w, h, data, type_id, item_slide): +def component(x, y, w, h, data, item_slide, type_id): return Component(x, y, w, h, data, item_slide.id, type_id) diff --git a/server/app/database/models.py b/server/app/database/models.py index ec29860f..29cbe3b5 100644 --- a/server/app/database/models.py +++ b/server/app/database/models.py @@ -1,6 +1,9 @@ +import json + from app.core import bcrypt, db from sqlalchemy.ext.hybrid import hybrid_method, hybrid_property from sqlalchemy.orm import backref +from sqlalchemy.types import TypeDecorator STRING_SIZE = 254 @@ -183,6 +186,22 @@ class QuestionAnswer(db.Model): self.team_id = team_id +class Dictionary(TypeDecorator): + + impl = db.Text(1024) + + def process_bind_param(self, value, dialect): + if value is not None: + value = json.dumps(value).replace("'", '"') + + return value + + def process_result_value(self, value, dialect): + if value is not None: + value = json.loads(value) + return value + + class Component(db.Model): # __mapper_args__ = {"polymorphic_on": type, "polymorphic_identity": "component"} id = db.Column(db.Integer, primary_key=True) @@ -190,16 +209,16 @@ class Component(db.Model): y = db.Column(db.Integer, nullable=False, default=0) w = db.Column(db.Integer, nullable=False, default=1) h = db.Column(db.Integer, nullable=False, default=1) - data = db.Column(db.Text) # TODO: Don't save this as text - type_id = db.Column(db.Integer, db.ForeignKey("component_type.id"), nullable=False) + data = db.Column(Dictionary()) slide_id = db.Column(db.Integer, db.ForeignKey("slide.id"), nullable=False) + type_id = db.Column(db.Integer, db.ForeignKey("component_type.id"), nullable=False) def __init__(self, x, y, w, h, data, slide_id, type_id): self.x = x self.y = y self.w = w self.h = h - self.data = str(data) + self.data = data self.slide_id = slide_id self.type_id = type_id -- GitLab From 8db228bf8fe8e3bbd5c5adcc00add9e8a611b268 Mon Sep 17 00:00:00 2001 From: robban64 <carl@schonfelder.se> Date: Fri, 16 Apr 2021 19:23:59 +0200 Subject: [PATCH 13/21] fix: component and you now get all types by get misc/types --- server/app/apis/components.py | 17 +++++++- server/app/apis/misc.py | 39 +++++++++-------- server/app/core/dto.py | 3 ++ server/app/core/parsers.py | 5 ++- server/app/core/rich_schemas.py | 1 + server/app/core/schemas.py | 34 +++++++++------ server/app/database/controller/add.py | 16 ++++++- server/app/database/controller/delete.py | 4 ++ server/app/database/controller/edit.py | 17 ++++++++ server/app/database/controller/get.py | 23 +++++++++- server/app/database/models.py | 45 +++++++------------- server/configmodule.py | 3 +- server/populate.py | 25 +++++++---- server/tests/test_app.py | 53 ++++++++++-------------- server/tests/test_helpers.py | 30 ++++++++------ 15 files changed, 194 insertions(+), 121 deletions(-) diff --git a/server/app/apis/components.py b/server/app/apis/components.py index 73e2a907..da211895 100644 --- a/server/app/apis/components.py +++ b/server/app/apis/components.py @@ -1,7 +1,8 @@ +import app.core.http_codes as codes import app.database.controller as dbc from app.apis import admin_required, item_response, list_response from app.core.dto import ComponentDTO -from app.core.parsers import component_parser +from app.core.parsers import component_create_parser, component_parser from app.database.models import Competition from flask.globals import request from flask_jwt_extended import jwt_required @@ -20,6 +21,18 @@ class ComponentByID(Resource): item = dbc.get.component(component_id) return item_response(schema.dump(item)) + @jwt_required + def put(self, CID, SID, component_id): + args = component_parser.parse_args() + item = dbc.edit.component(**args) + return item_response(schema.dump(item)) + + @jwt_required + def delete(self, CID, SID, component_id): + item = dbc.get.component(component_id) + dbc.delete.component(item) + return {}, codes.NO_CONTENT + @api.route("/") @api.param("CID, SID") @@ -31,7 +44,7 @@ class ComponentList(Resource): @jwt_required def post(self, CID, SID): - args = component_parser.parse_args() + args = component_create_parser.parse_args() item_slide = dbc.get.slide(CID, SID) item = dbc.add.component(item_slide=item_slide, **args) return item_response(schema.dump(item)) diff --git a/server/app/apis/misc.py b/server/app/apis/misc.py index 7fbb222d..b40f97a8 100644 --- a/server/app/apis/misc.py +++ b/server/app/apis/misc.py @@ -1,7 +1,7 @@ import app.database.controller as dbc from app.apis import admin_required, item_response, list_response from app.core.dto import MiscDTO -from app.database.models import City, MediaType, QuestionType, Role +from app.database.models import City, ComponentType, MediaType, QuestionType, Role, ViewType from flask_jwt_extended import jwt_required from flask_restx import Resource, reqparse @@ -9,6 +9,9 @@ api = MiscDTO.api question_type_schema = MiscDTO.question_type_schema media_type_schema = MiscDTO.media_type_schema +component_type_schema = MiscDTO.component_type_schema +view_type_schema = MiscDTO.view_type_schema + role_schema = MiscDTO.role_schema city_schema = MiscDTO.city_schema @@ -17,27 +20,23 @@ name_parser = reqparse.RequestParser() name_parser.add_argument("name", type=str, required=True, location="json") -@api.route("/media_types") -class MediaTypeList(Resource): - @jwt_required - def get(self): - items = MediaType.query.all() - return list_response(media_type_schema.dump(items)) - - -@api.route("/question_types") -class QuestionTypeList(Resource): +@api.route("/types") +class TypesList(Resource): @jwt_required def get(self): - items = QuestionType.query.all() - return list_response(question_type_schema.dump(items)) + result = {} + result["media_types"] = media_type_schema.dump(dbc.get.all(MediaType)) + result["component_types"] = component_type_schema.dump(dbc.get.all(ComponentType)) + result["question_types"] = question_type_schema.dump(dbc.get.all(QuestionType)) + result["view_types"] = view_type_schema.dump(dbc.get.all(ViewType)) + return result @api.route("/roles") class RoleList(Resource): @jwt_required def get(self): - items = Role.query.all() + items = dbc.get.all(Role) return list_response(role_schema.dump(items)) @@ -45,14 +44,14 @@ class RoleList(Resource): class CitiesList(Resource): @jwt_required def get(self): - items = City.query.all() + items = dbc.get.all(City) return list_response(city_schema.dump(items)) @jwt_required def post(self): args = name_parser.parse_args(strict=True) dbc.add.city(args["name"]) - items = City.query.all() + items = dbc.get.all(City) return list_response(city_schema.dump(items)) @@ -61,16 +60,16 @@ class CitiesList(Resource): class Cities(Resource): @jwt_required def put(self, ID): - item = City.query.filter(City.id == ID).first() + item = dbc.get.one(City, ID) args = name_parser.parse_args(strict=True) item.name = args["name"] dbc.commit_and_refresh(item) - items = City.query.all() + items = dbc.get.all(City) return list_response(city_schema.dump(items)) @jwt_required def delete(self, ID): - item = City.query.filter(City.id == ID).first() + item = dbc.get.one(City, ID) dbc.delete.default(item) - items = City.query.all() + items = dbc.get.all(City) return list_response(city_schema.dump(items)) diff --git a/server/app/core/dto.py b/server/app/core/dto.py index 3e6b0cda..a6899fc7 100644 --- a/server/app/core/dto.py +++ b/server/app/core/dto.py @@ -3,6 +3,7 @@ import app.core.schemas as schemas import marshmallow as ma from flask_restx import Namespace, fields from flask_uploads import IMAGES, UploadSet +from sqlalchemy.sql.expression import true class ComponentDTO: @@ -53,6 +54,8 @@ class MiscDTO: role_schema = schemas.RoleSchema(many=True) question_type_schema = schemas.QuestionTypeSchema(many=True) media_type_schema = schemas.MediaTypeSchema(many=True) + component_type_schema = schemas.ComponentTypeSchema(many=True) + view_type_schema = schemas.ViewTypeSchema(many=True) city_schema = schemas.CitySchema(many=True) diff --git a/server/app/core/parsers.py b/server/app/core/parsers.py index a8136c9c..151924ca 100644 --- a/server/app/core/parsers.py +++ b/server/app/core/parsers.py @@ -77,4 +77,7 @@ component_parser.add_argument("y", type=int, default=None, location="json") component_parser.add_argument("w", type=int, default=None, location="json") component_parser.add_argument("h", type=int, default=None, location="json") component_parser.add_argument("data", type=dict, default=None, location="json") -component_parser.add_argument("type_id", type=int, default=None, location="json") + +component_create_parser = component_parser.copy() +component_create_parser.replace_argument("data", type=dict, required=True, location="json") +component_create_parser.add_argument("type_id", type=int, required=True, location="json") diff --git a/server/app/core/rich_schemas.py b/server/app/core/rich_schemas.py index ab1b3abb..8c220e78 100644 --- a/server/app/core/rich_schemas.py +++ b/server/app/core/rich_schemas.py @@ -53,6 +53,7 @@ class SlideSchemaRich(RichSchema): timer = ma.auto_field() competition_id = ma.auto_field() questions = fields.Nested(QuestionSchemaRich, many=True) + components = fields.Nested(schemas.ComponentSchema, many=True) class CompetitionSchemaRich(RichSchema): diff --git a/server/app/core/schemas.py b/server/app/core/schemas.py index 058efed7..ee909e4b 100644 --- a/server/app/core/schemas.py +++ b/server/app/core/schemas.py @@ -10,12 +10,30 @@ class BaseSchema(ma.SQLAlchemySchema): include_relationships = False -class QuestionTypeSchema(BaseSchema): +class IdNameSchema(BaseSchema): + + id = fields.fields.Integer() + name = fields.fields.String() + + +class QuestionTypeSchema(IdNameSchema): class Meta(BaseSchema.Meta): model = models.QuestionType - id = ma.auto_field() - name = ma.auto_field() + +class MediaTypeSchema(IdNameSchema): + class Meta(BaseSchema.Meta): + model = models.MediaType + + +class ComponentTypeSchema(IdNameSchema): + class Meta(BaseSchema.Meta): + model = models.ComponentType + + +class ViewTypeSchema(IdNameSchema): + class Meta(BaseSchema.Meta): + model = models.ViewType class QuestionSchema(BaseSchema): @@ -40,14 +58,6 @@ class QuestionAnswerSchema(BaseSchema): team_id = ma.auto_field() -class MediaTypeSchema(BaseSchema): - class Meta(BaseSchema.Meta): - model = models.MediaType - - id = ma.auto_field() - name = ma.auto_field() - - class RoleSchema(BaseSchema): class Meta(BaseSchema.Meta): model = models.Role @@ -124,6 +134,6 @@ class ComponentSchema(BaseSchema): y = ma.auto_field() w = ma.auto_field() h = ma.auto_field() - data = ma.auto_field() # TODO: Convert this to dict + data = ma.Function(lambda obj: obj.data) slide_id = ma.auto_field() type_id = ma.auto_field() diff --git a/server/app/database/controller/add.py b/server/app/database/controller/add.py index 9e2a8e11..0e955a9e 100644 --- a/server/app/database/controller/add.py +++ b/server/app/database/controller/add.py @@ -5,6 +5,7 @@ from app.database.models import ( City, Competition, Component, + ComponentType, Media, MediaType, Question, @@ -13,6 +14,7 @@ from app.database.models import ( Slide, Team, User, + ViewType, ) from flask_restx import abort @@ -33,8 +35,8 @@ def db_add(func): @db_add -def component(x, y, w, h, data, item_slide, type_id): - return Component(x, y, w, h, data, item_slide.id, type_id) +def component(type_id, item_slide, data, x=0, y=0, w=0, h=0): + return Component(item_slide.id, type_id, data, x, y, w, h) @db_add @@ -83,6 +85,16 @@ def questionType(name): return QuestionType(name) +@db_add +def componentType(name): + return ComponentType(name) + + +@db_add +def viewType(name): + return ViewType(name) + + @db_add def role(name): return Role(name) diff --git a/server/app/database/controller/delete.py b/server/app/database/controller/delete.py index 65527ee9..2eb040c9 100644 --- a/server/app/database/controller/delete.py +++ b/server/app/database/controller/delete.py @@ -8,6 +8,10 @@ def default(item): db.session.commit() +def component(item_component): + default(item_component) + + def slide(item_slide): for item_question in item_slide.questions: question(item_question) diff --git a/server/app/database/controller/edit.py b/server/app/database/controller/edit.py index 0a1b190f..3afcb4d4 100644 --- a/server/app/database/controller/edit.py +++ b/server/app/database/controller/edit.py @@ -20,6 +20,23 @@ def switch_order(item1, item2): return item1 +def component(item, x, y, w, h, data): + if x: + item.x = x + if y: + item.y = y + if w: + item.w = w + if h: + item.h = h + if data: + item.data = data + + db.session.commit() + db.session.refresh(item) + return item + + def slide(item, title=None, timer=None): if title: item.title = title diff --git a/server/app/database/controller/get.py b/server/app/database/controller/get.py index fa6914b2..892d251b 100644 --- a/server/app/database/controller/get.py +++ b/server/app/database/controller/get.py @@ -1,4 +1,25 @@ -from app.database.models import Competition, Component, Question, Slide, Team, User +from app.core import db +from app.database.models import ( + City, + Competition, + Component, + ComponentType, + MediaType, + Question, + QuestionType, + Role, + Slide, + Team, + User, +) + + +def all(db_type): + return db_type.query.all() + + +def one(db_type, id, required=True, error_msg=None): + return db_type.query.filter(db_type.id == id).first_extended(required, error_msg) def user_exists(email): diff --git a/server/app/database/models.py b/server/app/database/models.py index 29cbe3b5..6396e035 100644 --- a/server/app/database/models.py +++ b/server/app/database/models.py @@ -203,7 +203,6 @@ class Dictionary(TypeDecorator): class Component(db.Model): - # __mapper_args__ = {"polymorphic_on": type, "polymorphic_identity": "component"} id = db.Column(db.Integer, primary_key=True) x = db.Column(db.Integer, nullable=False, default=0) y = db.Column(db.Integer, nullable=False, default=0) @@ -213,7 +212,7 @@ class Component(db.Model): slide_id = db.Column(db.Integer, db.ForeignKey("slide.id"), nullable=False) type_id = db.Column(db.Integer, db.ForeignKey("component_type.id"), nullable=False) - def __init__(self, x, y, w, h, data, slide_id, type_id): + def __init__(self, slide_id, type_id, data, x=0, y=0, w=1, h=1): self.x = x self.y = y self.w = w @@ -223,33 +222,6 @@ class Component(db.Model): self.type_id = type_id -""" -class ImageComponent(Component): - __mapper_args__ = {"polymorphic_identity": "image_component"} - - id = db.Column(db.Integer, db.ForeignKey("component.id"), primary_key=True) - # id = db.Column(db.Integer, primary_key=True) - image_id = db.Column(db.Integer, db.ForeignKey("media.id"), nullable=False) - image = db.relationship("Media", uselist=False) - - def __init__(self, id, image_id, x, y, w, h, slide_id, type): - super.__init__(x, y, w, h, slide_id, type) - self.id = id - self.image_id = image_id - - -class TextComponent(Component): - __mapper_args__ = {"polymorphic_identity": "text_component"} - id = db.Column(db.Integer, db.ForeignKey("component.id"), primary_key=True) - text = db.Column(db.Text, default="", nullable=False) - - def __init__(self, id, text, x, y, w, h, slide_id, type): - super.__init__(x, y, w, h, slide_id, type) - self.id = id - self.text = text -""" - - class Code(db.Model): table_args = (db.UniqueConstraint("pointer", "type"),) id = db.Column(db.Integer, primary_key=True) @@ -258,18 +230,29 @@ class Code(db.Model): view_type_id = db.Column(db.Integer, db.ForeignKey("view_type.id"), nullable=False) + def __init__(self, code, pointer, view_type_id): + self.code = code + self.pointer = pointer + self.view_type_id = view_type_id + class ViewType(db.Model): id = db.Column(db.Integer, primary_key=True) - name = db.Column(db.Text) + name = db.Column(db.String(STRING_SIZE), unique=True) codes = db.relationship("Code", backref="view_type") + def __init__(self, name): + self.name = name + class ComponentType(db.Model): id = db.Column(db.Integer, primary_key=True) - name = db.Column(db.Text) + name = db.Column(db.String(STRING_SIZE), unique=True) components = db.relationship("Component", backref="component_type") + def __init__(self, name): + self.name = name + class MediaType(db.Model): id = db.Column(db.Integer, primary_key=True) diff --git a/server/configmodule.py b/server/configmodule.py index fcf23cbf..2d525424 100644 --- a/server/configmodule.py +++ b/server/configmodule.py @@ -15,12 +15,13 @@ class Config: JWT_REFRESH_TOKEN_EXPIRES = timedelta(days=30) UPLOADED_PHOTOS_DEST = "static/images" # os.getcwd() SECRET_KEY = os.urandom(24) + SQLALCHEMY_ECHO = False class DevelopmentConfig(Config): DEBUG = True SQLALCHEMY_DATABASE_URI = "sqlite:///database.db" - SQLALCHEMY_ECHO = True + SQLALCHEMY_ECHO = False class TestingConfig(Config): diff --git a/server/populate.py b/server/populate.py index e2ca4776..45eb6005 100644 --- a/server/populate.py +++ b/server/populate.py @@ -8,21 +8,30 @@ from app.database.models import City, Competition, MediaType, QuestionType, Role def _add_items(): media_types = ["Image", "Video"] question_types = ["Boolean", "Multiple", "Text"] + component_types = ["Text", "Image"] + view_types = ["Team", "Judge", "Audience"] + roles = ["Admin", "Editor"] cities = ["Linköping", "Stockholm", "Norrköping", "Örkelljunga"] teams = ["Gymnasieskola A", "Gymnasieskola B", "Gymnasieskola C"] - for team_name in media_types: - dbc.add.mediaType(team_name) + for name in media_types: + dbc.add.mediaType(name) + + for name in question_types: + dbc.add.questionType(name) + + for name in component_types: + dbc.add.componentType(name) - for team_name in question_types: - dbc.add.questionType(team_name) + for name in view_types: + dbc.add.viewType(name) - for team_name in roles: - dbc.add.role(team_name) + for name in roles: + dbc.add.role(name) - for team_name in cities: - dbc.add.city(team_name) + for name in cities: + dbc.add.city(name) admin_id = Role.query.filter(Role.name == "Admin").one().id editor_id = Role.query.filter(Role.name == "Editor").one().id diff --git a/server/tests/test_app.py b/server/tests/test_app.py index 1cbacc33..67dbac72 100644 --- a/server/tests/test_app.py +++ b/server/tests/test_app.py @@ -2,8 +2,7 @@ import app.core.http_codes as codes from app.database.models import Slide from tests import app, client, db -from tests.test_helpers import (add_default_values, change_order_test, delete, - get, post, put) +from tests.test_helpers import add_default_values, change_order_test, delete, get, post, put def test_misc_api(client): @@ -14,6 +13,14 @@ def test_misc_api(client): assert response.status_code == codes.OK headers = {"Authorization": "Bearer " + body["access_token"]} + # Get types + response, body = get(client, "/api/misc/types", headers=headers) + assert response.status_code == codes.OK + assert len(body["media_types"]) >= 2 + assert len(body["question_types"]) >= 3 + assert len(body["component_types"]) >= 2 + assert len(body["view_types"]) >= 2 + ## Get misc response, body = get(client, "/api/misc/roles", headers=headers) assert response.status_code == codes.OK @@ -21,44 +28,33 @@ def test_misc_api(client): response, body = get(client, "/api/misc/cities", headers=headers) assert response.status_code == codes.OK - assert body["count"] == 2 - assert body["items"][0]["name"] == "Linköping" - assert body["items"][1]["name"] == "Testköping" - - response, body = get(client, "/api/misc/media_types", headers=headers) - assert response.status_code == codes.OK assert body["count"] >= 2 - - response, body = get(client, "/api/misc/question_types", headers=headers) - assert response.status_code == codes.OK - assert body["count"] >= 3 + assert body["items"][0]["name"] == "Linköping" and body["items"][1]["name"] == "Testköping" ## Cities response, body = post(client, "/api/misc/cities", {"name": "Göteborg"}, headers=headers) assert response.status_code == codes.OK - assert body["count"] >= 2 - assert body["items"][2]["name"] == "Göteborg" + assert body["count"] >= 2 and body["items"][2]["name"] == "Göteborg" # Rename city response, body = put(client, "/api/misc/cities/3", {"name": "Gbg"}, headers=headers) assert response.status_code == codes.OK - assert body["count"] >= 2 - assert body["items"][2]["name"] == "Gbg" + assert body["count"] >= 2 and body["items"][2]["name"] == "Gbg" # Delete city # First checks current cities response, body = get(client, "/api/misc/cities", headers=headers) assert response.status_code == codes.OK - assert body["count"] == 3 + assert body["count"] >= 3 assert body["items"][0]["name"] == "Linköping" assert body["items"][1]["name"] == "Testköping" assert body["items"][2]["name"] == "Gbg" + # Deletes city response, body = delete(client, "/api/misc/cities/3", headers=headers) assert response.status_code == codes.OK - assert body["count"] == 2 - assert body["items"][0]["name"] == "Linköping" - assert body["items"][1]["name"] == "Testköping" + assert body["count"] >= 2 + assert body["items"][0]["name"] == "Linköping" and body["items"][1]["name"] == "Testköping" def test_competition_api(client): @@ -152,8 +148,7 @@ def test_auth_and_user_api(client): response, body = put(client, "/api/users", {"name": "carl carlsson", "city_id": 2, "role_id": 1}, headers=headers) assert response.status_code == codes.OK assert body["name"] == "Carl Carlsson" - assert body["city"]["id"] == 2 - assert body["role"]["id"] == 1 + assert body["city"]["id"] == 2 and body["role"]["id"] == 1 # Find other user response, body = get( @@ -244,16 +239,14 @@ def test_slide_api(client): # Get slides CID = 2 - num_slides = 3 response, body = get(client, f"/api/competitions/{CID}/slides", headers=headers) assert response.status_code == codes.OK - assert body["count"] == num_slides + assert body["count"] == 3 # Add slide response, body = post(client, f"/api/competitions/{CID}/slides", headers=headers) - num_slides += 1 assert response.status_code == codes.OK - assert body["count"] == num_slides + assert body["count"] == 4 # Get slide SID = 1 @@ -286,21 +279,19 @@ def test_slide_api(client): # Delete slide response, _ = delete(client, f"/api/competitions/{CID}/slides/{SID}", headers=headers) - num_slides -= 1 assert response.status_code == codes.NO_CONTENT # Checks that there are fewer slides response, body = get(client, f"/api/competitions/{CID}/slides", headers=headers) assert response.status_code == codes.OK - assert body["count"] == num_slides + assert body["count"] == 3 # Tries to delete slide again response, _ = delete(client, f"/api/competitions/{CID}/slides/{SID}", headers=headers) assert response.status_code == codes.NOT_FOUND # Changes the order to the same order - i = 0 - SID = body["items"][i]["id"] - order = body["items"][i]["order"] + SID = body["items"][0]["id"] + order = body["items"][0]["order"] response, _ = put(client, f"/api/competitions/{CID}/slides/{SID}/order", {"order": order}, headers=headers) assert response.status_code == codes.OK diff --git a/server/tests/test_helpers.py b/server/tests/test_helpers.py index 7cbdec87..6b6bf7a0 100644 --- a/server/tests/test_helpers.py +++ b/server/tests/test_helpers.py @@ -9,23 +9,29 @@ from app.database.models import City, Role def add_default_values(): media_types = ["Image", "Video"] question_types = ["Boolean", "Multiple", "Text"] + component_types = ["Text", "Image"] + view_types = ["Team", "Judge", "Audience"] + roles = ["Admin", "Editor"] cities = ["Linköping", "Testköping"] - # Add media types - for item in media_types: - dbc.add.mediaType(item) + for name in media_types: + dbc.add.mediaType(name) + + for name in question_types: + dbc.add.questionType(name) + + for name in component_types: + dbc.add.componentType(name) + + for name in view_types: + dbc.add.viewType(name) - # Add question types - for item in question_types: - dbc.add.questionType(item) + for name in roles: + dbc.add.role(name) - # Add roles - for item in roles: - dbc.add.role(item) - # Add cities - for item in cities: - dbc.add.city(item) + for name in cities: + dbc.add.city(name) item_admin = Role.query.filter(Role.name == "Admin").one() item_city = City.query.filter(City.name == "Linköping").one() -- GitLab From 79562da05a28e23eb28c0a793bc64b6524d9aee5 Mon Sep 17 00:00:00 2001 From: robban64 <carl@schonfelder.se> Date: Fri, 16 Apr 2021 19:59:01 +0200 Subject: [PATCH 14/21] add: reducer for all types --- client/src/Main.tsx | 9 ++++++++- client/src/actions/types.ts | 1 + client/src/actions/typesAction.ts | 15 ++++++++++++++ client/src/interfaces/ApiModels.ts | 11 ++++++++++- client/src/pages/admin/AdminPage.tsx | 15 +++++++------- client/src/reducers/allReducers.ts | 2 ++ client/src/reducers/typesReducer.ts | 29 ++++++++++++++++++++++++++++ 7 files changed, 72 insertions(+), 10 deletions(-) create mode 100644 client/src/actions/typesAction.ts create mode 100644 client/src/reducers/typesReducer.ts diff --git a/client/src/Main.tsx b/client/src/Main.tsx index f32aad9f..45286d54 100644 --- a/client/src/Main.tsx +++ b/client/src/Main.tsx @@ -1,5 +1,7 @@ -import React from 'react' +import React, { useEffect } from 'react' import { BrowserRouter, Route, Switch } from 'react-router-dom' +import { getTypes } from './actions/typesAction' +import { useAppDispatch } from './hooks' import AdminPage from './pages/admin/AdminPage' import LoginPage from './pages/login/LoginPage' import PresentationEditorPage from './pages/presentationEditor/PresentationEditorPage' @@ -11,6 +13,11 @@ import ViewSelectPage from './pages/views/ViewSelectPage' import SecureRoute from './utils/SecureRoute' const Main: React.FC = () => { + const dispatch = useAppDispatch() + useEffect(() => { + dispatch(getTypes()) + }, []) + return ( <BrowserRouter> <Switch> diff --git a/client/src/actions/types.ts b/client/src/actions/types.ts index a8736c4b..d1084c20 100644 --- a/client/src/actions/types.ts +++ b/client/src/actions/types.ts @@ -23,4 +23,5 @@ export default { SET_CITIES: 'SET_CITIES', SET_CITIES_TOTAL: 'SET_CITIES_TOTAL', SET_CITIES_COUNT: 'SET_CITIES_COUNT', + SET_TYPES: 'SET_TYPES', } diff --git a/client/src/actions/typesAction.ts b/client/src/actions/typesAction.ts new file mode 100644 index 00000000..4fcde4f5 --- /dev/null +++ b/client/src/actions/typesAction.ts @@ -0,0 +1,15 @@ +import axios from 'axios' +import { AppDispatch } from './../store' +import Types from './types' + +export const getTypes = () => async (dispatch: AppDispatch) => { + await axios + .get('/misc/types') + .then((res) => { + dispatch({ + type: Types.SET_TYPES, + payload: res.data, + }) + }) + .catch((err) => console.log(err)) +} diff --git a/client/src/interfaces/ApiModels.ts b/client/src/interfaces/ApiModels.ts index 194fb2e7..0ffc91af 100644 --- a/client/src/interfaces/ApiModels.ts +++ b/client/src/interfaces/ApiModels.ts @@ -1,4 +1,4 @@ -interface NameID { +export interface NameID { id: number name: string } @@ -6,6 +6,15 @@ export interface City extends NameID {} export interface Role extends NameID {} export interface MediaType extends NameID {} export interface QuestionType extends NameID {} +export interface ComponentType extends NameID {} +export interface ViewType extends NameID {} + +export interface AllTypes { + media_types: MediaType[] + question_types: QuestionType[] + component_types: ComponentType[] + view_types: ViewType[] +} export interface Media { id: number diff --git a/client/src/pages/admin/AdminPage.tsx b/client/src/pages/admin/AdminPage.tsx index a0dbe2bd..60c0fde2 100644 --- a/client/src/pages/admin/AdminPage.tsx +++ b/client/src/pages/admin/AdminPage.tsx @@ -57,12 +57,16 @@ const AdminView: React.FC = () => { const classes = useStyles() const [openIndex, setOpenIndex] = React.useState(0) const { path, url } = useRouteMatch() + const currentUser = useAppSelector((state) => state.user.userInfo) + const isAdmin = () => currentUser && currentUser.role.name === 'Admin' + const dispatch = useAppDispatch() const handleLogout = () => { dispatch(logoutUser()) } - const dispatch = useAppDispatch() - const currentUser = useAppSelector((state) => state.user.userInfo) - const isAdmin = () => currentUser && currentUser.role.name === 'Admin' + useEffect(() => { + dispatch(getCities()) + dispatch(getRoles()) + }, []) const menuAdminItems = [ { text: 'Startsida', icon: DashboardIcon }, @@ -93,11 +97,6 @@ const AdminView: React.FC = () => { )) } - useEffect(() => { - dispatch(getCities()) - dispatch(getRoles()) - }, []) - return ( <div className={classes.root}> <CssBaseline /> diff --git a/client/src/reducers/allReducers.ts b/client/src/reducers/allReducers.ts index 94743ff1..d0f9e801 100644 --- a/client/src/reducers/allReducers.ts +++ b/client/src/reducers/allReducers.ts @@ -6,6 +6,7 @@ import competitionsReducer from './competitionsReducer' import presentationReducer from './presentationReducer' import rolesReducer from './rolesReducer' import searchUserReducer from './searchUserReducer' +import typesReducer from './typesReducer' import uiReducer from './uiReducer' import userReducer from './userReducer' @@ -18,5 +19,6 @@ const allReducers = combineReducers({ presentation: presentationReducer, roles: rolesReducer, searchUsers: searchUserReducer, + types: typesReducer, }) export default allReducers diff --git a/client/src/reducers/typesReducer.ts b/client/src/reducers/typesReducer.ts new file mode 100644 index 00000000..3540ef86 --- /dev/null +++ b/client/src/reducers/typesReducer.ts @@ -0,0 +1,29 @@ +import { AnyAction } from 'redux' +import Types from '../actions/types' +import { ComponentType, MediaType, QuestionType, ViewType } from '../interfaces/ApiModels' + +interface TypesState { + componentTypes: ComponentType[] + viewTypes: ViewType[] + questionTypes: QuestionType[] + mediaTypes: MediaType[] +} +const initialState: TypesState = { + componentTypes: [], + viewTypes: [], + questionTypes: [], + mediaTypes: [], +} + +export default function (state = initialState, action: AnyAction) { + switch (action.type) { + case Types.SET_TYPES: + state.componentTypes = action.payload.component_types as ComponentType[] + state.viewTypes = action.payload.view_types as ViewType[] + state.questionTypes = action.payload.question_types as QuestionType[] + state.mediaTypes = action.payload.media_types as MediaType[] + return state + default: + return state + } +} -- GitLab From ec5459432d2697fd51d5d8522ee0a73792001e22 Mon Sep 17 00:00:00 2001 From: robban64 <carl@schonfelder.se> Date: Fri, 16 Apr 2021 20:09:44 +0200 Subject: [PATCH 15/21] moved getTypes to admin --- client/src/Main.tsx | 9 +-------- client/src/pages/admin/AdminPage.tsx | 2 ++ 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/client/src/Main.tsx b/client/src/Main.tsx index 45286d54..f32aad9f 100644 --- a/client/src/Main.tsx +++ b/client/src/Main.tsx @@ -1,7 +1,5 @@ -import React, { useEffect } from 'react' +import React from 'react' import { BrowserRouter, Route, Switch } from 'react-router-dom' -import { getTypes } from './actions/typesAction' -import { useAppDispatch } from './hooks' import AdminPage from './pages/admin/AdminPage' import LoginPage from './pages/login/LoginPage' import PresentationEditorPage from './pages/presentationEditor/PresentationEditorPage' @@ -13,11 +11,6 @@ import ViewSelectPage from './pages/views/ViewSelectPage' import SecureRoute from './utils/SecureRoute' const Main: React.FC = () => { - const dispatch = useAppDispatch() - useEffect(() => { - dispatch(getTypes()) - }, []) - return ( <BrowserRouter> <Switch> diff --git a/client/src/pages/admin/AdminPage.tsx b/client/src/pages/admin/AdminPage.tsx index 60c0fde2..8bbb68fa 100644 --- a/client/src/pages/admin/AdminPage.tsx +++ b/client/src/pages/admin/AdminPage.tsx @@ -20,6 +20,7 @@ import React, { useEffect } from 'react' import { Link, Route, Switch, useRouteMatch } from 'react-router-dom' import { getCities } from '../../actions/cities' import { getRoles } from '../../actions/roles' +import { getTypes } from '../../actions/typesAction' import { logoutUser } from '../../actions/user' import { useAppDispatch, useAppSelector } from '../../hooks' import CompetitionManager from './competitions/CompetitionManager' @@ -66,6 +67,7 @@ const AdminView: React.FC = () => { useEffect(() => { dispatch(getCities()) dispatch(getRoles()) + dispatch(getTypes()) }, []) const menuAdminItems = [ -- GitLab From fc3aec3c9c31e00f4a393d94775107a6014fb7c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20L=C3=B6fgren?= <viclo211@student.liu.se> Date: Fri, 16 Apr 2021 21:15:51 +0200 Subject: [PATCH 16/21] Move question api to competitions/cid/slides/sid --- server/app/apis/__init__.py | 6 ++---- server/app/apis/questions.py | 29 ++++++++++++++--------------- server/app/core/parsers.py | 1 - server/tests/test_app.py | 31 ++++++++++++++++--------------- 4 files changed, 32 insertions(+), 35 deletions(-) diff --git a/server/app/apis/__init__.py b/server/app/apis/__init__.py index 9e3bd034..b7172f96 100644 --- a/server/app/apis/__init__.py +++ b/server/app/apis/__init__.py @@ -59,8 +59,6 @@ flask_api.add_namespace(user_ns, path="/api/users") flask_api.add_namespace(auth_ns, path="/api/auth") flask_api.add_namespace(comp_ns, path="/api/competitions") flask_api.add_namespace(slide_ns, path="/api/competitions/<CID>/slides") -flask_api.add_namespace(component_ns, path="/api/competitions/<CID>/slides/<SID>/components/") - flask_api.add_namespace(team_ns, path="/api/competitions/<CID>/teams") -flask_api.add_namespace(question_ns, path="/api/competitions/<CID>/questions") -# flask_api.add_namespace(question_ns, path="/api/competitions/<CID>/slides/<SID>/question") +flask_api.add_namespace(question_ns, path="/api/competitions/<CID>") +flask_api.add_namespace(component_ns, path="/api/competitions/<CID>/slides/<SID>/components/") diff --git a/server/app/apis/questions.py b/server/app/apis/questions.py index 86929bb4..48857dcb 100644 --- a/server/app/apis/questions.py +++ b/server/app/apis/questions.py @@ -12,7 +12,7 @@ schema = QuestionDTO.schema list_schema = QuestionDTO.list_schema -@api.route("/") +@api.route("/questions") @api.param("CID") class QuestionsList(Resource): @jwt_required @@ -20,40 +20,39 @@ class QuestionsList(Resource): items = dbc.get.question_list(CID) return list_response(list_schema.dump(items)) + +@api.route("/slides/<SID>/questions") +@api.param("CID, SID") +class QuestionsList(Resource): @jwt_required - def post(self, CID): + def post(self, SID, CID): args = question_parser.parse_args(strict=True) - name = args.get("name") - total_score = args.get("total_score") - type_id = args.get("type_id") - slide_id = args.get("slide_id") - - item_slide = dbc.get.slide(CID, slide_id) - item = dbc.add.question(name, total_score, type_id, item_slide) + item_slide = dbc.get.slide(CID, SID) + item = dbc.add.question(item_slide=item_slide, **args) return item_response(schema.dump(item)) -@api.route("/<QID>") -@api.param("CID,QID") +@api.route("/slides/<SID>/questions/<QID>") +@api.param("CID, SID, QID") class Questions(Resource): @jwt_required - def get(self, CID, QID): + def get(self, CID, SID, QID): item_question = dbc.get.question(CID, QID) return item_response(schema.dump(item_question)) @jwt_required - def put(self, CID, QID): + def put(self, CID, SID, QID): args = question_parser.parse_args(strict=True) item_question = dbc.get.question(CID, QID) - item_question = dbc.edit.question(item_question, **args) + item_question = dbc.edit.question(item_question, slide_id=SID, **args) return item_response(schema.dump(item_question)) @jwt_required - def delete(self, CID, QID): + def delete(self, CID, SID, QID): item_question = dbc.get.question(CID, QID) dbc.delete.question(item_question) return {}, codes.NO_CONTENT diff --git a/server/app/core/parsers.py b/server/app/core/parsers.py index 151924ca..527961e5 100644 --- a/server/app/core/parsers.py +++ b/server/app/core/parsers.py @@ -57,7 +57,6 @@ slide_parser.add_argument("timer", type=int, default=None, location="json") question_parser = reqparse.RequestParser() question_parser.add_argument("name", type=str, default=None, location="json") question_parser.add_argument("total_score", type=int, default=None, location="json") -question_parser.add_argument("slide_id", type=int, default=None, location="json") question_parser.add_argument("type_id", type=int, default=None, location="json") diff --git a/server/tests/test_app.py b/server/tests/test_app.py index 67dbac72..7effe7ca 100644 --- a/server/tests/test_app.py +++ b/server/tests/test_app.py @@ -2,7 +2,8 @@ import app.core.http_codes as codes from app.database.models import Slide from tests import app, client, db -from tests.test_helpers import add_default_values, change_order_test, delete, get, post, put +from tests.test_helpers import (add_default_values, change_order_test, delete, + get, post, put) def test_misc_api(client): @@ -313,6 +314,7 @@ def test_question_api(client): # Get questions from empty competition CID = 1 # TODO: Fix api-calls so that the ones not using CID don't require one + SID = 1 response, body = get(client, f"/api/competitions/{CID}/questions", headers=headers) assert response.status_code == codes.OK assert body["count"] == 0 @@ -322,7 +324,7 @@ def test_question_api(client): num_questions = 3 response, body = get(client, f"/api/competitions/{CID}/questions", headers=headers) assert response.status_code == codes.OK - print(body) + # print(body) assert body["count"] == num_questions # # Get specific question @@ -349,11 +351,11 @@ def test_question_api(client): name = "Nytt namn" # total_score = 2 type_id = 2 - slide_id = 5 + SID = 5 response, item_question = post( client, - f"/api/competitions/{CID}/questions", - {"name": name, "type_id": type_id, "slide_id": slide_id}, + f"/api/competitions/{CID}/slides/{SID}/questions", + {"name": name, "type_id": type_id}, headers=headers, ) num_questions += 1 @@ -361,7 +363,7 @@ def test_question_api(client): assert item_question["name"] == name # # assert item_question["total_score"] == total_score assert item_question["type"]["id"] == type_id - assert item_question["slide_id"] == slide_id + assert item_question["slide_id"] == SID # Checks number of questions response, body = get(client, f"/api/competitions/{CID}/questions", headers=headers) assert response.status_code == codes.OK @@ -369,12 +371,12 @@ def test_question_api(client): # Try to get question in another competition QID = 1 - response, item_question = get(client, f"/api/competitions/{CID}/questions/{QID}", headers=headers) + response, item_question = get(client, f"/api/competitions/{CID}/slides/{SID}/questions/{QID}", headers=headers) assert response.status_code == codes.NOT_FOUND # Get question QID = 4 - response, item_question = get(client, f"/api/competitions/{CID}/questions/{QID}", headers=headers) + response, item_question = get(client, f"/api/competitions/{CID}/slides/{SID}/questions/{QID}", headers=headers) assert response.status_code == codes.OK assert item_question["id"] == QID @@ -386,9 +388,8 @@ def test_question_api(client): QID = 1 response, _ = put( client, - f"/api/competitions/{CID}/questions/{QID}", - # {"name": name, "total_score": total_score, "type_id": type_id, "slide_id": slide_id}, - {"name": name, "type_id": type_id, "slide_id": slide_id}, + f"/api/competitions/{CID}/slides/{SID}/questions/{QID}", + {"name": name, "type_id": type_id}, headers=headers, ) assert response.status_code == codes.NOT_FOUND @@ -409,9 +410,9 @@ def test_question_api(client): assert item_question["slide_id"] != slide_id response, item_question = put( client, - f"/api/competitions/{CID}/questions/{QID}", + f"/api/competitions/{CID}/slides/{SID}/questions/{QID}", # {"name": name, "total_score": total_score, "type_id": type_id, "slide_id": slide_id}, - {"name": name, "type_id": type_id, "slide_id": slide_id}, + {"name": name, "type_id": type_id}, headers=headers, ) assert response.status_code == codes.OK @@ -425,7 +426,7 @@ def test_question_api(client): assert body["count"] == num_questions # Delete question - response, _ = delete(client, f"/api/competitions/{CID}/questions/{QID}", headers=headers) + response, _ = delete(client, f"/api/competitions/{CID}/slides/{SID}/questions/{QID}", headers=headers) num_questions -= 1 assert response.status_code == codes.NO_CONTENT @@ -435,5 +436,5 @@ def test_question_api(client): assert body["count"] == num_questions # Tries to delete question again - response, _ = delete(client, f"/api/competitions/{CID}/questions/{QID}", headers=headers) + response, _ = delete(client, f"/api/competitions/{CID}/slides/{SID}/questions/{QID}", headers=headers) assert response.status_code == codes.NOT_FOUND -- GitLab From 0f4818a49801bcffb21755c229e0ff7a1679e30b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20L=C3=B6fgren?= <viclo211@student.liu.se> Date: Fri, 16 Apr 2021 22:50:21 +0200 Subject: [PATCH 17/21] Update questions api --- server/app/apis/questions.py | 9 +++++---- server/app/core/parsers.py | 2 +- server/app/database/controller/get.py | 4 ++-- server/tests/test_app.py | 19 ++++++++++--------- 4 files changed, 18 insertions(+), 16 deletions(-) diff --git a/server/app/apis/questions.py b/server/app/apis/questions.py index 48857dcb..55db2819 100644 --- a/server/app/apis/questions.py +++ b/server/app/apis/questions.py @@ -27,6 +27,7 @@ class QuestionsList(Resource): @jwt_required def post(self, SID, CID): args = question_parser.parse_args(strict=True) + del args["slide_id"] item_slide = dbc.get.slide(CID, SID) item = dbc.add.question(item_slide=item_slide, **args) @@ -39,20 +40,20 @@ class QuestionsList(Resource): class Questions(Resource): @jwt_required def get(self, CID, SID, QID): - item_question = dbc.get.question(CID, QID) + item_question = dbc.get.question(CID, SID, QID) return item_response(schema.dump(item_question)) @jwt_required def put(self, CID, SID, QID): args = question_parser.parse_args(strict=True) - item_question = dbc.get.question(CID, QID) - item_question = dbc.edit.question(item_question, slide_id=SID, **args) + item_question = dbc.get.question(CID, SID, QID) + item_question = dbc.edit.question(item_question, **args) return item_response(schema.dump(item_question)) @jwt_required def delete(self, CID, SID, QID): - item_question = dbc.get.question(CID, QID) + item_question = dbc.get.question(CID, SID, QID) dbc.delete.question(item_question) return {}, codes.NO_CONTENT diff --git a/server/app/core/parsers.py b/server/app/core/parsers.py index 527961e5..54f54239 100644 --- a/server/app/core/parsers.py +++ b/server/app/core/parsers.py @@ -58,7 +58,7 @@ question_parser = reqparse.RequestParser() question_parser.add_argument("name", type=str, default=None, location="json") question_parser.add_argument("total_score", type=int, default=None, location="json") question_parser.add_argument("type_id", type=int, default=None, location="json") - +question_parser.add_argument("slide_id", type=int, location="json") ###TEAM#### team_parser = reqparse.RequestParser() diff --git a/server/app/database/controller/get.py b/server/app/database/controller/get.py index 892d251b..1669f0ae 100644 --- a/server/app/database/controller/get.py +++ b/server/app/database/controller/get.py @@ -56,9 +56,9 @@ def team(CID, TID, required=True, error_msg=None): return Team.query.filter((Team.competition_id == CID) & (Team.id == TID)).first_extended(required, error_msg) -def question(CID, QID, required=True, error_msg=None): +def question(CID, SID, QID, required=True, error_msg=None): return ( - Question.query.join(Slide, (Slide.competition_id == CID) & (Slide.id == Question.slide_id)) + Question.query.join(Slide, (Slide.competition_id == CID) & (Slide.id == SID) & (Slide.id == Question.slide_id)) .filter(Question.id == QID) .first_extended(required, error_msg) ) diff --git a/server/tests/test_app.py b/server/tests/test_app.py index 7effe7ca..8086774e 100644 --- a/server/tests/test_app.py +++ b/server/tests/test_app.py @@ -2,8 +2,7 @@ import app.core.http_codes as codes from app.database.models import Slide from tests import app, client, db -from tests.test_helpers import (add_default_values, change_order_test, delete, - get, post, put) +from tests.test_helpers import add_default_values, change_order_test, delete, get, post, put def test_misc_api(client): @@ -376,6 +375,7 @@ def test_question_api(client): # Get question QID = 4 + SID = 4 response, item_question = get(client, f"/api/competitions/{CID}/slides/{SID}/questions/{QID}", headers=headers) assert response.status_code == codes.OK assert item_question["id"] == QID @@ -384,7 +384,7 @@ def test_question_api(client): name = "Nyare namn" # total_score = 2 type_id = 3 - slide_id = 1 + SID = 1 QID = 1 response, _ = put( client, @@ -402,31 +402,32 @@ def test_question_api(client): name = "Nyare namn" # total_score = 2 type_id = 3 - slide_id = 5 + SID = 4 + NEW_SID = 5 QID = 4 assert item_question["name"] != name # assert item_question["total_score"] != total_score assert item_question["type"]["id"] != type_id - assert item_question["slide_id"] != slide_id + assert item_question["slide_id"] != NEW_SID response, item_question = put( client, f"/api/competitions/{CID}/slides/{SID}/questions/{QID}", # {"name": name, "total_score": total_score, "type_id": type_id, "slide_id": slide_id}, - {"name": name, "type_id": type_id}, + {"name": name, "type_id": type_id, "slide_id": NEW_SID}, headers=headers, ) assert response.status_code == codes.OK assert item_question["name"] == name # # assert item_question["total_score"] == total_score assert item_question["type"]["id"] == type_id - assert item_question["slide_id"] == slide_id + assert item_question["slide_id"] == NEW_SID # Checks number of questions response, body = get(client, f"/api/competitions/{CID}/questions", headers=headers) assert response.status_code == codes.OK assert body["count"] == num_questions # Delete question - response, _ = delete(client, f"/api/competitions/{CID}/slides/{SID}/questions/{QID}", headers=headers) + response, _ = delete(client, f"/api/competitions/{CID}/slides/{NEW_SID}/questions/{QID}", headers=headers) num_questions -= 1 assert response.status_code == codes.NO_CONTENT @@ -436,5 +437,5 @@ def test_question_api(client): assert body["count"] == num_questions # Tries to delete question again - response, _ = delete(client, f"/api/competitions/{CID}/slides/{SID}/questions/{QID}", headers=headers) + response, _ = delete(client, f"/api/competitions/{CID}/slides/{NEW_SID}/questions/{QID}", headers=headers) assert response.status_code == codes.NOT_FOUND -- GitLab From a2f39b662e8ecf4a6db1433728d43a9f9c7aa960 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20L=C3=B6fgren?= <viclo211@student.liu.se> Date: Sat, 17 Apr 2021 00:30:22 +0200 Subject: [PATCH 18/21] Add api for competition codes --- server/app/apis/__init__.py | 2 ++ server/app/apis/auth.py | 17 +++++++++- server/app/apis/codes.py | 44 ++++++++++++++++++++++++++ server/app/core/codes.py | 15 +++++++++ server/app/core/dto.py | 6 ++++ server/app/core/parsers.py | 6 ++++ server/app/core/rich_schemas.py | 10 ++++++ server/app/core/schemas.py | 10 ++++++ server/app/database/controller/add.py | 7 ++++ server/app/database/controller/edit.py | 15 +++++++++ server/app/database/controller/get.py | 23 ++++++++++++++ server/app/database/models.py | 8 ++++- 12 files changed, 161 insertions(+), 2 deletions(-) create mode 100644 server/app/apis/codes.py create mode 100644 server/app/core/codes.py diff --git a/server/app/apis/__init__.py b/server/app/apis/__init__.py index b7172f96..5d6e45dc 100644 --- a/server/app/apis/__init__.py +++ b/server/app/apis/__init__.py @@ -43,6 +43,7 @@ def item_response(item, code=codes.OK): from flask_restx import Api from .auth import api as auth_ns +from .codes import api as code_ns from .competitions import api as comp_ns from .components import api as component_ns from .media import api as media_ns @@ -60,5 +61,6 @@ flask_api.add_namespace(auth_ns, path="/api/auth") flask_api.add_namespace(comp_ns, path="/api/competitions") flask_api.add_namespace(slide_ns, path="/api/competitions/<CID>/slides") flask_api.add_namespace(team_ns, path="/api/competitions/<CID>/teams") +flask_api.add_namespace(code_ns, path="/api/competitions/<CID>/codes") flask_api.add_namespace(question_ns, path="/api/competitions/<CID>") flask_api.add_namespace(component_ns, path="/api/competitions/<CID>/slides/<SID>/components/") diff --git a/server/app/apis/auth.py b/server/app/apis/auth.py index 1510df64..10d820f8 100644 --- a/server/app/apis/auth.py +++ b/server/app/apis/auth.py @@ -1,7 +1,8 @@ import app.core.http_codes as codes import app.database.controller as dbc from app.apis import admin_required, item_response, text_response -from app.core.dto import AuthDTO +from app.core.codes import verify_code +from app.core.dto import AuthDTO, CodeDTO from app.core.parsers import create_user_parser, login_parser from app.database.models import User from flask_jwt_extended import ( @@ -69,6 +70,20 @@ class AuthLogin(Resource): return response +@api.route("/login/<code>") +@api.param("code") +class AuthLogin(Resource): + def post(self, code): + if not verify_code(code): + api.abort(codes.BAD_REQUEST, "Invalid code") + + item_code = dbc.get.code_by_code(code) + if not item_code: + api.abort(codes.UNAUTHORIZED, "A presentation with that code does not exist") + + return item_response(CodeDTO.schema.dump(item_code)), codes.OK + + @api.route("/logout") class AuthLogout(Resource): @jwt_required diff --git a/server/app/apis/codes.py b/server/app/apis/codes.py new file mode 100644 index 00000000..70cf9341 --- /dev/null +++ b/server/app/apis/codes.py @@ -0,0 +1,44 @@ +import app.database.controller as dbc +from app.apis import admin_required, item_response, list_response +from app.core import http_codes as codes +from app.core.codes import generate_code +from app.core.dto import CodeDTO +from app.core.parsers import code_parser +from app.database.models import Competition +from flask_jwt_extended import jwt_required +from flask_restx import Resource + +api = CodeDTO.api +schema = CodeDTO.schema +list_schema = CodeDTO.list_schema + + +@api.route("/") +@api.param("CID") +class CodesList(Resource): + @jwt_required + def get(self, CID): + items = dbc.get.code_list(CID) + return list_response(list_schema.dump(items), len(items)), codes.OK + + @jwt_required + def post(self, CID): + args = code_parser.parse_args(strict=True) + item = dbc.add.code(**args) + return item_response(schema.dump(item)), codes.OK + + +@api.route("/<code_id>") +@api.param("CID, code_id") +class Competitions(Resource): + @jwt_required + def put(self, CID, code_id): + item_code = dbc.get.code(code_id) + item_code = dbc.edit.generate_new_code(item_code) + return item_response(schema.dump(item_code)), codes.OK + + # @jwt_required + # def delete(self, CID, code_id): + # item_code = dbc.get.code(code_id) + # dbc.delete.code(item_code) + # return {}, http_codes.NOT_FOUND diff --git a/server/app/core/codes.py b/server/app/core/codes.py new file mode 100644 index 00000000..c13fa84e --- /dev/null +++ b/server/app/core/codes.py @@ -0,0 +1,15 @@ +import random +import re +import string + +CODE_LENGTH = 6 +ALLOWED_CHARS = string.ascii_uppercase + string.digits +CODE_RE = re.compile(f"^[{ALLOWED_CHARS}]{{{CODE_LENGTH}}}$") + + +def generate_code(): + return "".join(random.choices(ALLOWED_CHARS, k=CODE_LENGTH)) + + +def verify_code(c): + return CODE_RE.search(c.upper()) is not None diff --git a/server/app/core/dto.py b/server/app/core/dto.py index a6899fc7..4533fc0c 100644 --- a/server/app/core/dto.py +++ b/server/app/core/dto.py @@ -37,6 +37,12 @@ class CompetitionDTO: list_schema = schemas.CompetitionSchema(many=True) +class CodeDTO: + api = Namespace("codes") + schema = rich_schemas.CodeSchemaRich(many=False) + list_schema = schemas.CodeSchema(many=True) + + class SlideDTO: api = Namespace("slides") schema = schemas.SlideSchema(many=False) diff --git a/server/app/core/parsers.py b/server/app/core/parsers.py index 54f54239..f691ea8d 100644 --- a/server/app/core/parsers.py +++ b/server/app/core/parsers.py @@ -60,6 +60,12 @@ question_parser.add_argument("total_score", type=int, default=None, location="js question_parser.add_argument("type_id", type=int, default=None, location="json") question_parser.add_argument("slide_id", type=int, location="json") +###QUESTION#### +code_parser = reqparse.RequestParser() +code_parser.add_argument("pointer", type=str, default=None, location="json") +code_parser.add_argument("view_type_id", type=int, default=None, location="json") + + ###TEAM#### team_parser = reqparse.RequestParser() team_parser.add_argument("name", type=str, location="json") diff --git a/server/app/core/rich_schemas.py b/server/app/core/rich_schemas.py index 8c220e78..fa6daac9 100644 --- a/server/app/core/rich_schemas.py +++ b/server/app/core/rich_schemas.py @@ -43,6 +43,16 @@ class TeamSchemaRich(RichSchema): question_answers = fields.Nested(schemas.QuestionAnswerSchema, many=True) +class CodeSchemaRich(RichSchema): + class Meta(RichSchema.Meta): + model = models.Code + + id = ma.auto_field() + code = ma.auto_field() + pointer = ma.auto_field() + view_type = fields.Nested(schemas.ViewTypeSchema, many=False) + + class SlideSchemaRich(RichSchema): class Meta(RichSchema.Meta): model = models.Slide diff --git a/server/app/core/schemas.py b/server/app/core/schemas.py index ee909e4b..4f9646a5 100644 --- a/server/app/core/schemas.py +++ b/server/app/core/schemas.py @@ -31,6 +31,16 @@ class ComponentTypeSchema(IdNameSchema): model = models.ComponentType +class CodeSchema(IdNameSchema): + class Meta(BaseSchema.Meta): + model = models.Code + + id = ma.auto_field() + code = ma.auto_field() + pointer = ma.auto_field() + view_type_id = ma.auto_field() + + class ViewTypeSchema(IdNameSchema): class Meta(BaseSchema.Meta): model = models.ViewType diff --git a/server/app/database/controller/add.py b/server/app/database/controller/add.py index 0e955a9e..07b49fde 100644 --- a/server/app/database/controller/add.py +++ b/server/app/database/controller/add.py @@ -1,8 +1,10 @@ import app.core.http_codes as codes from app.core import db +from app.core.codes import generate_code from app.database.models import ( Blacklist, City, + Code, Competition, Component, ComponentType, @@ -75,6 +77,11 @@ def team(name, item_competition): return Team(name, item_competition.id) +@db_add +def code(pointer, view_type_id): + return Code(pointer, view_type_id) + + @db_add def mediaType(name): return MediaType(name) diff --git a/server/app/database/controller/edit.py b/server/app/database/controller/edit.py index 3afcb4d4..7b3ef334 100644 --- a/server/app/database/controller/edit.py +++ b/server/app/database/controller/edit.py @@ -1,4 +1,6 @@ from app.core import db +from app.core.codes import generate_code +from app.database.models import Code def switch_order(item1, item2): @@ -109,3 +111,16 @@ def question(item_question, name=None, total_score=None, type_id=None, slide_id= db.session.refresh(item_question) return item_question + + +def generate_new_code(item_code): + + code = generate_code() + while db.session.query(Code).filter(Code.code == code).count(): + code = generate_code() + + item_code.code = code + db.session.commit() + db.session.refresh(item_code) + + return item_code diff --git a/server/app/database/controller/get.py b/server/app/database/controller/get.py index 1669f0ae..4e9a6aa5 100644 --- a/server/app/database/controller/get.py +++ b/server/app/database/controller/get.py @@ -1,6 +1,7 @@ from app.core import db from app.database.models import ( City, + Code, Competition, Component, ComponentType, @@ -11,8 +12,11 @@ from app.database.models import ( Slide, Team, User, + ViewType, ) +team_view_id = ViewType.query.filter(ViewType.name == "Team").one().id + def all(db_type): return db_type.query.all() @@ -34,6 +38,25 @@ def competition(CID, required=True, error_msg=None): return Competition.query.filter(Competition.id == CID).first_extended(required, error_msg) +def code(code_id, required=True, error_msg=None): + return Code.query.filter(Code.id == code_id).first_extended(required, error_msg) + + +def code_by_code(code): + return Code.query.filter(Code.code == code.upper()).first() + + +def code_list(competition_id): + return ( + Code.query.join(Team, (Code.view_type_id == team_view_id) & (Team.id == Code.pointer), isouter=True) + .filter( + ((Code.view_type_id != team_view_id) & (Code.pointer == competition_id)) + | ((Code.view_type_id == team_view_id) & (competition_id == Team.competition_id)) + ) + .all() + ) + + def user(UID, required=True, error_msg=None): return User.query.filter(User.id == UID).first_extended(required, error_msg) diff --git a/server/app/database/models.py b/server/app/database/models.py index 6396e035..9479d92c 100644 --- a/server/app/database/models.py +++ b/server/app/database/models.py @@ -1,6 +1,7 @@ import json from app.core import bcrypt, db +from app.core.codes import generate_code from sqlalchemy.ext.hybrid import hybrid_method, hybrid_property from sqlalchemy.orm import backref from sqlalchemy.types import TypeDecorator @@ -230,7 +231,12 @@ class Code(db.Model): view_type_id = db.Column(db.Integer, db.ForeignKey("view_type.id"), nullable=False) - def __init__(self, code, pointer, view_type_id): + def __init__(self, pointer, view_type_id): + + code = generate_code() + while db.session.query(Code).filter(Code.code == code).count(): + code = generate_code() + self.code = code self.pointer = pointer self.view_type_id = view_type_id -- GitLab From d595561cf292be2659ecfc7b1fdb897c3d5c67b7 Mon Sep 17 00:00:00 2001 From: robban64 <carl@schonfelder.se> Date: Sat, 17 Apr 2021 13:21:23 +0200 Subject: [PATCH 19/21] fix: dubble slashes in component url --- server/app/apis/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/app/apis/__init__.py b/server/app/apis/__init__.py index 9e3bd034..4e0f34df 100644 --- a/server/app/apis/__init__.py +++ b/server/app/apis/__init__.py @@ -59,7 +59,7 @@ flask_api.add_namespace(user_ns, path="/api/users") flask_api.add_namespace(auth_ns, path="/api/auth") flask_api.add_namespace(comp_ns, path="/api/competitions") flask_api.add_namespace(slide_ns, path="/api/competitions/<CID>/slides") -flask_api.add_namespace(component_ns, path="/api/competitions/<CID>/slides/<SID>/components/") +flask_api.add_namespace(component_ns, path="/api/competitions/<CID>/slides/<SID>/components") flask_api.add_namespace(team_ns, path="/api/competitions/<CID>/teams") flask_api.add_namespace(question_ns, path="/api/competitions/<CID>/questions") -- GitLab From fb0f6fc2c84fa17b951be1b8f6a807b6adcfa353 Mon Sep 17 00:00:00 2001 From: Emil <Emil> Date: Sat, 17 Apr 2021 16:44:03 +0200 Subject: [PATCH 20/21] Fixed tests, updated api models --- client/src/interfaces/ApiModels.ts | 17 +++------ client/src/interfaces/ApiRichModels.ts | 11 +++--- client/src/interfaces/Competition.ts | 10 ------ .../PresentationEditorPage.test.tsx | 35 +++++++++++++++++-- .../PresentationEditorPage.tsx | 2 ++ .../components/CompetitionSettings.test.tsx | 5 ++- .../components/CompetitionSettings.tsx | 9 ++--- .../components/SettingsPanel.test.tsx | 19 ++++++++-- .../components/SlideSettings.test.tsx | 11 +++++- .../components/SlideSettings.tsx | 10 +++--- .../src/reducers/presentationReducer.test.ts | 5 +-- 11 files changed, 83 insertions(+), 51 deletions(-) delete mode 100644 client/src/interfaces/Competition.ts diff --git a/client/src/interfaces/ApiModels.ts b/client/src/interfaces/ApiModels.ts index a154a0c8..d423d424 100644 --- a/client/src/interfaces/ApiModels.ts +++ b/client/src/interfaces/ApiModels.ts @@ -2,18 +2,11 @@ interface NameID { id: number name: string } -export interface City extends NameID { - users: User[] - competitions: Competition[] -} +export interface City extends NameID {} -export interface Role extends NameID { - users: User[] -} +export interface Role extends NameID {} -export interface QuestionType extends NameID { - questions: Question[] -} +export interface QuestionType extends NameID {} export interface Media { id: number @@ -22,9 +15,7 @@ export interface Media { user_id: number } -export interface MediaType extends NameID { - media: Media[] -} +export interface MediaType extends NameID {} export interface User extends NameID { email: string diff --git a/client/src/interfaces/ApiRichModels.ts b/client/src/interfaces/ApiRichModels.ts index 779dbd8a..e21ae5fa 100644 --- a/client/src/interfaces/ApiRichModels.ts +++ b/client/src/interfaces/ApiRichModels.ts @@ -1,4 +1,5 @@ -import { QuestionAlternative, QuestionAnswer } from './ApiModels' +import { Component } from 'react' +import { Media, QuestionAlternative, QuestionAnswer, QuestionType } from './ApiModels' export interface RichCompetition { name: string @@ -15,9 +16,9 @@ export interface RichSlide { timer: number title: string competition_id: number + components: Component[] + medias: Media[] questions: RichQuestion[] - body: string - settings: string } export interface RichTeam { @@ -33,7 +34,7 @@ export interface RichQuestion { name: string title: string total_score: number + question_type: QuestionType type_id: number - question_answers: QuestionAnswer[] - alternatives: QuestionAlternative[] + question_alternatives: QuestionAlternative[] } diff --git a/client/src/interfaces/Competition.ts b/client/src/interfaces/Competition.ts deleted file mode 100644 index c7c4d309..00000000 --- a/client/src/interfaces/Competition.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { City } from './ApiModels' -import { RichSlide } from './ApiRichModels' - -export interface Competition { - name: string - id: number - city: City - year: number - slides: RichSlide[] -} diff --git a/client/src/pages/presentationEditor/PresentationEditorPage.test.tsx b/client/src/pages/presentationEditor/PresentationEditorPage.test.tsx index 67e38b99..ebc96d73 100644 --- a/client/src/pages/presentationEditor/PresentationEditorPage.test.tsx +++ b/client/src/pages/presentationEditor/PresentationEditorPage.test.tsx @@ -1,12 +1,41 @@ import { render } from '@testing-library/react' +import mockedAxios from 'axios' import React from 'react' +import { Provider } from 'react-redux' import { BrowserRouter } from 'react-router-dom' +import store from '../../store' import PresentationEditorPage from './PresentationEditorPage' it('renders presentation editor', () => { + const competitionRes: any = { + data: { + name: '', + id: 0, + year: 0, + city_id: 0, + slides: [], + teams: [], + }, + } + const citiesRes: any = { + data: { + items: [ + { + name: '', + city_id: 0, + }, + ], + }, + } + ;(mockedAxios.get as jest.Mock).mockImplementation((path: string, params?: any) => { + if (path.startsWith('/competitions')) return Promise.resolve(competitionRes) + return Promise.resolve(citiesRes) + }) render( - <BrowserRouter> - <PresentationEditorPage /> - </BrowserRouter> + <Provider store={store}> + <BrowserRouter> + <PresentationEditorPage /> + </BrowserRouter> + </Provider> ) }) diff --git a/client/src/pages/presentationEditor/PresentationEditorPage.tsx b/client/src/pages/presentationEditor/PresentationEditorPage.tsx index e31aaabb..2993b12b 100644 --- a/client/src/pages/presentationEditor/PresentationEditorPage.tsx +++ b/client/src/pages/presentationEditor/PresentationEditorPage.tsx @@ -7,6 +7,7 @@ import ListItemText from '@material-ui/core/ListItemText' import { createStyles, makeStyles, Theme } from '@material-ui/core/styles' import React, { useEffect } from 'react' import { useParams } from 'react-router-dom' +import { getCities } from '../../actions/cities' import { getEditorCompetition } from '../../actions/editor' import { useAppDispatch, useAppSelector } from '../../hooks' import { Content } from '../views/styled' @@ -67,6 +68,7 @@ const PresentationEditorPage: React.FC = () => { // TODO: wait for dispatch to finish useEffect(() => { dispatch(getEditorCompetition(id)) + dispatch(getCities()) }, []) return ( <PresentationEditorContainer> diff --git a/client/src/pages/presentationEditor/components/CompetitionSettings.test.tsx b/client/src/pages/presentationEditor/components/CompetitionSettings.test.tsx index 592581d4..a655a30f 100644 --- a/client/src/pages/presentationEditor/components/CompetitionSettings.test.tsx +++ b/client/src/pages/presentationEditor/components/CompetitionSettings.test.tsx @@ -1,7 +1,10 @@ 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 }} />) + 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/CompetitionSettings.tsx b/client/src/pages/presentationEditor/components/CompetitionSettings.tsx index e5e1bc6a..2c61633f 100644 --- a/client/src/pages/presentationEditor/components/CompetitionSettings.tsx +++ b/client/src/pages/presentationEditor/components/CompetitionSettings.tsx @@ -13,9 +13,8 @@ import { import { createStyles, makeStyles, Theme } from '@material-ui/core/styles' import CloseIcon from '@material-ui/icons/Close' import axios from 'axios' -import React, { useEffect } from 'react' +import React from 'react' import { useParams } from 'react-router-dom' -import { getCities } from '../../../actions/cities' import { getEditorCompetition } from '../../../actions/editor' import { useAppDispatch, useAppSelector } from '../../../hooks' import { City } from '../../../interfaces/ApiModels' @@ -65,10 +64,6 @@ const CompetitionSettings: React.FC = () => { const { id }: CompetitionParams = useParams() const dispatch = useAppDispatch() const competition = useAppSelector((state) => state.editor.competition) - useEffect(() => { - dispatch(getEditorCompetition(id)) - dispatch(getCities()) - }, []) const updateCompetitionName = async (event: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => { await axios @@ -122,7 +117,7 @@ const CompetitionSettings: React.FC = () => { <InputLabel id="region-selection-label">Region</InputLabel> {/*TODO: fixa så cities laddar in i statet likt i CompetitionManager*/} <Select - value={cities[competition.city_id - 1] ? cities[0].name : ''} + value={cities.find((city) => city.id === competition.city_id)?.name || ''} label="RegionSelect" onChange={handleChange} > diff --git a/client/src/pages/presentationEditor/components/SettingsPanel.test.tsx b/client/src/pages/presentationEditor/components/SettingsPanel.test.tsx index b4daa57b..a7a71e25 100644 --- a/client/src/pages/presentationEditor/components/SettingsPanel.test.tsx +++ b/client/src/pages/presentationEditor/components/SettingsPanel.test.tsx @@ -1,15 +1,30 @@ import { render } from '@testing-library/react' import { mount } from 'enzyme' import React from 'react' +import { Provider } from 'react-redux' +import { BrowserRouter } from 'react-router-dom' +import store from '../../../store' import CompetitionSettings from './CompetitionSettings' import SettingsPanel from './SettingsPanel' it('renders settings panel', () => { - render(<SettingsPanel />) + render( + <Provider store={store}> + <BrowserRouter> + <SettingsPanel /> + </BrowserRouter> + </Provider> + ) }) it('renders slide settings tab', () => { - const wrapper = mount(<SettingsPanel />) + const wrapper = mount( + <Provider store={store}> + <BrowserRouter> + <SettingsPanel /> + </BrowserRouter> + </Provider> + ) const tabs = wrapper.find('.MuiTabs-flexContainer') expect(wrapper.find(CompetitionSettings).length).toEqual(1) tabs.children().at(1).simulate('click') diff --git a/client/src/pages/presentationEditor/components/SlideSettings.test.tsx b/client/src/pages/presentationEditor/components/SlideSettings.test.tsx index 4099e130..a271fcf5 100644 --- a/client/src/pages/presentationEditor/components/SlideSettings.test.tsx +++ b/client/src/pages/presentationEditor/components/SlideSettings.test.tsx @@ -1,7 +1,16 @@ import { render } from '@testing-library/react' import React from 'react' +import { Provider } from 'react-redux' +import { BrowserRouter } from 'react-router-dom' +import store from '../../../store' import SlideSettings from './SlideSettings' it('renders slide settings', () => { - render(<SlideSettings />) + render( + <Provider store={store}> + <BrowserRouter> + <SlideSettings /> + </BrowserRouter> + </Provider> + ) }) diff --git a/client/src/pages/presentationEditor/components/SlideSettings.tsx b/client/src/pages/presentationEditor/components/SlideSettings.tsx index d5ab4912..cd611060 100644 --- a/client/src/pages/presentationEditor/components/SlideSettings.tsx +++ b/client/src/pages/presentationEditor/components/SlideSettings.tsx @@ -114,7 +114,7 @@ const SlideSettings: React.FC = () => { const updateSlideType = async (event: React.ChangeEvent<{ value: unknown }>) => { await axios // TODO: implementera API för att kunna ändra i questions->type_id - .put(`/competitions/${id}/slides/${currentSlide.id}`, { type_id: event.target.value }) + .put(`/competitions/${id}/slides/${currentSlide?.id}`, { type_id: event.target.value }) .then(() => { dispatch(getEditorCompetition(id)) }) @@ -125,7 +125,7 @@ const SlideSettings: React.FC = () => { // Wheter the alternative is true or false await axios // TODO: implementera API för att kunna ändra i alternatives->value - .put(`/competitions/${id}/slides/${currentSlide.id}`, { value: event.target.value }) + .put(`/competitions/${id}/slides/${currentSlide?.id}`, { value: event.target.value }) .then(() => { dispatch(getEditorCompetition(id)) }) @@ -169,7 +169,7 @@ const SlideSettings: React.FC = () => { <div className={classes.whiteBackground}> <FormControl variant="outlined" className={classes.dropDown}> <InputLabel id="slide-type-selection-label">Sidtyp</InputLabel> - <Select value={currentSlide.questions[0].type_id} label="Sidtyp" onChange={updateSlideType}> + <Select value={currentSlide?.questions[0].type_id || 0} label="Sidtyp" onChange={updateSlideType}> <MenuItem value={0}> <Button>Informationssida</Button> </MenuItem> @@ -194,7 +194,7 @@ const SlideSettings: React.FC = () => { helperText="Lämna blank för att inte använda timerfunktionen" label="Timer" type="number" - value={currentSlide.timer} + value={currentSlide?.timer} /> </ListItem> @@ -206,7 +206,7 @@ const SlideSettings: React.FC = () => { secondary="(Fyll i rutan höger om textfältet för att markera korrekt svar)" /> </ListItem> - {currentSlide.questions[0].alternatives.map((alt) => ( + {(currentSlide?.questions[0].question_alternatives || []).map((alt) => ( <div key={alt.id}> <ListItem divider> <TextField diff --git a/client/src/reducers/presentationReducer.test.ts b/client/src/reducers/presentationReducer.test.ts index cf4ded41..15bcd337 100644 --- a/client/src/reducers/presentationReducer.test.ts +++ b/client/src/reducers/presentationReducer.test.ts @@ -7,10 +7,7 @@ const initialState = { competition: { name: '', id: 0, - city: { - id: 0, - name: '', - }, + city_id: 0, slides: [], year: 0, teams: [], -- GitLab From 33331109040187531ee11f4aa99f993b0419ef03 Mon Sep 17 00:00:00 2001 From: robban64 <carl@schonfelder.se> Date: Sat, 17 Apr 2021 16:49:43 +0200 Subject: [PATCH 21/21] fix: slide order --- server/app/__init__.py | 2 +- server/app/apis/__init__.py | 4 +- server/app/apis/codes.py | 24 +--- server/app/apis/competitions.py | 6 +- server/app/apis/components.py | 24 ++-- server/app/apis/misc.py | 2 +- server/app/apis/slides.py | 30 ++--- server/app/apis/teams.py | 2 +- server/app/core/__init__.py | 2 +- server/app/core/codes.py | 4 +- server/app/core/dto.py | 4 +- server/app/database/__init__.py | 55 ++++++++ server/app/database/base.py | 37 ------ server/app/database/controller/__init__.py | 15 +-- server/app/database/controller/add.py | 113 ++++++++--------- server/app/database/controller/delete.py | 22 +++- server/app/database/controller/edit.py | 15 --- server/app/database/controller/get.py | 86 ++++--------- server/app/database/controller/utils.py | 23 ++++ server/app/database/models.py | 27 +--- server/populate.py | 15 +-- server/tests/test_app.py | 140 ++++----------------- server/tests/test_helpers.py | 49 ++------ 23 files changed, 270 insertions(+), 431 deletions(-) delete mode 100644 server/app/database/base.py create mode 100644 server/app/database/controller/utils.py diff --git a/server/app/__init__.py b/server/app/__init__.py index 8aa7a08a..a2529395 100644 --- a/server/app/__init__.py +++ b/server/app/__init__.py @@ -1,5 +1,5 @@ from flask import Flask, redirect, request -from flask_uploads import IMAGES, UploadSet, configure_uploads +from flask_uploads import configure_uploads import app.database.models as models from app.core import bcrypt, db, jwt, ma diff --git a/server/app/apis/__init__.py b/server/app/apis/__init__.py index 50281141..b48b8b33 100644 --- a/server/app/apis/__init__.py +++ b/server/app/apis/__init__.py @@ -23,7 +23,7 @@ def admin_required(): def text_response(message, code=codes.OK): - return {"message": message}, codes.OK + return {"message": message}, code def list_response(items, total=None, code=codes.OK): @@ -63,4 +63,4 @@ flask_api.add_namespace(slide_ns, path="/api/competitions/<CID>/slides") flask_api.add_namespace(team_ns, path="/api/competitions/<CID>/teams") flask_api.add_namespace(code_ns, path="/api/competitions/<CID>/codes") flask_api.add_namespace(question_ns, path="/api/competitions/<CID>") -flask_api.add_namespace(component_ns, path="/api/competitions/<CID>/slides/<SID>/components") +flask_api.add_namespace(component_ns, path="/api/competitions/<CID>/slides/<SOrder>/components") diff --git a/server/app/apis/codes.py b/server/app/apis/codes.py index 70cf9341..af6aee84 100644 --- a/server/app/apis/codes.py +++ b/server/app/apis/codes.py @@ -1,10 +1,9 @@ import app.database.controller as dbc from app.apis import admin_required, item_response, list_response from app.core import http_codes as codes -from app.core.codes import generate_code from app.core.dto import CodeDTO from app.core.parsers import code_parser -from app.database.models import Competition +from app.database.models import Code, Competition from flask_jwt_extended import jwt_required from flask_restx import Resource @@ -21,24 +20,13 @@ class CodesList(Resource): items = dbc.get.code_list(CID) return list_response(list_schema.dump(items), len(items)), codes.OK - @jwt_required - def post(self, CID): - args = code_parser.parse_args(strict=True) - item = dbc.add.code(**args) - return item_response(schema.dump(item)), codes.OK - @api.route("/<code_id>") @api.param("CID, code_id") -class Competitions(Resource): +class CodesById(Resource): @jwt_required def put(self, CID, code_id): - item_code = dbc.get.code(code_id) - item_code = dbc.edit.generate_new_code(item_code) - return item_response(schema.dump(item_code)), codes.OK - - # @jwt_required - # def delete(self, CID, code_id): - # item_code = dbc.get.code(code_id) - # dbc.delete.code(item_code) - # return {}, http_codes.NOT_FOUND + item = dbc.get.one(Code, code_id) + item.code = dbc.utils.generate_unique_code() + dbc.utils.commit_and_refresh(item) + return item_response(schema.dump(item)), codes.OK diff --git a/server/app/apis/competitions.py b/server/app/apis/competitions.py index be376206..26b6f363 100644 --- a/server/app/apis/competitions.py +++ b/server/app/apis/competitions.py @@ -30,20 +30,20 @@ class CompetitionsList(Resource): class Competitions(Resource): @jwt_required def get(self, CID): - item = dbc.get.competition(CID) + item = dbc.get.one(Competition, CID) return item_response(schema.dump(item)) @jwt_required def put(self, CID): args = competition_parser.parse_args(strict=True) - item = dbc.get.competition(CID) + item = dbc.get.one(Competition, CID) item = dbc.edit.competition(item, **args) return item_response(schema.dump(item)) @jwt_required def delete(self, CID): - item = dbc.get.competition(CID) + item = dbc.get.one(Competition, CID) dbc.delete.competition(item) return "deleted" diff --git a/server/app/apis/components.py b/server/app/apis/components.py index da211895..f88e1e2d 100644 --- a/server/app/apis/components.py +++ b/server/app/apis/components.py @@ -3,7 +3,7 @@ import app.database.controller as dbc from app.apis import admin_required, item_response, list_response from app.core.dto import ComponentDTO from app.core.parsers import component_create_parser, component_parser -from app.database.models import Competition +from app.database.models import Competition, Component from flask.globals import request from flask_jwt_extended import jwt_required from flask_restx import Resource @@ -14,37 +14,37 @@ list_schema = ComponentDTO.list_schema @api.route("/<component_id>") -@api.param("CID, SID, component_id") +@api.param("CID, SOrder, component_id") class ComponentByID(Resource): @jwt_required - def get(self, CID, SID, component_id): - item = dbc.get.component(component_id) + def get(self, CID, SOrder, component_id): + item = dbc.get.one(Component, component_id) return item_response(schema.dump(item)) @jwt_required - def put(self, CID, SID, component_id): + def put(self, CID, SOrder, component_id): args = component_parser.parse_args() item = dbc.edit.component(**args) return item_response(schema.dump(item)) @jwt_required - def delete(self, CID, SID, component_id): - item = dbc.get.component(component_id) + def delete(self, CID, SOrder, component_id): + item = dbc.get.one(Component, component_id) dbc.delete.component(item) return {}, codes.NO_CONTENT @api.route("/") -@api.param("CID, SID") +@api.param("CID, SOrder") class ComponentList(Resource): @jwt_required - def get(self, CID, SID): - items = dbc.get.component_list(SID) + def get(self, CID, SOrder): + items = dbc.get.component_list(SOrder) return list_response(list_schema.dump(items)) @jwt_required - def post(self, CID, SID): + def post(self, CID, SOrder): args = component_create_parser.parse_args() - item_slide = dbc.get.slide(CID, SID) + item_slide = dbc.get.slide(CID, SOrder) item = dbc.add.component(item_slide=item_slide, **args) return item_response(schema.dump(item)) diff --git a/server/app/apis/misc.py b/server/app/apis/misc.py index b40f97a8..a78c2f8c 100644 --- a/server/app/apis/misc.py +++ b/server/app/apis/misc.py @@ -63,7 +63,7 @@ class Cities(Resource): item = dbc.get.one(City, ID) args = name_parser.parse_args(strict=True) item.name = args["name"] - dbc.commit_and_refresh(item) + dbc.utils.commit_and_refresh(item) items = dbc.get.all(City) return list_response(city_schema.dump(items)) diff --git a/server/app/apis/slides.py b/server/app/apis/slides.py index 972e1bd6..9aeb793a 100644 --- a/server/app/apis/slides.py +++ b/server/app/apis/slides.py @@ -22,49 +22,49 @@ class SlidesList(Resource): @jwt_required def post(self, CID): - item_comp = dbc.get.competition(CID) + item_comp = dbc.get.one(Competition, CID) item_slide = dbc.add.slide(item_comp) dbc.add.question(f"Fråga {item_slide.order + 1}", 10, 0, item_slide) - dbc.refresh(item_comp) + dbc.utils.refresh(item_comp) return list_response(list_schema.dump(item_comp.slides)) -@api.route("/<SID>") -@api.param("CID,SID") +@api.route("/<SOrder>") +@api.param("CID,SOrder") class Slides(Resource): @jwt_required - def get(self, CID, SID): - item_slide = dbc.get.slide(CID, SID) + def get(self, CID, SOrder): + item_slide = dbc.get.slide(CID, SOrder) return item_response(schema.dump(item_slide)) @jwt_required - def put(self, CID, SID): + def put(self, CID, SOrder): args = slide_parser.parse_args(strict=True) title = args.get("title") timer = args.get("timer") - item_slide = dbc.get.slide(CID, SID) + item_slide = dbc.get.slide(CID, SOrder) item_slide = dbc.edit.slide(item_slide, title, timer) return item_response(schema.dump(item_slide)) @jwt_required - def delete(self, CID, SID): - item_slide = dbc.get.slide(CID, SID) + def delete(self, CID, SOrder): + item_slide = dbc.get.slide(CID, SOrder) dbc.delete.slide(item_slide) return {}, codes.NO_CONTENT -@api.route("/<SID>/order") -@api.param("CID,SID") +@api.route("/<SOrder>/order") +@api.param("CID,SOrder") class SlidesOrder(Resource): @jwt_required - def put(self, CID, SID): + def put(self, CID, SOrder): args = slide_parser.parse_args(strict=True) order = args.get("order") - item_slide = dbc.get.slide(CID, SID) + item_slide = dbc.get.slide(CID, SOrder) if order == item_slide.order: return item_response(schema.dump(item_slide)) @@ -77,7 +77,7 @@ class SlidesOrder(Resource): order = order_count - 1 # get slide at the requested order - item_slide_order = dbc.get.slide_by_order(CID, order) + item_slide_order = dbc.get.slide(CID, order) # switch place between them item_slide = dbc.edit.switch_order(item_slide, item_slide_order) diff --git a/server/app/apis/teams.py b/server/app/apis/teams.py index 9600e2a4..6729ccf8 100644 --- a/server/app/apis/teams.py +++ b/server/app/apis/teams.py @@ -23,7 +23,7 @@ class TeamsList(Resource): @jwt_required def post(self, CID): args = team_parser.parse_args(strict=True) - item_comp = dbc.get.competition(CID) + item_comp = dbc.get.one(Competition,CID) item_team = dbc.add.team(args["name"], item_comp) return item_response(schema.dump(item_team)) diff --git a/server/app/core/__init__.py b/server/app/core/__init__.py index 09c321ef..acfca554 100644 --- a/server/app/core/__init__.py +++ b/server/app/core/__init__.py @@ -1,4 +1,4 @@ -from app.database.base import Base, ExtendedQuery +from app.database import Base, ExtendedQuery from flask_bcrypt import Bcrypt from flask_jwt_extended.jwt_manager import JWTManager from flask_marshmallow import Marshmallow diff --git a/server/app/core/codes.py b/server/app/core/codes.py index c13fa84e..47150767 100644 --- a/server/app/core/codes.py +++ b/server/app/core/codes.py @@ -3,11 +3,11 @@ import re import string CODE_LENGTH = 6 -ALLOWED_CHARS = string.ascii_uppercase + string.digits +ALLOWED_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" CODE_RE = re.compile(f"^[{ALLOWED_CHARS}]{{{CODE_LENGTH}}}$") -def generate_code(): +def generate_code_string(): return "".join(random.choices(ALLOWED_CHARS, k=CODE_LENGTH)) diff --git a/server/app/core/dto.py b/server/app/core/dto.py index 4533fc0c..6541ef75 100644 --- a/server/app/core/dto.py +++ b/server/app/core/dto.py @@ -1,9 +1,7 @@ import app.core.rich_schemas as rich_schemas import app.core.schemas as schemas -import marshmallow as ma -from flask_restx import Namespace, fields +from flask_restx import Namespace from flask_uploads import IMAGES, UploadSet -from sqlalchemy.sql.expression import true class ComponentDTO: diff --git a/server/app/database/__init__.py b/server/app/database/__init__.py index e69de29b..ead77d9c 100644 --- a/server/app/database/__init__.py +++ b/server/app/database/__init__.py @@ -0,0 +1,55 @@ +import json + +from flask_restx import abort +from flask_sqlalchemy import BaseQuery +from flask_sqlalchemy.model import Model +from sqlalchemy import Column, DateTime, Text +from sqlalchemy.sql import func +from sqlalchemy.types import TypeDecorator + + +class Base(Model): + __abstract__ = True + _created = Column(DateTime(timezone=True), server_default=func.now()) + _updated = Column(DateTime(timezone=True), onupdate=func.now()) + + +class ExtendedQuery(BaseQuery): + def first_extended(self, required=True, error_message=None, error_code=404): + item = self.first() + + if required and not item: + if not error_message: + error_message = "Object not found" + abort(error_code, error_message) + + return item + + def pagination(self, page=0, page_size=15, order_column=None, order=1): + query = self + if order_column: + if order == 1: + query = query.order_by(order_column) + else: + query = query.order_by(order_column.desc()) + + total = query.count() + query = query.limit(page_size).offset(page * page_size) + items = query.all() + return items, total + + +class Dictionary(TypeDecorator): + + impl = Text(1024) + + def process_bind_param(self, value, dialect): + if value is not None: + value = json.dumps(value).replace("'", '"') + + return value + + def process_result_value(self, value, dialect): + if value is not None: + value = json.loads(value) + return value diff --git a/server/app/database/base.py b/server/app/database/base.py deleted file mode 100644 index 7a16a752..00000000 --- a/server/app/database/base.py +++ /dev/null @@ -1,37 +0,0 @@ -import app.core.http_codes as codes -import sqlalchemy as sa -from flask_restx import abort -from flask_sqlalchemy import BaseQuery, SQLAlchemy -from flask_sqlalchemy.model import Model -from sqlalchemy.sql import func - - -class Base(Model): - __abstract__ = True - _created = sa.Column(sa.DateTime(timezone=True), server_default=func.now()) - _updated = sa.Column(sa.DateTime(timezone=True), onupdate=func.now()) - - -class ExtendedQuery(BaseQuery): - def first_extended(self, required=True, error_message=None, error_code=codes.NOT_FOUND): - item = self.first() - - if required and not item: - if not error_message: - error_message = "Object not found" - abort(error_code, error_message) - - return item - - def pagination(self, page=0, page_size=15, order_column=None, order=1): - query = self - if order_column: - if order == 1: - query = query.order_by(order_column) - else: - query = query.order_by(order_column.desc()) - - total = query.count() - query = query.limit(page_size).offset(page * page_size) - items = query.all() - return items, total diff --git a/server/app/database/controller/__init__.py b/server/app/database/controller/__init__.py index d31ded56..2865b523 100644 --- a/server/app/database/controller/__init__.py +++ b/server/app/database/controller/__init__.py @@ -1,16 +1,3 @@ # import add, get from app.core import db -from app.database.controller import add, delete, edit, get, search - - -def commit_and_refresh(item): - db.session.commit() - db.session.refresh(item) - - -def refresh(item): - db.session.refresh(item) - - -def commit(item): - db.session.commit() +from app.database.controller import add, delete, edit, get, search, utils diff --git a/server/app/database/controller/add.py b/server/app/database/controller/add.py index 07b49fde..755849bb 100644 --- a/server/app/database/controller/add.py +++ b/server/app/database/controller/add.py @@ -1,6 +1,6 @@ import app.core.http_codes as codes from app.core import db -from app.core.codes import generate_code +from app.database.controller import utils from app.database.models import ( Blacklist, City, @@ -21,92 +21,93 @@ from app.database.models import ( from flask_restx import abort -def db_add(func): - def wrapper(*args, **kwargs): - item = func(*args, **kwargs) - db.session.add(item) - db.session.commit() - db.session.refresh(item) +def db_add(item): + db.session.add(item) + db.session.commit() + db.session.refresh(item) - if not item: - abort(codes.BAD_REQUEST, f"Object could not be created") + if not item: + abort(codes.BAD_REQUEST, f"Object could not be created") - return item + return item - return wrapper +def blacklist(jti): + return db_add(Blacklist(jti)) -@db_add -def component(type_id, item_slide, data, x=0, y=0, w=0, h=0): - return Component(item_slide.id, type_id, data, x, y, w, h) +def mediaType(name): + return db_add(MediaType(name)) -@db_add -def blacklist(jti): - return Blacklist(jti) +def questionType(name): + return db_add(QuestionType(name)) -@db_add -def image(filename, user_id): - return Media(filename, 1, user_id) +def componentType(name): + return db_add(ComponentType(name)) -@db_add -def slide(item_competition): - order = Slide.query.filter(Slide.competition_id == item_competition.id).count() # first element has index 0 - return Slide(order, item_competition.id) +def viewType(name): + return db_add(ViewType(name)) -@db_add -def user(email, password, role_id, city_id, name=None): - return User(email, password, role_id, city_id, name) +def role(name): + return db_add(Role(name)) -@db_add -def question(name, total_score, type_id, item_slide): - return Question(name, total_score, type_id, item_slide.id) +def city(name): + return db_add(City(name)) -@db_add -def competition(name, year, city_id): - return Competition(name, year, city_id) + +def component(type_id, item_slide, data, x=0, y=0, w=0, h=0): + return db_add(Component(item_slide.id, type_id, data, x, y, w, h)) -@db_add -def team(name, item_competition): - return Team(name, item_competition.id) +def image(filename, user_id): + return db_add(Media(filename, 1, user_id)) + + +def user(email, password, role_id, city_id, name=None): + return db_add(User(email, password, role_id, city_id, name)) + + +def question(name, total_score, type_id, item_slide): + return db_add(Question(name, total_score, type_id, item_slide.id)) -@db_add def code(pointer, view_type_id): - return Code(pointer, view_type_id) + code_string = utils.generate_unique_code() + return db_add(Code(code_string, pointer, view_type_id)) -@db_add -def mediaType(name): - return MediaType(name) +def team(name, item_competition): + item = db_add(Team(name, item_competition.id)) + # Add code for the team + code(item.id, 1) -@db_add -def questionType(name): - return QuestionType(name) + return item -@db_add -def componentType(name): - return ComponentType(name) +def slide(item_competition): + order = Slide.query.filter(Slide.competition_id == item_competition.id).count() # first element has index 0 + return db_add(Slide(order, item_competition.id)) -@db_add -def viewType(name): - return ViewType(name) +def competition(name, year, city_id): + item_competition = db_add(Competition(name, year, city_id)) + # Add one slide for the competition + slide(item_competition) -@db_add -def role(name): - return Role(name) + # Add code for Judge view + code(item_competition.id, 2) + # Add code for Audience view + code(item_competition.id, 3) -@db_add -def city(name): - return City(name) + # Add two teams + + utils.refresh(item_competition) + return item_competition diff --git a/server/app/database/controller/delete.py b/server/app/database/controller/delete.py index 2eb040c9..33bea217 100644 --- a/server/app/database/controller/delete.py +++ b/server/app/database/controller/delete.py @@ -12,18 +12,26 @@ def component(item_component): default(item_component) -def slide(item_slide): +def _slide(item_slide): for item_question in item_slide.questions: question(item_question) - deleted_slide_competition_id = item_slide.competition_id - deleted_slide_order = item_slide.order + for item_component in item_slide.components: + default(item_component) + default(item_slide) + +def slide(item_slide): + competition_id = item_slide.competition_id + slide_order = item_slide.order + + _slide(item_slide) + # Update slide order for all slides after the deleted slide - slides_in_same_competition = dbc.get.slide_list(deleted_slide_competition_id) + slides_in_same_competition = dbc.get.slide_list(competition_id) for other_slide in slides_in_same_competition: - if other_slide.order > deleted_slide_order: + if other_slide.order > slide_order: other_slide.order -= 1 db.session.commit() @@ -53,7 +61,9 @@ def question_answers(item_question_answers): def competition(item_competition): for item_slide in item_competition.slides: - slide(item_slide) + _slide(item_slide) for item_team in item_competition.teams: team(item_team) + + # TODO codes default(item_competition) diff --git a/server/app/database/controller/edit.py b/server/app/database/controller/edit.py index 7b3ef334..3afcb4d4 100644 --- a/server/app/database/controller/edit.py +++ b/server/app/database/controller/edit.py @@ -1,6 +1,4 @@ from app.core import db -from app.core.codes import generate_code -from app.database.models import Code def switch_order(item1, item2): @@ -111,16 +109,3 @@ def question(item_question, name=None, total_score=None, type_id=None, slide_id= db.session.refresh(item_question) return item_question - - -def generate_new_code(item_code): - - code = generate_code() - while db.session.query(Code).filter(Code.code == code).count(): - code = generate_code() - - item_code.code = code - db.session.commit() - db.session.refresh(item_code) - - return item_code diff --git a/server/app/database/controller/get.py b/server/app/database/controller/get.py index 4e9a6aa5..269640b3 100644 --- a/server/app/database/controller/get.py +++ b/server/app/database/controller/get.py @@ -1,21 +1,8 @@ from app.core import db -from app.database.models import ( - City, - Code, - Competition, - Component, - ComponentType, - MediaType, - Question, - QuestionType, - Role, - Slide, - Team, - User, - ViewType, -) - -team_view_id = ViewType.query.filter(ViewType.name == "Team").one().id +from app.database.models import (City, Code, Competition, Component, + ComponentType, MediaType, Question, + QuestionType, Role, Slide, Team, User, + ViewType) def all(db_type): @@ -29,34 +16,10 @@ def one(db_type, id, required=True, error_msg=None): def user_exists(email): return User.query.filter(User.email == email).count() > 0 - -def component(ID, required=True, error_msg=None): - return Component.query.filter(Component.id == ID).first_extended(required, error_msg) - - -def competition(CID, required=True, error_msg=None): - return Competition.query.filter(Competition.id == CID).first_extended(required, error_msg) - - -def code(code_id, required=True, error_msg=None): - return Code.query.filter(Code.id == code_id).first_extended(required, error_msg) - - def code_by_code(code): return Code.query.filter(Code.code == code.upper()).first() -def code_list(competition_id): - return ( - Code.query.join(Team, (Code.view_type_id == team_view_id) & (Team.id == Code.pointer), isouter=True) - .filter( - ((Code.view_type_id != team_view_id) & (Code.pointer == competition_id)) - | ((Code.view_type_id == team_view_id) & (competition_id == Team.competition_id)) - ) - .all() - ) - - def user(UID, required=True, error_msg=None): return User.query.filter(User.id == UID).first_extended(required, error_msg) @@ -65,30 +28,38 @@ def user_by_email(email, required=True, error_msg=None): return User.query.filter(User.email == email).first_extended(required, error_msg) -def slide_by_order(CID, order, required=True, error_msg=None): - return Slide.query.filter((Slide.competition_id == CID) & (Slide.order == order)).first_extended( - required, error_msg - ) - - -def slide(CID, SID, required=True, error_msg=None): - return Slide.query.filter((Slide.competition_id == CID) & (Slide.id == SID)).first_extended(required, error_msg) +def slide(CID, SOrder, required=True, error_msg=None): + filters = (Slide.competition_id == CID) & (Slide.order == SOrder) + return Slide.query.filter(filters).first_extended(required, error_msg) def team(CID, TID, required=True, error_msg=None): return Team.query.filter((Team.competition_id == CID) & (Team.id == TID)).first_extended(required, error_msg) -def question(CID, SID, QID, required=True, error_msg=None): - return ( - Question.query.join(Slide, (Slide.competition_id == CID) & (Slide.id == SID) & (Slide.id == Question.slide_id)) - .filter(Question.id == QID) - .first_extended(required, error_msg) +def question(CID, SOrder, QID, required=True, error_msg=None): + join_filters = (Slide.competition_id == CID) & (Slide.order == SOrder) & (Slide.id == Question.slide_id) + return Question.query.join(Slide, join_filters).filter(Question.id == QID).first_extended(required, error_msg) + + + +def code_list(competition_id): + team_view_id = 1 + join_filters = (Code.view_type_id == team_view_id) & (Team.id == Code.pointer) + filters = ((Code.view_type_id != team_view_id) & (Code.pointer == competition_id))( + (Code.view_type_id == team_view_id) & (competition_id == Team.competition_id) ) + return Code.query.join(Team, join_filters, isouter=True).filter(filters).all() def question_list(CID): - return Question.query.join(Slide, (Slide.competition_id == CID) & (Slide.id == Question.slide_id)).all() + join_filters = (Slide.competition_id == CID) & (Slide.id == Question.slide_id) + return Question.query.join(Slide, join_filters).all() + + +def component_list(CID, SOrder): + join_filters = (Slide.competition_id == CID) & (Slide.order == SOrder) & (Component.slide_id == Slide.id) + return Component.query.join(Slide, join_filters).all() def team_list(CID): @@ -99,10 +70,5 @@ def slide_list(CID): return Slide.query.filter(Slide.competition_id == CID).all() -def component_list(SID): - # TODO: Maybe take CID as argument and make sure that SID is in that competition? - return Component.query.filter(Component.slide_id == SID).all() - - def slide_count(CID): return Slide.query.filter(Slide.competition_id == CID).count() diff --git a/server/app/database/controller/utils.py b/server/app/database/controller/utils.py new file mode 100644 index 00000000..61be34ca --- /dev/null +++ b/server/app/database/controller/utils.py @@ -0,0 +1,23 @@ +from app.core import db +from app.core.codes import generate_code_string +from app.database.models import Code + + +def generate_unique_code(): + code = generate_code_string() + while db.session.query(Code).filter(Code.code == code).count(): + code = generate_code_string() + return code + + +def commit_and_refresh(item): + db.session.commit() + db.session.refresh(item) + + +def refresh(item): + db.session.refresh(item) + + +def commit(item): + db.session.commit() diff --git a/server/app/database/models.py b/server/app/database/models.py index 9479d92c..cfedd923 100644 --- a/server/app/database/models.py +++ b/server/app/database/models.py @@ -1,11 +1,6 @@ -import json - from app.core import bcrypt, db -from app.core.codes import generate_code from sqlalchemy.ext.hybrid import hybrid_method, hybrid_property -from sqlalchemy.orm import backref -from sqlalchemy.types import TypeDecorator - +from app.database import Dictionary STRING_SIZE = 254 @@ -187,20 +182,7 @@ class QuestionAnswer(db.Model): self.team_id = team_id -class Dictionary(TypeDecorator): - - impl = db.Text(1024) - - def process_bind_param(self, value, dialect): - if value is not None: - value = json.dumps(value).replace("'", '"') - return value - - def process_result_value(self, value, dialect): - if value is not None: - value = json.loads(value) - return value class Component(db.Model): @@ -231,12 +213,7 @@ class Code(db.Model): view_type_id = db.Column(db.Integer, db.ForeignKey("view_type.id"), nullable=False) - def __init__(self, pointer, view_type_id): - - code = generate_code() - while db.session.query(Code).filter(Code.code == code).count(): - code = generate_code() - + def __init__(self, code, pointer, view_type_id): self.code = code self.pointer = pointer self.view_type_id = view_type_id diff --git a/server/populate.py b/server/populate.py index 45eb6005..bebcaa8b 100644 --- a/server/populate.py +++ b/server/populate.py @@ -1,8 +1,6 @@ -from sqlalchemy.sql.expression import true - import app.database.controller as dbc from app import create_app, db -from app.database.models import City, Competition, MediaType, QuestionType, Role +from app.database.models import City, Competition, QuestionType, Role def _add_items(): @@ -49,14 +47,11 @@ def _add_items(): dbc.add.competition(f"Test{i+1}", 1971, city_id) item_comps = Competition.query.all() - # Add - for item_comp in item_comps: - for i in range(3): - # Add slide to competition - item_slide = dbc.add.slide(item_comp) - # Add question to competition - dbc.add.question(f"Q{i+1}", i + 1, text_id, item_slide) + for item_comp in item_comps: + for item_slide in item_comp.slides: + for i in range(3): + dbc.add.question(f"Q{i+1}", i + 1, text_id, item_slide) # Add teams to competition for team_name in teams: diff --git a/server/tests/test_app.py b/server/tests/test_app.py index 8086774e..948b9ae4 100644 --- a/server/tests/test_app.py +++ b/server/tests/test_app.py @@ -72,9 +72,6 @@ def test_competition_api(client): assert body["name"] == "c1" competition_id = body["id"] - # Save number of slides - num_slides = len(Slide.query.all()) - # Get competition response, body = get(client, f"/api/competitions/{competition_id}", headers=headers) assert response.status_code == codes.OK @@ -85,11 +82,9 @@ def test_competition_api(client): response, body = get(client, f"/api/competitions/{competition_id}/slides", headers=headers) assert response.status_code == codes.OK - assert len(body["items"]) == 2 + assert len(body["items"]) == 3 - response, body = put( - client, f"/api/competitions/{competition_id}/slides/{num_slides}/order", {"order": 1}, headers=headers - ) + response, body = put(client, f"/api/competitions/{competition_id}/slides/{2}/order", {"order": 1}, headers=headers) assert response.status_code == codes.OK response, body = post(client, f"/api/competitions/{competition_id}/teams", {"name": "t1"}, headers=headers) @@ -235,7 +230,7 @@ def test_slide_api(client): CID = 1 response, body = get(client, f"/api/competitions/{CID}/slides", headers=headers) assert response.status_code == codes.OK - assert body["count"] == 0 + assert body["count"] == 1 # Get slides CID = 2 @@ -247,12 +242,14 @@ def test_slide_api(client): response, body = post(client, f"/api/competitions/{CID}/slides", headers=headers) assert response.status_code == codes.OK assert body["count"] == 4 + # Add another slide + response, body = post(client, f"/api/competitions/{CID}/slides", headers=headers) # Get slide - SID = 1 - response, item_slide = get(client, f"/api/competitions/{CID}/slides/{SID}", headers=headers) + slide_order = 1 + response, item_slide = get(client, f"/api/competitions/{CID}/slides/{slide_order}", headers=headers) assert response.status_code == codes.OK - assert item_slide["id"] == SID + assert item_slide["order"] == slide_order # Edit slide order = 6 @@ -265,7 +262,7 @@ def test_slide_api(client): assert item_slide["timer"] != timer response, item_slide = put( client, - f"/api/competitions/{CID}/slides/{SID}", + f"/api/competitions/{CID}/slides/{slide_order}", # TODO: Implement so these commented lines can be edited # {"order": order, "title": title, "body": body, "timer": timer}, {"title": title, "timer": timer}, @@ -278,29 +275,26 @@ def test_slide_api(client): assert item_slide["timer"] == timer # Delete slide - response, _ = delete(client, f"/api/competitions/{CID}/slides/{SID}", headers=headers) + response, _ = delete(client, f"/api/competitions/{CID}/slides/{slide_order}", headers=headers) assert response.status_code == codes.NO_CONTENT # Checks that there are fewer slides response, body = get(client, f"/api/competitions/{CID}/slides", headers=headers) assert response.status_code == codes.OK - assert body["count"] == 3 + assert body["count"] == 4 - # Tries to delete slide again - response, _ = delete(client, f"/api/competitions/{CID}/slides/{SID}", headers=headers) - assert response.status_code == codes.NOT_FOUND + # Tries to delete slide again, should work since the order is now changed + response, _ = delete(client, f"/api/competitions/{CID}/slides/{slide_order}", headers=headers) + assert response.status_code == codes.NO_CONTENT # Changes the order to the same order - SID = body["items"][0]["id"] - order = body["items"][0]["order"] - response, _ = put(client, f"/api/competitions/{CID}/slides/{SID}/order", {"order": order}, headers=headers) + slide_order = body["items"][0]["order"] + response, _ = put( + client, f"/api/competitions/{CID}/slides/{slide_order}/order", {"order": slide_order}, headers=headers + ) assert response.status_code == codes.OK # Changes the order - change_order_test(client, CID, SID, order + 1, headers) - - # Changes order to 0 - SID = 7 - change_order_test(client, CID, SID, -1, headers) + change_order_test(client, CID, slide_order, slide_order + 1, headers) def test_question_api(client): @@ -313,7 +307,7 @@ def test_question_api(client): # Get questions from empty competition CID = 1 # TODO: Fix api-calls so that the ones not using CID don't require one - SID = 1 + slide_order = 1 response, body = get(client, f"/api/competitions/{CID}/questions", headers=headers) assert response.status_code == codes.OK assert body["count"] == 0 @@ -323,111 +317,30 @@ def test_question_api(client): num_questions = 3 response, body = get(client, f"/api/competitions/{CID}/questions", headers=headers) assert response.status_code == codes.OK - # print(body) assert body["count"] == num_questions - # # Get specific question - # name = "Q2" - # # total_score = 2 - # type_id = 5 - # slide_id = 5 - # response, body = get( - # client, - # f"/api/competitions/{CID}/questions/", - # headers=headers, - # ) - # # print(f"357: {body['items']}") - # assert response.status_code == codes.OK - # assert body["count"] == 1 - # item_question = body["items"][0] - # # print(f"338: {item_question}") - # assert item_question["name"] == name - # # assert item_question["total_score"] == total_score - # assert item_question["type_id"] == type_id - # assert item_question["slide_id"] == slide_id - # Add question name = "Nytt namn" - # total_score = 2 type_id = 2 - SID = 5 + slide_order = 1 response, item_question = post( client, - f"/api/competitions/{CID}/slides/{SID}/questions", + f"/api/competitions/{CID}/slides/{slide_order}/questions", {"name": name, "type_id": type_id}, headers=headers, ) - num_questions += 1 + num_questions = 4 assert response.status_code == codes.OK assert item_question["name"] == name - # # assert item_question["total_score"] == total_score assert item_question["type"]["id"] == type_id - assert item_question["slide_id"] == SID - # Checks number of questions - response, body = get(client, f"/api/competitions/{CID}/questions", headers=headers) - assert response.status_code == codes.OK - assert body["count"] == num_questions - - # Try to get question in another competition - QID = 1 - response, item_question = get(client, f"/api/competitions/{CID}/slides/{SID}/questions/{QID}", headers=headers) - assert response.status_code == codes.NOT_FOUND - # Get question - QID = 4 - SID = 4 - response, item_question = get(client, f"/api/competitions/{CID}/slides/{SID}/questions/{QID}", headers=headers) - assert response.status_code == codes.OK - assert item_question["id"] == QID - - # Try to edit question in another competition - name = "Nyare namn" - # total_score = 2 - type_id = 3 - SID = 1 - QID = 1 - response, _ = put( - client, - f"/api/competitions/{CID}/slides/{SID}/questions/{QID}", - {"name": name, "type_id": type_id}, - headers=headers, - ) - assert response.status_code == codes.NOT_FOUND - # Checks number of questions - response, body = get(client, f"/api/competitions/{CID}/questions", headers=headers) - assert response.status_code == codes.OK - assert body["count"] == num_questions - - # Edit question - name = "Nyare namn" - # total_score = 2 - type_id = 3 - SID = 4 - NEW_SID = 5 - QID = 4 - assert item_question["name"] != name - # assert item_question["total_score"] != total_score - assert item_question["type"]["id"] != type_id - assert item_question["slide_id"] != NEW_SID - response, item_question = put( - client, - f"/api/competitions/{CID}/slides/{SID}/questions/{QID}", - # {"name": name, "total_score": total_score, "type_id": type_id, "slide_id": slide_id}, - {"name": name, "type_id": type_id, "slide_id": NEW_SID}, - headers=headers, - ) - assert response.status_code == codes.OK - assert item_question["name"] == name - # # assert item_question["total_score"] == total_score - assert item_question["type"]["id"] == type_id - assert item_question["slide_id"] == NEW_SID # Checks number of questions response, body = get(client, f"/api/competitions/{CID}/questions", headers=headers) assert response.status_code == codes.OK assert body["count"] == num_questions - + """ # Delete question - response, _ = delete(client, f"/api/competitions/{CID}/slides/{NEW_SID}/questions/{QID}", headers=headers) + response, _ = delete(client, f"/api/competitions/{CID}/slides/{slide_order}/questions/{QID}", headers=headers) num_questions -= 1 assert response.status_code == codes.NO_CONTENT @@ -437,5 +350,6 @@ def test_question_api(client): assert body["count"] == num_questions # Tries to delete question again - response, _ = delete(client, f"/api/competitions/{CID}/slides/{NEW_SID}/questions/{QID}", headers=headers) + response, _ = delete(client, f"/api/competitions/{CID}/slides/{NEW_slide_order}/questions/{QID}", headers=headers) assert response.status_code == codes.NOT_FOUND + """ diff --git a/server/tests/test_helpers.py b/server/tests/test_helpers.py index 6b6bf7a0..fbd77d9e 100644 --- a/server/tests/test_helpers.py +++ b/server/tests/test_helpers.py @@ -39,23 +39,25 @@ def add_default_values(): dbc.add.user("test@test.se", "password", item_admin.id, item_city.id, "Olle Olsson") # Add competitions - dbc.add.competition("Tom tävling", 2012, item_city.id) + item_competition = dbc.add.competition("Tom tävling", 2012, item_city.id) for j in range(2): item_comp = dbc.add.competition(f"Tävling {j}", 2012, item_city.id) + # Add two more slides to competition + dbc.add.slide(item_comp) + dbc.add.slide(item_comp) # Add slides - for i in range(len(question_types)): - # Add slide to competition - item_slide = dbc.add.slide(item_comp) - + i = 1 + for item_slide in item_comp.slides: # Populate slide with data item_slide.title = f"Title {i}" item_slide.body = f"Body {i}" item_slide.timer = 100 + i # item_slide.settings = "{}" - + dbc.utils.commit_and_refresh(item_slide) # Add question to competition - dbc.add.question(name=f"Q{i+1}", total_score=i + 1, type_id=i + 1, item_slide=item_slide) + dbc.add.question(name=f"Q{i}", total_score=i, type_id=1, item_slide=item_slide) + i += 1 def get_body(response): @@ -124,37 +126,12 @@ def assert_object_values(obj, values): # Changes order of slides -def change_order_test(client, cid, sid, order, h): - sid_at_order = -1 - actual_order = 0 if order < 0 else order # used to find the slide_id - response, body = get(client, f"/api/competitions/{cid}/slides", headers=h) - assert response.status_code == codes.OK - - # Finds the slide_id of the slide that will be swapped with - for item_slide in body["items"]: - if item_slide["order"] == actual_order: - assert item_slide["id"] != sid - sid_at_order = item_slide["id"] - assert sid_at_order != -1 - - # Gets old versions of slides - response, item_slide_10 = get(client, f"/api/competitions/{cid}/slides/{sid}", headers=h) +def change_order_test(client, cid, order, new_order, h): + response, new_order_body = get(client, f"/api/competitions/{cid}/slides/{new_order}", headers=h) assert response.status_code == codes.OK - response, item_slide_20 = get(client, f"/api/competitions/{cid}/slides/{sid_at_order}", headers=h) + response, order_body = get(client, f"/api/competitions/{cid}/slides/{order}", headers=h) assert response.status_code == codes.OK # Changes order - response, _ = put( - client, f"/api/competitions/{cid}/slides/{sid}/order", {"order": order}, headers=h - ) # uses order to be able to test negative order + response, _ = put(client, f"/api/competitions/{cid}/slides/{order}/order", {"order": new_order}, headers=h) assert response.status_code == codes.OK - - # Gets new versions of slides - response, item_slide_11 = get(client, f"/api/competitions/{cid}/slides/{sid}", headers=h) - assert response.status_code == codes.OK - response, item_slide_21 = get(client, f"/api/competitions/{cid}/slides/{sid_at_order}", headers=h) - assert response.status_code == codes.OK - - # Checks that the order was indeed swapped - assert item_slide_10["order"] == item_slide_21["order"] - assert item_slide_11["order"] == item_slide_20["order"] -- GitLab