Skip to content
Snippets Groups Projects
Commit 09643923 authored by Carl Schönfelder's avatar Carl Schönfelder Committed by Josef Olsson
Browse files

Resolve "Rework QuestionAnswer"

parent 393ec4a8
No related branches found
No related tags found
1 merge request!149Resolve "Rework QuestionAnswer"
Pipeline #45668 passed with warnings
Showing
with 352 additions and 223 deletions
...@@ -66,11 +66,17 @@ export interface QuestionAlternative { ...@@ -66,11 +66,17 @@ export interface QuestionAlternative {
question_id: number question_id: number
} }
export interface QuestionAnswer { export interface QuestionAlternativeAnswer {
id: number id: number
question_id: number
team_id: number team_id: number
question_alternative_id: number
answer: string answer: string
}
export interface QuestionScore {
id: number
team_id: number
question_id: number
score: number score: number
} }
......
import { Component, Media, QuestionAlternative, QuestionAnswer, QuestionType } from './ApiModels' import { Component, Media, QuestionAlternative, QuestionAlternativeAnswer, QuestionScore } from './ApiModels'
export interface RichCompetition { export interface RichCompetition {
name: string name: string
...@@ -24,7 +24,8 @@ export interface RichSlide { ...@@ -24,7 +24,8 @@ export interface RichSlide {
export interface RichTeam { export interface RichTeam {
id: number id: number
name: string name: string
question_answers: QuestionAnswer[] question_alternative_answers: QuestionAlternativeAnswer[]
question_scores: QuestionScore[]
competition_id: number competition_id: number
} }
...@@ -34,7 +35,6 @@ export interface RichQuestion { ...@@ -34,7 +35,6 @@ export interface RichQuestion {
name: string name: string
title: string title: string
total_score: number total_score: number
question_type: QuestionType
type_id: number type_id: number
correcting_instructions: string correcting_instructions: string
alternatives: QuestionAlternative[] alternatives: QuestionAlternative[]
......
...@@ -186,7 +186,7 @@ const PresentationEditorPage: React.FC = () => { ...@@ -186,7 +186,7 @@ const PresentationEditorPage: React.FC = () => {
onContextMenu={(event) => handleRightClick(event, slide.id)} onContextMenu={(event) => handleRightClick(event, slide.id)}
> >
{renderSlideIcon(slide)} {renderSlideIcon(slide)}
<ListItemText primary={`Sida ${slide.id}`} /> <ListItemText primary={`Sida ${slide.order + 1}`} />
</SlideListItem> </SlideListItem>
</div> </div>
)} )}
......
...@@ -34,50 +34,38 @@ const AnswerMultiple = ({ variant, activeSlide, competitionId }: AnswerMultipleP ...@@ -34,50 +34,38 @@ const AnswerMultiple = ({ variant, activeSlide, competitionId }: AnswerMultipleP
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const teamId = useAppSelector((state) => state.competitionLogin.data?.team_id) const teamId = useAppSelector((state) => state.competitionLogin.data?.team_id)
const team = useAppSelector((state) => state.presentation.competition.teams.find((team) => team.id === teamId)) 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 decideChecked = (alternative: QuestionAlternative) => {
const teamAnswer = team?.question_answers.find((answer) => answer.answer === alternative.text)?.answer const answer = team?.question_alternative_answers.find(
if (alternative.text === teamAnswer) return true (questionAnswer) => questionAnswer.question_alternative_id == alternative.id
else return false )
if (answer) {
return answer.answer === '1'
}
return false
} }
const updateAnswer = async (alternative: QuestionAlternative) => { const updateAnswer = async (alternative: QuestionAlternative, checked: boolean) => {
// TODO: fix. Make list of alternatives and delete & post instead of put to allow multiple boxes checked. // TODO: fix. Make list of alternatives and delete & post instead of put to allow multiple boxes checked.
if (activeSlide) { if (!activeSlide) {
if (team?.question_answers.find((answer) => answer.question_id === activeSlide.questions[0].id)) { return
if (answer?.answer === alternative.text) {
// Uncheck checkbox
deleteAnswer()
} else {
// Check another box
// TODO
}
} else {
// Check first checkbox
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 () => { //const checkedValue = checked ? 1 : 0
//const correctAnswer = checkedValue === alternative.value ? 1 : 0
const url = `/api/competitions/${competitionId}/teams/${teamId}/answers/question_alternatives/${alternative.id}`
const payload = {
answer: checked ? 1 : 0,
}
await axios await axios
.delete(`/api/competitions/${competitionId}/teams/${teamId}/answers`) // TODO: fix .put(url, payload)
.then(() => { .then(() => {
dispatch(getEditorCompetition(competitionId)) if (variant === 'editor') {
dispatch(getEditorCompetition(competitionId))
} else {
dispatch(getPresentationCompetition(competitionId))
}
}) })
.catch(console.log) .catch(console.log)
} }
...@@ -105,10 +93,10 @@ const AnswerMultiple = ({ variant, activeSlide, competitionId }: AnswerMultipleP ...@@ -105,10 +93,10 @@ const AnswerMultiple = ({ variant, activeSlide, competitionId }: AnswerMultipleP
activeSlide.questions[0].alternatives.map((alt) => ( activeSlide.questions[0].alternatives.map((alt) => (
<div key={alt.id}> <div key={alt.id}>
<ListItem divider> <ListItem divider>
{ <GreenCheckbox
//<GreenCheckbox checked={checkbox} onChange={(event) => updateAnswer(alt, event.target.checked)} /> checked={decideChecked(alt)}
} onChange={(event: any) => updateAnswer(alt, event.target.checked)}
<GreenCheckbox checked={decideChecked(alt)} onChange={() => updateAnswer(alt)} /> />
<Typography style={{ wordBreak: 'break-all' }}>{alt.text}</Typography> <Typography style={{ wordBreak: 'break-all' }}>{alt.text}</Typography>
</ListItem> </ListItem>
</div> </div>
......
...@@ -12,9 +12,7 @@ ...@@ -12,9 +12,7 @@
* @module * @module
*/ */
import { Checkbox, ListItem, ListItemText, Typography, withStyles } from '@material-ui/core' import { ListItem, ListItemText, Typography } 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 RadioButtonCheckedIcon from '@material-ui/icons/RadioButtonCheckedOutlined'
import RadioButtonUncheckedIcon from '@material-ui/icons/RadioButtonUncheckedOutlined' import RadioButtonUncheckedIcon from '@material-ui/icons/RadioButtonUncheckedOutlined'
import axios from 'axios' import axios from 'axios'
...@@ -39,69 +37,42 @@ const AnswerSingle = ({ variant, activeSlide, competitionId }: AnswerSingleProps ...@@ -39,69 +37,42 @@ const AnswerSingle = ({ variant, activeSlide, competitionId }: AnswerSingleProps
if (variant === 'editor') return state.editor.competition.teams.find((team) => team.id === teamId) if (variant === 'editor') return state.editor.competition.teams.find((team) => team.id === teamId)
return state.presentation.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 decideChecked = (alternative: QuestionAlternative) => {
const teamAnswer = team?.question_answers.find((answer) => answer.answer === alternative.text)?.answer const answer = team?.question_alternative_answers.find(
if (teamAnswer) return true (questionAnswer) => questionAnswer.question_alternative_id == alternative.id
else return false )
if (answer) {
return answer.answer === '1'
}
return false
} }
const updateAnswer = async (alternative: QuestionAlternative) => { const updateAnswer = async (alternative: QuestionAlternative) => {
if (activeSlide) { if (!activeSlide) {
if (team?.question_answers[0]) { return
// If an alternative was already marked
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 {
// If no alternative was already marked
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 () => { // Unselect each radio button to only allow one selected alternative
const alternatives = activeSlide.questions[0].alternatives
for (const alt of alternatives) {
const url = `/api/competitions/${competitionId}/teams/${teamId}/answers/question_alternatives/${alt.id}`
await axios.put(url, { answer: 0 })
}
// Update selected alternative
const url = `/api/competitions/${competitionId}/teams/${teamId}/answers/question_alternatives/${alternative.id}`
await axios await axios
.delete(`/api/competitions/${competitionId}/teams/${teamId}/answers`) .put(url, { answer: 1 })
.then(() => { .then(() => {
dispatch(getEditorCompetition(competitionId)) if (variant === 'editor') {
dispatch(getEditorCompetition(competitionId))
} else {
dispatch(getPresentationCompetition(competitionId))
}
}) })
.catch(console.log) .catch(console.log)
} }
const GreenCheckbox = withStyles({
root: {
color: grey[900],
'&$checked': {
color: green[600],
},
},
checked: {},
})((props: CheckboxProps) => <Checkbox color="default" {...props} />)
/** /**
* Renders the radio button which the participants will click to mark their answer. * Renders the radio button which the participants will click to mark their answer.
*/ */
......
...@@ -31,7 +31,6 @@ const AnswerText = ({ activeSlide, competitionId }: AnswerTextProps) => { ...@@ -31,7 +31,6 @@ const AnswerText = ({ activeSlide, competitionId }: AnswerTextProps) => {
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const teamId = useAppSelector((state) => state.competitionLogin.data?.team_id) const teamId = useAppSelector((state) => state.competitionLogin.data?.team_id)
const team = useAppSelector((state) => state.presentation.competition.teams.find((team) => team.id === teamId)) 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) => { const onAnswerChange = (answer: string) => {
if (timerHandle) { if (timerHandle) {
...@@ -43,30 +42,28 @@ const AnswerText = ({ activeSlide, competitionId }: AnswerTextProps) => { ...@@ -43,30 +42,28 @@ const AnswerText = ({ activeSlide, competitionId }: AnswerTextProps) => {
} }
const updateAnswer = async (answer: string) => { const updateAnswer = async (answer: string) => {
if (activeSlide && team) { if (!activeSlide) {
console.log(team.question_answers) return
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(getPresentationCompetition(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(getPresentationCompetition(competitionId))
})
.catch(console.log)
}
} }
const alternative = activeSlide.questions[0].alternatives[0]
const url = `/api/competitions/${competitionId}/teams/${teamId}/answers/question_alternatives/${alternative.id}`
await axios
.put(url, { answer: answer })
.then(() => {
dispatch(getPresentationCompetition(competitionId))
})
.catch(console.log)
}
const getDefaultString = () => {
if (!team || !activeSlide) {
return
}
const activeAltId = activeSlide.questions[0].alternatives[0].id
return (
team.question_alternative_answers.find((questionAnswer) => questionAnswer.question_alternative_id === activeAltId)
?.answer || 'Svar...'
)
} }
return ( return (
...@@ -79,9 +76,7 @@ const AnswerText = ({ activeSlide, competitionId }: AnswerTextProps) => { ...@@ -79,9 +76,7 @@ const AnswerText = ({ activeSlide, competitionId }: AnswerTextProps) => {
<ListItem style={{ height: '100%' }}> <ListItem style={{ height: '100%' }}>
<TextField <TextField
disabled={team === undefined} disabled={team === undefined}
defaultValue={ defaultValue={getDefaultString()}
team?.question_answers.find((questionAnswer) => questionAnswer.id === answerId)?.answer || 'Svar...'
}
style={{ height: '100%' }} style={{ height: '100%' }}
variant="outlined" variant="outlined"
fullWidth={true} fullWidth={true}
......
...@@ -13,22 +13,53 @@ type ScoreDisplayProps = { ...@@ -13,22 +13,53 @@ type ScoreDisplayProps = {
const JudgeScoreDisplay = ({ teamIndex, activeSlide }: ScoreDisplayProps) => { const JudgeScoreDisplay = ({ teamIndex, activeSlide }: ScoreDisplayProps) => {
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const questionTypes = useAppSelector((state) => state.types.questionTypes)
const currentTeam = useAppSelector((state) => state.presentation.competition.teams[teamIndex]) const currentTeam = useAppSelector((state) => state.presentation.competition.teams[teamIndex])
const currentCompetititonId = useAppSelector((state) => state.presentation.competition.id) const currentCompetititonId = useAppSelector((state) => state.presentation.competition.id)
const activeQuestion = activeSlide.questions[0] const activeQuestion = activeSlide.questions[0]
const scores = currentTeam.question_answers.map((questionAnswer) => questionAnswer.score) const activeScore = currentTeam.question_scores.find((x) => x.question_id === activeQuestion?.id)
const questionMaxScore = activeQuestion?.total_score const questionMaxScore = activeQuestion?.total_score
const activeAnswer = currentTeam.question_answers.find(
(questionAnswer) => questionAnswer.question_id === activeQuestion?.id const scores = currentTeam.question_scores.map((questionAnswer) => questionAnswer.score)
) const textQuestionType = questionTypes.find((questionType) => questionType.name === 'Text')?.id || 0
const handleEditScore = async (newScore: number, answerId: number) => { const handleEditScore = async (newScore: number, questionId: number) => {
await axios await axios
.put(`/api/competitions/${currentCompetititonId}/teams/${currentTeam.id}/answers/${answerId}`, { .put(`/api/competitions/${currentCompetititonId}/teams/${currentTeam.id}/answers/question_scores/${questionId}`, {
score: newScore, score: newScore,
}) })
.then(() => dispatch(getPresentationCompetition(currentCompetititonId.toString()))) .then(() => dispatch(getPresentationCompetition(currentCompetititonId.toString())))
} }
const getAlternativeAnswers = () => {
const result: string[] = []
if (!activeQuestion) {
return result
}
for (const alt of activeQuestion.alternatives) {
const value = currentTeam.question_alternative_answers.find((x) => x.question_alternative_id === alt.id)
if (!value) {
continue
}
if (activeQuestion.type_id === 1) {
result.push(alt.text)
} else if (+value.answer > 0) {
result.push(alt.text)
}
/*
switch(activeQuestion.question_type.name){
case "Text":
result.push(alt.text)
break;
default:
}*/
}
return result
//const asdasd = currentTeam.question_alternative_answers.filter((x)=>x.question_alternative_id === activeQuestion.alternatives[0].io)
}
return ( return (
<ScoreDisplayContainer> <ScoreDisplayContainer>
<ScoreDisplayHeader> <ScoreDisplayHeader>
...@@ -36,25 +67,30 @@ const JudgeScoreDisplay = ({ teamIndex, activeSlide }: ScoreDisplayProps) => { ...@@ -36,25 +67,30 @@ const JudgeScoreDisplay = ({ teamIndex, activeSlide }: ScoreDisplayProps) => {
<Box fontWeight="fontWeightBold">{currentTeam.name}</Box> <Box fontWeight="fontWeightBold">{currentTeam.name}</Box>
</Typography> </Typography>
{activeAnswer && ( {activeQuestion && (
<ScoreInput <ScoreInput
label="Poäng" label="Poäng"
defaultValue={activeAnswer?.score} defaultValue={0}
value={activeScore ? activeScore.score : 0}
inputProps={{ style: { fontSize: 20 } }} inputProps={{ style: { fontSize: 20 } }}
InputProps={{ disableUnderline: true, inputProps: { min: 0, max: questionMaxScore } }} InputProps={{ disableUnderline: true, inputProps: { min: 0, max: questionMaxScore } }}
type="number" type="number"
onChange={(event) => handleEditScore(+event.target.value, activeAnswer.id)} onChange={(event) => handleEditScore(+event.target.value, activeQuestion.id)}
/> />
)} )}
</ScoreDisplayHeader> </ScoreDisplayHeader>
<Typography variant="h6">Alla poäng: [ {scores.map((score) => `${score} `)}]</Typography> <Typography variant="h6">Alla poäng: [ {scores.map((score) => `${score} `)}]</Typography>
<Typography variant="h6">Total poäng: {scores.reduce((a, b) => a + b, 0)}</Typography> <Typography variant="h6">Total poäng: {scores.reduce((a, b) => a + b, 0)}</Typography>
{activeAnswer && ( {activeQuestion && (
<AnswerContainer> <AnswerContainer>
<Typography variant="body1">{activeAnswer.answer}</Typography> {getAlternativeAnswers().map((v, k) => (
<Typography variant="body1" key={k}>
<span>&#8226;</span> {v}
</Typography>
))}
</AnswerContainer> </AnswerContainer>
)} )}
{!activeAnswer && <Typography variant="body1">Inget svar</Typography>} {!activeQuestion && <Typography variant="body1">Inget svar</Typography>}
</ScoreDisplayContainer> </ScoreDisplayContainer>
) )
} }
......
...@@ -24,8 +24,8 @@ const Scoreboard = ({ isOperator }: ScoreboardProps) => { ...@@ -24,8 +24,8 @@ const Scoreboard = ({ isOperator }: ScoreboardProps) => {
/** Sums the scores for the teams. */ /** Sums the scores for the teams. */
const addScore = (team: RichTeam) => { const addScore = (team: RichTeam) => {
let totalScore = 0 let totalScore = 0
for (let j = 0; j < team.question_answers.length; j++) { for (let j = 0; j < team.question_scores.length; j++) {
totalScore = totalScore + team.question_answers[j].score totalScore = totalScore + team.question_scores[j].score
} }
return totalScore return totalScore
} }
......
...@@ -31,6 +31,7 @@ export const ScoreInput = styled(TextField)` ...@@ -31,6 +31,7 @@ export const ScoreInput = styled(TextField)`
export const AnswerContainer = styled.div` export const AnswerContainer = styled.div`
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
flex-direction: column;
` `
export const JudgeScoringInstructionsContainer = styled(Paper)` export const JudgeScoringInstructionsContainer = styled(Paper)`
......
...@@ -5,64 +5,98 @@ Default route: /api/competitions/<competition_id>/teams/<team_id>/answers ...@@ -5,64 +5,98 @@ Default route: /api/competitions/<competition_id>/teams/<team_id>/answers
import app.database.controller as dbc import app.database.controller as dbc
from app.apis import item_response, list_response, protect_route from app.apis import item_response, list_response, protect_route
from app.core.dto import QuestionAnswerDTO from app.core.dto import QuestionAlternativeAnswerDTO, QuestionScoreDTO
from app.core.parsers import sentinel from app.core.parsers import sentinel
from flask_restx import Resource, reqparse from flask_restx import Resource, reqparse
api = QuestionAnswerDTO.api api = QuestionAlternativeAnswerDTO.api
schema = QuestionAnswerDTO.schema schema = QuestionAlternativeAnswerDTO.schema
list_schema = QuestionAnswerDTO.list_schema list_schema = QuestionAlternativeAnswerDTO.list_schema
score_schema = QuestionScoreDTO.schema
score_list_schema = QuestionScoreDTO.list_schema
answer_parser_add = reqparse.RequestParser() answer_parser_add = reqparse.RequestParser()
answer_parser_add.add_argument("answer", type=str, required=True, location="json") answer_parser_add.add_argument("answer", type=str, required=True, location="json")
answer_parser_add.add_argument("score", type=int, required=True, location="json")
answer_parser_add.add_argument("question_id", type=int, required=True, location="json")
answer_parser_edit = reqparse.RequestParser() answer_parser_edit = reqparse.RequestParser()
answer_parser_edit.add_argument("answer", type=str, default=sentinel, location="json") answer_parser_edit.add_argument("answer", type=str, default=sentinel, location="json")
answer_parser_edit.add_argument("score", type=int, default=sentinel, location="json")
score_parser_add = reqparse.RequestParser()
score_parser_add.add_argument("score", type=int, required=False, location="json")
score_parser_edit = reqparse.RequestParser()
score_parser_edit.add_argument("score", type=int, default=sentinel, location="json")
@api.route("/question_scores")
@api.param("competition_id, team_id")
class QuestionScoreList(Resource):
@protect_route(allowed_roles=["*"], allowed_views=["*"])
def get(self, competition_id, team_id):
""" Gets all question answers that the specified team has given. """
items = dbc.get.question_score_list(competition_id, team_id)
return list_response(score_list_schema.dump(items))
@api.route("") @api.route("/question_alternatives")
@api.param("competition_id, team_id") @api.param("competition_id, team_id")
class QuestionAnswerList(Resource): class QuestionAlternativeList(Resource):
@protect_route(allowed_roles=["*"], allowed_views=["*"]) @protect_route(allowed_roles=["*"], allowed_views=["*"])
def get(self, competition_id, team_id): def get(self, competition_id, team_id):
""" Gets all question answers that the specified team has given. """ """ Gets all question answers that the specified team has given. """
items = dbc.get.question_answer_list(competition_id, team_id) items = dbc.get.question_alternative_answer_list(competition_id, team_id)
return list_response(list_schema.dump(items)) return list_response(list_schema.dump(items))
@api.route("/question_scores/<question_id>")
@api.param("competition_id, team_id, question_id")
class QuestionScores(Resource):
@protect_route(allowed_roles=["*"], allowed_views=["*"]) @protect_route(allowed_roles=["*"], allowed_views=["*"])
def post(self, competition_id, team_id): def get(self, competition_id, team_id, question_id):
""" """ Gets the specified question answer. """
Posts a new question answer to the specified
question using the provided arguments. item = dbc.get.question_score(competition_id, team_id, question_id)
""" return item_response(score_schema.dump(item))
args = answer_parser_add.parse_args(strict=True) @protect_route(allowed_roles=["*"], allowed_views=["*"])
item = dbc.add.question_answer(**args, team_id=team_id) def put(self, competition_id, team_id, question_id):
return item_response(schema.dump(item)) """ Add or edit specified quesiton_answer. """
item = dbc.get.question_score(competition_id, team_id, question_id, required=False)
if item is None:
args = score_parser_add.parse_args(strict=True)
item = dbc.add.question_score(args.get("score"), question_id, team_id)
else:
args = score_parser_edit.parse_args(strict=True)
item = dbc.edit.default(item, **args)
return item_response(score_schema.dump(item))
@api.route("/<answer_id>") @api.route("/question_alternatives/<question_alternative_id>")
@api.param("competition_id, team_id, answer_id") @api.param("competition_id, team_id, question_alternative_id")
class QuestionAnswers(Resource): class QuestionAlternativeAnswers(Resource):
@protect_route(allowed_roles=["*"], allowed_views=["*"]) @protect_route(allowed_roles=["*"], allowed_views=["*"])
def get(self, competition_id, team_id, answer_id): def get(self, competition_id, team_id, question_alternative_id):
""" Gets the specified question answer. """ """ Gets the specified question answer. """
item = dbc.get.question_answer(competition_id, team_id, answer_id) item = dbc.get.question_alternative_answer(competition_id, team_id, question_alternative_id)
return item_response(schema.dump(item)) return item_response(schema.dump(item))
@protect_route(allowed_roles=["*"], allowed_views=["*"]) @protect_route(allowed_roles=["*"], allowed_views=["*"])
def put(self, competition_id, team_id, answer_id): def put(self, competition_id, team_id, question_alternative_id):
""" Edits the specified question answer with the provided arguments. """ """ Add or edit specified quesiton_answer. """
args = answer_parser_edit.parse_args(strict=True) item = dbc.get.question_alternative_answer(competition_id, team_id, question_alternative_id, required=False)
item = dbc.get.question_answer(competition_id, team_id, answer_id) if item is None:
item = dbc.edit.default(item, **args) args = answer_parser_add.parse_args(strict=True)
return item_response(schema.dump(item)) item = dbc.add.question_alternative_answer(args.get("answer"), question_alternative_id, team_id)
else:
args = answer_parser_edit.parse_args(strict=True)
item = dbc.edit.default(item, **args)
# No need to delete an answer. It only needs to be deleted return item_response(schema.dump(item))
# together with the question or the team.
...@@ -81,7 +81,13 @@ class QuestionAlternativeDTO: ...@@ -81,7 +81,13 @@ class QuestionAlternativeDTO:
list_schema = schemas.QuestionAlternativeSchema(many=True) list_schema = schemas.QuestionAlternativeSchema(many=True)
class QuestionAnswerDTO: class QuestionAlternativeAnswerDTO:
api = Namespace("answers") api = Namespace("answers")
schema = schemas.QuestionAnswerSchema(many=False) schema = schemas.QuestionAlternativeAnswerSchema(many=False)
list_schema = schemas.QuestionAnswerSchema(many=True) list_schema = schemas.QuestionAlternativeAnswerSchema(many=True)
class QuestionScoreDTO:
api = Namespace("answers")
schema = schemas.QuestionScoreSchema(many=False)
list_schema = schemas.QuestionScoreSchema(many=True)
...@@ -36,7 +36,8 @@ class TeamSchemaRich(RichSchema): ...@@ -36,7 +36,8 @@ class TeamSchemaRich(RichSchema):
id = ma.auto_field() id = ma.auto_field()
name = ma.auto_field() name = ma.auto_field()
competition_id = ma.auto_field() competition_id = ma.auto_field()
question_answers = fields.Nested(schemas.QuestionAnswerSchema, many=True) question_alternative_answers = fields.Nested(schemas.QuestionAlternativeAnswerSchema, many=True)
question_scores = fields.Nested(schemas.QuestionScoreSchema, many=True)
class SlideSchemaRich(RichSchema): class SlideSchemaRich(RichSchema):
......
...@@ -64,12 +64,21 @@ class QuestionSchema(BaseSchema): ...@@ -64,12 +64,21 @@ class QuestionSchema(BaseSchema):
correcting_instructions = ma.auto_field() correcting_instructions = ma.auto_field()
class QuestionAnswerSchema(BaseSchema): class QuestionAlternativeAnswerSchema(BaseSchema):
class Meta(BaseSchema.Meta): class Meta(BaseSchema.Meta):
model = models.QuestionAnswer model = models.QuestionAlternativeAnswer
id = ma.auto_field() id = ma.auto_field()
answer = ma.auto_field() answer = ma.auto_field()
question_alternative_id = ma.auto_field()
team_id = ma.auto_field()
class QuestionScoreSchema(BaseSchema):
class Meta(BaseSchema.Meta):
model = models.QuestionScore
id = ma.auto_field()
score = ma.auto_field() score = ma.auto_field()
question_id = ma.auto_field() question_id = ma.auto_field()
team_id = ma.auto_field() team_id = ma.auto_field()
...@@ -169,4 +178,4 @@ class ComponentSchema(BaseSchema): ...@@ -169,4 +178,4 @@ class ComponentSchema(BaseSchema):
text = fields.fields.String() text = fields.fields.String()
media = fields.Nested(MediaSchema, many=False) media = fields.Nested(MediaSchema, many=False)
question_id = fields.fields.Integer() question_id = fields.fields.Integer()
\ No newline at end of file
...@@ -18,8 +18,9 @@ from app.database.models import ( ...@@ -18,8 +18,9 @@ from app.database.models import (
MediaType, MediaType,
Question, Question,
QuestionAlternative, QuestionAlternative,
QuestionAnswer, QuestionAlternativeAnswer,
QuestionComponent, QuestionComponent,
QuestionScore,
QuestionType, QuestionType,
Role, Role,
Slide, Slide,
...@@ -284,10 +285,19 @@ def question_alternative(text, value, question_id): ...@@ -284,10 +285,19 @@ def question_alternative(text, value, question_id):
return db_add(QuestionAlternative(text, value, question_id)) return db_add(QuestionAlternative(text, value, question_id))
def question_answer(answer, score, question_id, team_id): def question_score(score, question_id, team_id):
""" """
Adds a question answer to the specified team Adds a question answer to the specified team
and question using the provided arguments. and question using the provided arguments.
""" """
return db_add(QuestionAnswer(answer, score, question_id, team_id)) return db_add(QuestionScore(score, question_id, team_id))
def question_alternative_answer(answer, question_alternative_id, team_id):
"""
Adds a question answer to the specified team
and question using the provided arguments.
"""
return db_add(QuestionAlternativeAnswer(answer, question_alternative_id, team_id))
...@@ -5,7 +5,7 @@ This file contains functionality to delete data to the database. ...@@ -5,7 +5,7 @@ This file contains functionality to delete data to the database.
import app.core.http_codes as codes import app.core.http_codes as codes
import app.database.controller as dbc import app.database.controller as dbc
from app.core import db from app.core import db
from app.database.models import Whitelist from app.database.models import QuestionAlternativeAnswer, QuestionScore, Whitelist
from flask_restx import abort from flask_restx import abort
from sqlalchemy.exc import IntegrityError from sqlalchemy.exc import IntegrityError
...@@ -79,8 +79,8 @@ def slide(item_slide): ...@@ -79,8 +79,8 @@ def slide(item_slide):
def team(item_team): def team(item_team):
""" Deletes team, its question answers and the code. """ """ Deletes team, its question answers and the code. """
for item_question_answer in item_team.question_answers: for item_question_answer in item_team.question_alternative_answers:
question_answers(item_question_answer) default(item_question_answer)
for item_code in item_team.code: for item_code in item_team.code:
code(item_code) code(item_code)
...@@ -90,8 +90,11 @@ def team(item_team): ...@@ -90,8 +90,11 @@ def team(item_team):
def question(item_question): def question(item_question):
""" Deletes question and its alternatives and answers. """ """ Deletes question and its alternatives and answers. """
for item_question_answer in item_question.question_answers: scores = QuestionScore.query.filter(QuestionScore.question_id == item_question.id).all()
question_answers(item_question_answer)
for item_question_score in scores:
default(item_question_score)
for item_alternative in item_question.alternatives: for item_alternative in item_question.alternatives:
alternatives(item_alternative) alternatives(item_alternative)
...@@ -100,16 +103,15 @@ def question(item_question): ...@@ -100,16 +103,15 @@ def question(item_question):
def alternatives(item_alternatives): def alternatives(item_alternatives):
""" Deletes question alternative. """ """ Deletes question alternative. """
answers = QuestionAlternativeAnswer.query.filter(
QuestionAlternativeAnswer.question_alternative_id == item_alternatives.id
).all()
for item_answer in answers:
default(item_answer)
default(item_alternatives) default(item_alternatives)
def question_answers(item_question_answers):
""" Deletes question answer. """
default(item_question_answers)
def competition(item_competition): def competition(item_competition):
""" Deletes competition, its slides, teams and codes. """ """ Deletes competition, its slides, teams and codes. """
......
...@@ -11,12 +11,14 @@ from app.database.models import ( ...@@ -11,12 +11,14 @@ from app.database.models import (
ImageComponent, ImageComponent,
Question, Question,
QuestionAlternative, QuestionAlternative,
QuestionAnswer, QuestionAlternativeAnswer,
QuestionScore,
Slide, Slide,
Team, Team,
TextComponent, TextComponent,
User, User,
) )
from sqlalchemy import func
from sqlalchemy.orm import joinedload from sqlalchemy.orm import joinedload
from sqlalchemy.orm.util import with_polymorphic from sqlalchemy.orm.util import with_polymorphic
...@@ -208,28 +210,68 @@ def question_alternative_list(competition_id, slide_id, question_id): ...@@ -208,28 +210,68 @@ def question_alternative_list(competition_id, slide_id, question_id):
### Question Answers ### ### Question Answers ###
def question_answer(competition_id, team_id, answer_id):
def question_score(competition_id, team_id, question_id, required=True):
"""
Get question answer for a given team based on its competition.
"""
join_competition = Competition.id == Team.competition_id
join_team = Team.id == QuestionScore.team_id
filters = (Competition.id == competition_id) & (Team.id == team_id) & (QuestionScore.question_id == question_id)
return (
QuestionScore.query.join(Competition, join_competition)
.join(Team, join_team)
.filter(filters)
.first_extended(required)
)
def question_score_list(competition_id, team_id):
"""
Get question answer for a given team based on its competition.
"""
join_competition = Competition.id == Team.competition_id
join_team = Team.id == QuestionScore.team_id
filters = (Competition.id == competition_id) & (Team.id == team_id)
return QuestionScore.query.join(Competition, join_competition).join(Team, join_team).filter(filters).all()
def question_alternative_answer(competition_id, team_id, question_alternative_id, required=True):
""" """
Get question answer for a given team based on its competition. Get question answer for a given team based on its competition.
""" """
join_competition = Competition.id == Team.competition_id join_competition = Competition.id == Team.competition_id
join_team = Team.id == QuestionAnswer.team_id join_team = Team.id == QuestionAlternativeAnswer.team_id
filters = (Competition.id == competition_id) & (Team.id == team_id) & (QuestionAnswer.id == answer_id) filters = (
(Competition.id == competition_id)
& (Team.id == team_id)
& (QuestionAlternativeAnswer.question_alternative_id == question_alternative_id)
)
return ( return (
QuestionAnswer.query.join(Competition, join_competition).join(Team, join_team).filter(filters).first_extended() QuestionAlternativeAnswer.query.join(Competition, join_competition)
.join(Team, join_team)
.filter(filters)
.first_extended(required)
) )
def question_answer_list(competition_id, team_id): def question_alternative_answer_list(competition_id, team_id):
""" """
Get a list of question answers for a given team based on its competition. Get a list of question answers for a given team based on its competition.
""" """
join_competition = Competition.id == Team.competition_id join_competition = Competition.id == Team.competition_id
join_team = Team.id == QuestionAnswer.team_id join_team = Team.id == QuestionAlternativeAnswer.team_id
filters = (Competition.id == competition_id) & (Team.id == team_id) filters = (Competition.id == competition_id) & (Team.id == team_id)
return QuestionAnswer.query.join(Competition, join_competition).join(Team, join_team).filter(filters).all() query = QuestionAlternativeAnswer.query.join(Competition, join_competition).join(Team, join_team).filter(filters)
# Get total score
# sum = query.with_entities(func.sum(QuestionAnswer.score)).all()
items = query.all()
return items
### Components ### ### Components ###
...@@ -271,5 +313,13 @@ def competition(competition_id): ...@@ -271,5 +313,13 @@ def competition(competition_id):
os1 = joinedload(Competition.slides).joinedload(Slide.components) os1 = joinedload(Competition.slides).joinedload(Slide.components)
os2 = joinedload(Competition.slides).joinedload(Slide.questions).joinedload(Question.alternatives) os2 = joinedload(Competition.slides).joinedload(Slide.questions).joinedload(Question.alternatives)
ot = joinedload(Competition.teams).joinedload(Team.question_answers) ot = joinedload(Competition.teams).joinedload(Team.question_alternative_answers)
return Competition.query.filter(Competition.id == competition_id).options(os1).options(os2).options(ot).first() ot1 = joinedload(Competition.teams).joinedload(Team.question_scores)
return (
Competition.query.filter(Competition.id == competition_id)
.options(os1)
.options(os2)
.options(ot)
.options(ot1)
.first()
)
...@@ -5,8 +5,7 @@ each other. ...@@ -5,8 +5,7 @@ each other.
""" """
from app.core import bcrypt, db from app.core import bcrypt, db
from app.database.types import (ID_IMAGE_COMPONENT, ID_QUESTION_COMPONENT, from app.database.types import ID_IMAGE_COMPONENT, ID_QUESTION_COMPONENT, ID_TEXT_COMPONENT
ID_TEXT_COMPONENT)
from sqlalchemy.ext.hybrid import hybrid_method, hybrid_property from sqlalchemy.ext.hybrid import hybrid_method, hybrid_property
STRING_SIZE = 254 STRING_SIZE = 254
...@@ -133,7 +132,9 @@ class Team(db.Model): ...@@ -133,7 +132,9 @@ class Team(db.Model):
name = db.Column(db.String(STRING_SIZE), nullable=False) name = db.Column(db.String(STRING_SIZE), nullable=False)
competition_id = db.Column(db.Integer, db.ForeignKey("competition.id"), nullable=False) competition_id = db.Column(db.Integer, db.ForeignKey("competition.id"), nullable=False)
question_answers = db.relationship("QuestionAnswer", backref="team") question_alternative_answers = db.relationship("QuestionAlternativeAnswer", backref="team")
question_scores = db.relationship("QuestionScore", backref="team")
code = db.relationship("Code", backref="team") code = db.relationship("Code", backref="team")
def __init__(self, name, competition_id): def __init__(self, name, competition_id):
...@@ -170,7 +171,6 @@ class Question(db.Model): ...@@ -170,7 +171,6 @@ class Question(db.Model):
slide_id = db.Column(db.Integer, db.ForeignKey("slide.id"), nullable=False) slide_id = db.Column(db.Integer, db.ForeignKey("slide.id"), nullable=False)
correcting_instructions = db.Column(db.Text, nullable=True, default=None) correcting_instructions = db.Column(db.Text, nullable=True, default=None)
question_answers = db.relationship("QuestionAnswer", backref="question")
alternatives = db.relationship("QuestionAlternative", backref="question") alternatives = db.relationship("QuestionAlternative", backref="question")
def __init__(self, name, total_score, type_id, slide_id, correcting_instructions): def __init__(self, name, total_score, type_id, slide_id, correcting_instructions):
...@@ -193,22 +193,34 @@ class QuestionAlternative(db.Model): ...@@ -193,22 +193,34 @@ class QuestionAlternative(db.Model):
self.question_id = question_id self.question_id = question_id
class QuestionAnswer(db.Model): class QuestionScore(db.Model):
__table_args__ = (db.UniqueConstraint("question_id", "team_id"),) __table_args__ = (db.UniqueConstraint("question_id", "team_id"),)
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
answer = db.Column(db.String(STRING_SIZE), nullable=False) score = db.Column(db.Integer, nullable=True, default=0)
score = db.Column(db.Integer, nullable=False, default=0)
question_id = db.Column(db.Integer, db.ForeignKey("question.id"), nullable=False) question_id = db.Column(db.Integer, db.ForeignKey("question_alternative.id"), nullable=False)
team_id = db.Column(db.Integer, db.ForeignKey("team.id"), nullable=False) team_id = db.Column(db.Integer, db.ForeignKey("team.id"), nullable=False)
def __init__(self, answer, score, question_id, team_id): def __init__(self, score, question_id, team_id):
self.answer = answer
self.score = score self.score = score
self.question_id = question_id self.question_id = question_id
self.team_id = team_id self.team_id = team_id
class QuestionAlternativeAnswer(db.Model):
__table_args__ = (db.UniqueConstraint("question_alternative_id", "team_id"),)
id = db.Column(db.Integer, primary_key=True)
answer = db.Column(db.String(STRING_SIZE), nullable=False)
question_alternative_id = db.Column(db.Integer, db.ForeignKey("question_alternative.id"), nullable=False)
team_id = db.Column(db.Integer, db.ForeignKey("team.id"), nullable=False)
def __init__(self, answer, question_alternative_id, team_id):
self.answer = answer
self.question_alternative_id = question_alternative_id
self.team_id = team_id
class Component(db.Model): class Component(db.Model):
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
x = db.Column(db.Integer, nullable=False, default=0) x = db.Column(db.Integer, nullable=False, default=0)
......
...@@ -106,12 +106,12 @@ def _add_items(): ...@@ -106,12 +106,12 @@ def _add_items():
dbc.add.team(f"{name}{i}", item_comp.id) dbc.add.team(f"{name}{i}", item_comp.id)
# question_answer(answer, score, question_id, team_id) # question_answer(answer, score, question_id, team_id)
dbc.add.question_answer("ett svar som ger 2p", 2, 1, 1) # dbc.add.question_answer("ett svar som ger 2p", 2, 1, 1)
dbc.add.question_answer("ett svar som ger 10p", 10, 2, 1) # dbc.add.question_answer("ett svar som ger 10p", 10, 2, 1)
dbc.add.question_answer("ett svar som ger 6p", 6, 3, 1) # dbc.add.question_answer("ett svar som ger 6p", 6, 3, 1)
dbc.add.question_answer("ett svar som ger 2p", 2, 1, 2) # dbc.add.question_answer("ett svar som ger 2p", 2, 1, 2)
dbc.add.question_answer("ett svar som ger 3p", 3, 1, 3) # dbc.add.question_answer("ett svar som ger 3p", 3, 1, 3)
if __name__ == "__main__": if __name__ == "__main__":
......
...@@ -471,11 +471,15 @@ def test_authorization(client): ...@@ -471,11 +471,15 @@ def test_authorization(client):
assert response.status_code == codes.UNAUTHORIZED assert response.status_code == codes.UNAUTHORIZED
# Get own answers # Get own answers
response, body = get(client, f"/api/competitions/{competition_id}/teams/{team_id}/answers", headers=headers) response, body = get(
client, f"/api/competitions/{competition_id}/teams/{team_id}/answers/question_alternatives", headers=headers
)
assert response.status_code == codes.OK assert response.status_code == codes.OK
# Try to get another teams answers # Try to get another teams answers
response, body = get(client, f"/api/competitions/{competition_id}/teams/{team_id+1}/answers", headers=headers) response, body = get(
client, f"/api/competitions/{competition_id}/teams/{team_id+1}/answers/question_alternatives", headers=headers
)
assert response.status_code == codes.UNAUTHORIZED assert response.status_code == codes.UNAUTHORIZED
#### JUDGE #### #### JUDGE ####
...@@ -499,9 +503,13 @@ def test_authorization(client): ...@@ -499,9 +503,13 @@ def test_authorization(client):
assert response.status_code == codes.UNAUTHORIZED assert response.status_code == codes.UNAUTHORIZED
# Get team answers # Get team answers
response, body = get(client, f"/api/competitions/{competition_id}/teams/{team_id}/answers", headers=headers) response, body = get(
client, f"/api/competitions/{competition_id}/teams/{team_id}/answers/question_alternatives", headers=headers
)
assert response.status_code == codes.OK assert response.status_code == codes.OK
# Also get antoher teams answers # Also get antoher teams answers
response, body = get(client, f"/api/competitions/{competition_id}/teams/{team_id+1}/answers", headers=headers) response, body = get(
client, f"/api/competitions/{competition_id}/teams/{team_id+1}/answers/question_alternatives", headers=headers
)
assert response.status_code == codes.OK assert response.status_code == codes.OK
...@@ -49,8 +49,8 @@ def add_default_values(): ...@@ -49,8 +49,8 @@ def add_default_values():
db.session.add(Code("111111", 1, item_competition.id, item_team1.id)) # Team db.session.add(Code("111111", 1, item_competition.id, item_team1.id)) # Team
db.session.add(Code("222222", 2, item_competition.id)) # Judge db.session.add(Code("222222", 2, item_competition.id)) # Judge
dbc.add.question_answer("hej", 5, item_question.id, item_team1.id) # dbc.add.question_answer("hej", 5, item_question.id, item_team1.id)
dbc.add.question_answer("", 5, item_question.id, item_team2.id) # dbc.add.question_answer("då", 5, item_question.id, item_team2.id)
db.session.commit() db.session.commit()
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment