diff --git a/client/src/pages/admin/competitions/AddCompetition.tsx b/client/src/pages/admin/competitions/AddCompetition.tsx index e9666770191249f79fb0f2c5295d6fb43875bb04..0ce9a6ffa4601cf543383e7efa3bdf55eee259f8 100644 --- a/client/src/pages/admin/competitions/AddCompetition.tsx +++ b/client/src/pages/admin/competitions/AddCompetition.tsx @@ -10,10 +10,17 @@ import { City } from '../../../interfaces/ApiModels' import { AddCompetitionModel, FormModel } from '../../../interfaces/FormModels' import { AddButton, AddContent, AddForm } from '../styledComp' +/** + * Component description: + * This component handles the functionality when adding a competition to the system + * This component is a child component to CompetitionManager.tsx + */ + type formType = FormModel<AddCompetitionModel> const noCitySelected = 'Välj stad' +//Description of the form and what is required const competitionSchema: Yup.SchemaOf<formType> = Yup.object({ model: Yup.object() .shape({ @@ -45,20 +52,25 @@ const AddCompetition: React.FC = (props: any) => { const dispatch = useAppDispatch() const id = open ? 'simple-popover' : undefined const currentYear = new Date().getFullYear() + + // Handles the actual submition to the database const handleCompetitionSubmit = async (values: formType, actions: FormikHelpers<formType>) => { + // The parameters sent const params = { name: values.model.name, year: values.model.year, city_id: selectedCity?.id as number, } + await axios - .post('/competitions', params) + .post('/competitions', params) // send to database .then(() => { - actions.resetForm() + actions.resetForm() // reset the form setAnchorEl(null) - dispatch(getCompetitions()) + dispatch(getCompetitions()) // refresh competitions setSelectedCity(undefined) }) + // if the post request fails .catch(({ response }) => { console.warn(response.data) if (response.data && response.data.message) @@ -83,6 +95,12 @@ const AddCompetition: React.FC = (props: any) => { > Ny Tävling </AddButton> + + {/** + * The "pop up" menu for adding a competition + * contains 3 fields; Name, Region and Year + * + */} <Popover id={id} open={open} diff --git a/client/src/pages/admin/competitions/CompetitionManager.tsx b/client/src/pages/admin/competitions/CompetitionManager.tsx index 81e644adc4f2b62661193ebc5402c5b81d0ec693..e7085a5e3f4478355e4dffe401523687f938e3ea 100644 --- a/client/src/pages/admin/competitions/CompetitionManager.tsx +++ b/client/src/pages/admin/competitions/CompetitionManager.tsx @@ -21,6 +21,13 @@ import { CompetitionFilterParams } from '../../../interfaces/FilterParams' import { FilterContainer, RemoveMenuItem, TopBar, YearFilterTextField } from '../styledComp' import AddCompetition from './AddCompetition' +/** + * Component description: + * This component shows a list of all the competitions which a user can search through + * We can also start, duplicate or delete a competition + */ + +// Use defined styling const useStyles = makeStyles((theme: Theme) => createStyles({ table: { @@ -41,6 +48,7 @@ const CompetitionManager: React.FC = (props: any) => { const filterParams = useAppSelector((state) => state.competitions.filterParams) const competitionTotal = useAppSelector((state) => state.competitions.total) const cities = useAppSelector((state) => state.cities.cities) + const classes = useStyles() const noFilterText = 'Alla' const dispatch = useAppDispatch() @@ -59,6 +67,7 @@ const CompetitionManager: React.FC = (props: any) => { dispatch(getCompetitions()) }, []) + // Search funtion to search for a specific string const onSearchChange = (event: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => { if (timerHandle) { clearTimeout(timerHandle) @@ -69,13 +78,14 @@ const CompetitionManager: React.FC = (props: any) => { dispatch(setFilterParams({ ...filterParams, name: event.target.value })) } + // Function to remove a competition from the systems database const handleDeleteCompetition = async () => { if (activeId) { await axios .delete(`/competitions/${activeId}`) .then(() => { setAnchorEl(null) - dispatch(getCompetitions()) + dispatch(getCompetitions()) // refresh the competition list }) .catch(({ response }) => { console.warn(response.data) @@ -177,6 +187,7 @@ const CompetitionManager: React.FC = (props: any) => { ))} </TableBody> </Table> + {/** We can't find any competitions at all or with a specific filter */} {(!competitions || competitions.length === 0) && ( <Typography>Inga tävlingar hittades med nuvarande filter</Typography> )} diff --git a/client/src/pages/admin/users/EditUser.tsx b/client/src/pages/admin/users/EditUser.tsx index a1be7c875b031080ccc16ca854a77ae7c2534cb0..7db5aeecd2be0c3f3f94c22a0c459a71cbc47aa4 100644 --- a/client/src/pages/admin/users/EditUser.tsx +++ b/client/src/pages/admin/users/EditUser.tsx @@ -1,6 +1,11 @@ import { Button, createStyles, + Dialog, + DialogActions, + DialogContent, + DialogContentText, + DialogTitle, FormControl, InputLabel, makeStyles, @@ -8,6 +13,8 @@ import { Popover, TextField, Theme, + useMediaQuery, + useTheme, } from '@material-ui/core' import MoreHorizIcon from '@material-ui/icons/MoreHoriz' import { Alert, AlertTitle } from '@material-ui/lab' @@ -62,6 +69,11 @@ type UserIdProps = { } const EditUser = ({ user }: UserIdProps) => { + // for dialog alert + const [openAlert, setOpen] = React.useState(false) + const theme = useTheme() + const fullScreen = useMediaQuery(theme.breakpoints.down('sm')) + const dispatch = useAppDispatch() const classes = useStyles() @@ -87,21 +99,25 @@ const EditUser = ({ user }: UserIdProps) => { setAnchorEl(event.currentTarget) } const handleClose = () => { + setOpen(false) setAnchorEl(null) } + const handleVerifyDelete = () => { + setOpen(true) + } + const handleDeleteUsers = async () => { - if (confirm('Are u sure?')) { - await axios - .delete(`/auth/delete/${user.id}`) - .then(() => { - setAnchorEl(null) - dispatch(getSearchUsers()) - }) - .catch(({ response }) => { - console.warn(response.data) - }) - } + setOpen(false) + await axios + .delete(`/auth/delete/${user.id}`) + .then(() => { + setAnchorEl(null) + dispatch(getSearchUsers()) + }) + .catch(({ response }) => { + console.warn(response.data) + }) } const handleSubmit = async (values: formType, actions: FormikHelpers<formType>) => { @@ -273,7 +289,7 @@ const EditUser = ({ user }: UserIdProps) => { Ändra </Button> <Button - onClick={handleDeleteUsers} + onClick={handleVerifyDelete} className={classes.deleteButton} fullWidth variant="contained" @@ -281,6 +297,27 @@ const EditUser = ({ user }: UserIdProps) => { > Ta bort </Button> + <Dialog + fullScreen={fullScreen} + open={openAlert} + onClose={handleClose} + aria-labelledby="responsive-dialog-title" + > + <DialogTitle id="responsive-dialog-title">{'Ta bort användare?'}</DialogTitle> + <DialogContent> + <DialogContentText> + Är du säker på att du vill ta bort användaren och all dess information från systemet? + </DialogContentText> + </DialogContent> + <DialogActions> + <Button autoFocus onClick={handleClose} color="primary"> + Avbryt + </Button> + <Button onClick={handleDeleteUsers} color="primary" autoFocus> + Ta bort + </Button> + </DialogActions> + </Dialog> {formik.errors.error && ( <Alert severity="error"> diff --git a/client/src/pages/admin/users/ResponsiveDialog.tsx b/client/src/pages/admin/users/ResponsiveDialog.tsx new file mode 100644 index 0000000000000000000000000000000000000000..e29a6786826e854bebf888be5b7cd904e0ec1063 --- /dev/null +++ b/client/src/pages/admin/users/ResponsiveDialog.tsx @@ -0,0 +1,53 @@ +import React from 'react'; +import Button from '@material-ui/core/Button'; +import Dialog from '@material-ui/core/Dialog'; +import DialogActions from '@material-ui/core/DialogActions'; +import DialogContent from '@material-ui/core/DialogContent'; +import DialogContentText from '@material-ui/core/DialogContentText'; +import DialogTitle from '@material-ui/core/DialogTitle'; +import useMediaQuery from '@material-ui/core/useMediaQuery'; +import { useTheme } from '@material-ui/core/styles'; + +export default function ResponsiveDialog() { + const [open, setOpen] = React.useState(false); + const theme = useTheme(); + const fullScreen = useMediaQuery(theme.breakpoints.down('sm')); + + const handleClickOpen = () => { + setOpen(true); + }; + + const handleClose = () => { + setOpen(false); + }; + + return ( + <div> + <Button variant="outlined" color="primary" onClick={handleClickOpen}> + Open responsive dialog + </Button> + <Dialog + fullScreen={fullScreen} + open={open} + onClose={handleClose} + aria-labelledby="responsive-dialog-title" + > + <DialogTitle id="responsive-dialog-title">{"Use Google's location service?"}</DialogTitle> + <DialogContent> + <DialogContentText> + Let Google help apps determine location. This means sending anonymous location data to + Google, even when no apps are running. + </DialogContentText> + </DialogContent> + <DialogActions> + <Button autoFocus onClick={handleClose} color="primary"> + Disagree + </Button> + <Button onClick={handleClose} color="primary" autoFocus> + Agree + </Button> + </DialogActions> + </Dialog> + </div> + ); +} \ No newline at end of file diff --git a/client/src/pages/views/AudienceViewPage.tsx b/client/src/pages/views/AudienceViewPage.tsx index 45ec132d162b1099bc9e77e6b68c87dc35ea65b6..00a821f35a4ad05354954fdb50694012cb2bda46 100644 --- a/client/src/pages/views/AudienceViewPage.tsx +++ b/client/src/pages/views/AudienceViewPage.tsx @@ -1,7 +1,8 @@ import React from 'react' +import SlideDisplay from './components/SlideDisplay' const AudienceViewPage: React.FC = () => { - return <div>Publik</div> + return <SlideDisplay /> } export default AudienceViewPage diff --git a/client/src/pages/views/JudgeViewPage.tsx b/client/src/pages/views/JudgeViewPage.tsx index 12e866a085b506df3d22eb38151dcca4acd222e6..60018624457a6242cbf1f27f4c8201fdb12ebb89 100644 --- a/client/src/pages/views/JudgeViewPage.tsx +++ b/client/src/pages/views/JudgeViewPage.tsx @@ -1,4 +1,4 @@ -import { Divider, List, ListItemText } from '@material-ui/core' +import { Divider, List, ListItemText, Typography } from '@material-ui/core' import { createStyles, makeStyles, Theme } from '@material-ui/core/styles' import React, { useEffect, useState } from 'react' import { useParams } from 'react-router-dom' @@ -10,6 +10,7 @@ import { } from '../../actions/presentation' import { useAppDispatch, useAppSelector } from '../../hooks' import { ViewParams } from '../../interfaces/ViewParams' +import { socket_connect } from '../../sockets' import { SlideListItem } from '../presentationEditor/styled' import JudgeScoreDisplay from './components/JudgeScoreDisplay' import SlideDisplay from './components/SlideDisplay' @@ -43,17 +44,20 @@ const JudgeViewPage: React.FC = () => { const { id, code }: ViewParams = useParams() const dispatch = useAppDispatch() const [activeSlideIndex, setActiveSlideIndex] = useState<number>(0) - useEffect(() => { - dispatch(getPresentationCompetition(id)) - dispatch(getPresentationTeams(id)) - dispatch(setPresentationCode(code)) - }, []) const teams = useAppSelector((state) => state.presentation.teams) const slides = useAppSelector((state) => state.presentation.competition.slides) const handleSelectSlide = (index: number) => { setActiveSlideIndex(index) dispatch(setCurrentSlide(slides[index])) } + + useEffect(() => { + socket_connect() + dispatch(getPresentationCompetition(id)) + dispatch(getPresentationTeams(id)) + dispatch(setPresentationCode(code)) + }, []) + return ( <div> <JudgeAppBar position="fixed"> @@ -80,6 +84,7 @@ const JudgeViewPage: React.FC = () => { button key={slide.id} > + <Typography variant="h6">Slide ID: {slide.id} </Typography> <ListItemText primary={slide.title} /> </SlideListItem> ))} @@ -103,7 +108,6 @@ const JudgeViewPage: React.FC = () => { ))} </List> </RightDrawer> - aaa <Content leftDrawerWidth={leftDrawerWidth} rightDrawerWidth={rightDrawerWidth}> <div className={classes.toolbar} /> <SlideDisplay /> diff --git a/client/src/pages/views/PresenterViewPage.tsx b/client/src/pages/views/PresenterViewPage.tsx index 22a672bac56b5cae4825128cd21216835f425d57..346aabcf5eb5ed131b2d2f44c7c57a9f1d5ce035 100644 --- a/client/src/pages/views/PresenterViewPage.tsx +++ b/client/src/pages/views/PresenterViewPage.tsx @@ -1,61 +1,183 @@ -import { List, ListItem, Popover } from '@material-ui/core' +import { + Button, + Dialog, + DialogActions, + DialogContent, + DialogContentText, + DialogTitle, + List, + ListItem, + Popover, + Tooltip, + Typography, + useMediaQuery, + useTheme, +} from '@material-ui/core' +import AssignmentIcon from '@material-ui/icons/Assignment' +import BackspaceIcon from '@material-ui/icons/Backspace' +import ChevronLeftIcon from '@material-ui/icons/ChevronLeft' import ChevronRightIcon from '@material-ui/icons/ChevronRight' +import PlayArrowIcon from '@material-ui/icons/PlayArrow' +import TimerIcon from '@material-ui/icons/Timer' import React, { useEffect } from 'react' import { useHistory, useParams } from 'react-router-dom' import { getPresentationCompetition, getPresentationTeams, setPresentationCode } from '../../actions/presentation' import { useAppDispatch, useAppSelector } from '../../hooks' import { ViewParams } from '../../interfaces/ViewParams' +import { + socketEndPresentation, + socketSetSlide, + socketSetSlideNext, + socketSetSlidePrev, + socketStartPresentation, + socketStartTimer, + socket_connect, +} from '../../sockets' import SlideDisplay from './components/SlideDisplay' -import SocketTest from './components/SocketTest' import Timer from './components/Timer' -import { PresenterButton, PresenterContainer, PresenterFooter, PresenterHeader } from './styled' +import { + PresenterButton, + PresenterContainer, + PresenterFooter, + PresenterHeader, + SlideCounter, + ToolBarContainer, +} from './styled' const PresenterViewPage: React.FC = () => { + // for dialog alert + const [openAlert, setOpen] = React.useState(false) + const theme = useTheme() + const fullScreen = useMediaQuery(theme.breakpoints.down('sm')) + const teams = useAppSelector((state) => state.presentation.teams) const [anchorEl, setAnchorEl] = React.useState<HTMLButtonElement | null>(null) const { id, code }: ViewParams = useParams() + const presentation = useAppSelector((state) => state.presentation) const history = useHistory() const dispatch = useAppDispatch() + useEffect(() => { + socket_connect() + socketSetSlide dispatch(getPresentationCompetition(id)) dispatch(getPresentationTeams(id)) dispatch(setPresentationCode(code)) }, []) + const handleOpenPopover = (event: React.MouseEvent<HTMLButtonElement>) => { setAnchorEl(event.currentTarget) } const handleClose = () => { + setOpen(false) setAnchorEl(null) } - const handleNextSlidePressed = () => { - // dispatch(setCurrentSlideNext()) - // syncSlide() + + const handleVerifyExit = () => { + setOpen(true) } - const handlePreviousSlidePressed = () => { - // dispatch(setCurrentSlidePrevious()) - // syncSlide() + + const startCompetition = () => { + socketStartPresentation() + const haveStarted = true + console.log('You have started the competition! GLHF!') + console.log(haveStarted) + } + + const endCompetition = () => { + setOpen(false) + const haveStarted = false + socketEndPresentation() + history.push('/admin') + window.location.reload(false) // TODO: fix this ugly hack, we "need" to refresh site to be able to run the competition correctly again } return ( <PresenterContainer> <PresenterHeader> - <PresenterButton onClick={handleOpenPopover} color="primary" variant="contained"> - Visa ställning - </PresenterButton> - <PresenterButton onClick={() => history.push('/admin')} variant="contained" color="secondary"> - Avsluta tävling - </PresenterButton> + <Tooltip title="Avsluta tävling" arrow> + <PresenterButton onClick={handleVerifyExit} variant="contained" color="secondary"> + <BackspaceIcon fontSize="large" /> + </PresenterButton> + </Tooltip> + <Dialog + fullScreen={fullScreen} + open={openAlert} + onClose={handleClose} + aria-labelledby="responsive-dialog-title" + > + <DialogTitle id="responsive-dialog-title">{'Vill du avsluta tävlingen?'}</DialogTitle> + <DialogContent> + <DialogContentText> + Genom att avsluta tävlingen kommer den avslutas för alla. Du kommer gå tillbaka till startsidan. + </DialogContentText> + </DialogContent> + <DialogActions> + <Button autoFocus onClick={handleClose} color="primary"> + Avbryt + </Button> + <Button onClick={endCompetition} color="primary" autoFocus> + Avsluta tävling + </Button> + </DialogActions> + </Dialog> + <SlideCounter> + <Typography variant="h3"> + {presentation.slide.id} / {presentation.competition.slides.length} + </Typography> + </SlideCounter> </PresenterHeader> <SlideDisplay /> <PresenterFooter> - <PresenterButton onClick={handlePreviousSlidePressed} variant="contained"> - <ChevronRightIcon fontSize="large" /> - </PresenterButton> - <SocketTest></SocketTest> - <Timer></Timer> - <PresenterButton onClick={handleNextSlidePressed} variant="contained"> - <ChevronRightIcon fontSize="large" /> - </PresenterButton> + <ToolBarContainer> + <Tooltip title="Previous Slide" arrow> + <PresenterButton onClick={socketSetSlidePrev} variant="contained"> + <ChevronLeftIcon fontSize="large" /> + </PresenterButton> + </Tooltip> + + <Tooltip title="Start Presentation" arrow> + <PresenterButton onClick={startCompetition} variant="contained"> + <PlayArrowIcon fontSize="large" /> + </PresenterButton> + </Tooltip> + + {/* + // This creates a join button, but presenter should not join others, others should join presenter + <Tooltip title="Join Presentation" arrow> + <PresenterButton onClick={socketJoinPresentation} variant="contained"> + <GroupAddIcon fontSize="large" /> + </PresenterButton> + </Tooltip> + + + // This creates another end button, it might not be needed since we already have one + <Tooltip title="End Presentation" arrow> + <PresenterButton onClick={socketEndPresentation} variant="contained"> + <CancelIcon fontSize="large" /> + </PresenterButton> + </Tooltip> + */} + + <Tooltip title="Start Timer" arrow> + <PresenterButton onClick={socketStartTimer} variant="contained"> + <TimerIcon fontSize="large" /> + <Timer></Timer> + </PresenterButton> + </Tooltip> + + <Tooltip title="Scoreboard" arrow> + <PresenterButton onClick={handleOpenPopover} variant="contained"> + <AssignmentIcon fontSize="large" /> + </PresenterButton> + </Tooltip> + + <Tooltip title="Next Slide" arrow> + <PresenterButton onClick={socketSetSlideNext} variant="contained"> + <ChevronRightIcon fontSize="large" /> + </PresenterButton> + </Tooltip> + </ToolBarContainer> </PresenterFooter> <Popover open={Boolean(anchorEl)} @@ -71,6 +193,9 @@ const PresenterViewPage: React.FC = () => { }} > <List> + {/** TODO: + * Fix scoreboard + */} {teams.map((team) => ( <ListItem key={team.id}>{team.name} score: 20</ListItem> ))} diff --git a/client/src/pages/views/components/SlideDisplay.tsx b/client/src/pages/views/components/SlideDisplay.tsx index 1c10f9cf03ac9c56d555afaa22dabd2dcaf23752..7ecffac50a735eade79ebcad046040045e37dd93 100644 --- a/client/src/pages/views/components/SlideDisplay.tsx +++ b/client/src/pages/views/components/SlideDisplay.tsx @@ -5,10 +5,15 @@ import { SlideContainer } from './styled' const SlideDisplay: React.FC = () => { const currentSlide = useAppSelector((state) => state.presentation.slide) + return ( - <SlideContainer> - <Typography variant="h3">{currentSlide.title}</Typography> - </SlideContainer> + <div> + <SlideContainer> + <Typography variant="h3">Slide Title: {currentSlide.title} </Typography> + <Typography variant="h3">Timer: {currentSlide.timer} </Typography> + <Typography variant="h3">Slide ID: {currentSlide.id} </Typography> + </SlideContainer> + </div> ) } diff --git a/client/src/pages/views/components/Timer.tsx b/client/src/pages/views/components/Timer.tsx index b4401a696c42d25cc8296f22059a780feb00e8e9..0cbd1fdfdbf0d63dc6ef5e687539400510af770e 100644 --- a/client/src/pages/views/components/Timer.tsx +++ b/client/src/pages/views/components/Timer.tsx @@ -38,8 +38,7 @@ const Timer: React.FC = (props: any) => { return ( <> - <div>Timer: {props.timer.value}</div> - <div>Enabled: {props.timer.enabled.toString()}</div> + <div>{props.timer.value}</div> </> ) } diff --git a/client/src/pages/views/components/styled.tsx b/client/src/pages/views/components/styled.tsx index 0034c39beceb8bcb4371589758959c7cae1dd1ab..b522b20d3548b4f11864373d5a91088424c45a58 100644 --- a/client/src/pages/views/components/styled.tsx +++ b/client/src/pages/views/components/styled.tsx @@ -3,7 +3,14 @@ import styled from 'styled-components' export const SlideContainer = styled.div` display: flex; + flex-direction: column; + margin-left: auto; + margin-right: auto; + margin-top: 5%; justify-content: center; + background-color: grey; + width: 1280px; + height: 720px; ` export const ScoreDisplayContainer = styled.div` diff --git a/client/src/pages/views/styled.tsx b/client/src/pages/views/styled.tsx index d4a9cf9eaf0a7a44c3f31b31a21e3c30b7222812..1f3a61c61789964b96346f8734422ca8c83518b7 100644 --- a/client/src/pages/views/styled.tsx +++ b/client/src/pages/views/styled.tsx @@ -50,8 +50,15 @@ export const PresenterFooter = styled.div` export const PresenterButton = styled(Button)` width: 100px; height: 100px; - padding-top: 16px; - padding-bottom: 16px; + margin-left: 16px; + margin-right: 16px; + margin-top: 16px; +` + +export const SlideCounter = styled(Button)` + margin-left: 16px; + margin-right: 16px; + margin-top: 16px; ` export const PresenterContainer = styled.div` @@ -61,6 +68,18 @@ export const PresenterContainer = styled.div` height: 100%; ` +export const ToolBarContainer = styled.div` + align-self: center; + display: flex; + flex-direction: row; + justify-content: space-between; + height: 100%; + width: auto; + margin-right: auto; + margin-left: auto; + margin-bottom: 20px; +` + interface DrawerProps { width: number } diff --git a/client/src/sockets.ts b/client/src/sockets.ts index 874bfc46ad3a003013d32c31585704aceb527f84..54f84c39840acec184fa023545c1a15332eecdbc 100644 --- a/client/src/sockets.ts +++ b/client/src/sockets.ts @@ -38,22 +38,27 @@ export const socket_connect = () => { export const socketStartPresentation = () => { socket.emit('start_presentation', { competition_id: store.getState().presentation.competition.id }) + console.log('START PRESENTATION') } export const socketJoinPresentation = () => { - socket.emit('join_presentation', { code: 'OEM1V4' }) // TODO: Send code gotten from auth/login/<code> api call + socket.emit('join_presentation', { code: 'CO0ART' }) // TODO: Send code gotten from auth/login/<code> api call + console.log('JOIN PRESENTATION') } export const socketEndPresentation = () => { socket.emit('end_presentation', { competition_id: store.getState().presentation.competition.id }) + console.log('END PRESENTATION') } export const socketSetSlideNext = () => { socketSetSlide(store.getState().presentation.slide.order + 1) // TODO: Check that this slide exists + console.log('NEXT SLIDE +1') } export const socketSetSlidePrev = () => { socketSetSlide(store.getState().presentation.slide.order - 1) // TODO: Check that this slide exists + console.log('PREVIOUS SLIDE -1') } export const socketSetSlide = (slide_order: number) => { @@ -69,6 +74,7 @@ export const socketSetSlide = (slide_order: number) => { } export const socketSetTimer = (timer: Timer) => { + console.log('SET TIMER') socket.emit('set_timer', { competition_id: store.getState().presentation.competition.id, timer: timer, @@ -76,5 +82,6 @@ export const socketSetTimer = (timer: Timer) => { } export const socketStartTimer = () => { + console.log('START TIMER') socketSetTimer({ enabled: true, value: store.getState().presentation.timer.value }) } diff --git a/server/app/apis/alternatives.py b/server/app/apis/alternatives.py index 2061b32fb67d51d70be2fbc6018ca68188ec9175..3586242e60874f5e8436fc8a899fd8e53e4dbff7 100644 --- a/server/app/apis/alternatives.py +++ b/server/app/apis/alternatives.py @@ -37,7 +37,7 @@ class QuestionAlternatives(Resource): def put(self, competition_id, slide_order, QID, AID): args = question_alternative_parser.parse_args(strict=True) item = dbc.get.one(QuestionAlternative, AID) - item = dbc.edit.question_alternative(item, **args) + item = dbc.edit.default(item, **args) return item_response(schema.dump(item)) @check_jwt(editor=True) diff --git a/server/app/apis/answers.py b/server/app/apis/answers.py index 95ce38dc6d618f297b6763a5d4fd15527df1361b..fb70dbb13177e21344b5773244b7491186020839 100644 --- a/server/app/apis/answers.py +++ b/server/app/apis/answers.py @@ -37,5 +37,5 @@ class QuestionAnswers(Resource): def put(self, competition_id, TID, AID): args = question_answer_edit_parser.parse_args(strict=True) item = dbc.get.one(QuestionAnswer, AID) - item = dbc.edit.question_answer(item, **args) + item = dbc.edit.default(item, **args) return item_response(schema.dump(item)) diff --git a/server/app/apis/competitions.py b/server/app/apis/competitions.py index 950fb8812feee927f2b86e01122f23ed66d7a4c5..eeea67bb49fb117186f9b8ee3383bac313732677 100644 --- a/server/app/apis/competitions.py +++ b/server/app/apis/competitions.py @@ -42,7 +42,7 @@ class Competitions(Resource): def put(self, competition_id): args = competition_parser.parse_args(strict=True) item = dbc.get.one(Competition, competition_id) - item = dbc.edit.competition(item, **args) + item = dbc.edit.default(item, **args) return item_response(schema.dump(item)) diff --git a/server/app/apis/components.py b/server/app/apis/components.py index c8620bc37f77b60f718240d17e7c59c4623e06e8..6e514039f207d11e4e453dd3f4781282ba5ee145 100644 --- a/server/app/apis/components.py +++ b/server/app/apis/components.py @@ -25,7 +25,7 @@ class ComponentByID(Resource): def put(self, competition_id, slide_order, component_id): args = component_parser.parse_args() item = dbc.get.one(Component, component_id) - item = dbc.edit.component(item, **args) + item = dbc.edit.default(item, **args) return item_response(schema.dump(item)) @check_jwt(editor=True) diff --git a/server/app/apis/questions.py b/server/app/apis/questions.py index 2ae7e91586c5e8a7dcc4ff2e63021b7e898e99c7..74ca76082fa7ef9ebff568e8539b54b4628e8f2b 100644 --- a/server/app/apis/questions.py +++ b/server/app/apis/questions.py @@ -32,25 +32,25 @@ class QuestionListForSlide(Resource): return item_response(schema.dump(item)) -@api.route("/slides/<slide_order>/questions/<question_order>") -@api.param("competition_id, slide_order, question_order") +@api.route("/slides/<slide_order>/questions/<question_id>") +@api.param("competition_id, slide_order, question_id") class QuestionById(Resource): @check_jwt(editor=True) - def get(self, competition_id, slide_order, question_order): - item_question = dbc.get.question(competition_id, slide_order, question_order) + def get(self, competition_id, slide_order, question_id): + item_question = dbc.get.question(competition_id, slide_order, question_id) return item_response(schema.dump(item_question)) @check_jwt(editor=True) - def put(self, competition_id, slide_order, question_order): + def put(self, competition_id, slide_order, question_id): args = question_parser.parse_args(strict=True) - item_question = dbc.get.question(competition_id, slide_order, question_order) - item_question = dbc.edit.question(item_question, **args) + item_question = dbc.get.question(competition_id, slide_order, question_id) + item_question = dbc.edit.default(item_question, **args) return item_response(schema.dump(item_question)) @check_jwt(editor=True) - def delete(self, competition_id, slide_order, question_order): - item_question = dbc.get.question(competition_id, slide_order, question_order) + def delete(self, competition_id, slide_order, question_id): + item_question = dbc.get.question(competition_id, slide_order, question_id) dbc.delete.question(item_question) return {}, codes.NO_CONTENT diff --git a/server/app/apis/slides.py b/server/app/apis/slides.py index 2724325fb1a227ea3b8d8a9b4d0c9e7cc01a8344..7c94caa22c1cd096086fd25453f2601e26b7b606 100644 --- a/server/app/apis/slides.py +++ b/server/app/apis/slides.py @@ -40,11 +40,9 @@ class Slides(Resource): @check_jwt(editor=True) def put(self, competition_id, slide_order): args = slide_parser.parse_args(strict=True) - title = args.get("title") - timer = args.get("timer") item_slide = dbc.get.slide(competition_id, slide_order) - item_slide = dbc.edit.slide(item_slide, title, timer) + item_slide = dbc.edit.default(item_slide, **args) return item_response(schema.dump(item_slide)) diff --git a/server/app/apis/teams.py b/server/app/apis/teams.py index 9a88ffb87348f7f483f019f31dde2bf3ffcbcc0a..8b66a48cca7a7a0bd6e9d651b206d7f1b38f4cc7 100644 --- a/server/app/apis/teams.py +++ b/server/app/apis/teams.py @@ -28,30 +28,30 @@ class TeamsList(Resource): return item_response(schema.dump(item_team)) -@api.route("/<TID>") -@api.param("competition_id,TID") +@api.route("/<team_id>") +@api.param("competition_id,team_id") class Teams(Resource): @jwt_required @check_jwt(editor=True) - def get(self, competition_id, TID): - item = dbc.get.team(competition_id, TID) + def get(self, competition_id, team_id): + item = dbc.get.team(competition_id, team_id) return item_response(schema.dump(item)) @jwt_required @check_jwt(editor=True) - def delete(self, competition_id, TID): - item_team = dbc.get.team(competition_id, TID) + def delete(self, competition_id, team_id): + item_team = dbc.get.team(competition_id, team_id) dbc.delete.team(item_team) return {}, codes.NO_CONTENT @jwt_required @check_jwt(editor=True) - def put(self, competition_id, TID): + def put(self, competition_id, team_id): args = team_parser.parse_args(strict=True) name = args.get("name") - item_team = dbc.get.team(competition_id, TID) + item_team = dbc.get.team(competition_id, team_id) - item_team = dbc.edit.team(item_team, name=name, competition_id=competition_id) + item_team = dbc.edit.default(item_team, name=name, competition_id=competition_id) return item_response(schema.dump(item_team)) diff --git a/server/app/apis/users.py b/server/app/apis/users.py index b9dba528a1a3529ec9e340d4418e44bc2e20fedb..38b24d6a9df51842c9e39364438de40f42fae2cd 100644 --- a/server/app/apis/users.py +++ b/server/app/apis/users.py @@ -19,7 +19,16 @@ def edit_user(item_user, args): if User.query.filter(User.email == args["email"]).count() > 0: api.abort(codes.BAD_REQUEST, "Email is already in use") - return dbc.edit.user(item_user, **args) + name = args.get("name") + if name: + args["name"] = args["name"].title() + """ + try: + args["name"] = args.get("name").title() + except Exception: + pass +""" + return dbc.edit.default(item_user, **args) @api.route("/") diff --git a/server/app/core/rich_schemas.py b/server/app/core/rich_schemas.py index bb1b139107ed407f8f9d859498fd98da2cf9243e..8852c392c4a060a065f618a66393e55b8a2627a8 100644 --- a/server/app/core/rich_schemas.py +++ b/server/app/core/rich_schemas.py @@ -17,7 +17,6 @@ class QuestionSchemaRich(RichSchema): id = ma.auto_field() name = ma.auto_field() - order = ma.auto_field() total_score = ma.auto_field() slide_id = ma.auto_field() type_id = ma.auto_field() diff --git a/server/app/core/schemas.py b/server/app/core/schemas.py index 60c36165bada90e058f716ee651081a593dfa70a..79bdbc1c48fd7460e85fe18f13a744f45a0a57e1 100644 --- a/server/app/core/schemas.py +++ b/server/app/core/schemas.py @@ -52,7 +52,6 @@ class QuestionSchema(BaseSchema): id = ma.auto_field() name = ma.auto_field() - order = ma.auto_field() total_score = ma.auto_field() type_id = ma.auto_field() slide_id = ma.auto_field() diff --git a/server/app/database/__init__.py b/server/app/database/__init__.py index 7deab3352fe533919186ecf8e217ce61d8eb02eb..784c006b8fab59ca1dcc9e6dbac8ebffe91cb013 100644 --- a/server/app/database/__init__.py +++ b/server/app/database/__init__.py @@ -45,7 +45,7 @@ class Dictionary(TypeDecorator): def process_bind_param(self, value, dialect): if value is not None: - value = json.dumps(value).replace("'", '"') + value = json.dumps(value) return value diff --git a/server/app/database/controller/add.py b/server/app/database/controller/add.py index f378b57062dece6823f43fddaab4c8e81bbe49f9..be6449afccad5948b8ff9f85ed44062c0d0f7362 100644 --- a/server/app/database/controller/add.py +++ b/server/app/database/controller/add.py @@ -114,7 +114,7 @@ def question(name, total_score, type_id, item_slide): """ order = Question.query.filter(Question.slide_id == item_slide.id).count() # first element has index 0 - return db_add(Question(name, order, total_score, type_id, item_slide.id)) + return db_add(Question(name, total_score, type_id, item_slide.id)) def question_alternative(text, value, question_id): diff --git a/server/app/database/controller/copy.py b/server/app/database/controller/copy.py index 9d41ca249251e592b3dbdd32a74c602f7540f1c5..a6b408ec301e093d6e47bca359968c052ac62aa1 100644 --- a/server/app/database/controller/copy.py +++ b/server/app/database/controller/copy.py @@ -20,7 +20,6 @@ def _question(item_question_old, slide_id): item_question_new = add.db_add( Question( item_question_old.name, - item_question_old.order, item_question_old.total_score, item_question_old.type_id, slide_id, diff --git a/server/app/database/controller/edit.py b/server/app/database/controller/edit.py index 49fcef87a4d28131501786782dbbf95fc58ac1ae..b1f5bc91e045e43c5f69618a685a39f86c7b081d 100644 --- a/server/app/database/controller/edit.py +++ b/server/app/database/controller/edit.py @@ -26,130 +26,28 @@ def switch_order(item1, item2): return item1 -def component(item, x, y, w, h, data): - """ Edits position, size and content of the provided component. """ - - 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): - """ Edits the title and timer of the slide. """ - - if title: - item.title = title - if timer: - item.timer = timer - - db.session.commit() - db.session.refresh(item) - return item - - -def team(item_team, name=None, competition_id=None): - """ Edits the name and competition of the team. """ - - if name: - item_team.name = name - if competition_id: - item_team.competition_id = competition_id - - db.session.commit() - db.session.refresh(item_team) - return item_team - - -def competition(item, name=None, year=None, city_id=None): - """ Edits the name and year of the competition. """ - - if name: - item.name = name - if year: - item.year = year - if city_id: - item.city_id = city_id - - db.session.commit() - db.session.refresh(item) - return item - - -def user(item, name=None, email=None, city_id=None, role_id=None): - """ Edits the name, email, city and role of the user. """ - - if name: - item.name = name.title() - - if email: - item.email = email - - if city_id: - item.city_id = city_id - - if role_id: - item.role_id = role_id - +def default(item, **kwargs): + """ + For every keyword argument, set that attribute on item to the given value. + Raise error if item doesn't already have that attribute. Do nothing if the + value for a given key is None. Works for any type of item. + + Example: + >>> user = default(user, name="Karl Karlsson") # Change name + >>> user.name + Karl Karlsson + >>> user = default(user, efternamn="Jönsson") # Try to set attribute that doesn't exist + AttributeError: Item of type <class 'app.database.models.User'> has no attribute 'efternamn' + >>> user = default(user, name=None) # Nothing happens if value is None + >>> user.name + Karl Karlsson + """ + + for key, value in kwargs.items(): + if not hasattr(item, key): + raise AttributeError(f"Item of type {type(item)} has no attribute '{key}'") + if value is not None: + setattr(item, key, value) db.session.commit() db.session.refresh(item) return item - - -def question(item_question, name=None, total_score=None, type_id=None, slide_id=None): - """ Edits the name, score, type and slide of the question. """ - - if name: - item_question.name = name - - if total_score: - item_question.total_score = total_score - - if type_id: - item_question.type_id = type_id - - if slide_id: - item_question.slide_id = slide_id - - db.session.commit() - db.session.refresh(item_question) - - return item_question - - -def question_alternative(item, text=None, value=None): - - if text: - item.text = text - - if value: - item.value = value - - db.session.commit() - db.session.refresh(item) - - return item - - -def question_answer(item, data=None, score=None): - - if data: - item.data = data - - if score: - item.score = score - - db.session.commit() - db.session.refresh(item) - - return item diff --git a/server/app/database/controller/get.py b/server/app/database/controller/get.py index 51b92afc78a5a77672b16b7fbafeaad74ded3d5b..571e1d6907054266193b676cf2c17f4d07c36e3a 100644 --- a/server/app/database/controller/get.py +++ b/server/app/database/controller/get.py @@ -48,10 +48,10 @@ def code_by_code(code, required=True, error_msg=None): return Code.query.filter(Code.code == code.upper()).first_extended(required, error_msg, codes.UNAUTHORIZED) -def user(UID, required=True, error_msg=None): +def user(user_id, required=True, error_msg=None): """ Gets the user object associated with the provided id. """ - return User.query.filter(User.id == UID).first_extended(required, error_msg) + return User.query.filter(User.id == user_id).first_extended(required, error_msg) def user_by_email(email, required=True, error_msg=None): @@ -67,35 +67,33 @@ def slide(competition_id, slide_order, required=True, error_msg=None): return Slide.query.filter(filters).first_extended(required, error_msg) -def team(competition_id, TID, required=True, error_msg=None): +def team(competition_id, team_id, required=True, error_msg=None): """ Gets the team object associated with the provided id and competition id. """ - return Team.query.filter((Team.competition_id == competition_id) & (Team.id == TID)).first_extended( + return Team.query.filter((Team.competition_id == competition_id) & (Team.id == team_id)).first_extended( required, error_msg ) -def question(competition_id, slide_order, question_order, required=True, error_msg=None): +def question(competition_id, slide_order, question_id, required=True, error_msg=None): """ Gets the question object associated with the provided id, slide order and competition id. """ join_filters = ( (Slide.competition_id == competition_id) & (Slide.order == slide_order) & (Slide.id == Question.slide_id) ) return ( - Question.query.join(Slide, join_filters) - .filter(Question.order == question_order) - .first_extended(required, error_msg) + Question.query.join(Slide, join_filters).filter(Question.id == question_id).first_extended(required, error_msg) ) -def question_alternatives(QID): +def question_alternatives(question_id): # join_filters = (Slide.competition_id == competition_id) & (Slide.order == slide_order) return QuestionAlternative.query.filter(QuestionAlternative.question_id == QID).all() -def question_answers(TID): +def question_answers(team_id): # join_filters = (Slide.competition_id == competition_id) & (Slide.order == slide_order) - return QuestionAnswer.query.filter(QuestionAnswer.team_id == TID).all() + return QuestionAnswer.query.filter(QuestionAnswer.team_id == team_id).all() def competition(competition_id): diff --git a/server/app/database/models.py b/server/app/database/models.py index 15efb6d1415e79bd006e31ce67790890b5c0e976..cded9ddf7aa00d2b5545a2ae5f40cf28eecba209 100644 --- a/server/app/database/models.py +++ b/server/app/database/models.py @@ -140,10 +140,8 @@ class Slide(db.Model): class Question(db.Model): - __table_args__ = (db.UniqueConstraint("slide_id", "order"),) id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(STRING_SIZE), nullable=False) - order = db.Column(db.Integer, nullable=False) total_score = db.Column(db.Integer, nullable=False, default=1) type_id = db.Column(db.Integer, db.ForeignKey("question_type.id"), nullable=False) slide_id = db.Column(db.Integer, db.ForeignKey("slide.id"), nullable=False) @@ -151,9 +149,8 @@ class Question(db.Model): question_answers = db.relationship("QuestionAnswer", backref="question") alternatives = db.relationship("QuestionAlternative", backref="question") - def __init__(self, name, order, total_score, type_id, slide_id): + def __init__(self, name, total_score, type_id, slide_id): self.name = name - self.order = order self.total_score = total_score self.type_id = type_id self.slide_id = slide_id