From fe48153687dc10acc9627fe4661dda2e0c226bd9 Mon Sep 17 00:00:00 2001 From: Albin Henriksson <albhe428@student.liu.se> Date: Sat, 24 Apr 2021 12:42:14 +0000 Subject: [PATCH] Resolve "Competition login" --- client/src/Main.tsx | 2 +- client/src/actions/competitionLogin.ts | 23 +++++++ client/src/actions/types.ts | 3 + client/src/pages/admin/AdminPage.tsx | 1 + .../src/pages/login/components/AdminLogin.tsx | 5 +- .../components/CompetitionLogin.test.tsx | 8 ++- .../login/components/CompetitionLogin.tsx | 47 ++++++-------- client/src/pages/views/JudgeViewPage.tsx | 5 +- .../pages/views/ParticipantViewPage.test.tsx | 9 ++- .../src/pages/views/ParticipantViewPage.tsx | 8 ++- client/src/pages/views/PresenterViewPage.tsx | 1 - .../src/pages/views/ViewSelectPage.test.tsx | 26 ++++++-- client/src/pages/views/ViewSelectPage.tsx | 63 +++++++++++++++---- client/src/reducers/allReducers.ts | 2 + .../src/reducers/competitionLoginReducer.ts | 38 +++++++++++ server/app/apis/auth.py | 2 +- server/app/database/controller/get.py | 2 +- 17 files changed, 188 insertions(+), 57 deletions(-) create mode 100644 client/src/actions/competitionLogin.ts create mode 100644 client/src/reducers/competitionLoginReducer.ts diff --git a/client/src/Main.tsx b/client/src/Main.tsx index bc0a2e85..b7c74b55 100644 --- a/client/src/Main.tsx +++ b/client/src/Main.tsx @@ -23,7 +23,7 @@ const Main: React.FC = () => { <SecureRoute login exact path="/" component={LoginPage} /> <SecureRoute path="/admin" component={AdminPage} /> <SecureRoute path="/editor/competition-id=:id" component={PresentationEditorPage} /> - <Route exact path="/view" component={ViewSelectPage} /> + <Route exact path="/:code" component={ViewSelectPage} /> <Route exact path="/participant/id=:id&code=:code" component={ParticipantViewPage} /> <SecureRoute exact path="/presenter/id=:id&code=:code" component={PresenterViewPage} /> <Route exact path="/judge/id=:id&code=:code" component={JudgeViewPage} /> diff --git a/client/src/actions/competitionLogin.ts b/client/src/actions/competitionLogin.ts new file mode 100644 index 00000000..ff177a2b --- /dev/null +++ b/client/src/actions/competitionLogin.ts @@ -0,0 +1,23 @@ +import axios from 'axios' +import { History } from 'history' +import { AppDispatch } from '../store' +import { AccountLoginModel } from './../interfaces/FormModels' +import Types from './types' + +export const loginCompetition = (code: string, history: History) => async (dispatch: AppDispatch) => { + dispatch({ type: Types.LOADING_COMPETITION_LOGIN }) + await axios + .post('/api/auth/login/code', { code }) + .then((res) => { + console.log(code, res.data[0]) + dispatch({ type: Types.CLEAR_COMPETITION_LOGIN_ERRORS }) // no error + // history.push('/admin') //redirecting to admin page after login success + if (res.data && res.data[0] && res.data[0].view_type_id) { + history.push(`/${code}`) + } + }) + .catch((err) => { + dispatch({ type: Types.SET_COMPETITION_LOGIN_ERRORS, payload: err && err.response && err.response.data }) + console.log(err) + }) +} diff --git a/client/src/actions/types.ts b/client/src/actions/types.ts index 5932deb0..512572bc 100644 --- a/client/src/actions/types.ts +++ b/client/src/actions/types.ts @@ -1,6 +1,7 @@ export default { LOADING_UI: 'LOADING_UI', LOADING_USER: 'LOADING_USER', + LOADING_COMPETITION_LOGIN: 'LOADING_COMPETITION_LOGIN', SET_ROLES: 'SET_ROLES', SET_USER: 'SET_USER', SET_SEARCH_USERS: 'SET_SEARCH_USERS', @@ -9,6 +10,8 @@ export default { SET_SEARCH_USERS_TOTAL_COUNT: 'SET_SEARCH_USERS_TOTAL_COUNT', SET_ERRORS: 'SET_ERRORS', CLEAR_ERRORS: 'CLEAR_ERRORS', + SET_COMPETITION_LOGIN_ERRORS: 'SET_COMPETITION_LOGIN_ERRORS', + CLEAR_COMPETITION_LOGIN_ERRORS: 'CLEAR_COMPETITION_LOGIN_ERRORS', SET_UNAUTHENTICATED: 'SET_UNAUTHENTICATED', SET_AUTHENTICATED: 'SET_AUTHENTICATED', SET_COMPETITIONS: 'SET_COMPETITIONS', diff --git a/client/src/pages/admin/AdminPage.tsx b/client/src/pages/admin/AdminPage.tsx index 1b543083..a8b1746a 100644 --- a/client/src/pages/admin/AdminPage.tsx +++ b/client/src/pages/admin/AdminPage.tsx @@ -16,6 +16,7 @@ import ExitToAppIcon from '@material-ui/icons/ExitToApp' import LocationCityIcon from '@material-ui/icons/LocationCity' import PeopleIcon from '@material-ui/icons/People' import SettingsOverscanIcon from '@material-ui/icons/SettingsOverscan' +import axios from 'axios' import React, { useEffect } from 'react' import { Link, Route, Switch, useRouteMatch } from 'react-router-dom' import { getCities } from '../../actions/cities' diff --git a/client/src/pages/login/components/AdminLogin.tsx b/client/src/pages/login/components/AdminLogin.tsx index 7f478caf..964fb8ab 100644 --- a/client/src/pages/login/components/AdminLogin.tsx +++ b/client/src/pages/login/components/AdminLogin.tsx @@ -1,4 +1,4 @@ -import { Button, TextField } from '@material-ui/core' +import { Button, TextField, Typography } from '@material-ui/core' import { Alert, AlertTitle } from '@material-ui/lab' import { Formik, FormikHelpers } from 'formik' import React, { useEffect, useState } from 'react' @@ -83,7 +83,8 @@ const AdminLogin: React.FC = () => { {errors.message && ( <Alert severity="error"> <AlertTitle>Error</AlertTitle> - {errors.message} + <Typography>Någonting gick fel. Kontrollera</Typography> + <Typography>dina användaruppgifter och försök igen</Typography> </Alert> )} {loading && <CenteredCircularProgress color="secondary" />} diff --git a/client/src/pages/login/components/CompetitionLogin.test.tsx b/client/src/pages/login/components/CompetitionLogin.test.tsx index 29213c94..862880bc 100644 --- a/client/src/pages/login/components/CompetitionLogin.test.tsx +++ b/client/src/pages/login/components/CompetitionLogin.test.tsx @@ -1,7 +1,13 @@ import { render } from '@testing-library/react' import React from 'react' +import { Provider } from 'react-redux' +import store from '../../../store' import CompetitionLogin from './CompetitionLogin' it('renders competition login', () => { - render(<CompetitionLogin />) + render( + <Provider store={store}> + <CompetitionLogin /> + </Provider> + ) }) diff --git a/client/src/pages/login/components/CompetitionLogin.tsx b/client/src/pages/login/components/CompetitionLogin.tsx index 75f73c5b..d89cafcf 100644 --- a/client/src/pages/login/components/CompetitionLogin.tsx +++ b/client/src/pages/login/components/CompetitionLogin.tsx @@ -1,53 +1,44 @@ -import { Button, TextField } from '@material-ui/core' +import { Button, TextField, Typography } from '@material-ui/core' import { Alert, AlertTitle } from '@material-ui/lab' import axios from 'axios' import { Formik, FormikHelpers } from 'formik' -import React from 'react' +import { useHistory } from 'react-router-dom' +import React, { useEffect, useState } from 'react' import * as Yup from 'yup' +import { loginCompetition } from '../../../actions/competitionLogin' +import { useAppDispatch, useAppSelector } from '../../../hooks' import { CompetitionLoginModel } from '../../../interfaces/FormModels' -import { LoginForm } from './styled' +import { CenteredCircularProgress, LoginForm } from './styled' interface CompetitionLoginFormModel { model: CompetitionLoginModel error?: string } -interface ServerResponse { - code: number +interface formError { message: string } const competitionSchema: Yup.SchemaOf<CompetitionLoginFormModel> = Yup.object({ model: Yup.object() .shape({ - code: Yup.string().required('Mata in kod').min(6, 'Koden måste vara minst 6 tecken'), + code: Yup.string().required('Mata in kod').length(6, 'Koden måste vara 6 tecken'), }) .required(), error: Yup.string().optional(), }) -const handleCompetitionSubmit = async ( - values: CompetitionLoginFormModel, - actions: FormikHelpers<CompetitionLoginFormModel> -) => { - await axios - .post<ServerResponse>(`users/login`, { code: values.model.code }) - .then(() => { - actions.resetForm() - }) - .catch(({ response }) => { - console.log(response.data.message) - actions.setFieldError('error', response.data.message) - }) - .finally(() => { - actions.setSubmitting(false) - }) -} - const CompetitionLogin: React.FC = () => { + const dispatch = useAppDispatch() + const history = useHistory() + const errors = useAppSelector((state) => state.competitionLogin.errors) + const loading = useAppSelector((state) => state.competitionLogin.loading) const competitionInitialValues: CompetitionLoginFormModel = { model: { code: '' }, } + const handleCompetitionSubmit = async (values: CompetitionLoginFormModel) => { + dispatch(loginCompetition(values.model.code, history)) + } return ( <Formik initialValues={competitionInitialValues} @@ -68,14 +59,14 @@ const CompetitionLogin: React.FC = () => { <Button type="submit" fullWidth variant="contained" color="secondary" disabled={!formik.isValid}> Anslut till tävling </Button> - {formik.errors.error ? ( + {errors && errors.message && ( <Alert severity="error"> <AlertTitle>Error</AlertTitle> - {formik.errors.error} + <Typography>En tävling med den koden hittades ej.</Typography> + <Typography>kontrollera koden och försök igen</Typography> </Alert> - ) : ( - <div /> )} + {loading && <CenteredCircularProgress color="secondary" />} </LoginForm> )} </Formik> diff --git a/client/src/pages/views/JudgeViewPage.tsx b/client/src/pages/views/JudgeViewPage.tsx index 60018624..66450f3a 100644 --- a/client/src/pages/views/JudgeViewPage.tsx +++ b/client/src/pages/views/JudgeViewPage.tsx @@ -14,6 +14,7 @@ import { socket_connect } from '../../sockets' import { SlideListItem } from '../presentationEditor/styled' import JudgeScoreDisplay from './components/JudgeScoreDisplay' import SlideDisplay from './components/SlideDisplay' +import { useHistory } from 'react-router-dom' import { Content, JudgeAnswersLabel, @@ -41,6 +42,7 @@ const useStyles = makeStyles((theme: Theme) => const JudgeViewPage: React.FC = () => { const classes = useStyles() + const history = useHistory() const { id, code }: ViewParams = useParams() const dispatch = useAppDispatch() const [activeSlideIndex, setActiveSlideIndex] = useState<number>(0) @@ -50,12 +52,13 @@ const JudgeViewPage: React.FC = () => { setActiveSlideIndex(index) dispatch(setCurrentSlide(slides[index])) } - useEffect(() => { socket_connect() dispatch(getPresentationCompetition(id)) dispatch(getPresentationTeams(id)) dispatch(setPresentationCode(code)) + //hides the url so people can't sneak peak + history.push('judge') }, []) return ( diff --git a/client/src/pages/views/ParticipantViewPage.test.tsx b/client/src/pages/views/ParticipantViewPage.test.tsx index 85360e4f..c0950b3c 100644 --- a/client/src/pages/views/ParticipantViewPage.test.tsx +++ b/client/src/pages/views/ParticipantViewPage.test.tsx @@ -1,13 +1,16 @@ import { render } from '@testing-library/react' import React from 'react' import { Provider } from 'react-redux' +import { BrowserRouter } from 'react-router-dom' import store from '../../store' import ParticipantViewPage from './ParticipantViewPage' it('renders participant view page', () => { render( - <Provider store={store}> - <ParticipantViewPage /> - </Provider> + <BrowserRouter> + <Provider store={store}> + <ParticipantViewPage /> + </Provider> + </BrowserRouter> ) }) diff --git a/client/src/pages/views/ParticipantViewPage.tsx b/client/src/pages/views/ParticipantViewPage.tsx index 55c28af0..f531ad76 100644 --- a/client/src/pages/views/ParticipantViewPage.tsx +++ b/client/src/pages/views/ParticipantViewPage.tsx @@ -1,7 +1,13 @@ -import React from 'react' +import React, { useEffect } from 'react' import SlideDisplay from './components/SlideDisplay' +import { useHistory } from 'react-router-dom' const ParticipantViewPage: React.FC = () => { + const history = useHistory() + useEffect(() => { + //hides the url so people can't sneak peak + history.push('participant') + }, []) return <SlideDisplay /> } diff --git a/client/src/pages/views/PresenterViewPage.tsx b/client/src/pages/views/PresenterViewPage.tsx index 40aa252d..1abeee92 100644 --- a/client/src/pages/views/PresenterViewPage.tsx +++ b/client/src/pages/views/PresenterViewPage.tsx @@ -52,7 +52,6 @@ const PresenterViewPage: React.FC = () => { 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() diff --git a/client/src/pages/views/ViewSelectPage.test.tsx b/client/src/pages/views/ViewSelectPage.test.tsx index 2e649977..83b71db0 100644 --- a/client/src/pages/views/ViewSelectPage.test.tsx +++ b/client/src/pages/views/ViewSelectPage.test.tsx @@ -1,12 +1,26 @@ import { render } from '@testing-library/react' import React from 'react' +import { Provider } from 'react-redux' import { BrowserRouter } from 'react-router-dom' +import store from '../../store' import ViewSelectPage from './ViewSelectPage' +import mockedAxios from 'axios' +import { act } from 'react-dom/test-utils' -it('renders view select page', () => { - render( - <BrowserRouter> - <ViewSelectPage /> - </BrowserRouter> - ) +it('renders view select page', async () => { + await act(async () => { + const res = { + data: {}, + } + ;(mockedAxios.post as jest.Mock).mockImplementation(() => { + return Promise.resolve(res) + }) + render( + <BrowserRouter> + <Provider store={store}> + <ViewSelectPage /> + </Provider> + </BrowserRouter> + ) + }) }) diff --git a/client/src/pages/views/ViewSelectPage.tsx b/client/src/pages/views/ViewSelectPage.tsx index 2781f60c..3c3599ed 100644 --- a/client/src/pages/views/ViewSelectPage.tsx +++ b/client/src/pages/views/ViewSelectPage.tsx @@ -1,22 +1,63 @@ import Button from '@material-ui/core/Button' -import React from 'react' +import React, { useEffect, useState } from 'react' import { Link, useRouteMatch } from 'react-router-dom' import { ViewSelectButtonGroup, ViewSelectContainer } from './styled' +import { useParams } from 'react-router-dom' +import { CircularProgress, Typography } from '@material-ui/core' +import ParticipantViewPage from './ParticipantViewPage' +import axios from 'axios' +import PresenterViewPage from './PresenterViewPage' +import JudgeViewPage from './JudgeViewPage' +import AudienceViewPage from './AudienceViewPage' +import { useAppSelector } from '../../hooks' +interface ViewSelectParams { + code: string +} const ViewSelectPage: React.FC = () => { - const url = useRouteMatch().url + const [loading, setLoading] = useState(true) + const [error, setError] = useState(false) + const [viewTypeId, setViewTypeId] = useState(undefined) + const [competitionId, setCompetitionId] = useState<number | undefined>(undefined) + const { code }: ViewSelectParams = useParams() + const viewType = useAppSelector((state) => state.types.viewTypes.find((viewType) => viewType.id === viewTypeId)?.name) + + const renderView = (viewTypeId: number | undefined) => { + //Renders the correct view depending on view type + if (competitionId) { + switch (viewType) { + case 'Team': + return <ParticipantViewPage /> + case 'Judge': + return <JudgeViewPage /> + case 'Audience': + return <AudienceViewPage /> + default: + return <Typography>Inkorrekt vy</Typography> + } + } + } + + useEffect(() => { + axios + .post('/api/auth/login/code', { code }) + .then((response) => { + setLoading(false) + setViewTypeId(response.data[0].view_type_id) + setCompetitionId(response.data[0].competition_id) + }) + .catch(() => { + setLoading(false) + setError(true) + }) + }, []) + return ( <ViewSelectContainer> <ViewSelectButtonGroup> - <Button color="primary" variant="contained" component={Link} to={`${url}/participant`}> - Deltagarvy - </Button> - <Button color="primary" variant="contained" component={Link} to={`${url}/audience`}> - Åskådarvy - </Button> - <Button color="primary" variant="contained" component={Link} to={`${url}/judge`}> - Domarvy - </Button> + {loading && <CircularProgress />} + {!loading && renderView(viewTypeId)} + {error && <Typography>Något gick fel, dubbelkolla koden och försök igen</Typography>} </ViewSelectButtonGroup> </ViewSelectContainer> ) diff --git a/client/src/reducers/allReducers.ts b/client/src/reducers/allReducers.ts index 398ec0a7..90cb2414 100644 --- a/client/src/reducers/allReducers.ts +++ b/client/src/reducers/allReducers.ts @@ -2,6 +2,7 @@ import { combineReducers } from 'redux' import citiesReducer from './citiesReducer' +import competitionLoginReducer from './competitionLoginReducer' import competitionsReducer from './competitionsReducer' import editorReducer from './editorReducer' import mediaReducer from './mediaReducer' @@ -26,5 +27,6 @@ const allReducers = combineReducers({ types: typesReducer, media: mediaReducer, statistics: statisticsReducer, + competitionLogin: competitionLoginReducer, }) export default allReducers diff --git a/client/src/reducers/competitionLoginReducer.ts b/client/src/reducers/competitionLoginReducer.ts new file mode 100644 index 00000000..81c426a4 --- /dev/null +++ b/client/src/reducers/competitionLoginReducer.ts @@ -0,0 +1,38 @@ +import { AnyAction } from 'redux' +import Types from '../actions/types' + +interface UIError { + message: string +} + +interface UserState { + loading: boolean + errors: null | UIError +} + +const initialState: UserState = { + loading: false, + errors: null, +} + +export default function (state = initialState, action: AnyAction) { + switch (action.type) { + case Types.SET_COMPETITION_LOGIN_ERRORS: + return { + errors: action.payload as UIError, + loading: false, + } + case Types.CLEAR_COMPETITION_LOGIN_ERRORS: + return { + loading: false, + errors: null, + } + case Types.LOADING_COMPETITION_LOGIN: + return { + ...state, + loading: true, + } + default: + return state + } +} diff --git a/server/app/apis/auth.py b/server/app/apis/auth.py index d249fec5..87d7f1d1 100644 --- a/server/app/apis/auth.py +++ b/server/app/apis/auth.py @@ -79,7 +79,7 @@ class AuthLoginCode(Resource): if not verify_code(code): api.abort(codes.BAD_REQUEST, "Invalid code") - item_code = dbc.get.code_by_code(code, True, "A presentation with that code does not exist") + item_code = dbc.get.code_by_code(code) return item_response(CodeDTO.schema.dump(item_code)), codes.OK diff --git a/server/app/database/controller/get.py b/server/app/database/controller/get.py index 57acb27e..3e5e7f87 100644 --- a/server/app/database/controller/get.py +++ b/server/app/database/controller/get.py @@ -34,7 +34,7 @@ def one(db_type, id): def code_by_code(code): """ Gets the code object associated with the provided code. """ - return Code.query.filter(Code.code == code.upper()).first_extended() + return Code.query.filter(Code.code == code.upper()).first_extended( True, "A presentation with that code does not exist") def code_list(competition_id): -- GitLab