From ddf5053602860facd27d7eae7f93fc9389a16bf7 Mon Sep 17 00:00:00 2001 From: Emil <Emil> Date: Wed, 28 Apr 2021 17:31:10 +0200 Subject: [PATCH 1/6] feat: add question component --- client/src/enum/ComponentTypes.ts | 2 +- client/src/interfaces/ApiModels.ts | 12 +++++- .../components/QuestionComponentDisplay.tsx | 19 ++++++++++ .../components/RndComponent.tsx | 13 ++++++- .../components/SlideSettings.tsx | 8 ++-- .../slideSettingsComponents/SlideType.tsx | 38 ++++++++++++++++++- .../components/PresentationComponent.tsx | 3 -- server/app/apis/components.py | 4 +- server/populate.py | 2 +- 9 files changed, 85 insertions(+), 16 deletions(-) create mode 100644 client/src/pages/presentationEditor/components/QuestionComponentDisplay.tsx diff --git a/client/src/enum/ComponentTypes.ts b/client/src/enum/ComponentTypes.ts index c0aa7384..7ff75bd8 100644 --- a/client/src/enum/ComponentTypes.ts +++ b/client/src/enum/ComponentTypes.ts @@ -1,5 +1,5 @@ export enum ComponentTypes { Text = 1, Image, - QuestionAlternative, + Question, } diff --git a/client/src/interfaces/ApiModels.ts b/client/src/interfaces/ApiModels.ts index a6fa68a0..6bc7aca9 100644 --- a/client/src/interfaces/ApiModels.ts +++ b/client/src/interfaces/ApiModels.ts @@ -93,6 +93,16 @@ export interface TextComponent extends Component { font: string } -export interface QuestionAlternativeComponent extends Component { +export interface QuestionComponent extends Component { + id: number + x: number + y: number + w: number + h: number + slide_id: number + type_id: number + view_type_id: number + text: string + media: Media question_id: number } diff --git a/client/src/pages/presentationEditor/components/QuestionComponentDisplay.tsx b/client/src/pages/presentationEditor/components/QuestionComponentDisplay.tsx new file mode 100644 index 00000000..0371991b --- /dev/null +++ b/client/src/pages/presentationEditor/components/QuestionComponentDisplay.tsx @@ -0,0 +1,19 @@ +import { Typography } from '@material-ui/core' +import React from 'react' +import { QuestionComponent } from '../../../interfaces/ApiModels' + +type QuestionComponentProps = { + component: QuestionComponent + width: number + height: number +} + +const QuestionComponentDisplay = ({ component, width, height }: QuestionComponentProps) => { + return ( + <div> + <Typography>Frågekomponent</Typography> + </div> + ) +} + +export default QuestionComponentDisplay diff --git a/client/src/pages/presentationEditor/components/RndComponent.tsx b/client/src/pages/presentationEditor/components/RndComponent.tsx index 77ed5de3..32470457 100644 --- a/client/src/pages/presentationEditor/components/RndComponent.tsx +++ b/client/src/pages/presentationEditor/components/RndComponent.tsx @@ -4,13 +4,14 @@ import React, { useEffect, useState } from 'react' import { Rnd } from 'react-rnd' import { ComponentTypes } from '../../../enum/ComponentTypes' import { useAppSelector } from '../../../hooks' -import { Component, ImageComponent, QuestionAlternativeComponent, TextComponent } from '../../../interfaces/ApiModels' +import { Component, ImageComponent, QuestionComponent, TextComponent } from '../../../interfaces/ApiModels' import { Position, Size } from '../../../interfaces/Components' import CheckboxComponent from './CheckboxComponent' import ImageComponentDisplay from './ImageComponentDisplay' import { HoverContainer } from './styled' import FormatAlignCenterIcon from '@material-ui/icons/FormatAlignCenter' import TextComponentDisplay from './TextComponentDisplay' +import QuestionComponentDisplay from './QuestionComponentDisplay' type RndComponentProps = { component: Component @@ -86,6 +87,16 @@ const RndComponent = ({ component, width, height, scale }: RndComponentProps) => /> </HoverContainer> ) + case ComponentTypes.Question: + return ( + <HoverContainer hover={hover}> + <QuestionComponentDisplay + height={currentSize.h * scale} + width={currentSize.w * scale} + component={component as QuestionComponent} + /> + </HoverContainer> + ) default: break } diff --git a/client/src/pages/presentationEditor/components/SlideSettings.tsx b/client/src/pages/presentationEditor/components/SlideSettings.tsx index d48b4565..a39a95d4 100644 --- a/client/src/pages/presentationEditor/components/SlideSettings.tsx +++ b/client/src/pages/presentationEditor/components/SlideSettings.tsx @@ -36,13 +36,11 @@ const SlideSettings: React.FC = () => { </SettingsList> {activeSlide?.questions[0] && <QuestionSettings activeSlide={activeSlide} competitionId={competitionId} />} + { - // Choose answer alternatives depending on the slide type + // Choose answer alternatives, depending on the slide type } - {activeSlide?.questions[0]?.type_id === 1 && ( - <Instructions activeSlide={activeSlide} competitionId={competitionId} /> - )} - {activeSlide?.questions[0]?.type_id === 2 && ( + {(activeSlide?.questions[0]?.type_id === 1 || activeSlide?.questions[0]?.type_id === 2) && ( <Instructions activeSlide={activeSlide} competitionId={competitionId} /> )} {activeSlide?.questions[0]?.type_id === 3 && ( diff --git a/client/src/pages/presentationEditor/components/slideSettingsComponents/SlideType.tsx b/client/src/pages/presentationEditor/components/slideSettingsComponents/SlideType.tsx index bc251b91..1c976e53 100644 --- a/client/src/pages/presentationEditor/components/slideSettingsComponents/SlideType.tsx +++ b/client/src/pages/presentationEditor/components/slideSettingsComponents/SlideType.tsx @@ -15,8 +15,8 @@ import { import axios from 'axios' import React, { useState } from 'react' import { getEditorCompetition } from '../../../../actions/editor' -import { useAppDispatch } from '../../../../hooks' -import { RichSlide } from '../../../../interfaces/ApiRichModels' +import { useAppDispatch, useAppSelector } from '../../../../hooks' +import { RichQuestion, RichSlide } from '../../../../interfaces/ApiRichModels' import { Center, FirstItem } from '../styled' type SlideTypeProps = { @@ -30,6 +30,11 @@ const SlideType = ({ activeSlide, competitionId }: SlideTypeProps) => { // For "slide type" dialog const [selectedSlideType, setSelectedSlideType] = useState(0) const [slideTypeDialog, setSlideTypeDialog] = useState(false) + 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) setSlideTypeDialog(true) @@ -41,6 +46,7 @@ const SlideType = ({ activeSlide, competitionId }: SlideTypeProps) => { const updateSlideType = async () => { closeSlideTypeDialog() if (activeSlide) { + deleteQuestionComponent(questionComponentId) if (activeSlide.questions[0] && activeSlide.questions[0].type_id !== selectedSlideType) { if (selectedSlideType === 0) { // Change slide type from a question type to information @@ -67,6 +73,7 @@ const SlideType = ({ activeSlide, competitionId }: SlideTypeProps) => { }) .then(() => { dispatch(getEditorCompetition(competitionId)) + createQuestionComponent() }) .catch(console.log) } @@ -80,11 +87,38 @@ const SlideType = ({ activeSlide, competitionId }: SlideTypeProps) => { }) .then(() => { dispatch(getEditorCompetition(competitionId)) + createQuestionComponent() }) .catch(console.log) } } } + + const createQuestionComponent = () => { + axios + .post(`/api/competitions/${competitionId}/slides/${activeSlide.id}/components`, { + x: 0, + y: 0, + w: 400, + h: 250, + type_id: 3, + view_type_id: 1, + question_id: activeSlide.questions[0].id, + }) + .then(() => { + dispatch(getEditorCompetition(competitionId)) + }) + .catch(console.log) + } + + const deleteQuestionComponent = (componentId: number | undefined) => { + if (componentId) { + axios + .delete(`/api/competitions/${competitionId}/slides/${activeSlide.id}/components/${componentId}`) + .catch(console.log) + } + } + return ( <FirstItem> <ListItem> diff --git a/client/src/pages/views/components/PresentationComponent.tsx b/client/src/pages/views/components/PresentationComponent.tsx index a41f7912..743db4bf 100644 --- a/client/src/pages/views/components/PresentationComponent.tsx +++ b/client/src/pages/views/components/PresentationComponent.tsx @@ -1,12 +1,9 @@ -import { Typography } from '@material-ui/core' import React from 'react' import { Rnd } from 'react-rnd' import { ComponentTypes } from '../../../enum/ComponentTypes' -import { useAppSelector } from '../../../hooks' import { Component, ImageComponent, TextComponent } from '../../../interfaces/ApiModels' import ImageComponentDisplay from '../../presentationEditor/components/ImageComponentDisplay' import TextComponentDisplay from '../../presentationEditor/components/TextComponentDisplay' -import { SlideContainer } from './styled' type PresentationComponentProps = { component: Component diff --git a/server/app/apis/components.py b/server/app/apis/components.py index a1982763..0368fc82 100644 --- a/server/app/apis/components.py +++ b/server/app/apis/components.py @@ -11,7 +11,7 @@ schema = ComponentDTO.schema list_schema = ComponentDTO.list_schema component_parser_add = reqparse.RequestParser() -component_parser_add.add_argument("x", type=str, default=0, location="json") +component_parser_add.add_argument("x", type=int, default=0, location="json") component_parser_add.add_argument("y", type=int, default=0, location="json") component_parser_add.add_argument("w", type=int, default=1, location="json") component_parser_add.add_argument("h", type=int, default=1, location="json") @@ -22,7 +22,7 @@ component_parser_add.add_argument("media_id", type=int, default=None, location=" component_parser_add.add_argument("question_id", type=int, default=None, location="json") component_parser_edit = reqparse.RequestParser() -component_parser_edit.add_argument("x", type=str, default=sentinel, location="json") +component_parser_edit.add_argument("x", type=int, default=sentinel, location="json") component_parser_edit.add_argument("y", type=int, default=sentinel, location="json") component_parser_edit.add_argument("w", type=int, default=sentinel, location="json") component_parser_edit.add_argument("h", type=int, default=sentinel, location="json") diff --git a/server/populate.py b/server/populate.py index e0bb0bd9..95983f96 100644 --- a/server/populate.py +++ b/server/populate.py @@ -12,7 +12,7 @@ from app.database.models import City, QuestionType, Role def _add_items(): media_types = ["Image", "Video"] question_types = ["Boolean", "Multiple", "Text"] - component_types = ["Text", "Image"] + component_types = ["Text", "Image", "Question"] view_types = ["Team", "Judge", "Audience", "Operator"] roles = ["Admin", "Editor"] -- GitLab From 1aa155cab9fd1e3531f37292bdab231a99575b75 Mon Sep 17 00:00:00 2001 From: Emil <Emil> Date: Thu, 29 Apr 2021 13:32:38 +0200 Subject: [PATCH 2/6] pre dev pull --- .../components/QuestionComponentDisplay.tsx | 67 ++++++++++-- .../components/RndComponent.tsx | 14 +-- .../components/SlideDisplay.tsx | 10 +- .../answerComponents/AnswerMultiple.tsx | 101 ++++++++++++++++++ .../presentationEditor/components/styled.tsx | 4 + .../components/PresentationComponent.tsx | 7 +- server/populate.py | 2 +- 7 files changed, 174 insertions(+), 31 deletions(-) create mode 100644 client/src/pages/presentationEditor/components/answerComponents/AnswerMultiple.tsx diff --git a/client/src/pages/presentationEditor/components/QuestionComponentDisplay.tsx b/client/src/pages/presentationEditor/components/QuestionComponentDisplay.tsx index 0371991b..de839e21 100644 --- a/client/src/pages/presentationEditor/components/QuestionComponentDisplay.tsx +++ b/client/src/pages/presentationEditor/components/QuestionComponentDisplay.tsx @@ -1,18 +1,67 @@ -import { Typography } from '@material-ui/core' +import { Card, Divider, ListItem, Typography } from '@material-ui/core' import React from 'react' -import { QuestionComponent } from '../../../interfaces/ApiModels' +import { useAppSelector } from '../../../hooks' +import AnswerMultiple from './answerComponents/AnswerMultiple' +import { Center } from './styled' type QuestionComponentProps = { - component: QuestionComponent - width: number - height: number + variant: 'editor' | 'presentation' } -const QuestionComponentDisplay = ({ component, width, height }: QuestionComponentProps) => { +const QuestionComponentDisplay = ({ variant }: QuestionComponentProps) => { + const activeSlide = useAppSelector((state) => { + if (variant === 'editor') + return state.editor.competition.slides.find((slide) => slide.id === state.editor.activeSlideId) + return state.presentation.competition.slides.find((slide) => slide.id === state.presentation.slide?.id) + }) + + const timer = activeSlide?.timer + const total_score = activeSlide?.questions[0].total_score + const questionName = activeSlide?.questions[0].name + + const questionTypeId = activeSlide?.questions[0].type_id + const questionTypeName = useAppSelector( + (state) => state.types.questionTypes.find((qType) => qType.id === questionTypeId)?.name + ) + + const getAlternatives = () => { + switch (questionTypeName) { + case 'Text': + return + + case 'Practical': + return + + case 'Multiple': + if (activeSlide) { + return ( + <AnswerMultiple + teamId={1} + variant={variant} + activeSlide={activeSlide} + competitionId={activeSlide.competition_id.toString()} + /> + ) + } + return + + default: + break + } + } + return ( - <div> - <Typography>Frågekomponent</Typography> - </div> + <Card> + <ListItem> + <Center style={{ justifyContent: 'space-evenly' }}> + <Typography>Poäng: {total_score}</Typography> + <Typography>{questionName}</Typography> + <Typography>Timer: {timer}</Typography> + </Center> + </ListItem> + <Divider /> + {getAlternatives()} + </Card> ) } diff --git a/client/src/pages/presentationEditor/components/RndComponent.tsx b/client/src/pages/presentationEditor/components/RndComponent.tsx index 32470457..b0148272 100644 --- a/client/src/pages/presentationEditor/components/RndComponent.tsx +++ b/client/src/pages/presentationEditor/components/RndComponent.tsx @@ -1,15 +1,13 @@ -import { Button, Card, IconButton, Tooltip, Typography } from '@material-ui/core' +import { Card, IconButton, Tooltip } from '@material-ui/core' import axios from 'axios' import React, { useEffect, useState } from 'react' import { Rnd } from 'react-rnd' import { ComponentTypes } from '../../../enum/ComponentTypes' import { useAppSelector } from '../../../hooks' -import { Component, ImageComponent, QuestionComponent, TextComponent } from '../../../interfaces/ApiModels' +import { Component, ImageComponent, TextComponent } from '../../../interfaces/ApiModels' import { Position, Size } from '../../../interfaces/Components' -import CheckboxComponent from './CheckboxComponent' import ImageComponentDisplay from './ImageComponentDisplay' import { HoverContainer } from './styled' -import FormatAlignCenterIcon from '@material-ui/icons/FormatAlignCenter' import TextComponentDisplay from './TextComponentDisplay' import QuestionComponentDisplay from './QuestionComponentDisplay' @@ -90,11 +88,7 @@ const RndComponent = ({ component, width, height, scale }: RndComponentProps) => case ComponentTypes.Question: return ( <HoverContainer hover={hover}> - <QuestionComponentDisplay - height={currentSize.h * scale} - width={currentSize.w * scale} - component={component as QuestionComponent} - /> + <QuestionComponentDisplay variant="editor" /> </HoverContainer> ) default: @@ -104,6 +98,8 @@ const RndComponent = ({ component, width, height, scale }: RndComponentProps) => return ( <Rnd + disableDragging={true} + enableResizing={false} minWidth={75 * scale} minHeight={75 * scale} bounds="parent" diff --git a/client/src/pages/presentationEditor/components/SlideDisplay.tsx b/client/src/pages/presentationEditor/components/SlideDisplay.tsx index aef4ca7c..e6b60f29 100644 --- a/client/src/pages/presentationEditor/components/SlideDisplay.tsx +++ b/client/src/pages/presentationEditor/components/SlideDisplay.tsx @@ -77,15 +77,7 @@ const SlideDisplay = ({ variant, activeViewTypeId }: SlideDisplayProps) => { scale={scale} /> ) - return ( - <PresentationComponent - height={height} - width={width} - key={component.id} - component={component} - scale={scale} - /> - ) + return <PresentationComponent key={component.id} component={component} scale={scale} /> })} </SlideEditorPaper> </SlideEditorContainerRatio> diff --git a/client/src/pages/presentationEditor/components/answerComponents/AnswerMultiple.tsx b/client/src/pages/presentationEditor/components/answerComponents/AnswerMultiple.tsx new file mode 100644 index 00000000..4e80ec68 --- /dev/null +++ b/client/src/pages/presentationEditor/components/answerComponents/AnswerMultiple.tsx @@ -0,0 +1,101 @@ +import { Card, 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 axios from 'axios' +import React, { useState } from 'react' +import { getEditorCompetition } from '../../../../actions/editor' +import { useAppDispatch, useAppSelector } from '../../../../hooks' +import { QuestionAlternative } from '../../../../interfaces/ApiModels' +import { RichSlide } from '../../../../interfaces/ApiRichModels' +import { Center, QuestionComponent, SettingsList } from '../styled' + +type AnswerMultipleProps = { + teamId: number + variant: 'editor' | 'presentation' + activeSlide: RichSlide | undefined + competitionId: string +} + +const AnswerMultiple = ({ teamId, variant, activeSlide, competitionId }: AnswerMultipleProps) => { + const dispatch = useAppDispatch() + 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 decideChecked = (alternative: QuestionAlternative) => { + const teamAnswer = team?.question_answers.find((answer) => answer.answer === alternative.text)?.answer + if (alternative.text === teamAnswer) return true + else return false + } + + const updateAnswer = async (alternative: QuestionAlternative) => { + console.log('CHECK________') + if (teamId && activeSlide) { + if (team?.question_answers) { + await axios + .put(`/api/competitions/${competitionId}/teams/${teamId}/answers`, { answer: alternative.text }) + .then(() => { + dispatch(getEditorCompetition(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(() => { + dispatch(getEditorCompetition(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} />) + + return ( + <div> + <ListItem divider> + <Center> + <ListItemText primary="Välj ett eller flera svar:" /> + </Center> + </ListItem> + {activeSlide && + activeSlide.questions[0] && + activeSlide.questions[0].alternatives && + activeSlide.questions[0].alternatives.map((alt) => ( + <div key={alt.id}> + <ListItem divider> + { + //<GreenCheckbox checked={checkbox} onChange={(event) => updateAnswer(alt, event.target.checked)} /> + } + <GreenCheckbox checked={decideChecked(alt)} onChange={() => updateAnswer(alt)} /> + <Typography style={{ wordBreak: 'break-all' }}>{alt.text}</Typography> + </ListItem> + </div> + ))} + </div> + ) +} + +export default AnswerMultiple diff --git a/client/src/pages/presentationEditor/components/styled.tsx b/client/src/pages/presentationEditor/components/styled.tsx index 605972d2..bac63da4 100644 --- a/client/src/pages/presentationEditor/components/styled.tsx +++ b/client/src/pages/presentationEditor/components/styled.tsx @@ -148,3 +148,7 @@ export const HoverContainer = styled.div<HoverContainerProps>` export const ImageNameText = styled(ListItemText)` word-break: break-all; ` + +export const QuestionComponent = styled.div` + outline-style: double; +` diff --git a/client/src/pages/views/components/PresentationComponent.tsx b/client/src/pages/views/components/PresentationComponent.tsx index 743db4bf..0a688dc1 100644 --- a/client/src/pages/views/components/PresentationComponent.tsx +++ b/client/src/pages/views/components/PresentationComponent.tsx @@ -3,16 +3,15 @@ import { Rnd } from 'react-rnd' import { ComponentTypes } from '../../../enum/ComponentTypes' import { Component, ImageComponent, TextComponent } from '../../../interfaces/ApiModels' import ImageComponentDisplay from '../../presentationEditor/components/ImageComponentDisplay' +import QuestionComponentDisplay from '../../presentationEditor/components/QuestionComponentDisplay' import TextComponentDisplay from '../../presentationEditor/components/TextComponentDisplay' type PresentationComponentProps = { component: Component - width: number - height: number scale: number } -const PresentationComponent = ({ component, width, height, scale }: PresentationComponentProps) => { +const PresentationComponent = ({ component, scale }: PresentationComponentProps) => { const renderInnerComponent = () => { switch (component.type_id) { case ComponentTypes.Text: @@ -25,6 +24,8 @@ const PresentationComponent = ({ component, width, height, scale }: Presentation component={component as ImageComponent} /> ) + case ComponentTypes.Question: + return <QuestionComponentDisplay variant="presentation" /> default: break } diff --git a/server/populate.py b/server/populate.py index 95983f96..4c26a521 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 = ["Boolean", "Multiple", "Text"] + question_types = ["Text", "Practical", "Multiple"] component_types = ["Text", "Image", "Question"] view_types = ["Team", "Judge", "Audience", "Operator"] -- GitLab From c0ba1c547ac9feb53353a7be9b1057216d27a513 Mon Sep 17 00:00:00 2001 From: Emil <Emil> Date: Thu, 29 Apr 2021 14:25:49 +0200 Subject: [PATCH 3/6] added: question components --- .../components/QuestionComponentDisplay.tsx | 1 - .../components/RndComponent.tsx | 4 +--- .../answerComponents/AnswerMultiple.tsx | 20 ++++++++++--------- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/client/src/pages/presentationEditor/components/QuestionComponentDisplay.tsx b/client/src/pages/presentationEditor/components/QuestionComponentDisplay.tsx index de839e21..9df7fe9a 100644 --- a/client/src/pages/presentationEditor/components/QuestionComponentDisplay.tsx +++ b/client/src/pages/presentationEditor/components/QuestionComponentDisplay.tsx @@ -36,7 +36,6 @@ const QuestionComponentDisplay = ({ variant }: QuestionComponentProps) => { if (activeSlide) { return ( <AnswerMultiple - teamId={1} variant={variant} activeSlide={activeSlide} competitionId={activeSlide.competition_id.toString()} diff --git a/client/src/pages/presentationEditor/components/RndComponent.tsx b/client/src/pages/presentationEditor/components/RndComponent.tsx index fa7ffeb6..c13126be 100644 --- a/client/src/pages/presentationEditor/components/RndComponent.tsx +++ b/client/src/pages/presentationEditor/components/RndComponent.tsx @@ -7,9 +7,9 @@ import { useAppSelector } from '../../../hooks' import { Component, ImageComponent, TextComponent } from '../../../interfaces/ApiModels' import { Position, Size } from '../../../interfaces/Components' import ImageComponentDisplay from './ImageComponentDisplay' +import QuestionComponentDisplay from './QuestionComponentDisplay' import { HoverContainer } from './styled' import TextComponentDisplay from './TextComponentDisplay' -import QuestionComponentDisplay from './QuestionComponentDisplay' type RndComponentProps = { component: Component @@ -96,8 +96,6 @@ const RndComponent = ({ component, width, height, scale }: RndComponentProps) => return ( <Rnd - disableDragging={true} - enableResizing={false} minWidth={75 * scale} minHeight={75 * scale} bounds="parent" diff --git a/client/src/pages/presentationEditor/components/answerComponents/AnswerMultiple.tsx b/client/src/pages/presentationEditor/components/answerComponents/AnswerMultiple.tsx index 4e80ec68..b1510fe0 100644 --- a/client/src/pages/presentationEditor/components/answerComponents/AnswerMultiple.tsx +++ b/client/src/pages/presentationEditor/components/answerComponents/AnswerMultiple.tsx @@ -1,27 +1,28 @@ -import { Card, Checkbox, ListItem, ListItemText, Typography, withStyles } from '@material-ui/core' +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 axios from 'axios' -import React, { useState } from 'react' +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 { Center, QuestionComponent, SettingsList } from '../styled' +import { Center } from '../styled' type AnswerMultipleProps = { - teamId: number variant: 'editor' | 'presentation' activeSlide: RichSlide | undefined competitionId: string } -const AnswerMultiple = ({ teamId, variant, activeSlide, competitionId }: 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 decideChecked = (alternative: QuestionAlternative) => { const teamAnswer = team?.question_answers.find((answer) => answer.answer === alternative.text)?.answer @@ -30,11 +31,12 @@ const AnswerMultiple = ({ teamId, variant, activeSlide, competitionId }: AnswerM } const updateAnswer = async (alternative: QuestionAlternative) => { - console.log('CHECK________') - if (teamId && activeSlide) { - if (team?.question_answers) { + if (activeSlide) { + if (team?.question_answers[0]) { await axios - .put(`/api/competitions/${competitionId}/teams/${teamId}/answers`, { answer: alternative.text }) + .put(`/api/competitions/${competitionId}/teams/${teamId}/answers/${answerId}`, { + answer: alternative.text, + }) .then(() => { dispatch(getEditorCompetition(competitionId)) }) -- GitLab From 4a371123a108b79a5e142cdc18f003cae2deb0ec Mon Sep 17 00:00:00 2001 From: Albin Henriksson <albhe428@student.liu.se> Date: Thu, 29 Apr 2021 16:27:54 +0200 Subject: [PATCH 4/6] Add text answer component --- .../components/QuestionComponentDisplay.tsx | 6 +- .../answerComponents/AnswerText.tsx | 80 +++++++++++++++++++ .../components/answerComponents/styled.tsx | 5 ++ server/app/database/models.py | 8 +- 4 files changed, 94 insertions(+), 5 deletions(-) create mode 100644 client/src/pages/presentationEditor/components/answerComponents/AnswerText.tsx create mode 100644 client/src/pages/presentationEditor/components/answerComponents/styled.tsx diff --git a/client/src/pages/presentationEditor/components/QuestionComponentDisplay.tsx b/client/src/pages/presentationEditor/components/QuestionComponentDisplay.tsx index 9df7fe9a..3ed8c985 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 AnswerText from './answerComponents/AnswerText' import { Center } from './styled' type QuestionComponentProps = { @@ -27,6 +28,9 @@ const QuestionComponentDisplay = ({ variant }: QuestionComponentProps) => { const getAlternatives = () => { switch (questionTypeName) { case 'Text': + if (activeSlide) { + return <AnswerText activeSlide={activeSlide} competitionId={activeSlide.competition_id.toString()} /> + } return case 'Practical': @@ -50,7 +54,7 @@ const QuestionComponentDisplay = ({ variant }: QuestionComponentProps) => { } return ( - <Card> + <Card style={{ maxHeight: '100%', overflowY: 'auto' }}> <ListItem> <Center style={{ justifyContent: 'space-evenly' }}> <Typography>Poäng: {total_score}</Typography> diff --git a/client/src/pages/presentationEditor/components/answerComponents/AnswerText.tsx b/client/src/pages/presentationEditor/components/answerComponents/AnswerText.tsx new file mode 100644 index 00000000..e54b0dd2 --- /dev/null +++ b/client/src/pages/presentationEditor/components/answerComponents/AnswerText.tsx @@ -0,0 +1,80 @@ +import { ListItem, ListItemText, TextField } from '@material-ui/core' +import axios from 'axios' +import React from 'react' +import { getEditorCompetition } from '../../../../actions/editor' +import { useAppDispatch, useAppSelector } from '../../../../hooks' +import { RichSlide } from '../../../../interfaces/ApiRichModels' +import { Center } from '../styled' +import { AnswerTextFieldContainer } from './styled' + +type AnswerTextProps = { + activeSlide: RichSlide | undefined + competitionId: string +} + +const AnswerText = ({ activeSlide, competitionId }: AnswerTextProps) => { + const [timerHandle, setTimerHandle] = React.useState<number | undefined>(undefined) + const dispatch = useAppDispatch() + const teamId = useAppSelector((state) => state.competitionLogin.data?.team_id) + const team = useAppSelector((state) => 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 onAnswerChange = (answer: string) => { + if (timerHandle) { + clearTimeout(timerHandle) + setTimerHandle(undefined) + } + //Only updates answer 100ms after last input was made + setTimerHandle(window.setTimeout(() => updateAnswer(answer), 100)) + } + + const updateAnswer = async (answer: string) => { + if (activeSlide && team) { + if (team?.question_answers.find((answer) => answer.question_id === activeSlide.questions[0].id)) { + await axios + .put(`/api/competitions/${competitionId}/teams/${teamId}/answers/${answerId}`, { + answer, + }) + .then(() => { + dispatch(getEditorCompetition(competitionId)) + }) + .catch(console.log) + } else { + await axios + .post(`/api/competitions/${competitionId}/teams/${teamId}/answers`, { + answer, + score: 0, + question_id: activeSlide.questions[0].id, + }) + .then(() => { + dispatch(getEditorCompetition(competitionId)) + }) + .catch(console.log) + } + } + } + + return ( + <AnswerTextFieldContainer> + <ListItem divider> + <Center> + <ListItemText primary="Skriv ditt svar nedan" /> + </Center> + </ListItem> + <ListItem style={{ height: '100%' }}> + <TextField + disabled={team === undefined} + defaultValue={ + team?.question_answers.find((questionAnswer) => questionAnswer.id === answerId)?.answer || 'Svar...' + } + style={{ height: '100%' }} + variant="outlined" + fullWidth={true} + multiline + onChange={(event) => onAnswerChange(event.target.value)} + /> + </ListItem> + </AnswerTextFieldContainer> + ) +} + +export default AnswerText diff --git a/client/src/pages/presentationEditor/components/answerComponents/styled.tsx b/client/src/pages/presentationEditor/components/answerComponents/styled.tsx new file mode 100644 index 00000000..9140e3c2 --- /dev/null +++ b/client/src/pages/presentationEditor/components/answerComponents/styled.tsx @@ -0,0 +1,5 @@ +import styled from 'styled-components' + +export const AnswerTextFieldContainer = styled.div` + height: calc(100% - 90px); +` diff --git a/server/app/database/models.py b/server/app/database/models.py index 0f909aee..76567fbf 100644 --- a/server/app/database/models.py +++ b/server/app/database/models.py @@ -5,10 +5,10 @@ each other. """ from app.core import bcrypt, db +from app.database.types import (ID_IMAGE_COMPONENT, ID_QUESTION_COMPONENT, + ID_TEXT_COMPONENT) from sqlalchemy.ext.hybrid import hybrid_method, hybrid_property -from app.database.types import ID_IMAGE_COMPONENT, ID_QUESTION_COMPONENT, ID_TEXT_COMPONENT - STRING_SIZE = 254 @@ -190,8 +190,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 -- GitLab From 092fd6cea4633ddb603ce61bc6f6890380c2770f Mon Sep 17 00:00:00 2001 From: Emil <Emil> Date: Thu, 29 Apr 2021 18:16:47 +0200 Subject: [PATCH 5/6] todo: fix AnswerMultiple.tsx --- .../components/QuestionComponentDisplay.tsx | 13 ++ .../components/SlideSettings.tsx | 19 ++- .../answerComponents/AnswerMultiple.tsx | 32 +++-- .../answerComponents/AnswerSingle.tsx | 132 ++++++++++++++++++ .../SingleChoiceAlternatives.tsx | 131 +++++++++++++++++ .../slideSettingsComponents/SlideType.tsx | 9 +- .../presentationEditor/components/styled.tsx | 14 +- client/src/utils/renderSlideIcon.tsx | 11 +- server/app/database/models.py | 7 +- server/populate.py | 2 +- 10 files changed, 324 insertions(+), 46 deletions(-) create mode 100644 client/src/pages/presentationEditor/components/answerComponents/AnswerSingle.tsx create mode 100644 client/src/pages/presentationEditor/components/slideSettingsComponents/SingleChoiceAlternatives.tsx diff --git a/client/src/pages/presentationEditor/components/QuestionComponentDisplay.tsx b/client/src/pages/presentationEditor/components/QuestionComponentDisplay.tsx index 9df7fe9a..28c84891 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 a39a95d4..029187f4 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 b1510fe0..d267ba4e 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 00000000..8c2fad17 --- /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 00000000..6e38c39f --- /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 1c976e53..bef33d94 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 bac63da4..31e40d51 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 ba1eafcb..22f6405a 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 0f909aee..74f7b39e 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 663ae700..b7496991 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"] -- GitLab From 645ce5cee31bbc740fe8cf39b11d5fabd9a4420d Mon Sep 17 00:00:00 2001 From: Emil <Emil> Date: Mon, 3 May 2021 13:37:57 +0200 Subject: [PATCH 6/6] Feat: question components, needed fix: AnswerMultiple.tsx --- client/package-lock.json | 3 ++- .../components/answerComponents/AnswerMultiple.tsx | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/client/package-lock.json b/client/package-lock.json index 72fcea71..2655ad2e 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -17285,7 +17285,8 @@ }, "ssri": { "version": "6.0.1", - "resolved": "", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz", + "integrity": "sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==", "requires": { "figgy-pudding": "^3.5.1" } diff --git a/client/src/pages/presentationEditor/components/answerComponents/AnswerMultiple.tsx b/client/src/pages/presentationEditor/components/answerComponents/AnswerMultiple.tsx index d267ba4e..7a2b1c59 100644 --- a/client/src/pages/presentationEditor/components/answerComponents/AnswerMultiple.tsx +++ b/client/src/pages/presentationEditor/components/answerComponents/AnswerMultiple.tsx @@ -29,14 +29,15 @@ const AnswerMultiple = ({ variant, activeSlide, competitionId }: AnswerMultipleP } const updateAnswer = async (alternative: QuestionAlternative) => { + // TODO: fix. Make list of alternatives and delete & post instead of put to allow multiple boxes checked. if (activeSlide) { 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 + // TODO } } else { // Check first checkbox @@ -60,7 +61,7 @@ const AnswerMultiple = ({ variant, activeSlide, competitionId }: AnswerMultipleP const deleteAnswer = async () => { await axios - .delete(`/api/competitions/${competitionId}/teams/${teamId}/answers`) + .delete(`/api/competitions/${competitionId}/teams/${teamId}/answers`) // TODO: fix .then(() => { dispatch(getEditorCompetition(competitionId)) }) -- GitLab