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