From 4bce3fcba0d56c82e7f8423732e5c745b4939ba2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Victor=20L=C3=B6fgren?= <viclo211@student.liu.se>
Date: Wed, 12 May 2021 16:07:24 +0200
Subject: [PATCH 01/15] Fix socket authorization

---
 client/src/sockets.ts      | 38 +++++++++++++++++++-------------------
 server/app/core/sockets.py | 12 ++++++------
 2 files changed, 25 insertions(+), 25 deletions(-)

diff --git a/client/src/sockets.ts b/client/src/sockets.ts
index dabaf4b9..e2767c10 100644
--- a/client/src/sockets.ts
+++ b/client/src/sockets.ts
@@ -30,30 +30,30 @@ let socket: SocketIOClient.Socket
  * in the documentation, no more needed.
  */
 export const socketConnect = (role: 'Judge' | 'Operator' | 'Team' | 'Audience') => {
-  if (!socket) {
-    const token = localStorage[role]
-    socket = io('localhost:5000', {
-      transportOptions: {
-        polling: {
-          extraHeaders: {
-            Authorization: token,
-          },
+  if (socket) return
+
+  const token = localStorage[`${role}Token`]
+  socket = io('localhost:5000', {
+    transportOptions: {
+      polling: {
+        extraHeaders: {
+          Authorization: token,
         },
       },
-    })
+    },
+  })
 
-    socket.on('set_slide', (data: SetSlideInterface) => {
-      setCurrentSlideByOrder(data.slide_order)(store.dispatch, store.getState)
-    })
+  socket.on('set_slide', (data: SetSlideInterface) => {
+    setCurrentSlideByOrder(data.slide_order)(store.dispatch, store.getState)
+  })
 
-    socket.on('set_timer', (data: SetTimerInterface) => {
-      setPresentationTimer(data.timer)(store.dispatch)
-    })
+  socket.on('set_timer', (data: SetTimerInterface) => {
+    setPresentationTimer(data.timer)(store.dispatch)
+  })
 
-    socket.on('end_presentation', () => {
-      socket.disconnect()
-    })
-  }
+  socket.on('end_presentation', () => {
+    socket.disconnect()
+  })
 }
 
 export const socketStartPresentation = () => {
diff --git a/server/app/core/sockets.py b/server/app/core/sockets.py
index 71871cda..78ca30d0 100644
--- a/server/app/core/sockets.py
+++ b/server/app/core/sockets.py
@@ -57,12 +57,12 @@ def protect_route(allowed_views=None):
     return wrapper
 
 
-@sio.on("connect")
+@sio.event
 def connect() -> None:
     logger.info(f"Client '{request.sid}' connected")
 
 
-@sio.on("disconnect")
+@sio.event
 def disconnect() -> None:
     """
     Remove client from the presentation it was in. Delete presentation if no
@@ -108,8 +108,8 @@ def start_presentation(data: Dict) -> None:
     logger.info(f"Client '{request.sid}' started competition '{competition_id}'")
 
 
+@sio.event
 @protect_route(allowed_views=["Operator"])
-@sio.on("end_presentation")
 def end_presentation(data: Dict) -> None:
     """
     End a presentation by sending end_presentation to all connected clients.
@@ -146,7 +146,7 @@ def end_presentation(data: Dict) -> None:
     logger.info(f"Client '{request.sid}' ended presentation '{competition_id}'")
 
 
-@sio.on("join_presentation")
+@sio.event
 def join_presentation(data: Dict) -> None:
     """
     Join a currently active presentation.
@@ -186,8 +186,8 @@ def join_presentation(data: Dict) -> None:
     logger.info(f"Client '{request.sid}' joined competition '{competition_id}'")
 
 
+@sio.event
 @protect_route(allowed_views=["Operator"])
-@sio.on("set_slide")
 def set_slide(data: Dict) -> None:
     """
     Sync slides between all clients in the same presentation by sending
@@ -234,8 +234,8 @@ def set_slide(data: Dict) -> None:
     logger.info(f"Client '{request.sid}' set slide '{slide_order}' in competition '{competition_id}'")
 
 
+@sio.event
 @protect_route(allowed_views=["Operator"])
-@sio.on("set_timer")
 def set_timer(data: Dict) -> None:
     """
     Sync slides between all clients in the same presentation by sending
-- 
GitLab


From db810fea4a18260471cc8922c5519595d6fbc90c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Victor=20L=C3=B6fgren?= <viclo211@student.liu.se>
Date: Wed, 12 May 2021 16:30:33 +0200
Subject: [PATCH 02/15] Sockets now use authorization headers to get
 competition_id instead of payload

---
 client/src/sockets.ts      | 12 +++++-------
 server/app/core/sockets.py | 20 +++++++++++---------
 2 files changed, 16 insertions(+), 16 deletions(-)

diff --git a/client/src/sockets.ts b/client/src/sockets.ts
index e2767c10..1d53a5a4 100644
--- a/client/src/sockets.ts
+++ b/client/src/sockets.ts
@@ -57,15 +57,15 @@ export const socketConnect = (role: 'Judge' | 'Operator' | 'Team' | 'Audience')
 }
 
 export const socketStartPresentation = () => {
-  socket.emit('start_presentation', { competition_id: store.getState().presentation.competition.id })
+  socket.emit('start_presentation')
 }
 
 export const socketJoinPresentation = () => {
-  socket.emit('join_presentation', { code: store.getState().presentation.code }) // TODO: Send code gotten from auth/login/<code> api call
+  socket.emit('join_presentation')
 }
 
 export const socketEndPresentation = () => {
-  socket.emit('end_presentation', { competition_id: store.getState().presentation.competition.id })
+  socket.emit('end_presentation')
 }
 
 export const socketSetSlideNext = () => {
@@ -73,7 +73,7 @@ export const socketSetSlideNext = () => {
     .getState()
     .presentation.competition.slides.find((slide) => slide.id === store.getState().presentation.activeSlideId)
   if (!activeSlide) return
-  socketSetSlide(activeSlide.order + 1) // TODO: Check that this slide exists
+  socketSetSlide(activeSlide.order + 1)
 }
 
 export const socketSetSlidePrev = () => {
@@ -81,7 +81,7 @@ export const socketSetSlidePrev = () => {
     .getState()
     .presentation.competition.slides.find((slide) => slide.id === store.getState().presentation.activeSlideId)
   if (!activeSlide) return
-  socketSetSlide(activeSlide.order - 1) // TODO: Check that this slide exists
+  socketSetSlide(activeSlide.order - 1)
 }
 
 /**
@@ -97,14 +97,12 @@ export const socketSetSlide = (slide_order: number) => {
   }
 
   socket.emit('set_slide', {
-    competition_id: store.getState().presentation.competition.id,
     slide_order: slide_order,
   })
 }
 
 export const socketSetTimer = (timer: Timer) => {
   socket.emit('set_timer', {
-    competition_id: store.getState().presentation.competition.id,
     timer: timer,
   })
 }
diff --git a/server/app/core/sockets.py b/server/app/core/sockets.py
index 78ca30d0..c5ec33ae 100644
--- a/server/app/core/sockets.py
+++ b/server/app/core/sockets.py
@@ -81,14 +81,14 @@ def disconnect() -> None:
     logger.info(f"Client '{request.sid}' disconnected")
 
 
+@sio.event
 @protect_route(allowed_views=["Operator"])
-@sio.on("start_presentation")
-def start_presentation(data: Dict) -> None:
+def start_presentation() -> None:
     """
     Starts a presentation if that competition is currently not active.
     """
 
-    competition_id = data["competition_id"]
+    competition_id = get_jwt_claims().get("competition_id")
 
     if competition_id in presentations:
         logger.error(
@@ -110,7 +110,7 @@ def start_presentation(data: Dict) -> None:
 
 @sio.event
 @protect_route(allowed_views=["Operator"])
-def end_presentation(data: Dict) -> None:
+def end_presentation() -> None:
     """
     End a presentation by sending end_presentation to all connected clients.
 
@@ -119,7 +119,8 @@ def end_presentation(data: Dict) -> None:
     Log error message if no presentation exists with the send id or if this
     client is not in that presentation.
     """
-    competition_id = data["competition_id"]
+
+    competition_id = get_jwt_claims().get("competition_id")
 
     if competition_id not in presentations:
         logger.error(
@@ -147,14 +148,15 @@ def end_presentation(data: Dict) -> None:
 
 
 @sio.event
-def join_presentation(data: Dict) -> None:
+@protect_route(allowed_views=["*"])
+def join_presentation() -> None:
     """
     Join a currently active presentation.
 
     Log error message if given code doesn't exist, if not presentation associated
     with that code exists or if client is already in the presentation.
     """
-    code = data["code"]
+    code = get_jwt_claims().get("code")
     item_code = db.session.query(Code).filter(Code.code == code).first()
 
     if not item_code:
@@ -197,7 +199,7 @@ def set_slide(data: Dict) -> None:
     that presentation or the client is not the one who started the presentation.
     """
 
-    competition_id = data["competition_id"]
+    competition_id = get_jwt_claims().get("competition_id")
     slide_order = data["slide_order"]
 
     if competition_id not in presentations:
@@ -244,7 +246,7 @@ def set_timer(data: Dict) -> None:
     Log error if the given competition_id is not active, if client is not in
     that presentation or the client is not the one who started the presentation.
     """
-    competition_id = data["competition_id"]
+    competition_id = get_jwt_claims().get("competition_id")
     timer = data["timer"]
 
     if competition_id not in presentations:
-- 
GitLab


From 26cb256f3bf5e6c54707bbbc5704042e79bfda75 Mon Sep 17 00:00:00 2001
From: Albin Henriksson <albhe428@student.liu.se>
Date: Wed, 12 May 2021 17:55:50 +0200
Subject: [PATCH 03/15] Refactor timer

---
 client/src/actions/presentation.ts          | 16 +------
 client/src/pages/views/JudgeViewPage.tsx    |  2 +
 client/src/pages/views/OperatorViewPage.tsx | 30 +++++++++----
 client/src/pages/views/components/Timer.tsx | 47 ++++++---------------
 client/src/reducers/presentationReducer.ts  | 15 ++-----
 client/src/sockets.ts                       | 30 ++-----------
 server/app/core/sockets.py                  |  6 +--
 7 files changed, 49 insertions(+), 97 deletions(-)

diff --git a/client/src/actions/presentation.ts b/client/src/actions/presentation.ts
index 0c3f2466..89b9e596 100644
--- a/client/src/actions/presentation.ts
+++ b/client/src/actions/presentation.ts
@@ -3,8 +3,7 @@ This file handles actions for the presentation redux state
 */
 
 import axios from 'axios'
-import { Timer } from '../interfaces/Timer'
-import store, { AppDispatch, RootState } from './../store'
+import { AppDispatch, RootState } from './../store'
 import Types from './types'
 
 /** Save competition in presentation state from input id */
@@ -36,17 +35,6 @@ export const setPresentationCode = (code: string) => (dispatch: AppDispatch) =>
   dispatch({ type: Types.SET_PRESENTATION_CODE, payload: code })
 }
 /** Set timer to input value */
-export const setPresentationTimer = (timer: Timer) => (dispatch: AppDispatch) => {
+export const setPresentationTimer = (timer: number | null) => (dispatch: AppDispatch) => {
   dispatch({ type: Types.SET_PRESENTATION_TIMER, payload: timer })
 }
-
-/** Decrement timer */
-export const setPresentationTimerDecrement = () => (dispatch: AppDispatch) => {
-  dispatch({
-    type: Types.SET_PRESENTATION_TIMER,
-    payload: {
-      enabled: store.getState().presentation.timer.enabled,
-      value: store.getState().presentation.timer.value - 1,
-    },
-  })
-}
diff --git a/client/src/pages/views/JudgeViewPage.tsx b/client/src/pages/views/JudgeViewPage.tsx
index a28348a7..be9e4885 100644
--- a/client/src/pages/views/JudgeViewPage.tsx
+++ b/client/src/pages/views/JudgeViewPage.tsx
@@ -11,6 +11,7 @@ import SlideDisplay from '../presentationEditor/components/SlideDisplay'
 import { SlideListItem } from '../presentationEditor/styled'
 import JudgeScoreDisplay from './components/JudgeScoreDisplay'
 import JudgeScoringInstructions from './components/JudgeScoringInstructions'
+import Timer from './components/Timer'
 import {
   Content,
   InnerContent,
@@ -77,6 +78,7 @@ const JudgeViewPage: React.FC = () => {
   return (
     <div style={{ height: '100%' }}>
       <JudgeAppBar position="fixed">
+        <Timer />
         <JudgeToolbar>
           <JudgeQuestionsLabel variant="h5">Frågor</JudgeQuestionsLabel>
           {operatorActiveSlideOrder !== undefined && (
diff --git a/client/src/pages/views/OperatorViewPage.tsx b/client/src/pages/views/OperatorViewPage.tsx
index 5bb96fcb..d873a731 100644
--- a/client/src/pages/views/OperatorViewPage.tsx
+++ b/client/src/pages/views/OperatorViewPage.tsx
@@ -34,11 +34,10 @@ import { RichTeam } from '../../interfaces/ApiRichModels'
 import {
   socketConnect,
   socketEndPresentation,
-  socketSetSlide,
   socketSetSlideNext,
   socketSetSlidePrev,
+  socketSetTimer,
   socketStartPresentation,
-  socketStartTimer,
 } from '../../sockets'
 import SlideDisplay from '../presentationEditor/components/SlideDisplay'
 import { Center } from '../presentationEditor/components/styled'
@@ -111,6 +110,7 @@ const OperatorViewPage: React.FC = () => {
   const competitionId = useAppSelector((state) => state.competitionLogin.data?.competition_id)
   const presentation = useAppSelector((state) => state.presentation)
   const activeId = useAppSelector((state) => state.presentation.competition.id)
+  const timer = useAppSelector((state) => state.presentation.timer)
   const history = useHistory()
   const viewTypes = useAppSelector((state) => state.types.viewTypes)
   const activeViewTypeId = viewTypes.find((viewType) => viewType.name === 'Audience')?.id
@@ -119,9 +119,11 @@ const OperatorViewPage: React.FC = () => {
     (state) =>
       state.presentation.competition.slides.find((slide) => slide.id === state.presentation.activeSlideId)?.order
   )
+  const slideTimer = useAppSelector((state) =>
+    activeSlideOrder !== undefined ? state.presentation.competition.slides[activeSlideOrder].timer : null
+  )
   useEffect(() => {
     socketConnect('Operator')
-    socketSetSlide
     handleOpenCodes()
     setTimeout(startCompetition, 1000) // Wait for socket to connect
   }, [])
@@ -222,6 +224,14 @@ const OperatorViewPage: React.FC = () => {
     return totalScore
   }
 
+  const handleStartTimer = () => {
+    if (!slideTimer) return
+    // has active timer already, so cancel it
+    if (timer && timer - Date.now() > 0) socketSetTimer(Date.now())
+    // Either has expired timer or no timer so start one
+    else socketSetTimer(Date.now() + 1000 * slideTimer)
+  }
+
   return (
     <OperatorContainer>
       <Dialog
@@ -325,12 +335,14 @@ const OperatorViewPage: React.FC = () => {
             </OperatorButton>
           </Tooltip>
 
-          <Tooltip title="Starta Timer" arrow>
-            <OperatorButton onClick={socketStartTimer} variant="contained">
-              <TimerIcon fontSize="large" />
-              <Timer></Timer>
-            </OperatorButton>
-          </Tooltip>
+          {slideTimer && (
+            <Tooltip title="Starta Timer" arrow>
+              <OperatorButton onClick={() => handleStartTimer()} variant="contained">
+                <TimerIcon fontSize="large" />
+                <Timer />
+              </OperatorButton>
+            </Tooltip>
+          )}
 
           <Tooltip title="Ställning" arrow>
             <OperatorButton onClick={handleOpenPopover} variant="contained">
diff --git a/client/src/pages/views/components/Timer.tsx b/client/src/pages/views/components/Timer.tsx
index 29f95349..f1612e6b 100644
--- a/client/src/pages/views/components/Timer.tsx
+++ b/client/src/pages/views/components/Timer.tsx
@@ -1,46 +1,27 @@
-import React, { useEffect } from 'react'
-import { setPresentationTimer, setPresentationTimerDecrement } from '../../../actions/presentation'
-import { useAppDispatch, useAppSelector } from '../../../hooks'
-import store from '../../../store'
-
-/* const mapStateToProps = (state: any) => {
-  return {
-    timer: state.presentation.timer,
-    timer_start_value: state.presentation.slide.timer,
-  }
-}
-
-const mapDispatchToProps = (dispatch: any) => {
-  return {
-    // tickTimer: () => dispatch(tickTimer(1)),
-  }
-} */
+import React, { useEffect, useState } from 'react'
+import { useAppSelector } from '../../../hooks'
 
 let timerIntervalId: NodeJS.Timeout
 
 const Timer: React.FC = () => {
-  const dispatch = useAppDispatch()
-  const slide = store
-    .getState()
-    .presentation.competition.slides.find((slide) => slide.id === store.getState().presentation.activeSlideId)
-  const timerStartValue = slide?.timer
   const timer = useAppSelector((state) => state.presentation.timer)
+  const [remainingTimer, setRemainingTimer] = useState<number>(0)
+  const slideTimer = useAppSelector(
+    (state) =>
+      state.presentation.competition.slides.find((slide) => slide.id === state.presentation.activeSlideId)?.timer
+  )
   useEffect(() => {
-    if (!slide) return
-    dispatch(setPresentationTimer({ enabled: false, value: slide.timer }))
-  }, [timerStartValue])
-
-  useEffect(() => {
-    if (timer.enabled) {
+    if (timer !== null && timer - Date.now() > 0) {
+      setRemainingTimer(timer - Date.now())
       timerIntervalId = setInterval(() => {
-        dispatch(setPresentationTimerDecrement())
-      }, 1000)
+        setRemainingTimer(timer - Date.now())
+      }, 500)
     } else {
       clearInterval(timerIntervalId)
     }
-  }, [timer.enabled])
-
-  return <div>{timer.value}</div>
+  }, [timer])
+  // :)
+  return <div>{timer !== null ? (timer - Date.now() > 0 ? Math.round(remainingTimer / 1000) : 0) : slideTimer}</div>
 }
 
 export default Timer
diff --git a/client/src/reducers/presentationReducer.ts b/client/src/reducers/presentationReducer.ts
index 4df814a8..cb74c1ba 100644
--- a/client/src/reducers/presentationReducer.ts
+++ b/client/src/reducers/presentationReducer.ts
@@ -1,6 +1,5 @@
 import { AnyAction } from 'redux'
 import Types from '../actions/types'
-import { Timer } from '../interfaces/Timer'
 import { RichCompetition } from './../interfaces/ApiRichModels'
 
 /** Define a type for the presentation state */
@@ -8,7 +7,7 @@ interface PresentationState {
   competition: RichCompetition
   activeSlideId: number
   code: string
-  timer: Timer
+  timer: number | null
 }
 
 /** Define the initial values for the presentation state */
@@ -24,10 +23,7 @@ const initialState: PresentationState = {
   },
   activeSlideId: -1,
   code: '',
-  timer: {
-    enabled: false,
-    value: 0,
-  },
+  timer: null,
 }
 
 /** Intercept actions for presentation state and update the state */
@@ -41,7 +37,7 @@ export default function (state = initialState, action: AnyAction) {
     case Types.SET_PRESENTATION_CODE:
       return {
         ...state,
-        code: action.payload,
+        code: action.payload as string,
       }
     case Types.SET_PRESENTATION_SLIDE_ID:
       return {
@@ -49,12 +45,9 @@ export default function (state = initialState, action: AnyAction) {
         activeSlideId: action.payload as number,
       }
     case Types.SET_PRESENTATION_TIMER:
-      if (action.payload.value == 0) {
-        action.payload.enabled = false
-      }
       return {
         ...state,
-        timer: action.payload,
+        timer: action.payload as number,
       }
     default:
       return state
diff --git a/client/src/sockets.ts b/client/src/sockets.ts
index 1d53a5a4..b5de4c49 100644
--- a/client/src/sockets.ts
+++ b/client/src/sockets.ts
@@ -1,34 +1,17 @@
-/**
- * This is a comment on the module level, i.e. the entire file.
- *
- * For this to appear in the documentation this is needed at the bottom.
- * @module
- */
-
 import io from 'socket.io-client'
 import { setCurrentSlideByOrder, setPresentationTimer } from './actions/presentation'
-import { Timer } from './interfaces/Timer'
 import store from './store'
 
 interface SetSlideInterface {
   slide_order: number
 }
 
-interface TimerInterface {
-  value: number
-  enabled: boolean
-}
-
 interface SetTimerInterface {
-  timer: TimerInterface
+  timer: number | null
 }
 
 let socket: SocketIOClient.Socket
 
-/**
- * You can also comment functions, like usual. This will automatically appear
- * in the documentation, no more needed.
- */
 export const socketConnect = (role: 'Judge' | 'Operator' | 'Team' | 'Audience') => {
   if (socket) return
 
@@ -84,13 +67,6 @@ export const socketSetSlidePrev = () => {
   socketSetSlide(activeSlide.order - 1)
 }
 
-/**
- * You can also comment a function like, adding more information to either
- * the paramters or the return value.
- *
- * @param slide_order This is a parameter to the function.
- * @returns This function returns nothing.
- */
 export const socketSetSlide = (slide_order: number) => {
   if (slide_order < 0 || store.getState().presentation.competition.slides.length <= slide_order) {
     return
@@ -101,12 +77,12 @@ export const socketSetSlide = (slide_order: number) => {
   })
 }
 
-export const socketSetTimer = (timer: Timer) => {
+export const socketSetTimer = (timer: number | null) => {
   socket.emit('set_timer', {
     timer: timer,
   })
 }
 
 export const socketStartTimer = () => {
-  socketSetTimer({ enabled: true, value: store.getState().presentation.timer.value })
+  socketSetTimer(store.getState().presentation.timer)
 }
diff --git a/server/app/core/sockets.py b/server/app/core/sockets.py
index c5ec33ae..9c26fa05 100644
--- a/server/app/core/sockets.py
+++ b/server/app/core/sockets.py
@@ -99,7 +99,7 @@ def start_presentation() -> None:
     presentations[competition_id] = {
         "clients": {request.sid: {"view_type": "Operator"}},
         "slide": None,
-        "timer": {"enabled": False, "start_value": None, "value": None},
+        "timer": None,
     }
 
     join_room(competition_id)
@@ -184,7 +184,7 @@ def join_presentation() -> None:
 
     join_room(competition_id)
     logger.debug(f"Client '{request.sid}' joined room {competition_id}")
-
+    emit("set_timer", {"timer": presentations[competition_id]["timer"]})
     logger.info(f"Client '{request.sid}' joined competition '{competition_id}'")
 
 
@@ -271,5 +271,5 @@ def set_timer(data: Dict) -> None:
 
     emit("set_timer", {"timer": timer}, room=competition_id, include_self=True)
     logger.debug(f"Emitting event 'set_timer' to room {competition_id} including self")
-
+    presentations[competition_id]["timer"] = timer
     logger.info(f"Client '{request.sid}' set timer '{timer}' in presentation '{competition_id}'")
-- 
GitLab


From 31eb3c4c59a1dc2985df721fa8a58874b3d9e68e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Victor=20L=C3=B6fgren?= <viclo211@student.liu.se>
Date: Wed, 12 May 2021 19:21:49 +0200
Subject: [PATCH 04/15] Refactor sockets on server

---
 server/app/core/sockets.py | 226 +++++++++----------------------------
 server/requirements.txt    | Bin 3330 -> 3366 bytes
 2 files changed, 52 insertions(+), 174 deletions(-)

diff --git a/server/app/core/sockets.py b/server/app/core/sockets.py
index 9c26fa05..ff53b284 100644
--- a/server/app/core/sockets.py
+++ b/server/app/core/sockets.py
@@ -8,7 +8,9 @@ from functools import wraps
 from typing import Dict
 
 from app.core import db
-from app.database.models import Code, Slide, ViewType
+from app.database.controller.add import competition
+from app.database.models import Slide
+from decorator import decorator
 from flask.globals import request
 from flask_jwt_extended import verify_jwt_in_request
 from flask_jwt_extended.utils import get_jwt_claims
@@ -18,7 +20,7 @@ logger = logging.getLogger(__name__)
 logger.propagate = False
 logger.setLevel(logging.INFO)
 
-formatter = logging.Formatter("[%(levelname)s] %(funcName)s: %(message)s")
+formatter = logging.Formatter("[%(levelname)s] %(message)s")
 stream_handler = logging.StreamHandler()
 stream_handler.setFormatter(formatter)
 logger.addHandler(stream_handler)
@@ -32,194 +34,92 @@ def _is_allowed(allowed, actual):
     return actual and "*" in allowed or actual in allowed
 
 
-def protect_route(allowed_views=None):
-    def wrapper(f):
-        @wraps(f)
-        def inner(*args, **kwargs):
-            try:
-                verify_jwt_in_request()
-            except:
-                logger.warning("Missing Authorization Header")
-                return
+@decorator
+def authorize_client(f, allowed_views=None, require_active_competition=True, *args, **kwargs):
+    try:
+        verify_jwt_in_request()
+    except:
+        logger.error("Cant call function '{f.__name__}': Missing Authorization Header")
+        return
 
-            nonlocal allowed_views
-            allowed_views = allowed_views or []
-            claims = get_jwt_claims()
-            view = claims.get("view")
-            if not _is_allowed(allowed_views, view):
-                logger.warning(f"View '{view}' is not allowed to access route only accessible by '{allowed_views}'")
-                return
+    claims = get_jwt_claims()
 
-            return f(*args, **kwargs)
+    competition_id = claims.get("competition_id")
+    if require_active_competition and competition_id not in presentations:
+        logger.error(f"Cant call function '{f.__name__}': Competition '{competition_id}' is not active")
+        return
 
-        return inner
+    allowed_views = allowed_views or []
+    view = claims.get("view")
+    if not _is_allowed(allowed_views, view):
+        logger.error(f"Cant call function '{f.__name__}': View '{view}' is not in '{''.join(allowed_views)}'")
+        return
 
-    return wrapper
+    return f(*args, **kwargs)
 
 
 @sio.event
+@authorize_client(require_active_competition=False, allowed_views=["*"])
 def connect() -> None:
-    logger.info(f"Client '{request.sid}' connected")
+
+    claims = get_jwt_claims()
+    view = claims.get("view")
+    competition_id = claims.get("competition_id")
+
+    if competition_id in presentations:
+        presentations[competition_id]["client_count"] += 1
+        join_room(competition_id)
+        emit("set_timer", {"timer": presentations[competition_id]["timer"]})
+        logger.info(f"Client '{request.sid}' with view '{view}' joined competition '{competition_id}'")
+    elif view == "Operator":
+        join_room(competition_id)
+        presentations[competition_id] = {
+            "client_count": 1,
+            "slide": None,
+            "timer": None,
+        }
+        logger.info(f"Client '{request.sid}' started competition {competition_id}")
 
 
 @sio.event
+@authorize_client(allowed_views=["*"])
 def disconnect() -> None:
     """
     Remove client from the presentation it was in. Delete presentation if no
     clients are connected to it.
     """
-    for competition_id, presentation in presentations.items():
-        if request.sid in presentation["clients"]:
-            del presentation["clients"][request.sid]
-            logger.debug(f"Client '{request.sid}' left presentation '{competition_id}'")
-            break
-
-    if presentations and not presentations[competition_id]["clients"]:
-        del presentations[competition_id]
-        logger.info(f"No people left in presentation '{competition_id}', ended presentation")
-
-    logger.info(f"Client '{request.sid}' disconnected")
-
-
-@sio.event
-@protect_route(allowed_views=["Operator"])
-def start_presentation() -> None:
-    """
-    Starts a presentation if that competition is currently not active.
-    """
 
     competition_id = get_jwt_claims().get("competition_id")
+    presentations[competition_id]["client_count"] -= 1
+    logger.info(f"Client '{request.sid}' disconnected")
 
-    if competition_id in presentations:
-        logger.error(
-            f"Client '{request.sid}' failed to start competition '{competition_id}', presentation already active"
-        )
-        return
-
-    presentations[competition_id] = {
-        "clients": {request.sid: {"view_type": "Operator"}},
-        "slide": None,
-        "timer": None,
-    }
-
-    join_room(competition_id)
-    logger.debug(f"Client '{request.sid}' joined room {competition_id}")
-
-    logger.info(f"Client '{request.sid}' started competition '{competition_id}'")
+    if presentations[competition_id]["client_count"] <= 0:
+        del presentations[competition_id]
+        logger.info(f"No people left in presentation '{competition_id}', ended presentation")
 
 
 @sio.event
-@protect_route(allowed_views=["Operator"])
+@authorize_client(allowed_views=["Operator"])
 def end_presentation() -> None:
     """
     End a presentation by sending end_presentation to all connected clients.
-
-    The only clients allowed to do this is the one that started the presentation.
-
-    Log error message if no presentation exists with the send id or if this
-    client is not in that presentation.
     """
 
     competition_id = get_jwt_claims().get("competition_id")
-
-    if competition_id not in presentations:
-        logger.error(
-            f"Client '{request.sid}' failed to end presentation '{competition_id}', no such presentation exists"
-        )
-        return
-
-    if request.sid not in presentations[competition_id]["clients"]:
-        logger.error(
-            f"Client '{request.sid}' failed to end presentation '{competition_id}', client not in presentation"
-        )
-        return
-
-    if presentations[competition_id]["clients"][request.sid]["view_type"] != "Operator":
-        logger.error(f"Client '{request.sid}' failed to end presentation '{competition_id}', client is not operator")
-        return
-
-    del presentations[competition_id]
-    logger.debug(f"Deleted presentation {competition_id}")
-
     emit("end_presentation", room=competition_id, include_self=True)
-    logger.debug(f"Emitting event 'end_presentation' to room {competition_id} including self")
-
-    logger.info(f"Client '{request.sid}' ended presentation '{competition_id}'")
 
 
 @sio.event
-@protect_route(allowed_views=["*"])
-def join_presentation() -> None:
-    """
-    Join a currently active presentation.
-
-    Log error message if given code doesn't exist, if not presentation associated
-    with that code exists or if client is already in the presentation.
-    """
-    code = get_jwt_claims().get("code")
-    item_code = db.session.query(Code).filter(Code.code == code).first()
-
-    if not item_code:
-        logger.error(f"Client '{request.sid}' failed to join presentation with code '{code}', no such code exists")
-        return
-
-    competition_id = item_code.competition_id
-
-    if competition_id not in presentations:
-        logger.error(
-            f"Client '{request.sid}' failed to join presentation '{competition_id}', no such presentation exists"
-        )
-        return
-
-    if request.sid in presentations[competition_id]["clients"]:
-        logger.error(
-            f"Client '{request.sid}' failed to join presentation '{competition_id}', client already in presentation"
-        )
-        return
-
-    # TODO: Write function in database controller to do this
-    view_type_name = db.session.query(ViewType).filter(ViewType.id == item_code.view_type_id).one().name
-
-    presentations[competition_id]["clients"][request.sid] = {"view_type": view_type_name}
-
-    join_room(competition_id)
-    logger.debug(f"Client '{request.sid}' joined room {competition_id}")
-    emit("set_timer", {"timer": presentations[competition_id]["timer"]})
-    logger.info(f"Client '{request.sid}' joined competition '{competition_id}'")
-
-
-@sio.event
-@protect_route(allowed_views=["Operator"])
+@authorize_client(allowed_views=["Operator"])
 def set_slide(data: Dict) -> None:
     """
     Sync slides between all clients in the same presentation by sending
     set_slide to them.
-
-    Log error if the given competition_id is not active, if client is not in
-    that presentation or the client is not the one who started the presentation.
     """
 
     competition_id = get_jwt_claims().get("competition_id")
     slide_order = data["slide_order"]
 
-    if competition_id not in presentations:
-        logger.error(
-            f"Client '{request.sid}' failed to set slide in presentation '{competition_id}', no such presentation exists"
-        )
-        return
-
-    if request.sid not in presentations[competition_id]["clients"]:
-        logger.error(
-            f"Client '{request.sid}' failed to set slide in presentation '{competition_id}', client not in presentation"
-        )
-        return
-
-    if presentations[competition_id]["clients"][request.sid]["view_type"] != "Operator":
-        logger.error(
-            f"Client '{request.sid}' failed to set slide in presentation '{competition_id}', client is not operator"
-        )
-        return
-
     num_slides = db.session.query(Slide).filter(Slide.competition_id == competition_id).count()
 
     if not (0 <= slide_order < num_slides):
@@ -231,13 +131,12 @@ def set_slide(data: Dict) -> None:
     presentations[competition_id]["slide"] = slide_order
 
     emit("set_slide", {"slide_order": slide_order}, room=competition_id, include_self=True)
-    logger.debug(f"Emitting event 'set_slide' to room {competition_id} including self")
 
     logger.info(f"Client '{request.sid}' set slide '{slide_order}' in competition '{competition_id}'")
 
 
 @sio.event
-@protect_route(allowed_views=["Operator"])
+@authorize_client(allowed_views=["Operator"])
 def set_timer(data: Dict) -> None:
     """
     Sync slides between all clients in the same presentation by sending
@@ -249,27 +148,6 @@ def set_timer(data: Dict) -> None:
     competition_id = get_jwt_claims().get("competition_id")
     timer = data["timer"]
 
-    if competition_id not in presentations:
-        logger.error(
-            f"Client '{request.sid}' failed to set slide in presentation '{competition_id}', no such presentation exists"
-        )
-        return
-
-    if request.sid not in presentations[competition_id]["clients"]:
-        logger.error(
-            f"Client '{request.sid}' failed to set slide in presentation '{competition_id}', client not in presentation"
-        )
-        return
-
-    if presentations[competition_id]["clients"][request.sid]["view_type"] != "Operator":
-        logger.error(
-            f"Client '{request.sid}' failed to set slide in presentation '{competition_id}', client is not operator"
-        )
-        return
-
-    # TODO: Save timer in presentation, maybe?
-
-    emit("set_timer", {"timer": timer}, room=competition_id, include_self=True)
-    logger.debug(f"Emitting event 'set_timer' to room {competition_id} including self")
     presentations[competition_id]["timer"] = timer
-    logger.info(f"Client '{request.sid}' set timer '{timer}' in presentation '{competition_id}'")
+    emit("set_timer", {"timer": timer}, room=competition_id, include_self=True)
+    logger.info(f"({competition_id}) Timer set to '{timer}' ")
diff --git a/server/requirements.txt b/server/requirements.txt
index 6a9e48fc29b527a9a3609716953d4ba5a52fe4e2..472b12cdf8229664d6d4bc679e6a85cb941341d7 100644
GIT binary patch
delta 42
scmZpYS|+u@fJru$A(<hcp@<=op#)6ZGT1VhGUzcF0I~UIO{SY%0M7IX?f?J)

delta 12
TcmZ1`)g-mSfN66G(<Lqd98Ux!

-- 
GitLab


From 90efe0c2624c92d02a4823678856802016dd5590 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Victor=20L=C3=B6fgren?= <viclo211@student.liu.se>
Date: Wed, 12 May 2021 19:37:26 +0200
Subject: [PATCH 05/15] Minor refactoring in sockets on backend

---
 server/app/core/sockets.py | 59 +++++++++++++++++---------------------
 1 file changed, 26 insertions(+), 33 deletions(-)

diff --git a/server/app/core/sockets.py b/server/app/core/sockets.py
index ff53b284..4ee37088 100644
--- a/server/app/core/sockets.py
+++ b/server/app/core/sockets.py
@@ -30,8 +30,13 @@ sio = SocketIO(cors_allowed_origins="http://localhost:3000")
 presentations = {}
 
 
-def _is_allowed(allowed, actual):
-    return actual and "*" in allowed or actual in allowed
+def unpack_claims():
+    claims = get_jwt_claims()
+    return (claims["competition_id"], claims["view"])
+
+
+def is_active_competition(competition_id):
+    return competition_id in presentations
 
 
 @decorator
@@ -39,20 +44,23 @@ def authorize_client(f, allowed_views=None, require_active_competition=True, *ar
     try:
         verify_jwt_in_request()
     except:
-        logger.error("Cant call function '{f.__name__}': Missing Authorization Header")
+        logger.error(f"Won't call function '{f.__name__}': Missing Authorization Header")
         return
 
+    def _is_allowed(allowed, actual):
+        return actual and "*" in allowed or actual in allowed
+
     claims = get_jwt_claims()
 
     competition_id = claims.get("competition_id")
-    if require_active_competition and competition_id not in presentations:
-        logger.error(f"Cant call function '{f.__name__}': Competition '{competition_id}' is not active")
+    if require_active_competition and not is_active_competition(competition_id):
+        logger.error(f"Won't call function '{f.__name__}': Competition '{competition_id}' is not active")
         return
 
     allowed_views = allowed_views or []
     view = claims.get("view")
     if not _is_allowed(allowed_views, view):
-        logger.error(f"Cant call function '{f.__name__}': View '{view}' is not in '{''.join(allowed_views)}'")
+        logger.error(f"Won't call function '{f.__name__}': View '{view}' is not in '{''.join(allowed_views)}'")
         return
 
     return f(*args, **kwargs)
@@ -62,13 +70,12 @@ def authorize_client(f, allowed_views=None, require_active_competition=True, *ar
 @authorize_client(require_active_competition=False, allowed_views=["*"])
 def connect() -> None:
 
-    claims = get_jwt_claims()
-    view = claims.get("view")
-    competition_id = claims.get("competition_id")
+    competition_id, view = unpack_claims()
 
-    if competition_id in presentations:
+    if is_active_competition(competition_id):
         presentations[competition_id]["client_count"] += 1
         join_room(competition_id)
+        emit("set_slide", {"slide": presentations[competition_id]["slide"]})
         emit("set_timer", {"timer": presentations[competition_id]["timer"]})
         logger.info(f"Client '{request.sid}' with view '{view}' joined competition '{competition_id}'")
     elif view == "Operator":
@@ -78,7 +85,7 @@ def connect() -> None:
             "slide": None,
             "timer": None,
         }
-        logger.info(f"Client '{request.sid}' started competition {competition_id}")
+        logger.info(f"Client '{request.sid}' started competition '{competition_id}'")
 
 
 @sio.event
@@ -89,9 +96,9 @@ def disconnect() -> None:
     clients are connected to it.
     """
 
-    competition_id = get_jwt_claims().get("competition_id")
+    competition_id, _ = unpack_claims()
     presentations[competition_id]["client_count"] -= 1
-    logger.info(f"Client '{request.sid}' disconnected")
+    logger.info(f"Client '{request.sid}' disconnected from competition '{competition_id}'")
 
     if presentations[competition_id]["client_count"] <= 0:
         del presentations[competition_id]
@@ -105,7 +112,7 @@ def end_presentation() -> None:
     End a presentation by sending end_presentation to all connected clients.
     """
 
-    competition_id = get_jwt_claims().get("competition_id")
+    competition_id, _ = unpack_claims()
     emit("end_presentation", room=competition_id, include_self=True)
 
 
@@ -117,22 +124,11 @@ def set_slide(data: Dict) -> None:
     set_slide to them.
     """
 
-    competition_id = get_jwt_claims().get("competition_id")
+    competition_id, _ = unpack_claims()
     slide_order = data["slide_order"]
-
-    num_slides = db.session.query(Slide).filter(Slide.competition_id == competition_id).count()
-
-    if not (0 <= slide_order < num_slides):
-        logger.error(
-            f"Client '{request.sid}' failed to set slide in presentation '{competition_id}', slide number {slide_order} does not exist"
-        )
-        return
-
     presentations[competition_id]["slide"] = slide_order
-
     emit("set_slide", {"slide_order": slide_order}, room=competition_id, include_self=True)
-
-    logger.info(f"Client '{request.sid}' set slide '{slide_order}' in competition '{competition_id}'")
+    logger.info(f"Client '{request.sid}' set slide to '{slide_order}' in competition '{competition_id}'")
 
 
 @sio.event
@@ -141,13 +137,10 @@ def set_timer(data: Dict) -> None:
     """
     Sync slides between all clients in the same presentation by sending
     set_timer to them.
-
-    Log error if the given competition_id is not active, if client is not in
-    that presentation or the client is not the one who started the presentation.
     """
-    competition_id = get_jwt_claims().get("competition_id")
-    timer = data["timer"]
 
+    competition_id, _ = unpack_claims()
+    timer = data["timer"]
     presentations[competition_id]["timer"] = timer
     emit("set_timer", {"timer": timer}, room=competition_id, include_self=True)
-    logger.info(f"({competition_id}) Timer set to '{timer}' ")
+    logger.info(f"Client '{request.sid}' set timer to '{timer}' in competition '{competition_id}'")
-- 
GitLab


From b563dac4e06297ebb9899357393482c7ff922dfd Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Victor=20L=C3=B6fgren?= <viclo211@student.liu.se>
Date: Wed, 12 May 2021 19:40:09 +0200
Subject: [PATCH 06/15] Add underscore to beginning of function names in
 sockets

---
 server/app/core/sockets.py | 24 +++++++++++-------------
 1 file changed, 11 insertions(+), 13 deletions(-)

diff --git a/server/app/core/sockets.py b/server/app/core/sockets.py
index 4ee37088..ef463c07 100644
--- a/server/app/core/sockets.py
+++ b/server/app/core/sockets.py
@@ -30,12 +30,12 @@ sio = SocketIO(cors_allowed_origins="http://localhost:3000")
 presentations = {}
 
 
-def unpack_claims():
+def _unpack_claims():
     claims = get_jwt_claims()
-    return (claims["competition_id"], claims["view"])
+    return claims["competition_id"], claims["view"]
 
 
-def is_active_competition(competition_id):
+def _is_active_competition(competition_id):
     return competition_id in presentations
 
 
@@ -50,15 +50,13 @@ def authorize_client(f, allowed_views=None, require_active_competition=True, *ar
     def _is_allowed(allowed, actual):
         return actual and "*" in allowed or actual in allowed
 
-    claims = get_jwt_claims()
+    competition_id, view = _unpack_claims()
 
-    competition_id = claims.get("competition_id")
-    if require_active_competition and not is_active_competition(competition_id):
+    if require_active_competition and not _is_active_competition(competition_id):
         logger.error(f"Won't call function '{f.__name__}': Competition '{competition_id}' is not active")
         return
 
     allowed_views = allowed_views or []
-    view = claims.get("view")
     if not _is_allowed(allowed_views, view):
         logger.error(f"Won't call function '{f.__name__}': View '{view}' is not in '{''.join(allowed_views)}'")
         return
@@ -70,9 +68,9 @@ def authorize_client(f, allowed_views=None, require_active_competition=True, *ar
 @authorize_client(require_active_competition=False, allowed_views=["*"])
 def connect() -> None:
 
-    competition_id, view = unpack_claims()
+    competition_id, view = _unpack_claims()
 
-    if is_active_competition(competition_id):
+    if _is_active_competition(competition_id):
         presentations[competition_id]["client_count"] += 1
         join_room(competition_id)
         emit("set_slide", {"slide": presentations[competition_id]["slide"]})
@@ -96,7 +94,7 @@ def disconnect() -> None:
     clients are connected to it.
     """
 
-    competition_id, _ = unpack_claims()
+    competition_id, _ = _unpack_claims()
     presentations[competition_id]["client_count"] -= 1
     logger.info(f"Client '{request.sid}' disconnected from competition '{competition_id}'")
 
@@ -112,7 +110,7 @@ def end_presentation() -> None:
     End a presentation by sending end_presentation to all connected clients.
     """
 
-    competition_id, _ = unpack_claims()
+    competition_id, _ = _unpack_claims()
     emit("end_presentation", room=competition_id, include_self=True)
 
 
@@ -124,7 +122,7 @@ def set_slide(data: Dict) -> None:
     set_slide to them.
     """
 
-    competition_id, _ = unpack_claims()
+    competition_id, _ = _unpack_claims()
     slide_order = data["slide_order"]
     presentations[competition_id]["slide"] = slide_order
     emit("set_slide", {"slide_order": slide_order}, room=competition_id, include_self=True)
@@ -139,7 +137,7 @@ def set_timer(data: Dict) -> None:
     set_timer to them.
     """
 
-    competition_id, _ = unpack_claims()
+    competition_id, _ = _unpack_claims()
     timer = data["timer"]
     presentations[competition_id]["timer"] = timer
     emit("set_timer", {"timer": timer}, room=competition_id, include_self=True)
-- 
GitLab


From e7a47ce4d108c436b817bb7e9dc56c0463a1d6ec Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Victor=20L=C3=B6fgren?= <viclo211@student.liu.se>
Date: Wed, 12 May 2021 20:19:13 +0200
Subject: [PATCH 07/15] Refactor setTimer and setSlide to just sync

---
 client/src/pages/views/AudienceViewPage.tsx |  3 +-
 client/src/pages/views/JudgeViewPage.tsx    |  3 +-
 client/src/pages/views/OperatorViewPage.tsx |  8 ++---
 client/src/pages/views/TeamViewPage.tsx     |  3 +-
 client/src/sockets.ts                       | 34 +++++-------------
 server/app/core/sockets.py                  | 40 +++++++++------------
 6 files changed, 31 insertions(+), 60 deletions(-)

diff --git a/client/src/pages/views/AudienceViewPage.tsx b/client/src/pages/views/AudienceViewPage.tsx
index af1f18b3..e78db8b0 100644
--- a/client/src/pages/views/AudienceViewPage.tsx
+++ b/client/src/pages/views/AudienceViewPage.tsx
@@ -2,7 +2,7 @@ import { Snackbar, Typography } from '@material-ui/core'
 import { Alert } from '@material-ui/lab'
 import React, { useEffect, useState } from 'react'
 import { useAppSelector } from '../../hooks'
-import { socketConnect, socketJoinPresentation } from '../../sockets'
+import { socketConnect } from '../../sockets'
 import SlideDisplay from '../presentationEditor/components/SlideDisplay'
 import { PresentationBackground, PresentationContainer } from './styled'
 
@@ -15,7 +15,6 @@ const AudienceViewPage: React.FC = () => {
   useEffect(() => {
     if (code && code !== '') {
       socketConnect('Audience')
-      socketJoinPresentation()
     }
   }, [])
   if (activeViewTypeId) {
diff --git a/client/src/pages/views/JudgeViewPage.tsx b/client/src/pages/views/JudgeViewPage.tsx
index be9e4885..b77f145e 100644
--- a/client/src/pages/views/JudgeViewPage.tsx
+++ b/client/src/pages/views/JudgeViewPage.tsx
@@ -5,7 +5,7 @@ import React, { useEffect, useState } from 'react'
 import { getPresentationCompetition } from '../../actions/presentation'
 import { useAppDispatch, useAppSelector } from '../../hooks'
 import { RichSlide } from '../../interfaces/ApiRichModels'
-import { socketConnect, socketJoinPresentation } from '../../sockets'
+import { socketConnect } from '../../sockets'
 import { renderSlideIcon } from '../../utils/renderSlideIcon'
 import SlideDisplay from '../presentationEditor/components/SlideDisplay'
 import { SlideListItem } from '../presentationEditor/styled'
@@ -64,7 +64,6 @@ const JudgeViewPage: React.FC = () => {
   useEffect(() => {
     if (code && code !== '') {
       socketConnect('Judge')
-      socketJoinPresentation()
     }
   }, [])
   useEffect(() => {
diff --git a/client/src/pages/views/OperatorViewPage.tsx b/client/src/pages/views/OperatorViewPage.tsx
index d873a731..fcede248 100644
--- a/client/src/pages/views/OperatorViewPage.tsx
+++ b/client/src/pages/views/OperatorViewPage.tsx
@@ -36,8 +36,7 @@ import {
   socketEndPresentation,
   socketSetSlideNext,
   socketSetSlidePrev,
-  socketSetTimer,
-  socketStartPresentation,
+  socketSyncTimer,
 } from '../../sockets'
 import SlideDisplay from '../presentationEditor/components/SlideDisplay'
 import { Center } from '../presentationEditor/components/styled'
@@ -145,7 +144,6 @@ const OperatorViewPage: React.FC = () => {
   }
 
   const startCompetition = () => {
-    socketStartPresentation() // Calls the socket to start competition
     console.log('started competition for')
     console.log(competitionId)
   }
@@ -227,9 +225,9 @@ const OperatorViewPage: React.FC = () => {
   const handleStartTimer = () => {
     if (!slideTimer) return
     // has active timer already, so cancel it
-    if (timer && timer - Date.now() > 0) socketSetTimer(Date.now())
+    if (timer && timer - Date.now() > 0) socketSyncTimer(Date.now())
     // Either has expired timer or no timer so start one
-    else socketSetTimer(Date.now() + 1000 * slideTimer)
+    else socketSyncTimer(Date.now() + 1000 * slideTimer)
   }
 
   return (
diff --git a/client/src/pages/views/TeamViewPage.tsx b/client/src/pages/views/TeamViewPage.tsx
index fd51c884..e98bd0e5 100644
--- a/client/src/pages/views/TeamViewPage.tsx
+++ b/client/src/pages/views/TeamViewPage.tsx
@@ -2,7 +2,7 @@ import { Snackbar } from '@material-ui/core'
 import { Alert } from '@material-ui/lab'
 import React, { useEffect, useState } from 'react'
 import { useAppSelector } from '../../hooks'
-import { socketConnect, socketJoinPresentation } from '../../sockets'
+import { socketConnect } from '../../sockets'
 import SlideDisplay from '../presentationEditor/components/SlideDisplay'
 import { PresentationBackground, PresentationContainer } from './styled'
 
@@ -19,7 +19,6 @@ const TeamViewPage: React.FC = () => {
   useEffect(() => {
     if (code && code !== '') {
       socketConnect('Team')
-      socketJoinPresentation()
     }
   }, [])
   return (
diff --git a/client/src/sockets.ts b/client/src/sockets.ts
index b5de4c49..a5f3ac42 100644
--- a/client/src/sockets.ts
+++ b/client/src/sockets.ts
@@ -2,11 +2,8 @@ import io from 'socket.io-client'
 import { setCurrentSlideByOrder, setPresentationTimer } from './actions/presentation'
 import store from './store'
 
-interface SetSlideInterface {
+interface SyncInterface {
   slide_order: number
-}
-
-interface SetTimerInterface {
   timer: number | null
 }
 
@@ -26,11 +23,8 @@ export const socketConnect = (role: 'Judge' | 'Operator' | 'Team' | 'Audience')
     },
   })
 
-  socket.on('set_slide', (data: SetSlideInterface) => {
+  socket.on('sync', (data: SyncInterface) => {
     setCurrentSlideByOrder(data.slide_order)(store.dispatch, store.getState)
-  })
-
-  socket.on('set_timer', (data: SetTimerInterface) => {
     setPresentationTimer(data.timer)(store.dispatch)
   })
 
@@ -39,14 +33,6 @@ export const socketConnect = (role: 'Judge' | 'Operator' | 'Team' | 'Audience')
   })
 }
 
-export const socketStartPresentation = () => {
-  socket.emit('start_presentation')
-}
-
-export const socketJoinPresentation = () => {
-  socket.emit('join_presentation')
-}
-
 export const socketEndPresentation = () => {
   socket.emit('end_presentation')
 }
@@ -56,7 +42,7 @@ export const socketSetSlideNext = () => {
     .getState()
     .presentation.competition.slides.find((slide) => slide.id === store.getState().presentation.activeSlideId)
   if (!activeSlide) return
-  socketSetSlide(activeSlide.order + 1)
+  socketSyncSlide(activeSlide.order + 1)
 }
 
 export const socketSetSlidePrev = () => {
@@ -64,25 +50,21 @@ export const socketSetSlidePrev = () => {
     .getState()
     .presentation.competition.slides.find((slide) => slide.id === store.getState().presentation.activeSlideId)
   if (!activeSlide) return
-  socketSetSlide(activeSlide.order - 1)
+  socketSyncSlide(activeSlide.order - 1)
 }
 
-export const socketSetSlide = (slide_order: number) => {
+export const socketSyncSlide = (slide_order: number) => {
   if (slide_order < 0 || store.getState().presentation.competition.slides.length <= slide_order) {
     return
   }
 
-  socket.emit('set_slide', {
+  socket.emit('sync', {
     slide_order: slide_order,
   })
 }
 
-export const socketSetTimer = (timer: number | null) => {
-  socket.emit('set_timer', {
+export const socketSyncTimer = (timer: number | null) => {
+  socket.emit('sync', {
     timer: timer,
   })
 }
-
-export const socketStartTimer = () => {
-  socketSetTimer(store.getState().presentation.timer)
-}
diff --git a/server/app/core/sockets.py b/server/app/core/sockets.py
index ef463c07..7057f8e9 100644
--- a/server/app/core/sockets.py
+++ b/server/app/core/sockets.py
@@ -39,6 +39,10 @@ def _is_active_competition(competition_id):
     return competition_id in presentations
 
 
+def _get_sync_variables(presentation):
+    return {key: value for key, value in presentation.items() if key != "client_count"}
+
+
 @decorator
 def authorize_client(f, allowed_views=None, require_active_competition=True, *args, **kwargs):
     try:
@@ -71,16 +75,16 @@ def connect() -> None:
     competition_id, view = _unpack_claims()
 
     if _is_active_competition(competition_id):
-        presentations[competition_id]["client_count"] += 1
+        presentation = presentations[competition_id]
+        presentation["client_count"] += 1
         join_room(competition_id)
-        emit("set_slide", {"slide": presentations[competition_id]["slide"]})
-        emit("set_timer", {"timer": presentations[competition_id]["timer"]})
+        emit("sync", _get_sync_variables(presentation))
         logger.info(f"Client '{request.sid}' with view '{view}' joined competition '{competition_id}'")
     elif view == "Operator":
         join_room(competition_id)
         presentations[competition_id] = {
             "client_count": 1,
-            "slide": None,
+            "slide_order": None,
             "timer": None,
         }
         logger.info(f"Client '{request.sid}' started competition '{competition_id}'")
@@ -116,29 +120,19 @@ def end_presentation() -> None:
 
 @sio.event
 @authorize_client(allowed_views=["Operator"])
-def set_slide(data: Dict) -> None:
+def sync(data: Dict) -> None:
     """
-    Sync slides between all clients in the same presentation by sending
-    set_slide to them.
+    Sync presentation for all clients connected to competition.
     """
 
     competition_id, _ = _unpack_claims()
-    slide_order = data["slide_order"]
-    presentations[competition_id]["slide"] = slide_order
-    emit("set_slide", {"slide_order": slide_order}, room=competition_id, include_self=True)
-    logger.info(f"Client '{request.sid}' set slide to '{slide_order}' in competition '{competition_id}'")
+    presentation = presentations[competition_id]
 
+    for key, value in data.items():
+        if key not in presentation:
+            logger.warning(f"Invalid sync data: '{key}':'{value}'")
 
-@sio.event
-@authorize_client(allowed_views=["Operator"])
-def set_timer(data: Dict) -> None:
-    """
-    Sync slides between all clients in the same presentation by sending
-    set_timer to them.
-    """
+        if value is not None:
+            presentation[key] = value
 
-    competition_id, _ = _unpack_claims()
-    timer = data["timer"]
-    presentations[competition_id]["timer"] = timer
-    emit("set_timer", {"timer": timer}, room=competition_id, include_self=True)
-    logger.info(f"Client '{request.sid}' set timer to '{timer}' in competition '{competition_id}'")
+    emit("sync", _get_sync_variables(presentation), room=competition_id, include_self=True)
-- 
GitLab


From 5624519cff39c6a266036de85d77b15f9082b771 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Victor=20L=C3=B6fgren?= <viclo211@student.liu.se>
Date: Wed, 12 May 2021 20:39:57 +0200
Subject: [PATCH 08/15] Remove unused import, comment some functions and add
 is_active_competition()

---
 server/app/apis/auth.py    |  4 +-
 server/app/core/sockets.py | 86 +++++++++++++++++++++++---------------
 2 files changed, 54 insertions(+), 36 deletions(-)

diff --git a/server/app/apis/auth.py b/server/app/apis/auth.py
index 26301d64..e29fd4ee 100644
--- a/server/app/apis/auth.py
+++ b/server/app/apis/auth.py
@@ -8,9 +8,9 @@ from datetime import datetime, timedelta
 import app.core.http_codes as codes
 import app.database.controller as dbc
 from app.apis import item_response, protect_route, text_response
-from app.core import sockets
 from app.core.codes import verify_code
 from app.core.dto import AuthDTO
+from app.core.sockets import is_active_competition
 from app.database.models import User, Whitelist
 from flask import current_app, has_app_context
 from flask_jwt_extended import create_access_token, get_jti, get_raw_jwt
@@ -164,7 +164,7 @@ class AuthLoginCode(Resource):
         item_code = dbc.get.code_by_code(code)
 
         if item_code.view_type_id != 4:
-            if item_code.competition_id not in sockets.presentations:
+            if not is_active_competition(item_code.competition_id):
                 api.abort(codes.UNAUTHORIZED, "Competition not active")
 
         # Create jwt that is only valid for 8 hours
diff --git a/server/app/core/sockets.py b/server/app/core/sockets.py
index 7057f8e9..341a269f 100644
--- a/server/app/core/sockets.py
+++ b/server/app/core/sockets.py
@@ -1,15 +1,9 @@
 """
-Contains all functionality related sockets. That is starting and ending a presentation, 
-joining and leaving a presentation and syncing slides and timer bewteen all clients
-connected to the same presentation.
+Contains all functionality related sockets. That is starting, joining, ending, 
+disconnecting from and syncing active competitions.
 """
 import logging
-from functools import wraps
-from typing import Dict
 
-from app.core import db
-from app.database.controller.add import competition
-from app.database.models import Slide
 from decorator import decorator
 from flask.globals import request
 from flask_jwt_extended import verify_jwt_in_request
@@ -27,24 +21,40 @@ logger.addHandler(stream_handler)
 
 sio = SocketIO(cors_allowed_origins="http://localhost:3000")
 
-presentations = {}
+active_competitions = {}
 
 
 def _unpack_claims():
+    """
+    :return: A tuple containing competition_id and view, gotten from claim
+    :rtype: tuple
+    """
+
     claims = get_jwt_claims()
     return claims["competition_id"], claims["view"]
 
 
-def _is_active_competition(competition_id):
-    return competition_id in presentations
+def is_active_competition(competition_id):
+    """
+    :return: True if competition with competition_id is currently active else False
+    :rtype: bool
+    """
+    return competition_id in active_competitions
 
 
-def _get_sync_variables(presentation):
-    return {key: value for key, value in presentation.items() if key != "client_count"}
+def _get_sync_variables(active_competition):
+    return {key: value for key, value in active_competition.items() if key != "client_count"}
 
 
 @decorator
 def authorize_client(f, allowed_views=None, require_active_competition=True, *args, **kwargs):
+    """
+    Decorator used to authorize a client that sends socket events. Check that
+    the client has authorization headers, that client view gotten from claims
+    is in allowed_views and that the competition the clients is in is active
+    if require_active_competition is True.
+    """
+
     try:
         verify_jwt_in_request()
     except:
@@ -56,7 +66,7 @@ def authorize_client(f, allowed_views=None, require_active_competition=True, *ar
 
     competition_id, view = _unpack_claims()
 
-    if require_active_competition and not _is_active_competition(competition_id):
+    if require_active_competition and not is_active_competition(competition_id):
         logger.error(f"Won't call function '{f.__name__}': Competition '{competition_id}' is not active")
         return
 
@@ -71,68 +81,76 @@ def authorize_client(f, allowed_views=None, require_active_competition=True, *ar
 @sio.event
 @authorize_client(require_active_competition=False, allowed_views=["*"])
 def connect() -> None:
+    """
+    Connect to a active competition. If competition with competition_id is not active,
+    start it if client is an operator, otherwise ignore it.
+    """
 
     competition_id, view = _unpack_claims()
 
-    if _is_active_competition(competition_id):
-        presentation = presentations[competition_id]
-        presentation["client_count"] += 1
+    if is_active_competition(competition_id):
+        active_competition = active_competitions[competition_id]
+        active_competition["client_count"] += 1
         join_room(competition_id)
-        emit("sync", _get_sync_variables(presentation))
+        emit("sync", _get_sync_variables(active_competition))
         logger.info(f"Client '{request.sid}' with view '{view}' joined competition '{competition_id}'")
     elif view == "Operator":
         join_room(competition_id)
-        presentations[competition_id] = {
+        active_competitions[competition_id] = {
             "client_count": 1,
             "slide_order": None,
             "timer": None,
         }
-        logger.info(f"Client '{request.sid}' started competition '{competition_id}'")
+        logger.info(f"Client '{request.sid}' with view '{view}' started competition '{competition_id}'")
+    else:
+        logger.error(
+            f"Client '{request.sid}' with view '{view}' tried to join non active competition '{competition_id}'"
+        )
 
 
 @sio.event
 @authorize_client(allowed_views=["*"])
 def disconnect() -> None:
     """
-    Remove client from the presentation it was in. Delete presentation if no
+    Remove client from the active_competition it was in. Delete active_competition if no
     clients are connected to it.
     """
 
     competition_id, _ = _unpack_claims()
-    presentations[competition_id]["client_count"] -= 1
+    active_competitions[competition_id]["client_count"] -= 1
     logger.info(f"Client '{request.sid}' disconnected from competition '{competition_id}'")
 
-    if presentations[competition_id]["client_count"] <= 0:
-        del presentations[competition_id]
-        logger.info(f"No people left in presentation '{competition_id}', ended presentation")
+    if active_competitions[competition_id]["client_count"] <= 0:
+        del active_competitions[competition_id]
+        logger.info(f"No people left in active_competition '{competition_id}', ended active_competition")
 
 
 @sio.event
 @authorize_client(allowed_views=["Operator"])
-def end_presentation() -> None:
+def end_active_competition() -> None:
     """
-    End a presentation by sending end_presentation to all connected clients.
+    End a active_competition by sending end_active_competition to all connected clients.
     """
 
     competition_id, _ = _unpack_claims()
-    emit("end_presentation", room=competition_id, include_self=True)
+    emit("end_active_competition", room=competition_id, include_self=True)
 
 
 @sio.event
 @authorize_client(allowed_views=["Operator"])
-def sync(data: Dict) -> None:
+def sync(data) -> None:
     """
-    Sync presentation for all clients connected to competition.
+    Sync active_competition for all clients connected to competition.
     """
 
     competition_id, _ = _unpack_claims()
-    presentation = presentations[competition_id]
+    active_competition = active_competitions[competition_id]
 
     for key, value in data.items():
-        if key not in presentation:
+        if key not in active_competition:
             logger.warning(f"Invalid sync data: '{key}':'{value}'")
 
         if value is not None:
-            presentation[key] = value
+            active_competition[key] = value
 
-    emit("sync", _get_sync_variables(presentation), room=competition_id, include_self=True)
+    emit("sync", _get_sync_variables(active_competition), room=competition_id, include_self=True)
-- 
GitLab


From a0df047b0911edcbb90cf5e0c5e7867e8094a48d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Victor=20L=C3=B6fgren?= <viclo211@student.liu.se>
Date: Wed, 12 May 2021 20:45:17 +0200
Subject: [PATCH 09/15] Update error message

---
 server/app/core/sockets.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/server/app/core/sockets.py b/server/app/core/sockets.py
index 341a269f..fd1b2fed 100644
--- a/server/app/core/sockets.py
+++ b/server/app/core/sockets.py
@@ -72,7 +72,7 @@ def authorize_client(f, allowed_views=None, require_active_competition=True, *ar
 
     allowed_views = allowed_views or []
     if not _is_allowed(allowed_views, view):
-        logger.error(f"Won't call function '{f.__name__}': View '{view}' is not in '{''.join(allowed_views)}'")
+        logger.error(f"Won't call function '{f.__name__}': View '{view}' is not {' or '.join(allowed_views)}")
         return
 
     return f(*args, **kwargs)
-- 
GitLab


From bc9375235bff207766dfaa9bc384bea6c091ef8d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Victor=20L=C3=B6fgren?= <viclo211@student.liu.se>
Date: Wed, 12 May 2021 20:48:17 +0200
Subject: [PATCH 10/15] Default to first slide on server

---
 server/app/core/sockets.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/server/app/core/sockets.py b/server/app/core/sockets.py
index fd1b2fed..fdb1a7f2 100644
--- a/server/app/core/sockets.py
+++ b/server/app/core/sockets.py
@@ -98,7 +98,7 @@ def connect() -> None:
         join_room(competition_id)
         active_competitions[competition_id] = {
             "client_count": 1,
-            "slide_order": None,
+            "slide_order": 0,
             "timer": None,
         }
         logger.info(f"Client '{request.sid}' with view '{view}' started competition '{competition_id}'")
-- 
GitLab


From 997eb6ae636cb7509876bea5f75ecdb176e72262 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Victor=20L=C3=B6fgren?= <viclo211@student.liu.se>
Date: Wed, 12 May 2021 21:09:44 +0200
Subject: [PATCH 11/15] Fix bug with wrong event name

---
 server/app/core/sockets.py | 11 +++++++----
 1 file changed, 7 insertions(+), 4 deletions(-)

diff --git a/server/app/core/sockets.py b/server/app/core/sockets.py
index fdb1a7f2..4c3c4a4c 100644
--- a/server/app/core/sockets.py
+++ b/server/app/core/sockets.py
@@ -127,13 +127,13 @@ def disconnect() -> None:
 
 @sio.event
 @authorize_client(allowed_views=["Operator"])
-def end_active_competition() -> None:
+def end_presentation() -> None:
     """
-    End a active_competition by sending end_active_competition to all connected clients.
+    End a active_competition by sending end_presentation to all connected clients.
     """
 
     competition_id, _ = _unpack_claims()
-    emit("end_active_competition", room=competition_id, include_self=True)
+    emit("end_presentation", room=competition_id, include_self=True)
 
 
 @sio.event
@@ -143,7 +143,7 @@ def sync(data) -> None:
     Sync active_competition for all clients connected to competition.
     """
 
-    competition_id, _ = _unpack_claims()
+    competition_id, view = _unpack_claims()
     active_competition = active_competitions[competition_id]
 
     for key, value in data.items():
@@ -154,3 +154,6 @@ def sync(data) -> None:
             active_competition[key] = value
 
     emit("sync", _get_sync_variables(active_competition), room=competition_id, include_self=True)
+    logger.info(
+        f"Client '{request.sid}' with view '{view}' synced values {_get_sync_variables(active_competition)} in competition'{competition_id}'"
+    )
-- 
GitLab


From 99e44586cd9248bba5085b9d8bcba202d301c465 Mon Sep 17 00:00:00 2001
From: Albin Henriksson <albhe428@student.liu.se>
Date: Fri, 14 May 2021 08:59:14 +0200
Subject: [PATCH 12/15] pull dev

---
 client/src/reducers/presentationReducer.test.ts | 5 +----
 1 file changed, 1 insertion(+), 4 deletions(-)

diff --git a/client/src/reducers/presentationReducer.test.ts b/client/src/reducers/presentationReducer.test.ts
index e0368f7f..f62bf2e9 100644
--- a/client/src/reducers/presentationReducer.test.ts
+++ b/client/src/reducers/presentationReducer.test.ts
@@ -13,10 +13,7 @@ const initialState = {
   },
   activeSlideId: -1,
   code: '',
-  timer: {
-    enabled: false,
-    value: 0,
-  },
+  timer: null,
 }
 
 it('should return the initial state', () => {
-- 
GitLab


From a840b23b1d19a403580c10141da400579dcb3c01 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Victor=20L=C3=B6fgren?= <viclo211@student.liu.se>
Date: Mon, 17 May 2021 14:29:29 +0200
Subject: [PATCH 13/15] Fix timer and refactor sockets on frontend

---
 client/src/actions/presentation.ts          |  4 +-
 client/src/interfaces/Timer.ts              |  4 +-
 client/src/pages/views/JudgeViewPage.tsx    | 18 +++---
 client/src/pages/views/OperatorViewPage.tsx | 42 +++++++------
 client/src/pages/views/components/Timer.tsx | 67 ++++++++++++++++-----
 client/src/reducers/presentationReducer.ts  | 10 ++-
 client/src/sockets.ts                       | 41 +++----------
 server/app/core/sockets.py                  | 18 +++---
 8 files changed, 113 insertions(+), 91 deletions(-)

diff --git a/client/src/actions/presentation.ts b/client/src/actions/presentation.ts
index 89b9e596..0faa64a2 100644
--- a/client/src/actions/presentation.ts
+++ b/client/src/actions/presentation.ts
@@ -3,6 +3,7 @@ This file handles actions for the presentation redux state
 */
 
 import axios from 'axios'
+import { TimerState } from '../interfaces/Timer'
 import { AppDispatch, RootState } from './../store'
 import Types from './types'
 
@@ -34,7 +35,8 @@ export const setCurrentSlideByOrder = (order: number) => (dispatch: AppDispatch,
 export const setPresentationCode = (code: string) => (dispatch: AppDispatch) => {
   dispatch({ type: Types.SET_PRESENTATION_CODE, payload: code })
 }
+
 /** Set timer to input value */
-export const setPresentationTimer = (timer: number | null) => (dispatch: AppDispatch) => {
+export const setPresentationTimer = (timer: TimerState) => (dispatch: AppDispatch) => {
   dispatch({ type: Types.SET_PRESENTATION_TIMER, payload: timer })
 }
diff --git a/client/src/interfaces/Timer.ts b/client/src/interfaces/Timer.ts
index 49d1909e..03704c83 100644
--- a/client/src/interfaces/Timer.ts
+++ b/client/src/interfaces/Timer.ts
@@ -1,4 +1,4 @@
-export interface Timer {
+export interface TimerState {
+  value: number | null
   enabled: boolean
-  value: number
 }
diff --git a/client/src/pages/views/JudgeViewPage.tsx b/client/src/pages/views/JudgeViewPage.tsx
index c07562b5..675da803 100644
--- a/client/src/pages/views/JudgeViewPage.tsx
+++ b/client/src/pages/views/JudgeViewPage.tsx
@@ -11,7 +11,6 @@ import SlideDisplay from '../presentationEditor/components/SlideDisplay'
 import { SlideListItem } from '../presentationEditor/styled'
 import JudgeScoreDisplay from './components/JudgeScoreDisplay'
 import JudgeScoringInstructions from './components/JudgeScoringInstructions'
-import Timer from './components/Timer'
 import {
   Content,
   InnerContent,
@@ -75,18 +74,17 @@ const JudgeViewPage: React.FC = () => {
       dispatch(getPresentationCompetition(competitionId.toString()))
     }
   }, [operatorActiveSlideId])
-  useEffect(() => {
-    // Every second tic of the timer, load new answers
-    // TODO: use a set interval that updates every second ( look in Timer.tsx in clien/src/pages/views/components )
-    // Then clear interval when timer - Date.now() is negative
-    if (timer !== null && timer - (Date.now() % 2) === 0 && competitionId) {
-      dispatch(getPresentationCompetition(competitionId.toString()))
-    }
-  }, [timer])
+  // useEffect(() => {
+  //   // Every second tic of the timer, load new answers
+  //   // TODO: use a set interval that updates every second ( look in Timer.tsx in clien/src/pages/views/components )
+  //   // Then clear interval when timer - Date.now() is negative
+  //   if (timer !== null && timer - (Date.now() % 2) === 0 && competitionId) {
+  //     dispatch(getPresentationCompetition(competitionId.toString()))
+  //   }
+  // }, [timer])
   return (
     <div style={{ height: '100%' }}>
       <JudgeAppBar position="fixed">
-        <Timer />
         <JudgeToolbar>
           <JudgeQuestionsLabel variant="h5">Frågor</JudgeQuestionsLabel>
           {operatorActiveSlideOrder !== undefined && (
diff --git a/client/src/pages/views/OperatorViewPage.tsx b/client/src/pages/views/OperatorViewPage.tsx
index 9a918cf2..6193b4ce 100644
--- a/client/src/pages/views/OperatorViewPage.tsx
+++ b/client/src/pages/views/OperatorViewPage.tsx
@@ -31,13 +31,7 @@ import React, { useEffect, useState } from 'react'
 import { useHistory } from 'react-router-dom'
 import { useAppSelector } from '../../hooks'
 import { RichTeam } from '../../interfaces/ApiRichModels'
-import {
-  socketConnect,
-  socketEndPresentation,
-  socketSetSlideNext,
-  socketSetSlidePrev,
-  socketSyncTimer,
-} from '../../sockets'
+import { socketConnect, socketEndPresentation, socketSync } from '../../sockets'
 import SlideDisplay from '../presentationEditor/components/SlideDisplay'
 import { Center } from '../presentationEditor/components/styled'
 import Timer from './components/Timer'
@@ -60,11 +54,6 @@ import {
  *
  *  ===========================================
  *  TODO:
- *  - Instead of copying code for others to join the competition, copy URL.
- *
- *
- *  - Fix scoreboard
- *
  *  - When two userers are connected to the same Localhost:5000 and updates/starts/end competition it
  *    creates a bug where the competition can't be started.
  * ===========================================
@@ -120,6 +109,8 @@ const OperatorViewPage: React.FC = () => {
   const slideTimer = useAppSelector((state) =>
     activeSlideOrder !== undefined ? state.presentation.competition.slides[activeSlideOrder].timer : null
   )
+  const isFirstSlide = activeSlideOrder === 0
+  const isLastSlide = useAppSelector((state) => activeSlideOrder === state.presentation.competition.slides.length - 1)
   useEffect(() => {
     socketConnect('Operator')
   }, [])
@@ -203,10 +194,19 @@ const OperatorViewPage: React.FC = () => {
 
   const handleStartTimer = () => {
     if (!slideTimer) return
-    // has active timer already, so cancel it
-    if (timer && timer - Date.now() > 0) socketSyncTimer(Date.now())
-    // Either has expired timer or no timer so start one
-    else socketSyncTimer(Date.now() + 1000 * slideTimer)
+
+    if (!timer.enabled) socketSync({ timer: { value: Date.now() + 1000 * slideTimer, enabled: true } })
+    else socketSync({ timer: { ...timer, enabled: false } })
+  }
+
+  const handleSetNextSlide = () => {
+    if (activeSlideOrder !== undefined)
+      socketSync({ slide_order: activeSlideOrder + 1, timer: { value: null, enabled: false } })
+  }
+
+  const handleSetPrevSlide = () => {
+    if (activeSlideOrder !== undefined)
+      socketSync({ slide_order: activeSlideOrder - 1, timer: { value: null, enabled: false } })
   }
 
   return (
@@ -302,14 +302,18 @@ const OperatorViewPage: React.FC = () => {
       <OperatorFooter>
         <ToolBarContainer>
           <Tooltip title="Föregående" arrow>
-            <OperatorButton onClick={socketSetSlidePrev} variant="contained">
+            <OperatorButton onClick={handleSetPrevSlide} variant="contained" disabled={isFirstSlide}>
               <ChevronLeftIcon fontSize="large" />
             </OperatorButton>
           </Tooltip>
 
           {slideTimer && (
             <Tooltip title="Starta Timer" arrow>
-              <OperatorButton onClick={() => handleStartTimer()} variant="contained">
+              <OperatorButton
+                onClick={handleStartTimer}
+                variant="contained"
+                disabled={timer.value !== null && !timer.enabled}
+              >
                 <TimerIcon fontSize="large" />
                 <Timer disableText />
               </OperatorButton>
@@ -329,7 +333,7 @@ const OperatorViewPage: React.FC = () => {
           </Tooltip>
 
           <Tooltip title="Nästa" arrow>
-            <OperatorButton onClick={socketSetSlideNext} variant="contained">
+            <OperatorButton onClick={handleSetNextSlide} variant="contained" disabled={isLastSlide}>
               <ChevronRightIcon fontSize="large" />
             </OperatorButton>
           </Tooltip>
diff --git a/client/src/pages/views/components/Timer.tsx b/client/src/pages/views/components/Timer.tsx
index 2b12af43..620fcd34 100644
--- a/client/src/pages/views/components/Timer.tsx
+++ b/client/src/pages/views/components/Timer.tsx
@@ -1,34 +1,69 @@
 import React, { useEffect, useState } from 'react'
-import { useAppSelector } from '../../../hooks'
-
-let timerIntervalId: NodeJS.Timeout
+import { setPresentationTimer } from '../../../actions/presentation'
+import { useAppDispatch, useAppSelector } from '../../../hooks'
 
 type TimerProps = {
   disableText?: boolean
 }
 
 const Timer = ({ disableText }: TimerProps) => {
+  const dispatch = useAppDispatch()
   const timer = useAppSelector((state) => state.presentation.timer)
   const [remainingTimer, setRemainingTimer] = useState<number>(0)
+  const [timerIntervalId, setTimerIntervalId] = useState<NodeJS.Timeout | null>(null)
   const slideTimer = useAppSelector(
     (state) =>
       state.presentation.competition.slides.find((slide) => slide.id === state.presentation.activeSlideId)?.timer
   )
+
   useEffect(() => {
-    if (timer !== null && timer - Date.now() > 0) {
-      setRemainingTimer(timer - Date.now())
-      timerIntervalId = setInterval(() => {
-        setRemainingTimer(timer - Date.now())
-      }, 500)
-    } else {
-      clearInterval(timerIntervalId)
+    if (slideTimer) setRemainingTimer(slideTimer)
+  }, [slideTimer])
+
+  useEffect(() => {
+    console.log(timer)
+    if (!timer.enabled) {
+      console.log('interval id: ', timerIntervalId)
+      console.log('slide timer: ', slideTimer)
+
+      if (timerIntervalId !== null) clearInterval(timerIntervalId)
+
+      console.log('timer enabled false')
+      console.log('timer: ', timer)
+
+      if (timer.value !== null) {
+        console.log('timer value not null')
+
+        setRemainingTimer(0)
+      } else if (slideTimer) {
+        console.log('timer value null and slideTimer has value')
+
+        setRemainingTimer(slideTimer * 1000)
+      }
+
+      return
     }
-  }, [timer])
-  return (
-    <div>{`${!disableText ? 'Tid kvar:' : ''} ${
-      timer !== null ? (timer - Date.now() > 0 ? Math.round(remainingTimer / 1000) : 0) : slideTimer
-    }`}</div>
-  )
+
+    setTimerIntervalId(
+      setInterval(() => {
+        console.log('interval tick')
+        if (timer.value === null) return
+        if (timer.enabled === false && timerIntervalId !== null) clearInterval(timerIntervalId)
+
+        if (timer.value - Date.now() < 0) {
+          console.log('timer reached zero')
+          setRemainingTimer(0)
+          dispatch(setPresentationTimer({ ...timer, enabled: false }))
+          if (timerIntervalId !== null) clearInterval(timerIntervalId)
+          return
+        }
+
+        setRemainingTimer(timer.value - Date.now())
+      }, 500)
+    )
+  }, [timer.enabled, slideTimer])
+
+  return <div>{`${!disableText ? 'Tid kvar:' : ''} ${Math.round(remainingTimer / 1000)}`}</div>
 }
 
 export default Timer
diff --git a/client/src/reducers/presentationReducer.ts b/client/src/reducers/presentationReducer.ts
index cb74c1ba..69c7817f 100644
--- a/client/src/reducers/presentationReducer.ts
+++ b/client/src/reducers/presentationReducer.ts
@@ -1,5 +1,6 @@
 import { AnyAction } from 'redux'
 import Types from '../actions/types'
+import { TimerState } from '../interfaces/Timer'
 import { RichCompetition } from './../interfaces/ApiRichModels'
 
 /** Define a type for the presentation state */
@@ -7,7 +8,7 @@ interface PresentationState {
   competition: RichCompetition
   activeSlideId: number
   code: string
-  timer: number | null
+  timer: TimerState
 }
 
 /** Define the initial values for the presentation state */
@@ -23,7 +24,10 @@ const initialState: PresentationState = {
   },
   activeSlideId: -1,
   code: '',
-  timer: null,
+  timer: {
+    value: null,
+    enabled: false,
+  },
 }
 
 /** Intercept actions for presentation state and update the state */
@@ -47,7 +51,7 @@ export default function (state = initialState, action: AnyAction) {
     case Types.SET_PRESENTATION_TIMER:
       return {
         ...state,
-        timer: action.payload as number,
+        timer: action.payload as TimerState,
       }
     default:
       return state
diff --git a/client/src/sockets.ts b/client/src/sockets.ts
index a5f3ac42..35c9abc6 100644
--- a/client/src/sockets.ts
+++ b/client/src/sockets.ts
@@ -1,10 +1,11 @@
 import io from 'socket.io-client'
 import { setCurrentSlideByOrder, setPresentationTimer } from './actions/presentation'
+import { TimerState } from './interfaces/Timer'
 import store from './store'
 
 interface SyncInterface {
-  slide_order: number
-  timer: number | null
+  slide_order?: number
+  timer?: TimerState
 }
 
 let socket: SocketIOClient.Socket
@@ -24,8 +25,9 @@ export const socketConnect = (role: 'Judge' | 'Operator' | 'Team' | 'Audience')
   })
 
   socket.on('sync', (data: SyncInterface) => {
-    setCurrentSlideByOrder(data.slide_order)(store.dispatch, store.getState)
-    setPresentationTimer(data.timer)(store.dispatch)
+    // The order of these is important, for some reason
+    if (data.timer !== undefined) setPresentationTimer(data.timer)(store.dispatch)
+    if (data.slide_order !== undefined) setCurrentSlideByOrder(data.slide_order)(store.dispatch, store.getState)
   })
 
   socket.on('end_presentation', () => {
@@ -37,34 +39,9 @@ export const socketEndPresentation = () => {
   socket.emit('end_presentation')
 }
 
-export const socketSetSlideNext = () => {
-  const activeSlide = store
-    .getState()
-    .presentation.competition.slides.find((slide) => slide.id === store.getState().presentation.activeSlideId)
-  if (!activeSlide) return
-  socketSyncSlide(activeSlide.order + 1)
-}
-
-export const socketSetSlidePrev = () => {
-  const activeSlide = store
-    .getState()
-    .presentation.competition.slides.find((slide) => slide.id === store.getState().presentation.activeSlideId)
-  if (!activeSlide) return
-  socketSyncSlide(activeSlide.order - 1)
-}
-
-export const socketSyncSlide = (slide_order: number) => {
-  if (slide_order < 0 || store.getState().presentation.competition.slides.length <= slide_order) {
-    return
-  }
-
-  socket.emit('sync', {
-    slide_order: slide_order,
-  })
-}
-
-export const socketSyncTimer = (timer: number | null) => {
+export const socketSync = ({ slide_order, timer }: SyncInterface) => {
   socket.emit('sync', {
-    timer: timer,
+    slide_order,
+    timer,
   })
 }
diff --git a/server/app/core/sockets.py b/server/app/core/sockets.py
index 4c3c4a4c..be9873c7 100644
--- a/server/app/core/sockets.py
+++ b/server/app/core/sockets.py
@@ -42,8 +42,8 @@ def is_active_competition(competition_id):
     return competition_id in active_competitions
 
 
-def _get_sync_variables(active_competition):
-    return {key: value for key, value in active_competition.items() if key != "client_count"}
+def _get_sync_variables(active_competition, sync_values):
+    return {key: value for key, value in active_competition.items() if key in sync_values}
 
 
 @decorator
@@ -92,14 +92,17 @@ def connect() -> None:
         active_competition = active_competitions[competition_id]
         active_competition["client_count"] += 1
         join_room(competition_id)
-        emit("sync", _get_sync_variables(active_competition))
+        emit("sync", _get_sync_variables(active_competition, ["slide_order", "timer"]))
         logger.info(f"Client '{request.sid}' with view '{view}' joined competition '{competition_id}'")
     elif view == "Operator":
         join_room(competition_id)
         active_competitions[competition_id] = {
             "client_count": 1,
             "slide_order": 0,
-            "timer": None,
+            "timer": {
+                "value": None,
+                "enabled": False,
+            },
         }
         logger.info(f"Client '{request.sid}' with view '{view}' started competition '{competition_id}'")
     else:
@@ -150,10 +153,9 @@ def sync(data) -> None:
         if key not in active_competition:
             logger.warning(f"Invalid sync data: '{key}':'{value}'")
 
-        if value is not None:
-            active_competition[key] = value
+        active_competition[key] = value
 
-    emit("sync", _get_sync_variables(active_competition), room=competition_id, include_self=True)
+    emit("sync", _get_sync_variables(active_competition, data), room=competition_id, include_self=True)
     logger.info(
-        f"Client '{request.sid}' with view '{view}' synced values {_get_sync_variables(active_competition)} in competition'{competition_id}'"
+        f"Client '{request.sid}' with view '{view}' synced values {_get_sync_variables(active_competition, data)} in competition '{competition_id}'"
     )
-- 
GitLab


From de60926ba424ef5664803e6957602201db05be9b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Victor=20L=C3=B6fgren?= <viclo211@student.liu.se>
Date: Mon, 17 May 2021 14:32:05 +0200
Subject: [PATCH 14/15] Fix backend tests

---
 server/tests/test_app.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/server/tests/test_app.py b/server/tests/test_app.py
index 880a41bd..4b704bd7 100644
--- a/server/tests/test_app.py
+++ b/server/tests/test_app.py
@@ -447,7 +447,7 @@ def test_authorization(client):
     add_default_values()
 
     # Fake that competition 1 is active
-    sockets.presentations[1] = {}
+    sockets.active_competitions[1] = {}
 
     #### TEAM ####
     # Login in with team code
-- 
GitLab


From b1ca087cfa465b4d7055843c2d6e2457074c8f93 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Victor=20L=C3=B6fgren?= <viclo211@student.liu.se>
Date: Mon, 17 May 2021 14:34:55 +0200
Subject: [PATCH 15/15] Fix frontend tests

---
 client/src/reducers/presentationReducer.test.ts | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/client/src/reducers/presentationReducer.test.ts b/client/src/reducers/presentationReducer.test.ts
index f62bf2e9..10362e68 100644
--- a/client/src/reducers/presentationReducer.test.ts
+++ b/client/src/reducers/presentationReducer.test.ts
@@ -13,7 +13,10 @@ const initialState = {
   },
   activeSlideId: -1,
   code: '',
-  timer: null,
+  timer: {
+    value: null,
+    enabled: false,
+  },
 }
 
 it('should return the initial state', () => {
-- 
GitLab