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