diff --git a/client/src/pages/presentationEditor/components/QuestionComponentDisplay.tsx b/client/src/pages/presentationEditor/components/QuestionComponentDisplay.tsx index 9df7fe9a93c95f469288882eb30a879ac2915375..28c8489120458cb0a01563a172875fa6420926a7 100644 --- a/client/src/pages/presentationEditor/components/QuestionComponentDisplay.tsx +++ b/client/src/pages/presentationEditor/components/QuestionComponentDisplay.tsx @@ -2,6 +2,7 @@ import { Card, Divider, ListItem, Typography } from '@material-ui/core' import React from 'react' import { useAppSelector } from '../../../hooks' import AnswerMultiple from './answerComponents/AnswerMultiple' +import AnswerSingle from './answerComponents/AnswerSingle' import { Center } from './styled' type QuestionComponentProps = { @@ -44,6 +45,18 @@ const QuestionComponentDisplay = ({ variant }: QuestionComponentProps) => { } return + case 'Single': + if (activeSlide) { + return ( + <AnswerSingle + variant={variant} + activeSlide={activeSlide} + competitionId={activeSlide.competition_id.toString()} + /> + ) + } + return + default: break } diff --git a/client/src/pages/presentationEditor/components/SlideSettings.tsx b/client/src/pages/presentationEditor/components/SlideSettings.tsx index a39a95d4750f3bb56fb65dcb24669c51484b6557..029187f49e06fcffbb147a82fc7e754ec09e3a0f 100644 --- a/client/src/pages/presentationEditor/components/SlideSettings.tsx +++ b/client/src/pages/presentationEditor/components/SlideSettings.tsx @@ -1,18 +1,19 @@ /* This file compiles and renders the right hand slide settings bar, under the tab "SIDA". */ -import { Divider, List, ListItem, ListItemText, TextField, Typography } from '@material-ui/core' -import React, { useState } from 'react' +import { Divider } from '@material-ui/core' +import React from 'react' import { useParams } from 'react-router-dom' import { useAppSelector } from '../../../hooks' +import BackgroundImageSelect from './BackgroundImageSelect' +import Images from './slideSettingsComponents/Images' import Instructions from './slideSettingsComponents/Instructions' import MultipleChoiceAlternatives from './slideSettingsComponents/MultipleChoiceAlternatives' +import QuestionSettings from './slideSettingsComponents/QuestionSettings' +import SingleChoiceAlternatives from './slideSettingsComponents/SingleChoiceAlternatives' import SlideType from './slideSettingsComponents/SlideType' -import { Center, ImportedImage, SettingsList, PanelContainer } from './styled' -import Timer from './slideSettingsComponents/Timer' -import Images from './slideSettingsComponents/Images' import Texts from './slideSettingsComponents/Texts' -import QuestionSettings from './slideSettingsComponents/QuestionSettings' -import BackgroundImageSelect from './BackgroundImageSelect' +import Timer from './slideSettingsComponents/Timer' +import { PanelContainer, SettingsList } from './styled' interface CompetitionParams { competitionId: string @@ -47,6 +48,10 @@ const SlideSettings: React.FC = () => { <MultipleChoiceAlternatives activeSlide={activeSlide} competitionId={competitionId} /> )} + {activeSlide?.questions[0]?.type_id === 4 && ( + <SingleChoiceAlternatives activeSlide={activeSlide} competitionId={competitionId} /> + )} + {activeSlide && ( <Texts activeViewTypeId={activeViewTypeId} activeSlide={activeSlide} competitionId={competitionId} /> )} diff --git a/client/src/pages/presentationEditor/components/answerComponents/AnswerMultiple.tsx b/client/src/pages/presentationEditor/components/answerComponents/AnswerMultiple.tsx index b1510fe0dade0b2c403c6f16d8c0bed7fcc6fd70..d267ba4e325e2e001f5ed8aff46075239c1dff72 100644 --- a/client/src/pages/presentationEditor/components/answerComponents/AnswerMultiple.tsx +++ b/client/src/pages/presentationEditor/components/answerComponents/AnswerMultiple.tsx @@ -4,6 +4,7 @@ import { green, grey } from '@material-ui/core/colors' import axios from 'axios' import React from 'react' import { getEditorCompetition } from '../../../../actions/editor' +import { getPresentationCompetition } from '../../../../actions/presentation' import { useAppDispatch, useAppSelector } from '../../../../hooks' import { QuestionAlternative } from '../../../../interfaces/ApiModels' import { RichSlide } from '../../../../interfaces/ApiRichModels' @@ -18,11 +19,8 @@ type AnswerMultipleProps = { const AnswerMultiple = ({ variant, activeSlide, competitionId }: AnswerMultipleProps) => { const dispatch = useAppDispatch() const teamId = useAppSelector((state) => state.competitionLogin.data?.team_id) - const team = useAppSelector((state) => { - if (variant === 'editor') return state.editor.competition.teams.find((team) => team.id === teamId) - return state.presentation.competition.teams.find((team) => team.id === teamId) - }) - const answerId = team?.question_answers.find((answer) => answer.question_id === activeSlide?.questions[0].id)?.id + const team = useAppSelector((state) => state.presentation.competition.teams.find((team) => team.id === teamId)) + const answer = team?.question_answers.find((answer) => answer.question_id === activeSlide?.questions[0].id) const decideChecked = (alternative: QuestionAlternative) => { const teamAnswer = team?.question_answers.find((answer) => answer.answer === alternative.text)?.answer @@ -32,16 +30,16 @@ const AnswerMultiple = ({ variant, activeSlide, competitionId }: AnswerMultipleP const updateAnswer = async (alternative: QuestionAlternative) => { if (activeSlide) { - if (team?.question_answers[0]) { - await axios - .put(`/api/competitions/${competitionId}/teams/${teamId}/answers/${answerId}`, { - answer: alternative.text, - }) - .then(() => { - dispatch(getEditorCompetition(competitionId)) - }) - .catch(console.log) + if (team?.question_answers.find((answer) => answer.question_id === activeSlide.questions[0].id)) { + console.log('CHECKKKKKKKKKKK') + if (answer?.answer === alternative.text) { + // Uncheck checkbox + deleteAnswer() + } else { + // Check another box + } } else { + // Check first checkbox await axios .post(`/api/competitions/${competitionId}/teams/${teamId}/answers`, { answer: alternative.text, @@ -49,7 +47,11 @@ const AnswerMultiple = ({ variant, activeSlide, competitionId }: AnswerMultipleP question_id: activeSlide.questions[0].id, }) .then(() => { - dispatch(getEditorCompetition(competitionId)) + if (variant === 'editor') { + dispatch(getEditorCompetition(competitionId)) + } else { + dispatch(getPresentationCompetition(competitionId)) + } }) .catch(console.log) } diff --git a/client/src/pages/presentationEditor/components/answerComponents/AnswerSingle.tsx b/client/src/pages/presentationEditor/components/answerComponents/AnswerSingle.tsx new file mode 100644 index 0000000000000000000000000000000000000000..8c2fad17395e9c602a7108a72301739064ab4533 --- /dev/null +++ b/client/src/pages/presentationEditor/components/answerComponents/AnswerSingle.tsx @@ -0,0 +1,132 @@ +import { Checkbox, ListItem, ListItemText, Typography, withStyles } from '@material-ui/core' +import { CheckboxProps } from '@material-ui/core/Checkbox' +import { green, grey } from '@material-ui/core/colors' +import RadioButtonCheckedIcon from '@material-ui/icons/RadioButtonCheckedOutlined' +import RadioButtonUncheckedIcon from '@material-ui/icons/RadioButtonUncheckedOutlined' +import axios from 'axios' +import React from 'react' +import { getEditorCompetition } from '../../../../actions/editor' +import { getPresentationCompetition } from '../../../../actions/presentation' +import { useAppDispatch, useAppSelector } from '../../../../hooks' +import { QuestionAlternative } from '../../../../interfaces/ApiModels' +import { RichSlide } from '../../../../interfaces/ApiRichModels' +import { Center, Clickable } from '../styled' + +type AnswerSingleProps = { + variant: 'editor' | 'presentation' + activeSlide: RichSlide | undefined + competitionId: string +} + +const AnswerSingle = ({ variant, activeSlide, competitionId }: AnswerSingleProps) => { + const dispatch = useAppDispatch() + const teamId = useAppSelector((state) => state.competitionLogin.data?.team_id) + const team = useAppSelector((state) => { + if (variant === 'editor') return state.editor.competition.teams.find((team) => team.id === teamId) + return state.presentation.competition.teams.find((team) => team.id === teamId) + }) + const answerId = team?.question_answers.find((answer) => answer.question_id === activeSlide?.questions[0].id)?.id + + const decideChecked = (alternative: QuestionAlternative) => { + const teamAnswer = team?.question_answers.find((answer) => answer.answer === alternative.text)?.answer + if (teamAnswer) return true + else return false + } + + const updateAnswer = async (alternative: QuestionAlternative) => { + if (activeSlide) { + // TODO: ignore API calls when an answer is already checked + if (team?.question_answers[0]) { + await axios + .put(`/api/competitions/${competitionId}/teams/${teamId}/answers/${answerId}`, { + answer: alternative.text, + }) + .then(() => { + if (variant === 'editor') { + dispatch(getEditorCompetition(competitionId)) + } else { + dispatch(getPresentationCompetition(competitionId)) + } + }) + .catch(console.log) + } else { + await axios + .post(`/api/competitions/${competitionId}/teams/${teamId}/answers`, { + answer: alternative.text, + score: 0, + question_id: activeSlide.questions[0].id, + }) + .then(() => { + if (variant === 'editor') { + dispatch(getEditorCompetition(competitionId)) + } else { + dispatch(getPresentationCompetition(competitionId)) + } + }) + .catch(console.log) + } + } + } + + const deleteAnswer = async () => { + await axios + .delete(`/api/competitions/${competitionId}/teams/${teamId}/answers`) + .then(() => { + dispatch(getEditorCompetition(competitionId)) + }) + .catch(console.log) + } + + const GreenCheckbox = withStyles({ + root: { + color: grey[900], + '&$checked': { + color: green[600], + }, + }, + checked: {}, + })((props: CheckboxProps) => <Checkbox color="default" {...props} />) + + const renderRadioButton = (alt: QuestionAlternative) => { + if (variant === 'presentation') { + if (decideChecked(alt)) { + return ( + <Clickable> + <RadioButtonCheckedIcon onClick={() => updateAnswer(alt)} /> + </Clickable> + ) + } else { + return ( + <Clickable> + <RadioButtonUncheckedIcon onClick={() => updateAnswer(alt)} /> + </Clickable> + ) + } + } else { + return <RadioButtonUncheckedIcon onClick={() => updateAnswer(alt)} /> + } + } + + return ( + <div> + <ListItem divider> + <Center> + <ListItemText primary="Välj ett svar:" /> + </Center> + </ListItem> + {activeSlide && + activeSlide.questions[0] && + activeSlide.questions[0].alternatives && + activeSlide.questions[0].alternatives.map((alt) => ( + <div key={alt.id}> + <ListItem divider> + {renderRadioButton(alt)} + <Typography style={{ wordBreak: 'break-all' }}>{alt.text}</Typography> + </ListItem> + </div> + ))} + </div> + ) +} + +export default AnswerSingle diff --git a/client/src/pages/presentationEditor/components/slideSettingsComponents/SingleChoiceAlternatives.tsx b/client/src/pages/presentationEditor/components/slideSettingsComponents/SingleChoiceAlternatives.tsx new file mode 100644 index 0000000000000000000000000000000000000000..6e38c39fe443b5c1e78810cb5731690870f8f614 --- /dev/null +++ b/client/src/pages/presentationEditor/components/slideSettingsComponents/SingleChoiceAlternatives.tsx @@ -0,0 +1,131 @@ +import { ListItem, ListItemText } from '@material-ui/core' +import CloseIcon from '@material-ui/icons/Close' +import RadioButtonCheckedIcon from '@material-ui/icons/RadioButtonCheckedOutlined' +import RadioButtonUncheckedIcon from '@material-ui/icons/RadioButtonUncheckedOutlined' +import axios from 'axios' +import React from 'react' +import { getEditorCompetition } from '../../../../actions/editor' +import { useAppDispatch, useAppSelector } from '../../../../hooks' +import { QuestionAlternative } from '../../../../interfaces/ApiModels' +import { RichSlide } from '../../../../interfaces/ApiRichModels' +import { AddButton, AlternativeTextField, Center, Clickable, SettingsList } from '../styled' + +type SingleChoiceAlternativeProps = { + activeSlide: RichSlide + competitionId: string +} + +const SingleChoiceAlternatives = ({ activeSlide, competitionId }: SingleChoiceAlternativeProps) => { + const dispatch = useAppDispatch() + const activeSlideId = useAppSelector((state) => state.editor.activeSlideId) + + const updateAlternativeValue = async (alternative: QuestionAlternative) => { + if (activeSlide && activeSlide.questions[0]) { + // Remove check from previously checked alternative + const previousCheckedAltId = activeSlide.questions[0].alternatives.find((alt) => alt.value === 1)?.id + if (previousCheckedAltId !== alternative.id) { + if (previousCheckedAltId) { + axios.put( + `/api/competitions/${competitionId}/slides/${activeSlide?.id}/questions/${activeSlide?.questions[0].id}/alternatives/${previousCheckedAltId}`, + { value: 0 } + ) + } + // Set new checked alternative + await axios + .put( + `/api/competitions/${competitionId}/slides/${activeSlide?.id}/questions/${activeSlide?.questions[0].id}/alternatives/${alternative.id}`, + { value: 1 } + ) + .then(() => { + dispatch(getEditorCompetition(competitionId)) + }) + .catch(console.log) + } + } + } + + const updateAlternativeText = async (alternative_id: number, newText: string) => { + if (activeSlide && activeSlide.questions[0]) { + await axios + .put( + `/api/competitions/${competitionId}/slides/${activeSlide?.id}/questions/${activeSlide?.questions[0].id}/alternatives/${alternative_id}`, + { text: newText } + ) + .then(() => { + dispatch(getEditorCompetition(competitionId)) + }) + .catch(console.log) + } + } + + const addAlternative = async () => { + if (activeSlide && activeSlide.questions[0]) { + await axios + .post( + `/api/competitions/${competitionId}/slides/${activeSlide?.id}/questions/${activeSlide?.questions[0].id}/alternatives`, + { text: '', value: 0 } + ) + .then(() => { + dispatch(getEditorCompetition(competitionId)) + }) + .catch(console.log) + } + } + + const handleCloseAnswerClick = async (alternative_id: number) => { + if (activeSlide && activeSlide.questions[0]) { + await axios + .delete( + `/api/competitions/${competitionId}/slides/${activeSlideId}/questions/${activeSlide?.questions[0].id}/alternatives/${alternative_id}` + ) + .then(() => { + dispatch(getEditorCompetition(competitionId)) + }) + .catch(console.log) + } + } + + const renderRadioButton = (alt: QuestionAlternative) => { + if (alt.value) return <RadioButtonCheckedIcon onClick={() => updateAlternativeValue(alt)} /> + else return <RadioButtonUncheckedIcon onClick={() => updateAlternativeValue(alt)} /> + } + + return ( + <SettingsList> + <ListItem divider> + <Center> + <ListItemText + primary="Svarsalternativ" + secondary="(Fyll i cirkeln höger om textfältet för att markera korrekt svar)" + /> + </Center> + </ListItem> + {activeSlide && + activeSlide.questions[0] && + activeSlide.questions[0].alternatives && + activeSlide.questions[0].alternatives.map((alt) => ( + <div key={alt.id}> + <ListItem divider> + <AlternativeTextField + id="outlined-basic" + defaultValue={alt.text} + onChange={(event) => updateAlternativeText(alt.id, event.target.value)} + variant="outlined" + /> + <Clickable>{renderRadioButton(alt)}</Clickable> + <Clickable> + <CloseIcon onClick={() => handleCloseAnswerClick(alt.id)} /> + </Clickable> + </ListItem> + </div> + ))} + <ListItem button onClick={addAlternative}> + <Center> + <AddButton variant="button">Lägg till svarsalternativ</AddButton> + </Center> + </ListItem> + </SettingsList> + ) +} + +export default SingleChoiceAlternatives diff --git a/client/src/pages/presentationEditor/components/slideSettingsComponents/SlideType.tsx b/client/src/pages/presentationEditor/components/slideSettingsComponents/SlideType.tsx index 1c976e53298ba0a9bbabc5d3b9224f452bc15f97..bef33d943883981ee62e9dc49dcd09902d907737 100644 --- a/client/src/pages/presentationEditor/components/slideSettingsComponents/SlideType.tsx +++ b/client/src/pages/presentationEditor/components/slideSettingsComponents/SlideType.tsx @@ -16,7 +16,7 @@ import axios from 'axios' import React, { useState } from 'react' import { getEditorCompetition } from '../../../../actions/editor' import { useAppDispatch, useAppSelector } from '../../../../hooks' -import { RichQuestion, RichSlide } from '../../../../interfaces/ApiRichModels' +import { RichSlide } from '../../../../interfaces/ApiRichModels' import { Center, FirstItem } from '../styled' type SlideTypeProps = { @@ -142,7 +142,12 @@ const SlideType = ({ activeSlide, competitionId }: SlideTypeProps) => { </MenuItem> <MenuItem value={3}> <Typography variant="button" onClick={() => openSlideTypeDialog(3)}> - Flervalsfråga + Kryssfråga + </Typography> + </MenuItem> + <MenuItem value={4}> + <Typography variant="button" onClick={() => openSlideTypeDialog(4)}> + Alternativfråga </Typography> </MenuItem> </Select> diff --git a/client/src/pages/presentationEditor/components/styled.tsx b/client/src/pages/presentationEditor/components/styled.tsx index bac63da41ae20c0dbc1d8f6be434630548e546b5..31e40d51de8a4ecb45fbddf1cda9b7e96d4151f5 100644 --- a/client/src/pages/presentationEditor/components/styled.tsx +++ b/client/src/pages/presentationEditor/components/styled.tsx @@ -1,16 +1,4 @@ -import { - FormControl, - List, - Tab, - TextField, - Typography, - Button, - Card, - ListItem, - Select, - InputLabel, - ListItemText, -} from '@material-ui/core' +import { Button, Card, List, ListItemText, Tab, TextField, Typography } from '@material-ui/core' import styled from 'styled-components' export const SettingsTab = styled(Tab)` diff --git a/client/src/utils/renderSlideIcon.tsx b/client/src/utils/renderSlideIcon.tsx index ba1eafcb8052469dd8b627b95d2eee518bfd5fe8..22f6405abbc8574ae17bf86dd8b745ba98e3b4cf 100644 --- a/client/src/utils/renderSlideIcon.tsx +++ b/client/src/utils/renderSlideIcon.tsx @@ -1,9 +1,10 @@ -import { RichSlide } from '../interfaces/ApiRichModels' -import React from 'react' import BuildOutlinedIcon from '@material-ui/icons/BuildOutlined' +import CheckBoxOutlinedIcon from '@material-ui/icons/CheckBoxOutlined' import CreateOutlinedIcon from '@material-ui/icons/CreateOutlined' -import DnsOutlinedIcon from '@material-ui/icons/DnsOutlined' import InfoOutlinedIcon from '@material-ui/icons/InfoOutlined' +import RadioButtonCheckedIcon from '@material-ui/icons/RadioButtonChecked' +import React from 'react' +import { RichSlide } from '../interfaces/ApiRichModels' export const renderSlideIcon = (slide: RichSlide) => { if (slide.questions && slide.questions[0] && slide.questions[0].type_id) { @@ -13,7 +14,9 @@ export const renderSlideIcon = (slide: RichSlide) => { case 2: return <BuildOutlinedIcon /> // practical qustion case 3: - return <DnsOutlinedIcon /> // multiple choice question + return <CheckBoxOutlinedIcon /> // multiple choice question + case 4: + return <RadioButtonCheckedIcon /> // single choice question } } else { return <InfoOutlinedIcon /> // information slide diff --git a/server/app/database/models.py b/server/app/database/models.py index 0f909aee7f7ebf33efba3b5145601c9a6f9bced5..74f7b39e5dad9ab69a22b10445e9e86baa08125d 100644 --- a/server/app/database/models.py +++ b/server/app/database/models.py @@ -5,9 +5,8 @@ each other. """ from app.core import bcrypt, db -from sqlalchemy.ext.hybrid import hybrid_method, hybrid_property - from app.database.types import ID_IMAGE_COMPONENT, ID_QUESTION_COMPONENT, ID_TEXT_COMPONENT +from sqlalchemy.ext.hybrid import hybrid_method, hybrid_property STRING_SIZE = 254 @@ -190,8 +189,8 @@ class QuestionAnswer(db.Model): question_id = db.Column(db.Integer, db.ForeignKey("question.id"), nullable=False) team_id = db.Column(db.Integer, db.ForeignKey("team.id"), nullable=False) - def __init__(self, data, score, question_id, team_id): - self.data = data + def __init__(self, answer, score, question_id, team_id): + self.answer = answer self.score = score self.question_id = question_id self.team_id = team_id diff --git a/server/populate.py b/server/populate.py index 663ae700541cd9926e8620bbfbed209660947d62..b7496991b1baef41914541d546c916c73f14551b 100644 --- a/server/populate.py +++ b/server/populate.py @@ -11,7 +11,7 @@ from app.database.models import City, QuestionType, Role def _add_items(): media_types = ["Image", "Video"] - question_types = ["Text", "Practical", "Multiple"] + question_types = ["Text", "Practical", "Multiple", "Single"] component_types = ["Text", "Image", "Question"] view_types = ["Team", "Judge", "Audience", "Operator"]