diff --git a/client/src/interfaces/ApiModels.ts b/client/src/interfaces/ApiModels.ts index 5ea091a0217bf973ec9630e515350bf6cd2820a5..7b5617d8185f1e98d677fd866510e1ad77db741e 100644 --- a/client/src/interfaces/ApiModels.ts +++ b/client/src/interfaces/ApiModels.ts @@ -66,11 +66,17 @@ export interface QuestionAlternative { question_id: number } -export interface QuestionAnswer { +export interface QuestionAlternativeAnswer { id: number - question_id: number team_id: number + question_alternative_id: number answer: string +} + +export interface QuestionScore { + id: number + team_id: number + question_id: number score: number } diff --git a/client/src/interfaces/ApiRichModels.ts b/client/src/interfaces/ApiRichModels.ts index 9eac01813c970e7500f3d4eaadf98e4a22e3acc5..4251f2e644491b8c3e1a6f5fcad6fa13f42c95bb 100644 --- a/client/src/interfaces/ApiRichModels.ts +++ b/client/src/interfaces/ApiRichModels.ts @@ -1,4 +1,4 @@ -import { Component, Media, QuestionAlternative, QuestionAnswer, QuestionType } from './ApiModels' +import { Component, Media, QuestionAlternative, QuestionAlternativeAnswer, QuestionScore } from './ApiModels' export interface RichCompetition { name: string @@ -24,7 +24,8 @@ export interface RichSlide { export interface RichTeam { id: number name: string - question_answers: QuestionAnswer[] + question_alternative_answers: QuestionAlternativeAnswer[] + question_scores: QuestionScore[] competition_id: number } @@ -34,7 +35,6 @@ export interface RichQuestion { name: string title: string total_score: number - question_type: QuestionType type_id: number correcting_instructions: string alternatives: QuestionAlternative[] diff --git a/client/src/pages/presentationEditor/PresentationEditorPage.tsx b/client/src/pages/presentationEditor/PresentationEditorPage.tsx index d3286065499cb5c3b4462f33fda358f582578405..a46262af821771ec8ada13466e4980dfc9b2b850 100644 --- a/client/src/pages/presentationEditor/PresentationEditorPage.tsx +++ b/client/src/pages/presentationEditor/PresentationEditorPage.tsx @@ -186,7 +186,7 @@ const PresentationEditorPage: React.FC = () => { onContextMenu={(event) => handleRightClick(event, slide.id)} > {renderSlideIcon(slide)} - <ListItemText primary={`Sida ${slide.id}`} /> + <ListItemText primary={`Sida ${slide.order + 1}`} /> </SlideListItem> </div> )} diff --git a/client/src/pages/presentationEditor/components/answerComponents/AnswerMultiple.tsx b/client/src/pages/presentationEditor/components/answerComponents/AnswerMultiple.tsx index 2e6bdc91cbb4d31cc98d27c5510a00dbc7ca9e92..0dd7efcf9b0a9b223a43666fc088100ffc2ae17d 100644 --- a/client/src/pages/presentationEditor/components/answerComponents/AnswerMultiple.tsx +++ b/client/src/pages/presentationEditor/components/answerComponents/AnswerMultiple.tsx @@ -34,50 +34,38 @@ const AnswerMultiple = ({ variant, activeSlide, competitionId }: AnswerMultipleP 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 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 - if (alternative.text === teamAnswer) return true - else return false + const answer = team?.question_alternative_answers.find( + (questionAnswer) => questionAnswer.question_alternative_id == alternative.id + ) + 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. - if (activeSlide) { - if (team?.question_answers.find((answer) => answer.question_id === activeSlide.questions[0].id)) { - 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) - } + if (!activeSlide) { + return } - } - 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 - .delete(`/api/competitions/${competitionId}/teams/${teamId}/answers`) // TODO: fix + .put(url, payload) .then(() => { - dispatch(getEditorCompetition(competitionId)) + if (variant === 'editor') { + dispatch(getEditorCompetition(competitionId)) + } else { + dispatch(getPresentationCompetition(competitionId)) + } }) .catch(console.log) } @@ -105,10 +93,10 @@ const AnswerMultiple = ({ variant, activeSlide, competitionId }: AnswerMultipleP 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)} /> + <GreenCheckbox + checked={decideChecked(alt)} + onChange={(event: any) => updateAnswer(alt, event.target.checked)} + /> <Typography style={{ wordBreak: 'break-all' }}>{alt.text}</Typography> </ListItem> </div> diff --git a/client/src/pages/presentationEditor/components/answerComponents/AnswerSingle.tsx b/client/src/pages/presentationEditor/components/answerComponents/AnswerSingle.tsx index c3c7bb713b8703ee52a9f3328a2b295dea385997..514200d10068d840e28cf3d8e0dbe6953861902e 100644 --- a/client/src/pages/presentationEditor/components/answerComponents/AnswerSingle.tsx +++ b/client/src/pages/presentationEditor/components/answerComponents/AnswerSingle.tsx @@ -12,9 +12,7 @@ * @module */ -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 { ListItem, ListItemText, Typography } from '@material-ui/core' import RadioButtonCheckedIcon from '@material-ui/icons/RadioButtonCheckedOutlined' import RadioButtonUncheckedIcon from '@material-ui/icons/RadioButtonUncheckedOutlined' import axios from 'axios' @@ -39,69 +37,42 @@ const AnswerSingle = ({ variant, activeSlide, competitionId }: AnswerSingleProps 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 answer = team?.question_alternative_answers.find( + (questionAnswer) => questionAnswer.question_alternative_id == alternative.id + ) + if (answer) { + return answer.answer === '1' + } + return false } const updateAnswer = async (alternative: QuestionAlternative) => { - if (activeSlide) { - if (team?.question_answers[0]) { - // 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) - } + if (!activeSlide) { + return } - } - 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 - .delete(`/api/competitions/${competitionId}/teams/${teamId}/answers`) + .put(url, { answer: 1 }) .then(() => { - dispatch(getEditorCompetition(competitionId)) + if (variant === 'editor') { + dispatch(getEditorCompetition(competitionId)) + } else { + dispatch(getPresentationCompetition(competitionId)) + } }) .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. */ diff --git a/client/src/pages/presentationEditor/components/answerComponents/AnswerText.tsx b/client/src/pages/presentationEditor/components/answerComponents/AnswerText.tsx index b19388f10e4949a775454e8c5bfd1bafe05ac804..9a15233e9b8828145ff980dce20bcbae828e202d 100644 --- a/client/src/pages/presentationEditor/components/answerComponents/AnswerText.tsx +++ b/client/src/pages/presentationEditor/components/answerComponents/AnswerText.tsx @@ -31,7 +31,6 @@ const AnswerText = ({ activeSlide, competitionId }: AnswerTextProps) => { 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) { @@ -43,30 +42,28 @@ const AnswerText = ({ activeSlide, competitionId }: AnswerTextProps) => { } const updateAnswer = async (answer: string) => { - if (activeSlide && team) { - console.log(team.question_answers) - 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) - } + if (!activeSlide) { + return } + 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 ( @@ -79,9 +76,7 @@ const AnswerText = ({ activeSlide, competitionId }: AnswerTextProps) => { <ListItem style={{ height: '100%' }}> <TextField disabled={team === undefined} - defaultValue={ - team?.question_answers.find((questionAnswer) => questionAnswer.id === answerId)?.answer || 'Svar...' - } + defaultValue={getDefaultString()} style={{ height: '100%' }} variant="outlined" fullWidth={true} diff --git a/client/src/pages/views/components/JudgeScoreDisplay.tsx b/client/src/pages/views/components/JudgeScoreDisplay.tsx index 0b5e584574e08678f67159401438c76e413851fe..332d4ac78cb8eb931b7ab3f16e9b943dc7ed09c6 100644 --- a/client/src/pages/views/components/JudgeScoreDisplay.tsx +++ b/client/src/pages/views/components/JudgeScoreDisplay.tsx @@ -13,22 +13,53 @@ type ScoreDisplayProps = { const JudgeScoreDisplay = ({ teamIndex, activeSlide }: ScoreDisplayProps) => { const dispatch = useAppDispatch() + const questionTypes = useAppSelector((state) => state.types.questionTypes) const currentTeam = useAppSelector((state) => state.presentation.competition.teams[teamIndex]) const currentCompetititonId = useAppSelector((state) => state.presentation.competition.id) + 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 activeAnswer = currentTeam.question_answers.find( - (questionAnswer) => questionAnswer.question_id === activeQuestion?.id - ) - const handleEditScore = async (newScore: number, answerId: number) => { + + const scores = currentTeam.question_scores.map((questionAnswer) => questionAnswer.score) + const textQuestionType = questionTypes.find((questionType) => questionType.name === 'Text')?.id || 0 + const handleEditScore = async (newScore: number, questionId: number) => { await axios - .put(`/api/competitions/${currentCompetititonId}/teams/${currentTeam.id}/answers/${answerId}`, { + .put(`/api/competitions/${currentCompetititonId}/teams/${currentTeam.id}/answers/question_scores/${questionId}`, { score: newScore, }) .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 ( <ScoreDisplayContainer> <ScoreDisplayHeader> @@ -36,25 +67,30 @@ const JudgeScoreDisplay = ({ teamIndex, activeSlide }: ScoreDisplayProps) => { <Box fontWeight="fontWeightBold">{currentTeam.name}</Box> </Typography> - {activeAnswer && ( + {activeQuestion && ( <ScoreInput label="Poäng" - defaultValue={activeAnswer?.score} + defaultValue={0} + value={activeScore ? activeScore.score : 0} inputProps={{ style: { fontSize: 20 } }} InputProps={{ disableUnderline: true, inputProps: { min: 0, max: questionMaxScore } }} type="number" - onChange={(event) => handleEditScore(+event.target.value, activeAnswer.id)} + onChange={(event) => handleEditScore(+event.target.value, activeQuestion.id)} /> )} </ScoreDisplayHeader> <Typography variant="h6">Alla poäng: [ {scores.map((score) => `${score} `)}]</Typography> <Typography variant="h6">Total poäng: {scores.reduce((a, b) => a + b, 0)}</Typography> - {activeAnswer && ( + {activeQuestion && ( <AnswerContainer> - <Typography variant="body1">{activeAnswer.answer}</Typography> + {getAlternativeAnswers().map((v, k) => ( + <Typography variant="body1" key={k}> + <span>•</span> {v} + </Typography> + ))} </AnswerContainer> )} - {!activeAnswer && <Typography variant="body1">Inget svar</Typography>} + {!activeQuestion && <Typography variant="body1">Inget svar</Typography>} </ScoreDisplayContainer> ) } diff --git a/client/src/pages/views/components/Scoreboard.tsx b/client/src/pages/views/components/Scoreboard.tsx index 7591a3cdbd27126c15a3cc66ce565a0d47528c93..4cbc0c36605716b3edc7ab3439c2cd75c4008464 100644 --- a/client/src/pages/views/components/Scoreboard.tsx +++ b/client/src/pages/views/components/Scoreboard.tsx @@ -24,8 +24,8 @@ const Scoreboard = ({ isOperator }: ScoreboardProps) => { /** Sums the scores for the teams. */ const addScore = (team: RichTeam) => { let totalScore = 0 - for (let j = 0; j < team.question_answers.length; j++) { - totalScore = totalScore + team.question_answers[j].score + for (let j = 0; j < team.question_scores.length; j++) { + totalScore = totalScore + team.question_scores[j].score } return totalScore } diff --git a/client/src/pages/views/components/styled.tsx b/client/src/pages/views/components/styled.tsx index a5142d0b8a7353212b18fad8739e9b32eedd1625..b2fa87726816179791c6361fc97cfebb5eb84eb3 100644 --- a/client/src/pages/views/components/styled.tsx +++ b/client/src/pages/views/components/styled.tsx @@ -31,6 +31,7 @@ export const ScoreInput = styled(TextField)` export const AnswerContainer = styled.div` display: flex; flex-wrap: wrap; + flex-direction: column; ` export const JudgeScoringInstructionsContainer = styled(Paper)` diff --git a/server/app/apis/answers.py b/server/app/apis/answers.py index c1ccd68b94d8d86faa0c214b5248670db260a73d..853b97a0186f0235f69167a878b0a19f1f628ae8 100644 --- a/server/app/apis/answers.py +++ b/server/app/apis/answers.py @@ -5,64 +5,98 @@ Default route: /api/competitions/<competition_id>/teams/<team_id>/answers import app.database.controller as dbc 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 flask_restx import Resource, reqparse -api = QuestionAnswerDTO.api -schema = QuestionAnswerDTO.schema -list_schema = QuestionAnswerDTO.list_schema +api = QuestionAlternativeAnswerDTO.api +schema = QuestionAlternativeAnswerDTO.schema +list_schema = QuestionAlternativeAnswerDTO.list_schema + +score_schema = QuestionScoreDTO.schema +score_list_schema = QuestionScoreDTO.list_schema + answer_parser_add = reqparse.RequestParser() 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.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") -class QuestionAnswerList(Resource): +class QuestionAlternativeList(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_answer_list(competition_id, team_id) + items = dbc.get.question_alternative_answer_list(competition_id, team_id) 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=["*"]) - def post(self, competition_id, team_id): - """ - Posts a new question answer to the specified - question using the provided arguments. - """ - - args = answer_parser_add.parse_args(strict=True) - item = dbc.add.question_answer(**args, team_id=team_id) - return item_response(schema.dump(item)) + def get(self, competition_id, team_id, question_id): + """ Gets the specified question answer. """ + + item = dbc.get.question_score(competition_id, team_id, question_id) + return item_response(score_schema.dump(item)) + + @protect_route(allowed_roles=["*"], allowed_views=["*"]) + def put(self, competition_id, team_id, question_id): + """ 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.param("competition_id, team_id, answer_id") -class QuestionAnswers(Resource): +@api.route("/question_alternatives/<question_alternative_id>") +@api.param("competition_id, team_id, question_alternative_id") +class QuestionAlternativeAnswers(Resource): @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. """ - 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)) @protect_route(allowed_roles=["*"], allowed_views=["*"]) - def put(self, competition_id, team_id, answer_id): - """ Edits the specified question answer with the provided arguments. """ + def put(self, competition_id, team_id, question_alternative_id): + """ Add or edit specified quesiton_answer. """ - args = answer_parser_edit.parse_args(strict=True) - item = dbc.get.question_answer(competition_id, team_id, answer_id) - item = dbc.edit.default(item, **args) - return item_response(schema.dump(item)) + item = dbc.get.question_alternative_answer(competition_id, team_id, question_alternative_id, required=False) + if item is None: + args = answer_parser_add.parse_args(strict=True) + 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 - # together with the question or the team. + return item_response(schema.dump(item)) diff --git a/server/app/core/dto.py b/server/app/core/dto.py index 6bbbdf621f52855bf6565fa162007cd3956bbd85..e4e0bf0d34feb93e93597460c19a422501b4dba4 100644 --- a/server/app/core/dto.py +++ b/server/app/core/dto.py @@ -81,7 +81,13 @@ class QuestionAlternativeDTO: list_schema = schemas.QuestionAlternativeSchema(many=True) -class QuestionAnswerDTO: +class QuestionAlternativeAnswerDTO: api = Namespace("answers") - schema = schemas.QuestionAnswerSchema(many=False) - list_schema = schemas.QuestionAnswerSchema(many=True) + schema = schemas.QuestionAlternativeAnswerSchema(many=False) + list_schema = schemas.QuestionAlternativeAnswerSchema(many=True) + + +class QuestionScoreDTO: + api = Namespace("answers") + schema = schemas.QuestionScoreSchema(many=False) + list_schema = schemas.QuestionScoreSchema(many=True) diff --git a/server/app/core/rich_schemas.py b/server/app/core/rich_schemas.py index 60f7ac309d37b3be15c0378a94a71dc10cdf7cc8..9696f68e29d8ed091cd1b460bba16f8eca6a9729 100644 --- a/server/app/core/rich_schemas.py +++ b/server/app/core/rich_schemas.py @@ -36,7 +36,8 @@ class TeamSchemaRich(RichSchema): id = ma.auto_field() name = 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): diff --git a/server/app/core/schemas.py b/server/app/core/schemas.py index d9331ae713991c1ecd652f5752532261275ab310..10c1dfc1c697c81e15b25085133cdabb4151cd59 100644 --- a/server/app/core/schemas.py +++ b/server/app/core/schemas.py @@ -64,12 +64,21 @@ class QuestionSchema(BaseSchema): correcting_instructions = ma.auto_field() -class QuestionAnswerSchema(BaseSchema): +class QuestionAlternativeAnswerSchema(BaseSchema): class Meta(BaseSchema.Meta): - model = models.QuestionAnswer + model = models.QuestionAlternativeAnswer id = 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() question_id = ma.auto_field() team_id = ma.auto_field() @@ -169,4 +178,4 @@ class ComponentSchema(BaseSchema): text = fields.fields.String() media = fields.Nested(MediaSchema, many=False) - question_id = fields.fields.Integer() \ No newline at end of file + question_id = fields.fields.Integer() diff --git a/server/app/database/controller/add.py b/server/app/database/controller/add.py index 9ba36dc911afb6846862f66160c88ccdf0934889..3d2ec4e6bbd6ebf9aa2e8ded7ea3b03ee2ac421a 100644 --- a/server/app/database/controller/add.py +++ b/server/app/database/controller/add.py @@ -18,8 +18,9 @@ from app.database.models import ( MediaType, Question, QuestionAlternative, - QuestionAnswer, + QuestionAlternativeAnswer, QuestionComponent, + QuestionScore, QuestionType, Role, Slide, @@ -284,10 +285,19 @@ def question_alternative(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 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)) diff --git a/server/app/database/controller/delete.py b/server/app/database/controller/delete.py index 51070ed9a44693d2f17acde08f0219448721f906..d3f017bade09890b9820f80fd0ba8300a8fe317e 100644 --- a/server/app/database/controller/delete.py +++ b/server/app/database/controller/delete.py @@ -5,7 +5,7 @@ This file contains functionality to delete data to the database. import app.core.http_codes as codes import app.database.controller as dbc 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 sqlalchemy.exc import IntegrityError @@ -79,8 +79,8 @@ def slide(item_slide): def team(item_team): """ Deletes team, its question answers and the code. """ - for item_question_answer in item_team.question_answers: - question_answers(item_question_answer) + for item_question_answer in item_team.question_alternative_answers: + default(item_question_answer) for item_code in item_team.code: code(item_code) @@ -90,8 +90,11 @@ def team(item_team): def question(item_question): """ Deletes question and its alternatives and answers. """ - for item_question_answer in item_question.question_answers: - question_answers(item_question_answer) + scores = QuestionScore.query.filter(QuestionScore.question_id == item_question.id).all() + + for item_question_score in scores: + default(item_question_score) + for item_alternative in item_question.alternatives: alternatives(item_alternative) @@ -100,16 +103,15 @@ def question(item_question): def alternatives(item_alternatives): """ 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) -def question_answers(item_question_answers): - """ Deletes question answer. """ - - default(item_question_answers) - - def competition(item_competition): """ Deletes competition, its slides, teams and codes. """ diff --git a/server/app/database/controller/get.py b/server/app/database/controller/get.py index abdb9e15d1d815d80a48d7e80256e6595c7d7620..ac59cd231eca130375b0990513d91beb65f2f904 100644 --- a/server/app/database/controller/get.py +++ b/server/app/database/controller/get.py @@ -11,12 +11,14 @@ from app.database.models import ( ImageComponent, Question, QuestionAlternative, - QuestionAnswer, + QuestionAlternativeAnswer, + QuestionScore, Slide, Team, TextComponent, User, ) +from sqlalchemy import func from sqlalchemy.orm import joinedload from sqlalchemy.orm.util import with_polymorphic @@ -208,28 +210,68 @@ def question_alternative_list(competition_id, slide_id, question_id): ### 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. """ join_competition = Competition.id == Team.competition_id - join_team = Team.id == QuestionAnswer.team_id - filters = (Competition.id == competition_id) & (Team.id == team_id) & (QuestionAnswer.id == answer_id) + join_team = Team.id == QuestionAlternativeAnswer.team_id + filters = ( + (Competition.id == competition_id) + & (Team.id == team_id) + & (QuestionAlternativeAnswer.question_alternative_id == question_alternative_id) + ) 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. """ 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) - 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 ### @@ -271,5 +313,13 @@ def competition(competition_id): os1 = joinedload(Competition.slides).joinedload(Slide.components) os2 = joinedload(Competition.slides).joinedload(Slide.questions).joinedload(Question.alternatives) - ot = joinedload(Competition.teams).joinedload(Team.question_answers) - return Competition.query.filter(Competition.id == competition_id).options(os1).options(os2).options(ot).first() + ot = joinedload(Competition.teams).joinedload(Team.question_alternative_answers) + 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() + ) diff --git a/server/app/database/models.py b/server/app/database/models.py index bc13c094cc3b6a75e0935005a81de991068d6856..45ae3797154b0cb706dd2716fe9bcac7928a46f3 100644 --- a/server/app/database/models.py +++ b/server/app/database/models.py @@ -5,8 +5,7 @@ each other. """ from app.core import bcrypt, db -from app.database.types import (ID_IMAGE_COMPONENT, ID_QUESTION_COMPONENT, - ID_TEXT_COMPONENT) +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 @@ -133,7 +132,9 @@ class Team(db.Model): name = db.Column(db.String(STRING_SIZE), 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") def __init__(self, name, competition_id): @@ -170,7 +171,6 @@ class Question(db.Model): slide_id = db.Column(db.Integer, db.ForeignKey("slide.id"), nullable=False) correcting_instructions = db.Column(db.Text, nullable=True, default=None) - question_answers = db.relationship("QuestionAnswer", backref="question") alternatives = db.relationship("QuestionAlternative", backref="question") def __init__(self, name, total_score, type_id, slide_id, correcting_instructions): @@ -193,22 +193,34 @@ class QuestionAlternative(db.Model): self.question_id = question_id -class QuestionAnswer(db.Model): +class QuestionScore(db.Model): __table_args__ = (db.UniqueConstraint("question_id", "team_id"),) id = db.Column(db.Integer, primary_key=True) - answer = db.Column(db.String(STRING_SIZE), nullable=False) - score = db.Column(db.Integer, nullable=False, default=0) + score = db.Column(db.Integer, nullable=True, 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) - def __init__(self, answer, score, question_id, team_id): - self.answer = answer + def __init__(self, score, question_id, team_id): self.score = score self.question_id = question_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): id = db.Column(db.Integer, primary_key=True) x = db.Column(db.Integer, nullable=False, default=0) diff --git a/server/populate.py b/server/populate.py index 9981f0c02f430d56c88738a9b67e89d5fc17af89..ef6f6b431bdbfec67a03216fc65f8ab09b54bbca 100644 --- a/server/populate.py +++ b/server/populate.py @@ -106,12 +106,12 @@ def _add_items(): dbc.add.team(f"{name}{i}", item_comp.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 10p", 10, 2, 1) - dbc.add.question_answer("ett svar som ger 6p", 6, 3, 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 6p", 6, 3, 1) - 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 2p", 2, 1, 2) + # dbc.add.question_answer("ett svar som ger 3p", 3, 1, 3) if __name__ == "__main__": diff --git a/server/tests/test_app.py b/server/tests/test_app.py index 4b704bd7de51ebb624b8979d45ed4d17fe92032d..6439c465deb3b8662df68f457d967d3bfec63759 100644 --- a/server/tests/test_app.py +++ b/server/tests/test_app.py @@ -471,11 +471,15 @@ def test_authorization(client): assert response.status_code == codes.UNAUTHORIZED # 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 # 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 #### JUDGE #### @@ -499,9 +503,13 @@ def test_authorization(client): assert response.status_code == codes.UNAUTHORIZED # 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 # 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 diff --git a/server/tests/test_helpers.py b/server/tests/test_helpers.py index c3a73754c70aa492c6211ccba158a0cf209efab9..47fa647dcac0098dd5ef4e09313a834d1ca58c16 100644 --- a/server/tests/test_helpers.py +++ b/server/tests/test_helpers.py @@ -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("222222", 2, item_competition.id)) # Judge - dbc.add.question_answer("hej", 5, item_question.id, item_team1.id) - dbc.add.question_answer("dÃ¥", 5, item_question.id, item_team2.id) + # dbc.add.question_answer("hej", 5, item_question.id, item_team1.id) + # dbc.add.question_answer("dÃ¥", 5, item_question.id, item_team2.id) db.session.commit()