Skip to content
Snippets Groups Projects
Commit 93baed60 authored by robban64's avatar robban64
Browse files
parents 6c4654c1 25f9acc3
No related branches found
No related tags found
No related merge requests found
Pipeline #45924 passed with warnings
......@@ -59,10 +59,10 @@ const AnswerText = ({ activeSlide, competitionId }: AnswerTextProps) => {
if (!team || !activeSlide) {
return
}
const activeAltId = activeSlide.questions[0].alternatives[0].id
const activeAltId = activeSlide.questions[0]?.alternatives[0]?.id
return (
team.question_alternative_answers.find((questionAnswer) => questionAnswer.question_alternative_id === activeAltId)
?.answer || 'Svar...'
?.answer || ''
)
}
......
......@@ -39,7 +39,6 @@ const SlideType = ({ activeSlide, competitionId }: SlideTypeProps) => {
const components = useAppSelector(
(state) => state.editor.competition.slides.find((slide) => slide.id === state.editor.activeSlideId)?.components
)
const questionComponentId = components?.find((qCompId) => qCompId.type_id === 3)?.id
const openSlideTypeDialog = (type_id: number) => {
setSelectedSlideType(type_id)
......@@ -82,6 +81,21 @@ const SlideType = ({ activeSlide, competitionId }: SlideTypeProps) => {
removeQuestionComponent().then(() => createQuestionComponent(data.id))
})
.catch(console.log)
if (selectedSlideType === 1) {
// Add an alternative to text questions to allow giving answers.
await axios
.post(
`/api/competitions/${competitionId}/slides/${activeSlide.id}/questions/${activeSlide.questions[0].id}/alternatives`,
{
text: '',
value: 1,
}
)
.then(({ data }) => {
dispatch(getEditorCompetition(competitionId))
})
.catch(console.log)
}
}
} else if (!activeSlide.questions[0] && selectedSlideType !== 0) {
// Change slide type from information to a question type
......@@ -96,6 +110,21 @@ const SlideType = ({ activeSlide, competitionId }: SlideTypeProps) => {
createQuestionComponent(data.id)
})
.catch(console.log)
if (selectedSlideType === 1) {
// Add an alternative to text questions to allow giving answers.
await axios
.post(
`/api/competitions/${competitionId}/slides/${activeSlide.id}/questions/${activeSlide.questions[0].id}/alternatives`,
{
text: '',
value: 1,
}
)
.then(({ data }) => {
dispatch(getEditorCompetition(competitionId))
})
.catch(console.log)
}
}
}
}
......@@ -157,7 +186,7 @@ const SlideType = ({ activeSlide, competitionId }: SlideTypeProps) => {
<DialogContent>
<DialogContentText>
Om du ändrar sidtypen kommer eventuella frågeinställningar gå förlorade. Det inkluderar: frågans namn,
poäng och svarsalternativ.{' '}
poäng, svarsalternativ och svar från lagen.{' '}
</DialogContentText>
</DialogContent>
<DialogActions>
......
......@@ -2,7 +2,7 @@ import { Divider, List, ListItemText, Snackbar, Typography } from '@material-ui/
import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'
import { Alert } from '@material-ui/lab'
import React, { useEffect, useState } from 'react'
import { getPresentationCompetition } from '../../actions/presentation'
import { getPresentationCompetition, setPresentationTimer } from '../../actions/presentation'
import { useAppDispatch, useAppSelector } from '../../hooks'
import { RichSlide } from '../../interfaces/ApiRichModels'
import { socketConnect } from '../../sockets'
......@@ -53,7 +53,6 @@ const JudgeViewPage: React.FC = () => {
const [currentSlide, setCurrentSlide] = useState<RichSlide | undefined>(undefined)
const currentQuestion = currentSlide?.questions[0]
const operatorActiveSlideId = useAppSelector((state) => state.presentation.activeSlideId)
const timer = useAppSelector((state) => state.presentation.timer)
const operatorActiveSlideOrder = useAppSelector(
(state) => state.presentation.competition.slides.find((slide) => slide.id === operatorActiveSlideId)?.order
)
......@@ -74,14 +73,35 @@ 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])
const timer = useAppSelector((state) => state.presentation.timer)
const [timerIntervalId, setTimerIntervalId] = useState<NodeJS.Timeout | null>(null)
useEffect(() => {
if (!timer.enabled) {
if (timerIntervalId !== null && competitionId) {
clearInterval(timerIntervalId)
dispatch(getPresentationCompetition(competitionId.toString()))
}
return
}
setTimerIntervalId(
setInterval(() => {
if (timer.value === null) return
if (timer.value - Date.now() < 0) {
if (competitionId) {
dispatch(getPresentationCompetition(competitionId.toString()))
}
dispatch(setPresentationTimer({ ...timer, enabled: false }))
return
}
if (competitionId) {
dispatch(getPresentationCompetition(competitionId.toString()))
}
}, 1000)
)
}, [timer.enabled])
return (
<div style={{ height: '100%' }}>
<JudgeAppBar position="fixed">
......
import { Box, Typography } from '@material-ui/core'
import { Box, Divider, Typography } from '@material-ui/core'
import axios from 'axios'
import React from 'react'
import { getPresentationCompetition } from '../../../actions/presentation'
import { useAppDispatch, useAppSelector } from '../../../hooks'
import { RichSlide } from '../../../interfaces/ApiRichModels'
import { AnswerContainer, ScoreDisplayContainer, ScoreDisplayHeader, ScoreInput } from './styled'
import {
AnswerContainer,
Answers,
AnswersDisplay,
ScoreDisplayContainer,
ScoreDisplayHeader,
ScoreInput,
} from './styled'
type ScoreDisplayProps = {
teamIndex: number
......@@ -13,7 +20,6 @@ type ScoreDisplayProps = {
const JudgeScoreDisplay = ({ teamIndex, activeSlide }: ScoreDisplayProps) => {
const dispatch = useAppDispatch()
const questionTypes = useAppSelector((state) => state.types.questionTypes)
const currentTeam = useAppSelector((state) => state.presentation.competition.teams[teamIndex])
const currentCompetititonId = useAppSelector((state) => state.presentation.competition.id)
......@@ -22,7 +28,6 @@ const JudgeScoreDisplay = ({ teamIndex, activeSlide }: ScoreDisplayProps) => {
const questionMaxScore = activeQuestion?.total_score
const scores = currentTeam.question_scores.map((questionAnswer) => questionAnswer.score)
const textQuestionType = questionTypes.find((questionType) => questionType.name === 'Text')?.id || 0
const handleEditScore = async (newScore: number, questionId: number) => {
await axios
.put(`/api/competitions/${currentCompetititonId}/teams/${currentTeam.id}/answers/question_scores/${questionId}`, {
......@@ -31,33 +36,38 @@ const JudgeScoreDisplay = ({ teamIndex, activeSlide }: ScoreDisplayProps) => {
.then(() => dispatch(getPresentationCompetition(currentCompetititonId.toString())))
}
const getAlternativeAnswers = () => {
const getAnswers = () => {
const result: string[] = []
if (!activeQuestion) {
return result
}
for (const alt of activeQuestion.alternatives) {
const value = currentTeam.question_alternative_answers.find((x) => x.question_alternative_id === alt.id)
if (!value) {
const ans = currentTeam.question_alternative_answers.find((x) => x.question_alternative_id === alt.id)
if (!ans) {
continue
}
if (activeQuestion.type_id === 1) {
result.push(alt.text)
} else if (+value.answer > 0) {
// Text question
result.push(ans.answer)
} else if (+ans.answer > 0) {
result.push(alt.text)
}
/*
switch(activeQuestion.question_type.name){
case "Text":
result.push(alt.text)
break;
default:
}*/
}
return result
}
const getAlternatives = () => {
const result: string[] = []
if (!activeQuestion) {
return result
}
for (const alt of activeQuestion.alternatives) {
if (activeQuestion.type_id !== 1 && +alt.value > 0) {
// Not text question and correct answer
result.push(alt.text)
}
}
return result
//const asdasd = currentTeam.question_alternative_answers.filter((x)=>x.question_alternative_id === activeQuestion.alternatives[0].io)
}
return (
......@@ -81,15 +91,37 @@ const JudgeScoreDisplay = ({ teamIndex, activeSlide }: ScoreDisplayProps) => {
</ScoreDisplayHeader>
<Typography variant="h6">Alla poäng: [ {scores.map((score) => `${score} `)}]</Typography>
<Typography variant="h6">Total poäng: {scores.reduce((a, b) => a + b, 0)}</Typography>
{activeQuestion && (
<AnswerContainer>
{getAlternativeAnswers().map((v, k) => (
<Typography variant="body1" key={k}>
<span>&#8226;</span> {v}
</Typography>
))}
</AnswerContainer>
)}
<AnswersDisplay>
<Answers>
<Divider />
<Typography variant="body1">Lagets svar:</Typography>
{activeQuestion && (
<AnswerContainer>
{getAnswers().map((v, k) => (
<Typography variant="body1" key={k}>
<span>&#8226;</span> {v}
</Typography>
))}
</AnswerContainer>
)}
</Answers>
<Answers>
<Divider />
<Typography variant="body1">Korrekta svar:</Typography>
{activeQuestion && (
<AnswerContainer>
{getAlternatives().map((v, k) => (
<Typography variant="body1" key={k}>
<span>&#8226;</span> {v}
</Typography>
))}
</AnswerContainer>
)}
</Answers>
</AnswersDisplay>
{!activeQuestion && <Typography variant="body1">Inget svar</Typography>}
</ScoreDisplayContainer>
)
......
......@@ -48,3 +48,16 @@ export const ScoringInstructionsInner = styled.div`
align-items: center;
flex-direction: column;
`
export const AnswersDisplay = styled.div`
display: flex;
flex-direction: row;
`
export const Answers = styled.div`
margin-left: 15px;
margin-right: 15px;
display: flex;
align-items: center;
flex-direction: column;
`
/**
* Handles everything that has to do with syncing active competitions.
*
* @module
*/
import io from 'socket.io-client'
import { setCurrentSlideByOrder, setPresentationShowScoreboard, setPresentationTimer } from './actions/presentation'
import { TimerState } from './interfaces/Timer'
import store from './store'
/**
* The values that can be synced between clients connected to the same presentation.
*/
interface SyncInterface {
slide_order?: number
timer?: TimerState
......@@ -11,9 +20,15 @@ interface SyncInterface {
let socket: SocketIOClient.Socket
/**
* Connect to server, setup authorization header and listen to some events.
*
* @param role The role the connecting client has
*/
export const socketConnect = (role: 'Judge' | 'Operator' | 'Team' | 'Audience') => {
if (socket) return
// The token is the JWT returned from the login/code API call.
const token = localStorage[`${role}Token`]
socket = io('localhost:5000', {
transportOptions: {
......@@ -26,7 +41,7 @@ export const socketConnect = (role: 'Judge' | 'Operator' | 'Team' | 'Audience')
})
socket.on('sync', (data: SyncInterface) => {
// The order of these is important, for some reason
// The order of these is important, for some reason, so dont change it
if (data.timer !== undefined) setPresentationTimer(data.timer)(store.dispatch)
if (data.slide_order !== undefined) setCurrentSlideByOrder(data.slide_order)(store.dispatch, store.getState)
if (data.show_scoreboard !== undefined) setPresentationShowScoreboard(data.show_scoreboard)(store.dispatch)
......@@ -37,10 +52,18 @@ export const socketConnect = (role: 'Judge' | 'Operator' | 'Team' | 'Audience')
})
}
/**
* Disconnect all clients.
*/
export const socketEndPresentation = () => {
socket.emit('end_presentation')
}
/**
* Sync data between all connected clients.
*
* @param syncData The data to sync between all clients connected to the same presentation
*/
export const socketSync = (syncData: SyncInterface) => {
socket.emit('sync', syncData)
}
......@@ -26,7 +26,7 @@ active_competitions = {}
def _unpack_claims():
"""
:return: A tuple containing competition_id and view, gotten from claim
:return: A tuple containing competition_id and view from claim
:rtype: tuple
"""
......@@ -39,10 +39,19 @@ 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(active_competition, sync_values):
"""
Returns a dictionary with all values from active_competition that is to be
synced.
:return: A dicationary containg key-value pairs from active_competition
thats in sync_values
:rtype: dictionary
"""
return {key: value for key, value in active_competition.items() if key in sync_values}
......@@ -83,7 +92,7 @@ def authorize_client(f, allowed_views=None, require_active_competition=True, *ar
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.
start it if client is an operator, otherwise do nothing.
"""
competition_id, view = _unpack_claims()
......@@ -133,7 +142,7 @@ def disconnect() -> None:
@authorize_client(allowed_views=["Operator"])
def end_presentation() -> None:
"""
End a active_competition by sending end_presentation to all connected clients.
End a presentation by sending end_presentation to all connected clients.
"""
competition_id, _ = _unpack_claims()
......@@ -144,7 +153,8 @@ def end_presentation() -> None:
@authorize_client(allowed_views=["Operator"])
def sync(data) -> None:
"""
Sync active_competition for all clients connected to competition.
Update all values from data thats in an active_competitions. Also sync all
the updated values to all clients connected to the same competition.
"""
competition_id, view = _unpack_claims()
......@@ -153,6 +163,7 @@ def sync(data) -> None:
for key, value in data.items():
if key not in active_competition:
logger.warning(f"Invalid sync data: '{key}':'{value}'")
continue
active_competition[key] = value
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment