diff --git a/client/src/actions/presentation.ts b/client/src/actions/presentation.ts index 223b802dc1d26f8ac30a1b7e6c2b6ef5495b0ddc..90b728c54aa18a483ed6ab000f718c407b46abfd 100644 --- a/client/src/actions/presentation.ts +++ b/client/src/actions/presentation.ts @@ -5,11 +5,11 @@ This file handles actions for the presentation redux state import axios from 'axios' import { Slide } from '../interfaces/ApiModels' import { Timer } from '../interfaces/Timer' -import store, { AppDispatch } from './../store' +import store, { AppDispatch, RootState } from './../store' import Types from './types' // Save competition in presentation state from input id -export const getPresentationCompetition = (id: string) => async (dispatch: AppDispatch) => { +export const getPresentationCompetition = (id: string) => async (dispatch: AppDispatch, getState: () => RootState) => { await axios .get(`/api/competitions/${id}`) .then((res) => { @@ -17,6 +17,9 @@ export const getPresentationCompetition = (id: string) => async (dispatch: AppDi type: Types.SET_PRESENTATION_COMPETITION, payload: res.data, }) + if (getState().presentation.slide.id === -1 && res.data.slides[0]) { + setCurrentSlideByOrder(0)(dispatch) + } }) .catch((err) => { console.log(err) diff --git a/client/src/interfaces/ApiModels.ts b/client/src/interfaces/ApiModels.ts index faecc98f43bdaee26b871616c84cca7aac155913..a6fa68a0e9b58dcc63ad67d29884bbf3e781a64a 100644 --- a/client/src/interfaces/ApiModels.ts +++ b/client/src/interfaces/ApiModels.ts @@ -54,7 +54,6 @@ export interface Team extends NameID { export interface Question extends NameID { slide_id: number - title: string total_score: number type_id: number } diff --git a/client/src/pages/presentationEditor/PresentationEditorPage.tsx b/client/src/pages/presentationEditor/PresentationEditorPage.tsx index 47a746f2943a29a065ccca408658c2eaa00006c1..65720dc9a416264e58938a03fc96246e5a0975ab 100644 --- a/client/src/pages/presentationEditor/PresentationEditorPage.tsx +++ b/client/src/pages/presentationEditor/PresentationEditorPage.tsx @@ -6,10 +6,6 @@ import Drawer from '@material-ui/core/Drawer' import ListItemText from '@material-ui/core/ListItemText' import { createStyles, makeStyles, Theme, withStyles } from '@material-ui/core/styles' import AddOutlinedIcon from '@material-ui/icons/AddOutlined' -import BuildOutlinedIcon from '@material-ui/icons/BuildOutlined' -import CreateOutlinedIcon from '@material-ui/icons/CreateOutlined' -import DnsOutlinedIcon from '@material-ui/icons/DnsOutlined' -import InfoOutlinedIcon from '@material-ui/icons/InfoOutlined' import axios from 'axios' import React, { useEffect, useState } from 'react' import { Link, useParams } from 'react-router-dom' @@ -18,6 +14,7 @@ import { getEditorCompetition, setEditorSlideId, setEditorViewId } from '../../a import { getTypes } from '../../actions/typesAction' import { useAppDispatch, useAppSelector } from '../../hooks' import { RichSlide } from '../../interfaces/ApiRichModels' +import { renderSlideIcon } from '../../utils/renderSlideIcon' import { RemoveMenuItem } from '../admin/styledComp' import { Content, InnerContent } from '../views/styled' import SettingsPanel from './components/SettingsPanel' @@ -141,21 +138,6 @@ const PresentationEditorPage: React.FC = () => { setContextState(initialState) } - const renderSlideIcon = (slide: RichSlide) => { - if (slide.questions && slide.questions[0] && slide.questions[0].type_id) { - switch (slide.questions[0].type_id) { - case 1: - return <CreateOutlinedIcon /> // text question - case 2: - return <BuildOutlinedIcon /> // practical qustion - case 3: - return <DnsOutlinedIcon /> // multiple choice question - } - } else { - return <InfoOutlinedIcon /> // information slide - } - } - const GreenCheckbox = withStyles({ root: { color: '#FFFFFF', diff --git a/client/src/pages/presentationEditor/components/SlideDisplay.tsx b/client/src/pages/presentationEditor/components/SlideDisplay.tsx index b2ca9233e253dd5963ec53fbd8534398632409ab..42096d254b4746fa260863935a23328a6be80ef4 100644 --- a/client/src/pages/presentationEditor/components/SlideDisplay.tsx +++ b/client/src/pages/presentationEditor/components/SlideDisplay.tsx @@ -15,7 +15,7 @@ const SlideDisplay = ({ variant, activeViewTypeId }: SlideDisplayProps) => { const components = useAppSelector((state) => { if (variant === 'editor') return state.editor.competition.slides.find((slide) => slide.id === state.editor.activeSlideId)?.components - return state.presentation.competition.slides.find((slide) => slide.id === state.presentation.slide.id)?.components + return state.presentation.competition.slides.find((slide) => slide.id === state.presentation.slide?.id)?.components }) const dispatch = useAppDispatch() const editorPaperRef = useRef<HTMLDivElement>(null) diff --git a/client/src/pages/views/JudgeViewPage.test.tsx b/client/src/pages/views/JudgeViewPage.test.tsx index 29de4d12e22fcaac0f6af990bc504077a977b470..2e15f0904705726d364672b87af478ddaf922fbb 100644 --- a/client/src/pages/views/JudgeViewPage.test.tsx +++ b/client/src/pages/views/JudgeViewPage.test.tsx @@ -9,7 +9,7 @@ import JudgeViewPage from './JudgeViewPage' it('renders judge view page', () => { const compRes: any = { data: { - slides: [{ id: 0, title: '' }], + slides: [{ id: 0, title: '', questions: [{ id: 0 }] }], }, } const teamsRes: any = { diff --git a/client/src/pages/views/JudgeViewPage.tsx b/client/src/pages/views/JudgeViewPage.tsx index f72c052db601d0c346cf8937f58114d173662841..677a080d04ab34e8232120daae018b6d1a2b29c4 100644 --- a/client/src/pages/views/JudgeViewPage.tsx +++ b/client/src/pages/views/JudgeViewPage.tsx @@ -1,4 +1,4 @@ -import { Divider, List, ListItemText, Typography } from '@material-ui/core' +import { Card, Divider, List, ListItem, ListItemText, Paper, Typography } from '@material-ui/core' import { createStyles, makeStyles, Theme } from '@material-ui/core/styles' import React, { useEffect, useState } from 'react' import { getPresentationCompetition, setCurrentSlide, setPresentationCode } from '../../actions/presentation' @@ -18,8 +18,13 @@ import { JudgeToolbar, LeftDrawer, RightDrawer, + ScoreHeaderPadding, + ScoreHeaderPaper, + ScoreFooterPadding, } from './styled' import SlideDisplay from '../presentationEditor/components/SlideDisplay' +import JudgeScoringInstructions from './components/JudgeScoringInstructions' +import { renderSlideIcon } from '../../utils/renderSlideIcon' const leftDrawerWidth = 150 const rightDrawerWidth = 700 @@ -50,6 +55,7 @@ const JudgeViewPage = ({ competitionId, code }: JudgeViewPageProps) => { const activeViewTypeId = viewTypes.find((viewType) => viewType.name === 'Judge')?.id const teams = useAppSelector((state) => state.presentation.competition.teams) const slides = useAppSelector((state) => state.presentation.competition.slides) + const currentQuestion = slides[activeSlideIndex]?.questions[0] const handleSelectSlide = (index: number) => { setActiveSlideIndex(index) dispatch(setCurrentSlide(slides[index])) @@ -88,8 +94,8 @@ const JudgeViewPage = ({ competitionId, code }: JudgeViewPageProps) => { button key={slide.id} > - <Typography variant="h6">Slide ID: {slide.id} </Typography> - <ListItemText primary={slide.title} /> + {renderSlideIcon(slide)} + <ListItemText primary={`Sida ${slide.order + 1}`} /> </SlideListItem> ))} </List> @@ -103,7 +109,13 @@ const JudgeViewPage = ({ competitionId, code }: JudgeViewPageProps) => { anchor="right" > <div className={classes.toolbar} /> - <List> + {currentQuestion && ( + <ScoreHeaderPaper $rightDrawerWidth={rightDrawerWidth} elevation={4}> + <Typography variant="h4">{`${currentQuestion.name} (${currentQuestion.total_score}p)`}</Typography> + </ScoreHeaderPaper> + )} + <ScoreHeaderPadding /> + <List style={{ overflowY: 'scroll', overflowX: 'hidden' }}> {teams && teams.map((answer, index) => ( <div key={answer.name}> @@ -112,8 +124,10 @@ const JudgeViewPage = ({ competitionId, code }: JudgeViewPageProps) => { </div> ))} </List> + <ScoreFooterPadding /> + <JudgeScoringInstructions question={currentQuestion} /> </RightDrawer> - <div style={{ height: 64 }} /> + <div className={classes.toolbar} /> <Content leftDrawerWidth={leftDrawerWidth} rightDrawerWidth={rightDrawerWidth}> <InnerContent> {activeViewTypeId && <SlideDisplay variant="presentation" activeViewTypeId={activeViewTypeId} />} diff --git a/client/src/pages/views/components/JudgeScoreDisplay.tsx b/client/src/pages/views/components/JudgeScoreDisplay.tsx index 6308e39b0576a81967068eae4365d43e6ca9befc..2745e12970f3d5a20d2057adbc76ecc0ce2e6d6f 100644 --- a/client/src/pages/views/components/JudgeScoreDisplay.tsx +++ b/client/src/pages/views/components/JudgeScoreDisplay.tsx @@ -1,15 +1,35 @@ import { Box, Typography } from '@material-ui/core' +import axios from 'axios' import React from 'react' -import { useAppSelector } from '../../../hooks' +import { getPresentationCompetition } from '../../../actions/presentation' +import { useAppDispatch, useAppSelector } from '../../../hooks' import { AnswerContainer, ScoreDisplayContainer, ScoreDisplayHeader, ScoreInput } from './styled' type ScoreDisplayProps = { teamIndex: number } -const questionMaxScore = 5 const JudgeScoreDisplay = ({ teamIndex }: ScoreDisplayProps) => { + const dispatch = useAppDispatch() const currentTeam = useAppSelector((state) => state.presentation.competition.teams[teamIndex]) + const currentCompetititonId = useAppSelector((state) => state.presentation.competition.id) + const activeQuestion = useAppSelector( + (state) => + state.presentation.competition.slides.find((slide) => slide.id === state.presentation.slide?.id)?.questions[0] + ) + const scores = currentTeam.question_answers.map((questionAnswer) => questionAnswer.score) + const questionMaxScore = activeQuestion?.total_score + const activeAnswer = currentTeam.question_answers.find( + (questionAnswer) => questionAnswer.question_id === activeQuestion?.id + ) + const handleEditScore = async (newScore: number, answerId: number) => { + await axios + .put(`/api/competitions/${currentCompetititonId}/teams/${currentTeam.id}/answers/${answerId}`, { + score: newScore, + }) + .then(() => dispatch(getPresentationCompetition(currentCompetititonId.toString()))) + } + return ( <ScoreDisplayContainer> <ScoreDisplayHeader> @@ -17,21 +37,25 @@ const JudgeScoreDisplay = ({ teamIndex }: ScoreDisplayProps) => { <Box fontWeight="fontWeightBold">{currentTeam.name}</Box> </Typography> - <ScoreInput - label="Poäng" - defaultValue={0} - inputProps={{ style: { fontSize: 20 } }} - InputProps={{ disableUnderline: true, inputProps: { min: 0, max: questionMaxScore } }} - type="number" - ></ScoreInput> + {activeAnswer && ( + <ScoreInput + label="Poäng" + defaultValue={activeAnswer?.score} + inputProps={{ style: { fontSize: 20 } }} + InputProps={{ disableUnderline: true, inputProps: { min: 0, max: questionMaxScore } }} + type="number" + onChange={(event) => handleEditScore(+event.target.value, activeAnswer.id)} + /> + )} </ScoreDisplayHeader> - <Typography variant="h6">Alla poäng: 2 0 0 0 0 0 0 0 0</Typography> - <Typography variant="h6">Total poäng: 9</Typography> - <AnswerContainer> - <Typography variant="body1"> - Svar: blablablablablablablablablabla blablablablabla blablablablabla blablablablablablablablablabla{' '} - </Typography> - </AnswerContainer> + <Typography variant="h6">Alla poäng: [ {scores.map((score) => `${score} `)}]</Typography> + <Typography variant="h6">Total poäng: {scores.reduce((a, b) => a + b, 0)}</Typography> + {activeAnswer && ( + <AnswerContainer> + <Typography variant="body1">{activeAnswer.answer}</Typography> + </AnswerContainer> + )} + {!activeAnswer && <Typography variant="body1">Inget svar</Typography>} </ScoreDisplayContainer> ) } diff --git a/client/src/pages/views/components/JudgeScoringInstructions.tsx b/client/src/pages/views/components/JudgeScoringInstructions.tsx new file mode 100644 index 0000000000000000000000000000000000000000..3cc803194a9754bea8a9b23b3f2b073d6547c05c --- /dev/null +++ b/client/src/pages/views/components/JudgeScoringInstructions.tsx @@ -0,0 +1,28 @@ +import { Box, Card, Typography } from '@material-ui/core' +import axios from 'axios' +import React from 'react' +import { getPresentationCompetition } from '../../../actions/presentation' +import { useAppDispatch, useAppSelector } from '../../../hooks' +import { RichQuestion } from '../../../interfaces/ApiRichModels' +import { + AnswerContainer, + JudgeScoringInstructionsContainer, + ScoreDisplayContainer, + ScoreDisplayHeader, + ScoreInput, +} from './styled' + +type JudgeScoringInstructionsProps = { + question: RichQuestion +} + +const JudgeScoringInstructions = ({ question }: JudgeScoringInstructionsProps) => { + return ( + <JudgeScoringInstructionsContainer elevation={3}> + <Typography variant="h4">Rättningsinstruktioner</Typography> + <Typography variant="body1">Såhär rättar du denhär frågan</Typography> + </JudgeScoringInstructionsContainer> + ) +} + +export default JudgeScoringInstructions diff --git a/client/src/pages/views/components/PresentationComponent.tsx b/client/src/pages/views/components/PresentationComponent.tsx index cb95576f9e908d170eee33d78b7f92a285e61be1..a41f7912469256a6e946522790f4f8203f8da60f 100644 --- a/client/src/pages/views/components/PresentationComponent.tsx +++ b/client/src/pages/views/components/PresentationComponent.tsx @@ -37,6 +37,7 @@ const PresentationComponent = ({ component, width, height, scale }: Presentation minWidth={75 * scale} minHeight={75 * scale} disableDragging={true} + enableResizing={false} bounds="parent" //Multiply by scale to show components correctly for current screen size size={{ width: component.w * scale, height: component.h * scale }} diff --git a/client/src/pages/views/components/styled.tsx b/client/src/pages/views/components/styled.tsx index b522b20d3548b4f11864373d5a91088424c45a58..fef186f792dafe8ee259faa693cf6707975d06ba 100644 --- a/client/src/pages/views/components/styled.tsx +++ b/client/src/pages/views/components/styled.tsx @@ -1,4 +1,4 @@ -import { TextField } from '@material-ui/core' +import { Card, Paper, TextField } from '@material-ui/core' import styled from 'styled-components' export const SlideContainer = styled.div` @@ -32,3 +32,13 @@ export const AnswerContainer = styled.div` display: flex; flex-wrap: wrap; ` + +export const JudgeScoringInstructionsContainer = styled(Paper)` + position: absolute; + bottom: 0; + height: 250px; + width: 100%; + display: flex; + align-items: center; + flex-direction: column; +` diff --git a/client/src/pages/views/styled.tsx b/client/src/pages/views/styled.tsx index 17d09581501876a9f44e389c031e2f64f04fda82..8f63892ade7928842c12d4dc73df988a505f4cea 100644 --- a/client/src/pages/views/styled.tsx +++ b/client/src/pages/views/styled.tsx @@ -1,4 +1,4 @@ -import { AppBar, Button, Drawer, Toolbar, Typography } from '@material-ui/core' +import { AppBar, Button, Card, Drawer, Paper, Toolbar, Typography } from '@material-ui/core' import styled from 'styled-components' export const JudgeAppBar = styled(AppBar)` @@ -146,3 +146,26 @@ export const PresenterInnerContent = styled.div` export const ParticipantContainer = styled.div` max-width: calc((100vh / 9) * 16); ` + +interface ScoreHeaderPaperProps { + $rightDrawerWidth: number +} + +export const ScoreHeaderPaper = styled(Card)<ScoreHeaderPaperProps>` + position: absolute; + top: 66px; + width: ${(props) => (props ? props.$rightDrawerWidth : 0)}px; + height: 71px; + display: flex; + justify-content: center; + align-items: center; + z-index: 10; +` + +export const ScoreHeaderPadding = styled.div` + min-height: 71px; +` + +export const ScoreFooterPadding = styled.div` + min-height: 250px; +` diff --git a/client/src/reducers/presentationReducer.test.ts b/client/src/reducers/presentationReducer.test.ts index ee08e0685125f095274474430008f4ad4eaa357e..202d9406853628a32c77b9e42e1bcc502299889d 100644 --- a/client/src/reducers/presentationReducer.test.ts +++ b/client/src/reducers/presentationReducer.test.ts @@ -6,7 +6,8 @@ import presentationReducer from './presentationReducer' const initialState = { competition: { name: '', - id: 0, + id: 1, + background_image: undefined, city_id: 0, slides: [], year: 0, @@ -14,7 +15,8 @@ const initialState = { }, slide: { competition_id: 0, - id: 0, + background_image: undefined, + id: -1, order: 0, timer: 0, title: '', @@ -48,7 +50,7 @@ it('should handle SET_PRESENTATION_COMPETITION', () => { }) ).toEqual({ competition: testCompetition, - slide: testCompetition.slides[0], + slide: initialState.slide, code: initialState.code, timer: initialState.timer, }) diff --git a/client/src/reducers/presentationReducer.ts b/client/src/reducers/presentationReducer.ts index b657f5a463b339a4fbfc8309f79157ed78320f9e..b0c4791e9ab1f0ef05c931caa1fc4e9fe8268917 100644 --- a/client/src/reducers/presentationReducer.ts +++ b/client/src/reducers/presentationReducer.ts @@ -14,7 +14,7 @@ interface PresentationState { const initialState: PresentationState = { competition: { name: '', - id: 0, + id: 1, city_id: 0, slides: [], year: 0, @@ -23,7 +23,7 @@ const initialState: PresentationState = { }, slide: { competition_id: 0, - id: 0, + id: -1, order: 0, timer: 0, title: '', @@ -41,7 +41,6 @@ export default function (state = initialState, action: AnyAction) { case Types.SET_PRESENTATION_COMPETITION: return { ...state, - slide: action.payload.slides[0] as Slide, competition: action.payload as RichCompetition, } case Types.SET_PRESENTATION_CODE: diff --git a/client/src/utils/renderSlideIcon.tsx b/client/src/utils/renderSlideIcon.tsx new file mode 100644 index 0000000000000000000000000000000000000000..ba1eafcb8052469dd8b627b95d2eee518bfd5fe8 --- /dev/null +++ b/client/src/utils/renderSlideIcon.tsx @@ -0,0 +1,21 @@ +import { RichSlide } from '../interfaces/ApiRichModels' +import React from 'react' +import BuildOutlinedIcon from '@material-ui/icons/BuildOutlined' +import CreateOutlinedIcon from '@material-ui/icons/CreateOutlined' +import DnsOutlinedIcon from '@material-ui/icons/DnsOutlined' +import InfoOutlinedIcon from '@material-ui/icons/InfoOutlined' + +export const renderSlideIcon = (slide: RichSlide) => { + if (slide.questions && slide.questions[0] && slide.questions[0].type_id) { + switch (slide.questions[0].type_id) { + case 1: + return <CreateOutlinedIcon /> // text question + case 2: + return <BuildOutlinedIcon /> // practical qustion + case 3: + return <DnsOutlinedIcon /> // multiple choice question + } + } else { + return <InfoOutlinedIcon /> // information slide + } +} diff --git a/server/app/apis/__init__.py b/server/app/apis/__init__.py index 5eab3f829ae81e17ecc269d98f568eeacc45a68c..e5ec3d2ca1e1d9de6d4c5cca5ec1e3fba40f33ee 100644 --- a/server/app/apis/__init__.py +++ b/server/app/apis/__init__.py @@ -13,24 +13,62 @@ def validate_editor(db_item, *views): abort(http_codes.UNAUTHORIZED) -def check_jwt(editor=False, *views): - def wrapper(fn): - @wraps(fn) - def decorator(*args, **kwargs): +def _is_allowed(allowed, actual): + return actual and "*" in allowed or actual in allowed + + +def _has_access(in_claim, in_route): + in_route = int(in_route) if in_route else None + return not in_route or in_claim and in_claim == in_route + + +def protect_route(allowed_roles=None, allowed_views=None): + def wrapper(func): + def inner(*args, **kwargs): verify_jwt_in_request() claims = get_jwt_claims() + + # Authorize request if roles has access to the route # + + nonlocal allowed_roles + allowed_roles = allowed_roles or [] role = claims.get("role") + if _is_allowed(allowed_roles, role): + return func(*args, **kwargs) + + # Authorize request if view has access and is trying to access the + # competition its in. Also check team if client is a team. + # Allow request if route doesn't belong to any competition. + + nonlocal allowed_views + allowed_views = allowed_views or [] view = claims.get("view") - if role == "Admin": - return fn(*args, **kwargs) - elif editor and role == "Editor": - return fn(*args, **kwargs) - elif view in views: - return fn(*args, **kwargs) - else: - abort(http_codes.UNAUTHORIZED) - - return decorator + if not _is_allowed(allowed_views, view): + abort( + http_codes.UNAUTHORIZED, + f"Client with view '{view}' is not allowed to access route with allowed views {allowed_views}.", + ) + + claim_competition_id = claims.get("competition_id") + route_competition_id = kwargs.get("competition_id") + if not _has_access(claim_competition_id, route_competition_id): + abort( + http_codes.UNAUTHORIZED, + f"Client in competition '{claim_competition_id}' is not allowed to access competition '{route_competition_id}'.", + ) + + if view == "Team": + claim_team_id = claims.get("team_id") + route_team_id = kwargs.get("team_id") + if not _has_access(claim_team_id, route_team_id): + abort( + http_codes.UNAUTHORIZED, + f"Client in team '{claim_team_id}' is not allowed to access team '{route_team_id}'.", + ) + + return func(*args, **kwargs) + + return inner return wrapper diff --git a/server/app/apis/alternatives.py b/server/app/apis/alternatives.py index 2adad553ddb34bbd43e1405223c7c45962d66e19..0ce74d53b0a742ea31689d826f898e1b12f33b41 100644 --- a/server/app/apis/alternatives.py +++ b/server/app/apis/alternatives.py @@ -1,6 +1,6 @@ import app.core.http_codes as codes import app.database.controller as dbc -from app.apis import check_jwt, item_response, list_response +from app.apis import item_response, list_response, protect_route from app.core.dto import QuestionAlternativeDTO from flask_restx import Resource from flask_restx import reqparse @@ -17,12 +17,12 @@ question_alternative_parser.add_argument("value", type=int, default=None, locati @api.route("") @api.param("competition_id, slide_id, question_id") class QuestionAlternativeList(Resource): - @check_jwt(editor=True) + @protect_route(allowed_roles=["*"], allowed_views=["*"]) def get(self, competition_id, slide_id, question_id): items = dbc.get.question_alternative_list(competition_id, slide_id, question_id) return list_response(list_schema.dump(items)) - @check_jwt(editor=True) + @protect_route(allowed_roles=["*"]) def post(self, competition_id, slide_id, question_id): args = question_alternative_parser.parse_args(strict=True) item = dbc.add.question_alternative(**args, question_id=question_id) @@ -32,19 +32,19 @@ class QuestionAlternativeList(Resource): @api.route("/<alternative_id>") @api.param("competition_id, slide_id, question_id, alternative_id") class QuestionAlternatives(Resource): - @check_jwt(editor=True) + @protect_route(allowed_roles=["*"], allowed_views=["*"]) def get(self, competition_id, slide_id, question_id, alternative_id): items = dbc.get.question_alternative(competition_id, slide_id, question_id, alternative_id) return item_response(schema.dump(items)) - @check_jwt(editor=True) + @protect_route(allowed_roles=["*"]) def put(self, competition_id, slide_id, question_id, alternative_id): args = question_alternative_parser.parse_args(strict=True) item = dbc.get.question_alternative(competition_id, slide_id, question_id, alternative_id) item = dbc.edit.default(item, **args) return item_response(schema.dump(item)) - @check_jwt(editor=True) + @protect_route(allowed_roles=["*"]) def delete(self, competition_id, slide_id, question_id, alternative_id): item = dbc.get.question_alternative(competition_id, slide_id, question_id, alternative_id) dbc.delete.default(item) diff --git a/server/app/apis/answers.py b/server/app/apis/answers.py index e3e225b9d0fbdb1076a03d4bd12f9fd018f25b90..8f72d3bde8610c255083df9deefc69b89a68560a 100644 --- a/server/app/apis/answers.py +++ b/server/app/apis/answers.py @@ -1,6 +1,6 @@ import app.core.http_codes as codes import app.database.controller as dbc -from app.apis import check_jwt, item_response, list_response +from app.apis import item_response, list_response, protect_route from app.core.dto import QuestionAnswerDTO from flask_restx import Resource from flask_restx import reqparse @@ -22,12 +22,12 @@ question_answer_edit_parser.add_argument("score", type=int, default=None, locati @api.route("") @api.param("competition_id, team_id") class QuestionAnswerList(Resource): - @check_jwt(editor=True) + @protect_route(allowed_roles=["*"], allowed_views=["*"]) def get(self, competition_id, team_id): items = dbc.get.question_answer_list(competition_id, team_id) return list_response(list_schema.dump(items)) - @check_jwt(editor=True) + @protect_route(allowed_roles=["*"], allowed_views=["*"]) def post(self, competition_id, team_id): args = question_answer_parser.parse_args(strict=True) item = dbc.add.question_answer(**args, team_id=team_id) @@ -37,12 +37,12 @@ class QuestionAnswerList(Resource): @api.route("/<answer_id>") @api.param("competition_id, team_id, answer_id") class QuestionAnswers(Resource): - @check_jwt(editor=True) + @protect_route(allowed_roles=["*"], allowed_views=["*"]) def get(self, competition_id, team_id, answer_id): item = dbc.get.question_answer(competition_id, team_id, answer_id) return item_response(schema.dump(item)) - @check_jwt(editor=True) + @protect_route(allowed_roles=["*"], allowed_views=["*"]) def put(self, competition_id, team_id, answer_id): args = question_answer_edit_parser.parse_args(strict=True) item = dbc.get.question_answer(competition_id, team_id, answer_id) diff --git a/server/app/apis/auth.py b/server/app/apis/auth.py index 37da9e98d4b5970cd82b47dd54ad940cc53b6736..9c9ed24a876a9b130c385194bb8cc322bef65c9d 100644 --- a/server/app/apis/auth.py +++ b/server/app/apis/auth.py @@ -1,6 +1,6 @@ import app.core.http_codes as codes import app.database.controller as dbc -from app.apis import check_jwt, item_response, text_response +from app.apis import item_response, protect_route, text_response from app.core.codes import verify_code from app.core.dto import AuthDTO, CodeDTO from flask_jwt_extended import ( @@ -12,6 +12,8 @@ from flask_jwt_extended import ( ) from flask_restx import Resource from flask_restx import inputs, reqparse +from datetime import timedelta +from app.core import sockets api = AuthDTO.api schema = AuthDTO.schema @@ -33,9 +35,13 @@ def get_user_claims(item_user): return {"role": item_user.role.name, "city_id": item_user.city_id} +def get_code_claims(item_code): + return {"view": item_code.view_type.name, "competition_id": item_code.competition_id, "team_id": item_code.team_id} + + @api.route("/signup") class AuthSignup(Resource): - @check_jwt(editor=False) + @protect_route(allowed_roles=["Admin"]) def post(self): args = create_user_parser.parse_args(strict=True) email = args.get("email") @@ -50,7 +56,7 @@ class AuthSignup(Resource): @api.route("/delete/<ID>") @api.param("ID") class AuthDelete(Resource): - @check_jwt(editor=False) + @protect_route(allowed_roles=["Admin"]) def delete(self, ID): item_user = dbc.get.user(ID) @@ -86,24 +92,38 @@ class AuthLoginCode(Resource): code = args["code"] if not verify_code(code): - api.abort(codes.BAD_REQUEST, "Invalid code") + api.abort(codes.UNAUTHORIZED, "Invalid code") item_code = dbc.get.code_by_code(code) - return item_response(CodeDTO.schema.dump(item_code)) + + if item_code.competition_id not in sockets.presentations: + api.abort(codes.UNAUTHORIZED, "Competition not active") + + access_token = create_access_token( + item_code.id, user_claims=get_code_claims(item_code), expires_delta=timedelta(hours=8) + ) + + response = { + "competition_id": item_code.competition_id, + "view_type_id": item_code.view_type_id, + "team_id": item_code.team_id, + "access_token": access_token, + } + return response @api.route("/logout") class AuthLogout(Resource): - @check_jwt(editor=True) + @protect_route(allowed_roles=["*"], allowed_views=["*"]) def post(self): jti = get_raw_jwt()["jti"] dbc.add.blacklist(jti) - return text_response("User logout") + return text_response("Logout") @api.route("/refresh") class AuthRefresh(Resource): - @check_jwt(editor=True) + @protect_route(allowed_roles=["*"]) @jwt_refresh_token_required def post(self): old_jti = get_raw_jwt()["jti"] diff --git a/server/app/apis/codes.py b/server/app/apis/codes.py index 8a4105d24a77f07b04da1e1e2ed477746ea7a932..d07e17435aed31417254acfd83ed9315f1477ba3 100644 --- a/server/app/apis/codes.py +++ b/server/app/apis/codes.py @@ -1,5 +1,5 @@ import app.database.controller as dbc -from app.apis import check_jwt, item_response, list_response +from app.apis import item_response, list_response, protect_route from app.core import http_codes as codes from app.core.dto import CodeDTO from app.database.models import Code @@ -13,7 +13,7 @@ list_schema = CodeDTO.list_schema @api.route("") @api.param("competition_id") class CodesList(Resource): - @check_jwt(editor=True) + @protect_route(allowed_roles=["*"]) def get(self, competition_id): items = dbc.get.code_list(competition_id) return list_response(list_schema.dump(items), len(items)) @@ -22,7 +22,7 @@ class CodesList(Resource): @api.route("/<code_id>") @api.param("competition_id, code_id") class CodesById(Resource): - @check_jwt(editor=False) + @protect_route(allowed_roles=["*"]) def put(self, competition_id, code_id): item = dbc.get.one(Code, code_id) item.code = dbc.utils.generate_unique_code() diff --git a/server/app/apis/competitions.py b/server/app/apis/competitions.py index fded52ec97df7d72acaf531eb5f0033c25227669..d1fd95722f026cb3aa67e9bb8354a6823af9bbee 100644 --- a/server/app/apis/competitions.py +++ b/server/app/apis/competitions.py @@ -1,7 +1,7 @@ import time import app.database.controller as dbc -from app.apis import check_jwt, item_response, list_response +from app.apis import item_response, list_response, protect_route from app.core.dto import CompetitionDTO from app.database.models import Competition from flask_restx import Resource @@ -29,7 +29,7 @@ competition_search_parser.add_argument("city_id", type=int, default=None, locati @api.route("") class CompetitionsList(Resource): - @check_jwt(editor=True) + @protect_route(allowed_roles=["*"]) def post(self): args = competition_parser.parse_args(strict=True) @@ -44,20 +44,21 @@ class CompetitionsList(Resource): @api.route("/<competition_id>") @api.param("competition_id") class Competitions(Resource): + @protect_route(allowed_roles=["*"], allowed_views=["*"]) def get(self, competition_id): item = dbc.get.competition(competition_id) return item_response(rich_schema.dump(item)) - @check_jwt(editor=True) + @protect_route(allowed_roles=["*"]) def put(self, competition_id): args = competition_edit_parser.parse_args(strict=True) item = dbc.get.one(Competition, competition_id) - item = dbc.edit.default(item, **args) + item = dbc.edit.competition(item, **args) return item_response(schema.dump(item)) - @check_jwt(editor=True) + @protect_route(allowed_roles=["*"]) def delete(self, competition_id): item = dbc.get.one(Competition, competition_id) dbc.delete.competition(item) @@ -67,7 +68,7 @@ class Competitions(Resource): @api.route("/search") class CompetitionSearch(Resource): - @check_jwt(editor=True) + @protect_route(allowed_roles=["*"]) def get(self): args = competition_search_parser.parse_args(strict=True) items, total = dbc.search.competition(**args) @@ -77,7 +78,7 @@ class CompetitionSearch(Resource): @api.route("/<competition_id>/copy") @api.param("competition_id") class SlidesOrder(Resource): - @check_jwt(editor=True) + @protect_route(allowed_roles=["*"]) def post(self, competition_id): item_competition = dbc.get.competition(competition_id) diff --git a/server/app/apis/components.py b/server/app/apis/components.py index 8f2531513a54fd6392983c7bd0b0657c3d4c6455..43018695e959f4ff00fa4377f2f3600e8a1cd494 100644 --- a/server/app/apis/components.py +++ b/server/app/apis/components.py @@ -1,6 +1,6 @@ import app.core.http_codes as codes import app.database.controller as dbc -from app.apis import check_jwt, item_response, list_response +from app.apis import item_response, list_response, protect_route from app.core.dto import ComponentDTO from flask_restx import Resource from flask_restx import reqparse @@ -28,12 +28,12 @@ component_create_parser.add_argument("view_type_id", type=int, required=True, lo @api.route("/<component_id>") @api.param("competition_id, slide_id, component_id") class ComponentByID(Resource): - @check_jwt(editor=True) + @protect_route(allowed_roles=["*"], allowed_views=["*"]) def get(self, competition_id, slide_id, component_id): item = dbc.get.component(competition_id, slide_id, component_id) return item_response(schema.dump(item)) - @check_jwt(editor=True) + @protect_route(allowed_roles=["*"]) def put(self, competition_id, slide_id, component_id): args = component_edit_parser.parse_args(strict=True) item = dbc.get.component(competition_id, slide_id, component_id) @@ -41,7 +41,7 @@ class ComponentByID(Resource): item = dbc.edit.default(item, **args_without_none) return item_response(schema.dump(item)) - @check_jwt(editor=True) + @protect_route(allowed_roles=["*"]) def delete(self, competition_id, slide_id, component_id): item = dbc.get.component(competition_id, slide_id, component_id) dbc.delete.component(item) @@ -51,12 +51,12 @@ class ComponentByID(Resource): @api.route("") @api.param("competition_id, slide_id") class ComponentList(Resource): - @check_jwt(editor=True) + @protect_route(allowed_roles=["*"], allowed_views=["*"]) def get(self, competition_id, slide_id): items = dbc.get.component_list(competition_id, slide_id) return list_response(list_schema.dump(items)) - @check_jwt(editor=True) + @protect_route(allowed_roles=["*"]) def post(self, competition_id, slide_id): args = component_create_parser.parse_args() item = dbc.add.component(slide_id=slide_id, **args) diff --git a/server/app/apis/media.py b/server/app/apis/media.py index 5d89550ac19aaff78643a34ae0a3059c011aed23..c7de8c4d4c1cbd3f482d386e654a9bfd0063370b 100644 --- a/server/app/apis/media.py +++ b/server/app/apis/media.py @@ -1,6 +1,6 @@ import app.core.http_codes as codes import app.database.controller as dbc -from app.apis import check_jwt, item_response, list_response +from app.apis import item_response, list_response, protect_route from app.core.dto import MediaDTO from app.core.parsers import search_parser from app.database.models import Media @@ -22,13 +22,13 @@ media_parser_search.add_argument("filename", type=str, default=None, location="a @api.route("/images") class ImageList(Resource): - @check_jwt(editor=True) + @protect_route(allowed_roles=["*"]) def get(self): args = media_parser_search.parse_args(strict=True) items, total = dbc.search.image(**args) return list_response(list_schema.dump(items), total) - @check_jwt(editor=True) + @protect_route(allowed_roles=["*"]) def post(self): if "image" not in request.files: api.abort(codes.BAD_REQUEST, "Missing image in request.files") @@ -48,12 +48,12 @@ class ImageList(Resource): @api.route("/images/<ID>") @api.param("ID") class ImageList(Resource): - @check_jwt(editor=True) + @protect_route(allowed_roles=["*"], allowed_views=["*"]) def get(self, ID): item = dbc.get.one(Media, ID) return item_response(schema.dump(item)) - @check_jwt(editor=True) + @protect_route(allowed_roles=["*"]) def delete(self, ID): item = dbc.get.one(Media, ID) try: diff --git a/server/app/apis/misc.py b/server/app/apis/misc.py index ab52362d46d026d8483c52f5c4d8384f32de755d..20a84e4c17c138b3a94ea6e3902e67154036cabc 100644 --- a/server/app/apis/misc.py +++ b/server/app/apis/misc.py @@ -1,5 +1,5 @@ import app.database.controller as dbc -from app.apis import check_jwt, list_response +from app.apis import list_response, protect_route from app.core import http_codes from app.core.dto import MiscDTO from app.database.models import City, Competition, ComponentType, MediaType, QuestionType, Role, User, ViewType @@ -23,6 +23,7 @@ name_parser.add_argument("name", type=str, required=True, location="json") @api.route("/types") class TypesList(Resource): + @protect_route(allowed_roles=["*"], allowed_views=["*"]) def get(self): result = {} result["media_types"] = media_type_schema.dump(dbc.get.all(MediaType)) @@ -34,7 +35,7 @@ class TypesList(Resource): @api.route("/roles") class RoleList(Resource): - @check_jwt(editor=True) + @protect_route(allowed_roles=["*"]) def get(self): items = dbc.get.all(Role) return list_response(role_schema.dump(items)) @@ -42,12 +43,12 @@ class RoleList(Resource): @api.route("/cities") class CitiesList(Resource): - @check_jwt(editor=True) + @protect_route(allowed_roles=["*"]) def get(self): items = dbc.get.all(City) return list_response(city_schema.dump(items)) - @check_jwt(editor=False) + @protect_route(allowed_roles=["Admin"]) def post(self): args = name_parser.parse_args(strict=True) dbc.add.city(args["name"]) @@ -58,7 +59,7 @@ class CitiesList(Resource): @api.route("/cities/<ID>") @api.param("ID") class Cities(Resource): - @check_jwt(editor=False) + @protect_route(allowed_roles=["Admin"]) def put(self, ID): item = dbc.get.one(City, ID) args = name_parser.parse_args(strict=True) @@ -67,7 +68,7 @@ class Cities(Resource): items = dbc.get.all(City) return list_response(city_schema.dump(items)) - @check_jwt(editor=False) + @protect_route(allowed_roles=["Admin"]) def delete(self, ID): item = dbc.get.one(City, ID) dbc.delete.default(item) @@ -77,7 +78,7 @@ class Cities(Resource): @api.route("/statistics") class Statistics(Resource): - @check_jwt(editor=True) + @protect_route(allowed_roles=["*"]) def get(self): user_count = User.query.count() competition_count = Competition.query.count() diff --git a/server/app/apis/questions.py b/server/app/apis/questions.py index aaefafc40608d14bf3a4e8a2a3b7fdfbda559f2c..de40a310db5f11a1b0c03cb20e1546f74682af70 100644 --- a/server/app/apis/questions.py +++ b/server/app/apis/questions.py @@ -1,6 +1,6 @@ import app.core.http_codes as codes import app.database.controller as dbc -from app.apis import check_jwt, item_response, list_response +from app.apis import item_response, list_response, protect_route from app.core.dto import QuestionDTO from flask_restx import Resource from flask_restx import reqparse @@ -18,7 +18,7 @@ question_parser.add_argument("type_id", type=int, default=None, location="json") @api.route("/questions") @api.param("competition_id") class QuestionList(Resource): - @check_jwt(editor=True) + @protect_route(allowed_roles=["*"]) def get(self, competition_id): items = dbc.get.question_list_for_competition(competition_id) return list_response(list_schema.dump(items)) @@ -27,12 +27,12 @@ class QuestionList(Resource): @api.route("/slides/<slide_id>/questions") @api.param("competition_id, slide_id") class QuestionListForSlide(Resource): - @check_jwt(editor=True) + @protect_route(allowed_roles=["*"]) def get(self, competition_id, slide_id): items = dbc.get.question_list(competition_id, slide_id) return list_response(list_schema.dump(items)) - @check_jwt(editor=True) + @protect_route(allowed_roles=["*"]) def post(self, competition_id, slide_id): args = question_parser.parse_args(strict=True) item = dbc.add.question(slide_id=slide_id, **args) @@ -42,12 +42,12 @@ class QuestionListForSlide(Resource): @api.route("/slides/<slide_id>/questions/<question_id>") @api.param("competition_id, slide_id, question_id") class QuestionById(Resource): - @check_jwt(editor=True) + @protect_route(allowed_roles=["*"]) def get(self, competition_id, slide_id, question_id): item_question = dbc.get.question(competition_id, slide_id, question_id) return item_response(schema.dump(item_question)) - @check_jwt(editor=True) + @protect_route(allowed_roles=["*"]) def put(self, competition_id, slide_id, question_id): args = question_parser.parse_args(strict=True) @@ -56,7 +56,7 @@ class QuestionById(Resource): return item_response(schema.dump(item_question)) - @check_jwt(editor=True) + @protect_route(allowed_roles=["*"]) def delete(self, competition_id, slide_id, question_id): item_question = dbc.get.question(competition_id, slide_id, question_id) dbc.delete.question(item_question) diff --git a/server/app/apis/slides.py b/server/app/apis/slides.py index da29633a794cdb1262d2c307b8c80a94c794830e..52553796260493507bcc47ef32f8977f241a2a88 100644 --- a/server/app/apis/slides.py +++ b/server/app/apis/slides.py @@ -1,6 +1,6 @@ import app.core.http_codes as codes import app.database.controller as dbc -from app.apis import check_jwt, item_response, list_response +from app.apis import item_response, list_response, protect_route from app.core.dto import SlideDTO from flask_restx import Resource from flask_restx import reqparse @@ -19,12 +19,12 @@ slide_parser.add_argument("background_image_id", default=None, type=int, locatio @api.route("") @api.param("competition_id") class SlidesList(Resource): - @check_jwt(editor=True) + @protect_route(allowed_roles=["*"]) def get(self, competition_id): items = dbc.get.slide_list(competition_id) return list_response(list_schema.dump(items)) - @check_jwt(editor=True) + @protect_route(allowed_roles=["*"]) def post(self, competition_id): item_slide = dbc.add.slide(competition_id) return item_response(schema.dump(item_slide)) @@ -33,21 +33,21 @@ class SlidesList(Resource): @api.route("/<slide_id>") @api.param("competition_id,slide_id") class Slides(Resource): - @check_jwt(editor=True) + @protect_route(allowed_roles=["*"]) def get(self, competition_id, slide_id): item_slide = dbc.get.slide(competition_id, slide_id) return item_response(schema.dump(item_slide)) - @check_jwt(editor=True) + @protect_route(allowed_roles=["*"]) def put(self, competition_id, slide_id): args = slide_parser.parse_args(strict=True) item_slide = dbc.get.slide(competition_id, slide_id) - item_slide = dbc.edit.default(item_slide, **args) + item_slide = dbc.edit.slide(item_slide, **args) return item_response(schema.dump(item_slide)) - @check_jwt(editor=True) + @protect_route(allowed_roles=["*"]) def delete(self, competition_id, slide_id): item_slide = dbc.get.slide(competition_id, slide_id) @@ -58,7 +58,7 @@ class Slides(Resource): @api.route("/<slide_id>/order") @api.param("competition_id,slide_id") class SlideOrder(Resource): - @check_jwt(editor=True) + @protect_route(allowed_roles=["*"]) def put(self, competition_id, slide_id): args = slide_parser.parse_args(strict=True) order = args.get("order") @@ -87,7 +87,7 @@ class SlideOrder(Resource): @api.route("/<slide_id>/copy") @api.param("competition_id,slide_id") class SlideCopy(Resource): - @check_jwt(editor=True) + @protect_route(allowed_roles=["*"]) def post(self, competition_id, slide_id): item_slide = dbc.get.slide(competition_id, slide_id) diff --git a/server/app/apis/teams.py b/server/app/apis/teams.py index 3fcb276c959c8880be30713bf08869d6b30b8dbb..c951ae53e6b7839d1457e60ccce60ea398ee617d 100644 --- a/server/app/apis/teams.py +++ b/server/app/apis/teams.py @@ -1,6 +1,6 @@ import app.core.http_codes as codes import app.database.controller as dbc -from app.apis import check_jwt, item_response, list_response +from app.apis import item_response, list_response, protect_route from app.core.dto import TeamDTO from flask_restx import Resource, reqparse from flask_restx import reqparse @@ -16,12 +16,12 @@ team_parser.add_argument("name", type=str, location="json") @api.route("") @api.param("competition_id") class TeamsList(Resource): - @check_jwt(editor=True) + @protect_route(allowed_roles=["*"]) def get(self, competition_id): items = dbc.get.team_list(competition_id) return list_response(list_schema.dump(items)) - @check_jwt(editor=True) + @protect_route(allowed_roles=["*"]) def post(self, competition_id): args = team_parser.parse_args(strict=True) item_team = dbc.add.team(args["name"], competition_id) @@ -31,19 +31,19 @@ class TeamsList(Resource): @api.route("/<team_id>") @api.param("competition_id,team_id") class Teams(Resource): - @check_jwt(editor=True) + @protect_route(allowed_roles=["*"]) def get(self, competition_id, team_id): item = dbc.get.team(competition_id, team_id) return item_response(schema.dump(item)) - @check_jwt(editor=True) + @protect_route(allowed_roles=["*"]) 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 - @check_jwt(editor=True) + @protect_route(allowed_roles=["*"]) def put(self, competition_id, team_id): args = team_parser.parse_args(strict=True) name = args.get("name") diff --git a/server/app/apis/users.py b/server/app/apis/users.py index a5b2d58b01f9ce8931fd29cba3af9d1d8b825cc0..50470db724ebf3648a2ff3a206a87b53650417ee 100644 --- a/server/app/apis/users.py +++ b/server/app/apis/users.py @@ -1,6 +1,6 @@ import app.core.http_codes as codes import app.database.controller as dbc -from app.apis import check_jwt, item_response, list_response +from app.apis import item_response, list_response, protect_route from app.core.dto import UserDTO from flask_jwt_extended import get_jwt_identity from flask_restx import Resource @@ -40,12 +40,12 @@ def _edit_user(item_user, args): @api.route("") class UsersList(Resource): - @check_jwt(editor=True) + @protect_route(allowed_roles=["*"]) def get(self): item = dbc.get.user(get_jwt_identity()) return item_response(schema.dump(item)) - @check_jwt(editor=True) + @protect_route(allowed_roles=["*"]) def put(self): args = user_parser.parse_args(strict=True) item = dbc.get.user(get_jwt_identity()) @@ -56,12 +56,12 @@ class UsersList(Resource): @api.route("/<ID>") @api.param("ID") class Users(Resource): - @check_jwt(editor=True) + @protect_route(allowed_roles=["*"]) def get(self, ID): item = dbc.get.user(ID) return item_response(schema.dump(item)) - @check_jwt(editor=False) + @protect_route(allowed_roles=["Admin"]) def put(self, ID): args = user_parser.parse_args(strict=True) item = dbc.get.user(ID) @@ -71,7 +71,7 @@ class Users(Resource): @api.route("/search") class UserSearch(Resource): - @check_jwt(editor=True) + @protect_route(allowed_roles=["*"]) def get(self): args = user_search_parser.parse_args(strict=True) items, total = dbc.search.user(**args) diff --git a/server/app/database/controller/add.py b/server/app/database/controller/add.py index f7b0edbb17ac91ea9ef3786182ff58614b43cd45..50321626d0164fb01ef61d11e642bb679e729ead 100644 --- a/server/app/database/controller/add.py +++ b/server/app/database/controller/add.py @@ -151,9 +151,13 @@ def competition(name, year, city_id): # Add code for Judge view code(2, item_competition.id) + # Add code for Audience view code(3, item_competition.id) + # Add code for Operator view + code(4, item_competition.id) + item_competition = utils.refresh(item_competition) return item_competition diff --git a/server/app/database/controller/edit.py b/server/app/database/controller/edit.py index b1f5bc91e045e43c5f69618a685a39f86c7b081d..94bc6008a1f5b8173df1ddbba7d6745f7c6ea5f0 100644 --- a/server/app/database/controller/edit.py +++ b/server/app/database/controller/edit.py @@ -51,3 +51,17 @@ def default(item, **kwargs): db.session.commit() db.session.refresh(item) return item + + +def competition(item, **kwargs): + if kwargs["background_image_id"] == -1: + item.background_image_id = None + del kwargs["background_image_id"] + return default(item, **kwargs) + + +def slide(item, **kwargs): + if kwargs["background_image_id"] == -1: + item.background_image_id = None + del kwargs["background_image_id"] + return default(item, **kwargs) \ No newline at end of file diff --git a/server/app/database/models.py b/server/app/database/models.py index c84677b2d04fc7ce2554548d67fb3d8a21d5ccf2..c56aa3f0051ba6033e7376b0912cbf4f45928252 100644 --- a/server/app/database/models.py +++ b/server/app/database/models.py @@ -231,6 +231,8 @@ class Code(db.Model): competition_id = db.Column(db.Integer, db.ForeignKey("competition.id"), nullable=False) team_id = db.Column(db.Integer, db.ForeignKey("team.id"), nullable=True) + view_type = db.relationship("ViewType", uselist=False) + def __init__(self, code, view_type_id, competition_id=None, team_id=None): self.code = code self.view_type_id = view_type_id @@ -241,7 +243,6 @@ class Code(db.Model): class ViewType(db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(STRING_SIZE), unique=True) - codes = db.relationship("Code", backref="view_type") def __init__(self, name): self.name = name diff --git a/server/tests/test_app.py b/server/tests/test_app.py index 199df387e878fdbbde759a80e6aa3d9ea486ea7d..d59428a6380b74abe8853c6c09c4df0bafc031e0 100644 --- a/server/tests/test_app.py +++ b/server/tests/test_app.py @@ -3,7 +3,9 @@ This file tests the api function calls. """ import app.core.http_codes as codes +from app.database.controller.add import competition from app.database.models import Slide +from app.core import sockets from tests import app, client, db from tests.test_helpers import add_default_values, change_order_test, delete, get, post, put @@ -342,7 +344,7 @@ def test_question_api(client): slide_order = 1 response, body = get(client, f"/api/competitions/{CID}/questions", headers=headers) assert response.status_code == codes.OK - assert body["count"] == 1 + assert body["count"] == 2 # Get questions from another competition that should have some questions CID = 3 @@ -385,3 +387,67 @@ def test_question_api(client): response, _ = delete(client, f"/api/competitions/{CID}/slides/{NEW_slide_order}/questions/{QID}", headers=headers) assert response.status_code == codes.NOT_FOUND """ + + +def test_authorization(client): + add_default_values() + + # Fake that competition 1 is active + sockets.presentations[1] = {} + + #### TEAM #### + # Login in with team code + response, body = post(client, "/api/auth/login/code", {"code": "111111"}) + assert response.status_code == codes.OK + headers = {"Authorization": "Bearer " + body["access_token"]} + + competition_id = body["competition_id"] + team_id = body["team_id"] + + # Get competition team is in + response, body = get(client, f"/api/competitions/{competition_id}", headers=headers) + assert response.status_code == codes.OK + + # Try to delete competition team is in + response, body = delete(client, f"/api/competitions/{competition_id}", headers=headers) + assert response.status_code == codes.UNAUTHORIZED + + # Try to get a different competition + response, body = get(client, f"/api/competitions/{competition_id+1}", headers=headers) + assert response.status_code == codes.UNAUTHORIZED + + # Get own answers + response, body = get(client, f"/api/competitions/{competition_id}/teams/{team_id}/answers", headers=headers) + assert response.status_code == codes.OK + + # Try to get another teams answers + response, body = get(client, f"/api/competitions/{competition_id}/teams/{team_id+1}/answers", headers=headers) + assert response.status_code == codes.UNAUTHORIZED + + #### JUDGE #### + # Login in with judge code + response, body = post(client, "/api/auth/login/code", {"code": "222222"}) + assert response.status_code == codes.OK + headers = {"Authorization": "Bearer " + body["access_token"]} + + competition_id = body["competition_id"] + + # Get competition judge is in + response, body = get(client, f"/api/competitions/{competition_id}", headers=headers) + assert response.status_code == codes.OK + + # Try to delete competition judge is in + response, body = delete(client, f"/api/competitions/{competition_id}", headers=headers) + assert response.status_code == codes.UNAUTHORIZED + + # Try to get a different competition + response, body = get(client, f"/api/competitions/{competition_id+1}", headers=headers) + assert response.status_code == codes.UNAUTHORIZED + + # Get team answers + response, body = get(client, f"/api/competitions/{competition_id}/teams/{team_id}/answers", headers=headers) + assert response.status_code == codes.OK + + # Also get antoher teams answers + response, body = get(client, f"/api/competitions/{competition_id}/teams/{team_id+1}/answers", headers=headers) + assert response.status_code == codes.OK \ No newline at end of file diff --git a/server/tests/test_helpers.py b/server/tests/test_helpers.py index 9603fa59d5e8b11872367dd1a73c83e334e600f1..f55aa68251a5a3b9ea73411c959939acc93f1bc6 100644 --- a/server/tests/test_helpers.py +++ b/server/tests/test_helpers.py @@ -3,14 +3,14 @@ import json import app.core.http_codes as codes import app.database.controller as dbc from app.core import db -from app.database.models import City, Role +from app.database.models import City, Code, Role def add_default_values(): media_types = ["Image", "Video"] question_types = ["Boolean", "Multiple", "Text"] component_types = ["Text", "Image"] - view_types = ["Team", "Judge", "Audience"] + view_types = ["Team", "Judge", "Audience", "Operator"] roles = ["Admin", "Editor"] cities = ["Linköping", "Testköping"] @@ -40,6 +40,20 @@ def add_default_values(): # Add competitions item_competition = dbc.add.competition("Tom tävling", 2012, item_city.id) + + item_question = dbc.add.question("hej", 5, 1, item_competition.slides[0].id) + + item_team1 = dbc.add.team("Hej lag 3", item_competition.id) + item_team2 = dbc.add.team("Hej lag 4", item_competition.id) + + db.session.add(Code("111111", 1, item_competition.id, item_team1.id)) # Team + db.session.add(Code("222222", 2, item_competition.id)) # Judge + + dbc.add.QuestionAnswer("hej", 5, item_question.id, item_team1) + dbc.add.QuestionAnswer("då", 5, item_question.id, item_team2) + + db.session.commit() + for j in range(2): item_comp = dbc.add.competition(f"Tävling {j}", 2012, item_city.id) # Add two more slides to competition