From bd5dacbffb6b02f5d223c7b0c7ec7cd9b5483a55 Mon Sep 17 00:00:00 2001 From: heikki Date: Wed, 19 May 2021 17:09:49 +0300 Subject: [PATCH 1/5] added tests --- ...19094353_multiple-choice-grading-policy.ts | 25 ++ .../seeds/multipleChoiceGradingPolicy.ts | 152 ++++++++ packages/backendv2/knexfile.ts | 1 + packages/backendv2/package-lock.json | 12 +- .../backendv2/src/controllers/widget/index.ts | 3 +- packages/backendv2/src/models/course.ts | 5 +- .../src/models/course_translation.ts | 3 + .../backendv2/src/models/kafka_message.ts | 3 + packages/backendv2/src/models/language.ts | 3 + packages/backendv2/src/models/peer_review.ts | 12 +- .../src/models/peer_review_collection.ts | 12 +- .../peer_review_collection_translation.ts | 5 + .../src/models/peer_review_question.ts | 13 +- .../src/models/peer_review_question_answer.ts | 7 +- .../peer_review_question_translation.ts | 4 + packages/backendv2/src/models/quiz.ts | 14 +- packages/backendv2/src/models/quiz_answer.ts | 15 +- .../models/quiz_answer_status_modification.ts | 5 + packages/backendv2/src/models/quiz_item.ts | 21 +- .../backendv2/src/models/quiz_item_answer.ts | 9 +- .../src/models/quiz_item_translation.ts | 3 + packages/backendv2/src/models/quiz_option.ts | 8 +- .../src/models/quiz_option_answer.ts | 3 + .../src/models/quiz_option_translation.ts | 3 + .../backendv2/src/models/quiz_translation.ts | 3 + packages/backendv2/src/models/spam_flag.ts | 3 + packages/backendv2/src/models/user.ts | 6 +- .../src/models/user_course_part_state.ts | 3 + .../backendv2/src/models/user_course_role.ts | 3 + .../backendv2/src/models/user_quiz_state.ts | 3 + ...emMultipleChoiceGradingPolicyTests.test.ts | 327 ++++++++++++++++++ 31 files changed, 646 insertions(+), 43 deletions(-) create mode 100644 packages/backendv2/database/migrations/20210519094353_multiple-choice-grading-policy.ts create mode 100644 packages/backendv2/database/seeds/multipleChoiceGradingPolicy.ts create mode 100644 packages/backendv2/tests/quizItemMultipleChoiceGradingPolicyTests.test.ts diff --git a/packages/backendv2/database/migrations/20210519094353_multiple-choice-grading-policy.ts b/packages/backendv2/database/migrations/20210519094353_multiple-choice-grading-policy.ts new file mode 100644 index 000000000..8f5f97ea9 --- /dev/null +++ b/packages/backendv2/database/migrations/20210519094353_multiple-choice-grading-policy.ts @@ -0,0 +1,25 @@ +import * as Knex from "knex" + +export async function up(knex: Knex): Promise { + await knex.schema.table("quiz_item", table => { + table + .enu( + "multiple_choice_grading_policy", + ["NeedToSelectAllCorrectOptions", "NeedToSelectNCorrectOptions"], + { + useNative: true, + enumName: "multiple_selected_options_grading_policy_enum", + }, + ) + .defaultTo("NeedToSelectAllCorrectOptions") + + table.integer("multiple_selected_options_grading_policy_n").defaultTo(0) + }) +} + +export async function down(knex: Knex): Promise { + await knex.schema.table("quiz_item", table => { + table.dropColumn("multiple_choice_grading_policy") + table.dropColumn("multiple_selected_options_grading_policy_n") + }) +} diff --git a/packages/backendv2/database/seeds/multipleChoiceGradingPolicy.ts b/packages/backendv2/database/seeds/multipleChoiceGradingPolicy.ts new file mode 100644 index 000000000..e907ad9df --- /dev/null +++ b/packages/backendv2/database/seeds/multipleChoiceGradingPolicy.ts @@ -0,0 +1,152 @@ +import * as Knex from "knex" + +export async function seed(knex: Knex): Promise { + await knex("language").insert([ + { + id: "xy_YZ", + country: "country", + name: "language", + }, + ]) + + await knex("course").insert([ + { + id: "cd02cb32-d8d9-407f-b95f-4357864bee7a", + min_score_to_pass: null, + min_progress_to_pass: null, + min_peer_reviews_received: 2, + min_peer_reviews_given: 3, + min_review_average: 2, + max_spam_flags: 1, + organization_id: null, + moocfi_id: "f3e59b24-ff79-4f67-a9aa-21c94977240a", + max_review_spam_flags: 3, + }, + ]) + + await knex("course_translation").insert([ + { + course_id: "cd02cb32-d8d9-407f-b95f-4357864bee7a", + language_id: "xy_YZ", + title: "course 1", + body: "course", + abbreviation: "course", + }, + ]) + + await knex("quiz").insert([ + { + id: "d7389c86-7a3a-4593-b810-b2be35319520", + course_id: "cd02cb32-d8d9-407f-b95f-4357864bee7a", + part: 1, + section: 1, + points: 1, + deadline: null, + open: null, + excluded_from_score: false, + auto_confirm: true, + tries: 1, + tries_limited: true, + award_points_even_if_wrong: false, + grant_points_policy: "grant_whenever_possible", + auto_reject: true, + }, + ]) + + await knex("quiz_translation").insert([ + { + quiz_id: "d7389c86-7a3a-4593-b810-b2be35319520", + language_id: "xy_YZ", + title: "quiz 1", + body: "body", + submit_message: "nice one!", + }, + ]) + + await knex("quiz_item").insert([ + { + id: "40f7a704-fbbe-4411-8176-31449a5968fe", + quiz_id: "d7389c86-7a3a-4593-b810-b2be35319520", + type: "multiple-choice", + order: 1, + uses_shared_option_feedback_message: false, + multiple_choice_grading_policy: "NeedToSelectAllCorrectOptions", + multi: true, + }, + { + id: "ae7cd864-236d-4294-a6d8-57cf8ab71383", + quiz_id: "d7389c86-7a3a-4593-b810-b2be35319520", + type: "multiple-choice", + order: 2, + uses_shared_option_feedback_message: false, + multiple_choice_grading_policy: "NeedToSelectNCorrectOptions", + multiple_selected_options_grading_policy_n: 2, + multi: true, + }, + ]) + + await knex("quiz_item_translation").insert([ + { + quiz_item_id: "40f7a704-fbbe-4411-8176-31449a5968fe", + language_id: "xy_YZ", + title: "multiple-choice", + body: "item", + success_message: "yay!", + failure_message: "boo!", + shared_option_feedback_message: null, + }, + ]) + + await knex("quiz_option").insert([ + { + id: "6b906647-7956-44b5-ac0f-77157c6c8a74", + quiz_item_id: "40f7a704-fbbe-4411-8176-31449a5968fe", + order: 1, + correct: false, + }, + { + id: "12a56d49-1f21-46ba-bcf7-5e90da61ecd1", + quiz_item_id: "40f7a704-fbbe-4411-8176-31449a5968fe", + order: 2, + correct: false, + }, + { + id: "08e648dd-176c-4ba9-a1e8-44231aef221f", + quiz_item_id: "40f7a704-fbbe-4411-8176-31449a5968fe", + order: 3, + correct: true, + }, + { + id: "53c77121-daf7-485e-805d-78550b6e435d", + quiz_item_id: "40f7a704-fbbe-4411-8176-31449a5968fe", + order: 4, + correct: true, + }, + { + id: "0862ddd4-e9d2-485b-b605-03cdfc94bcc4", + quiz_item_id: "ae7cd864-236d-4294-a6d8-57cf8ab71383", + order: 1, + correct: false, + }, + { + id: "853e536a-a374-4e80-ba81-23c75f475529", + quiz_item_id: "ae7cd864-236d-4294-a6d8-57cf8ab71383", + order: 2, + correct: false, + }, + { + id: "43142a08-2fd6-4356-b5cc-1a4b2d9ea085", + quiz_item_id: "ae7cd864-236d-4294-a6d8-57cf8ab71383", + order: 3, + correct: true, + }, + { + id: "2f5ad9fd-59c2-4909-bf85-42931149e47c", + quiz_item_id: "ae7cd864-236d-4294-a6d8-57cf8ab71383", + order: 4, + correct: true, + }, + ]) + + await knex("user").insert([{ id: 1234 }]) +} diff --git a/packages/backendv2/knexfile.ts b/packages/backendv2/knexfile.ts index c96e3ec66..607631c7e 100644 --- a/packages/backendv2/knexfile.ts +++ b/packages/backendv2/knexfile.ts @@ -8,6 +8,7 @@ types.setTypeParser(types.builtins.INT8, value => parseInt(value)) const configOptions: { [env: string]: Config } = { development: { + debug: true, client: "pg", connection: { host: env.DB_HOST || "/var/run/postgresql", diff --git a/packages/backendv2/package-lock.json b/packages/backendv2/package-lock.json index 3302f5ebe..70d24844b 100644 --- a/packages/backendv2/package-lock.json +++ b/packages/backendv2/package-lock.json @@ -5502,6 +5502,11 @@ "minimist": "^1.2.5" } }, + "jsonparse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.0.0.tgz", + "integrity": "sha1-JiL05mwI4arH7b63YFPJt+EhH3Y=" + }, "jsonstream": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/jsonstream/-/jsonstream-1.0.3.tgz", @@ -5509,13 +5514,6 @@ "requires": { "jsonparse": "~1.0.0", "through": ">=2.2.7 <3" - }, - "dependencies": { - "jsonparse": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", - "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=" - } } }, "jsprim": { diff --git a/packages/backendv2/src/controllers/widget/index.ts b/packages/backendv2/src/controllers/widget/index.ts index 02fdfa235..d2d2d54e9 100644 --- a/packages/backendv2/src/controllers/widget/index.ts +++ b/packages/backendv2/src/controllers/widget/index.ts @@ -82,7 +82,8 @@ const widget = new Router({ .post("/answer", accessControl(), async ctx => { const user = ctx.state.user const answer = ctx.request.body - ctx.body = await QuizAnswer.newAnswer(user.id, QuizAnswer.fromJson(answer)) + const newQuizAnswer = QuizAnswer.fromJson(answer) + ctx.body = await QuizAnswer.newAnswer(user.id, newQuizAnswer) }) .post("/answers/report-spam", accessControl(), async ctx => { diff --git a/packages/backendv2/src/models/course.ts b/packages/backendv2/src/models/course.ts index 79643e03f..ce09e1c6d 100644 --- a/packages/backendv2/src/models/course.ts +++ b/packages/backendv2/src/models/course.ts @@ -4,9 +4,10 @@ import { BadRequestError, NotFoundError } from "./../util/error" import stringify from "csv-stringify" import Quiz from "./quiz" import Language from "./language" -import CourseTranslation from "./course_translation" +import CourseTranslation, { CourseTranslationType } from "./course_translation" import knex from "../../database/knex" import BaseModel from "./base_model" +import { ModelObject } from "objection" class Course extends BaseModel { id!: string @@ -407,4 +408,6 @@ class Course extends BaseModel { } } +export type CourseType = ModelObject + export default Course diff --git a/packages/backendv2/src/models/course_translation.ts b/packages/backendv2/src/models/course_translation.ts index 43c0d9433..d75441b1f 100644 --- a/packages/backendv2/src/models/course_translation.ts +++ b/packages/backendv2/src/models/course_translation.ts @@ -1,3 +1,4 @@ +import { ModelObject } from "objection" import { EditCoursePayloadFields } from "./../types/index" import BaseModel from "./base_model" import Course from "./course" @@ -47,4 +48,6 @@ class CourseTranslation extends BaseModel { } } +export type CourseTranslationType = ModelObject + export default CourseTranslation diff --git a/packages/backendv2/src/models/kafka_message.ts b/packages/backendv2/src/models/kafka_message.ts index 74a754e69..f7ec5e192 100644 --- a/packages/backendv2/src/models/kafka_message.ts +++ b/packages/backendv2/src/models/kafka_message.ts @@ -1,4 +1,5 @@ import Knex from "knex" +import { ModelObject } from "objection" import { ProgressMessage, QuizAnswerMessage, QuizMessage } from "../types" import BaseModel from "./base_model" @@ -45,4 +46,6 @@ class KafkaMessage extends BaseModel { } } +export type KafkaMessageType = ModelObject + export default KafkaMessage diff --git a/packages/backendv2/src/models/language.ts b/packages/backendv2/src/models/language.ts index 7a60b4bee..dd928eb4c 100644 --- a/packages/backendv2/src/models/language.ts +++ b/packages/backendv2/src/models/language.ts @@ -1,3 +1,4 @@ +import { ModelObject } from "objection" import BaseModel from "./base_model" class Language extends BaseModel { @@ -11,4 +12,6 @@ class Language extends BaseModel { } } +export type LanguageType = ModelObject + export default Language diff --git a/packages/backendv2/src/models/peer_review.ts b/packages/backendv2/src/models/peer_review.ts index dc5f38a2a..785c3674b 100644 --- a/packages/backendv2/src/models/peer_review.ts +++ b/packages/backendv2/src/models/peer_review.ts @@ -2,12 +2,16 @@ import { BadRequestError, NotFoundError } from "./../util/error" import QuizAnswer from "./quiz_answer" import UserQuizState from "./user_quiz_state" import Quiz from "./quiz" -import PeerReviewQuestionAnswer from "./peer_review_question_answer" -import PeerReviewQuestion from "./peer_review_question" +import PeerReviewQuestionAnswer, { + PeerReviewQuestionAnswerType, +} from "./peer_review_question_answer" +import PeerReviewQuestion, { + PeerReviewQuestionType, +} from "./peer_review_question" import knex from "../../database/knex" import BaseModel from "./base_model" import softDelete from "objection-soft-delete" -import { mixin } from "objection" +import { mixin, ModelObject } from "objection" import { Transaction } from "knex" class PeerReview extends BaseModel { @@ -182,4 +186,6 @@ class PeerReview extends BaseModel { } } +export type PeerReviewType = ModelObject + export default PeerReview diff --git a/packages/backendv2/src/models/peer_review_collection.ts b/packages/backendv2/src/models/peer_review_collection.ts index 354eb3106..b44a90562 100644 --- a/packages/backendv2/src/models/peer_review_collection.ts +++ b/packages/backendv2/src/models/peer_review_collection.ts @@ -1,8 +1,12 @@ import Quiz from "./quiz_item" -import PeerReviewCollectionTranslation from "./peer_review_collection_translation" -import PeerReviewQuestion from "./peer_review_question" +import PeerReviewCollectionTranslation, { + PeerReviewCollectionTranslationType, +} from "./peer_review_collection_translation" +import PeerReviewQuestion, { + PeerReviewQuestionType, +} from "./peer_review_question" import BaseModel from "./base_model" -import { mixin } from "objection" +import { mixin, ModelObject } from "objection" import softDelete from "objection-soft-delete" export class PeerReviewCollection extends mixin(BaseModel, [ @@ -51,4 +55,6 @@ export class PeerReviewCollection extends mixin(BaseModel, [ } } +export type PeerReviewCollectionType = ModelObject + export default PeerReviewCollection diff --git a/packages/backendv2/src/models/peer_review_collection_translation.ts b/packages/backendv2/src/models/peer_review_collection_translation.ts index 102441912..c6f67aa37 100644 --- a/packages/backendv2/src/models/peer_review_collection_translation.ts +++ b/packages/backendv2/src/models/peer_review_collection_translation.ts @@ -1,3 +1,4 @@ +import { ModelObject } from "objection" import BaseModel from "./base_model" import PeerReviewCollection from "./peer_review_collection" @@ -25,4 +26,8 @@ class PeerReviewCollectionTranslation extends BaseModel { } } +export type PeerReviewCollectionTranslationType = ModelObject< + PeerReviewCollectionTranslation +> + export default PeerReviewCollectionTranslation diff --git a/packages/backendv2/src/models/peer_review_question.ts b/packages/backendv2/src/models/peer_review_question.ts index 7261907bf..b96659c13 100644 --- a/packages/backendv2/src/models/peer_review_question.ts +++ b/packages/backendv2/src/models/peer_review_question.ts @@ -1,7 +1,11 @@ import BaseModel from "./base_model" -import PeerReviewCollection from "./peer_review_collection" -import PeerReviewQuestionTranslation from "./peer_review_question_translation" -import { mixin } from "objection" +import PeerReviewCollection, { + PeerReviewCollectionType, +} from "./peer_review_collection" +import PeerReviewQuestionTranslation, { + PeerReviewQuestionTranslationType, +} from "./peer_review_question_translation" +import { mixin, ModelObject } from "objection" import softDelete from "objection-soft-delete" class PeerReviewQuestion extends mixin(BaseModel, [ @@ -41,4 +45,7 @@ class PeerReviewQuestion extends mixin(BaseModel, [ return await this.query().findById(id) } } + +export type PeerReviewQuestionType = ModelObject + export default PeerReviewQuestion diff --git a/packages/backendv2/src/models/peer_review_question_answer.ts b/packages/backendv2/src/models/peer_review_question_answer.ts index 694581dd6..bfacb8194 100644 --- a/packages/backendv2/src/models/peer_review_question_answer.ts +++ b/packages/backendv2/src/models/peer_review_question_answer.ts @@ -1,6 +1,9 @@ +import { ModelObject } from "objection" import BaseModel from "./base_model" import PeerReview from "./peer_review" -import PeerReviewQuestion from "./peer_review_question" +import PeerReviewQuestion, { + PeerReviewQuestionType, +} from "./peer_review_question" class PeerReviewQuestionAnswer extends BaseModel { value!: number @@ -34,4 +37,6 @@ class PeerReviewQuestionAnswer extends BaseModel { } } +export type PeerReviewQuestionAnswerType = ModelObject + export default PeerReviewQuestionAnswer diff --git a/packages/backendv2/src/models/peer_review_question_translation.ts b/packages/backendv2/src/models/peer_review_question_translation.ts index ab4bb643c..867ef8ebd 100644 --- a/packages/backendv2/src/models/peer_review_question_translation.ts +++ b/packages/backendv2/src/models/peer_review_question_translation.ts @@ -1,3 +1,4 @@ +import { ModelObject } from "objection" import BaseModel from "./base_model" import PeerReviewQuestion from "./peer_review_question" @@ -29,4 +30,7 @@ class PeerReviewQuestionTranslation extends BaseModel { } } +export type PeerReviewQuestionTranslationType = ModelObject< + PeerReviewQuestionTranslation +> export default PeerReviewQuestionTranslation diff --git a/packages/backendv2/src/models/quiz.ts b/packages/backendv2/src/models/quiz.ts index df8c0746a..c90d0bc94 100644 --- a/packages/backendv2/src/models/quiz.ts +++ b/packages/backendv2/src/models/quiz.ts @@ -1,7 +1,9 @@ -import QuizItem from "./quiz_item" -import QuizTranslation from "./quiz_translation" -import PeerReviewCollection from "./peer_review_collection" -import Course from "./course" +import QuizItem, { QuizItemType } from "./quiz_item" +import QuizTranslation, { QuizTranslationType } from "./quiz_translation" +import PeerReviewCollection, { + PeerReviewCollectionType, +} from "./peer_review_collection" +import Course, { CourseType } from "./course" import { NotFoundError } from "../util/error" import moduleInitializer from "../util/initializer" import stringify from "csv-stringify" @@ -11,7 +13,7 @@ import Knex from "knex" import UserQuizState from "./user_quiz_state" import * as Kafka from "../services/kafka" import PeerReviewQuestion from "./peer_review_question" -import { NotNullViolationError } from "objection" +import { ModelObject, NotNullViolationError } from "objection" import BaseModel from "./base_model" import knex from "../../database/knex" @@ -398,4 +400,6 @@ export class Quiz extends BaseModel { } } +export type QuizType = ModelObject + export default Quiz diff --git a/packages/backendv2/src/models/quiz_answer.ts b/packages/backendv2/src/models/quiz_answer.ts index 110bf2a48..d33ac5ce2 100644 --- a/packages/backendv2/src/models/quiz_answer.ts +++ b/packages/backendv2/src/models/quiz_answer.ts @@ -1,10 +1,10 @@ import Knex from "knex" import { NotFoundError } from "./../util/error" -import QuizItemAnswer from "./quiz_item_answer" -import User from "./user" -import PeerReview from "./peer_review" -import Quiz from "./quiz" -import UserQuizState from "./user_quiz_state" +import QuizItemAnswer, { QuizItemAnswerType } from "./quiz_item_answer" +import User, { UserType } from "./user" +import PeerReview, { PeerReviewType } from "./peer_review" +import Quiz, { QuizType } from "./quiz" +import UserQuizState, { UserQuizStateType } from "./user_quiz_state" import { BadRequestError } from "../util/error" import { removeNonPrintingCharacters } from "../util/tools" import knex from "../../database/knex" @@ -14,7 +14,7 @@ import UserCoursePartState from "./user_course_part_state" import * as Kafka from "../services/kafka" import SpamFlag from "./spam_flag" import _, { cond } from "lodash" -import Objection, { raw } from "objection" +import Objection, { ModelObject, raw } from "objection" import BaseModel from "./base_model" import QuizOptionAnswer from "./quiz_option_answer" import softDelete from "objection-soft-delete" @@ -647,7 +647,6 @@ class QuizAnswer extends mixin(BaseModel, [ quizItemAnswer.correct = false continue } - if (quizItem.allAnswersCorrect) { quizItemAnswer.correct = true continue @@ -1182,4 +1181,6 @@ class QuizAnswer extends mixin(BaseModel, [ } } +export type QuizAnswerType = ModelObject + export default QuizAnswer diff --git a/packages/backendv2/src/models/quiz_answer_status_modification.ts b/packages/backendv2/src/models/quiz_answer_status_modification.ts index 7b7626486..9e7864210 100644 --- a/packages/backendv2/src/models/quiz_answer_status_modification.ts +++ b/packages/backendv2/src/models/quiz_answer_status_modification.ts @@ -1,4 +1,5 @@ import Knex from "knex" +import { ModelObject } from "objection" import { TStatusModificationOperation } from "./../types/index" import BaseModel from "./base_model" import User from "./user" @@ -76,4 +77,8 @@ class QuizAnswerStatusModification extends BaseModel { } } +export type QuizAnswerStatusModificationType = ModelObject< + QuizAnswerStatusModification +> + export default QuizAnswerStatusModification diff --git a/packages/backendv2/src/models/quiz_item.ts b/packages/backendv2/src/models/quiz_item.ts index 866fa1050..a484292bc 100644 --- a/packages/backendv2/src/models/quiz_item.ts +++ b/packages/backendv2/src/models/quiz_item.ts @@ -1,11 +1,13 @@ import Quiz from "./quiz" -import QuizOption from "./quiz_option" -import QuizItemTranslation from "./quiz_item_translation" +import QuizOption, { QuizOptionType } from "./quiz_option" +import QuizItemTranslation, { + QuizItemTranslationType, +} from "./quiz_item_translation" import BaseModel from "./base_model" -import { mixin } from "objection" +import { mixin, ModelObject } from "objection" import softDelete from "objection-soft-delete" -export type QuizItemType = +export type itemType = | "open" | "scale" | "essay" @@ -17,13 +19,17 @@ export type QuizItemType = | "multiple-choice-dropdown" | "clickable-multiple-choice" +export type MultipleChoiceGradingPolicy = + | "NeedToSelectAllCorrectOptions" + | "NeedToSelectNCorrectOptions" + class QuizItem extends mixin(BaseModel, [ softDelete({ columnName: "deleted" }), ]) { id!: string - type!: QuizItemType + type!: itemType validityRegex!: string - multi!: string + multi!: boolean texts!: QuizItemTranslation[] options!: QuizOption[] title!: string @@ -34,6 +40,7 @@ class QuizItem extends mixin(BaseModel, [ allAnswersCorrect!: string deleted!: boolean direction!: "row" | "column" + multipleChoiceGradingPolicy!: MultipleChoiceGradingPolicy static get tableName() { return "quiz_item" @@ -71,4 +78,6 @@ class QuizItem extends mixin(BaseModel, [ } } +export type QuizItemType = ModelObject + export default QuizItem diff --git a/packages/backendv2/src/models/quiz_item_answer.ts b/packages/backendv2/src/models/quiz_item_answer.ts index 36ac1d398..7983d5a94 100644 --- a/packages/backendv2/src/models/quiz_item_answer.ts +++ b/packages/backendv2/src/models/quiz_item_answer.ts @@ -1,7 +1,8 @@ +import { ModelObject } from "objection" import BaseModel from "./base_model" -import QuizAnswer from "./quiz_answer" -import QuizItem from "./quiz_item" -import QuizOptionAnswer from "./quiz_option_answer" +import QuizAnswer, { QuizAnswerType } from "./quiz_answer" +import QuizItem, { QuizItemType } from "./quiz_item" +import QuizOptionAnswer, { QuizOptionAnswerType } from "./quiz_option_answer" class QuizItemAnswer extends BaseModel { id!: string @@ -45,4 +46,6 @@ class QuizItemAnswer extends BaseModel { } } +export type QuizItemAnswerType = ModelObject + export default QuizItemAnswer diff --git a/packages/backendv2/src/models/quiz_item_translation.ts b/packages/backendv2/src/models/quiz_item_translation.ts index 134bb37ce..f4828d4a6 100644 --- a/packages/backendv2/src/models/quiz_item_translation.ts +++ b/packages/backendv2/src/models/quiz_item_translation.ts @@ -1,3 +1,4 @@ +import { ModelObject } from "objection" import BaseModel from "./base_model" import QuizItem from "./quiz_item" @@ -28,4 +29,6 @@ class QuizItemTranslation extends BaseModel { } } +export type QuizItemTranslationType = ModelObject + export default QuizItemTranslation diff --git a/packages/backendv2/src/models/quiz_option.ts b/packages/backendv2/src/models/quiz_option.ts index a22e80d37..b8ed5a3a3 100644 --- a/packages/backendv2/src/models/quiz_option.ts +++ b/packages/backendv2/src/models/quiz_option.ts @@ -1,7 +1,9 @@ import BaseModel from "./base_model" import QuizItem from "./quiz_item" -import QuizOptionTranslation from "./quiz_option_translation" -import { mixin } from "objection" +import QuizOptionTranslation, { + QuizOptionTranslationType, +} from "./quiz_option_translation" +import { mixin, ModelObject } from "objection" import softDelete from "objection-soft-delete" class QuizOption extends mixin(BaseModel, [ softDelete({ columnName: "deleted" }), @@ -43,4 +45,6 @@ class QuizOption extends mixin(BaseModel, [ } } +export type QuizOptionType = ModelObject + export default QuizOption diff --git a/packages/backendv2/src/models/quiz_option_answer.ts b/packages/backendv2/src/models/quiz_option_answer.ts index d8c440f78..cf904ee50 100644 --- a/packages/backendv2/src/models/quiz_option_answer.ts +++ b/packages/backendv2/src/models/quiz_option_answer.ts @@ -1,3 +1,4 @@ +import { ModelObject } from "objection" import BaseModel from "./base_model" import QuizItemAnswer from "./quiz_item_answer" @@ -20,4 +21,6 @@ class QuizOptionAnswer extends BaseModel { } } +export type QuizOptionAnswerType = ModelObject + export default QuizOptionAnswer diff --git a/packages/backendv2/src/models/quiz_option_translation.ts b/packages/backendv2/src/models/quiz_option_translation.ts index 9545d0d1f..d76974407 100644 --- a/packages/backendv2/src/models/quiz_option_translation.ts +++ b/packages/backendv2/src/models/quiz_option_translation.ts @@ -1,3 +1,4 @@ +import { ModelObject } from "objection" import BaseModel from "./base_model" import QuizOption from "./quiz_option" @@ -27,4 +28,6 @@ class QuizOptionTranslation extends BaseModel { } } +export type QuizOptionTranslationType = ModelObject + export default QuizOptionTranslation diff --git a/packages/backendv2/src/models/quiz_translation.ts b/packages/backendv2/src/models/quiz_translation.ts index d38c20677..636c10ad4 100644 --- a/packages/backendv2/src/models/quiz_translation.ts +++ b/packages/backendv2/src/models/quiz_translation.ts @@ -1,3 +1,4 @@ +import { ModelObject } from "objection" import BaseModel from "./base_model" import Quiz from "./quiz" @@ -36,4 +37,6 @@ class QuizTranslation extends BaseModel { } } +export type QuizTranslationType = ModelObject + export default QuizTranslation diff --git a/packages/backendv2/src/models/spam_flag.ts b/packages/backendv2/src/models/spam_flag.ts index e28562087..d6dc779a4 100644 --- a/packages/backendv2/src/models/spam_flag.ts +++ b/packages/backendv2/src/models/spam_flag.ts @@ -6,6 +6,7 @@ import UserQuizState from "./user_quiz_state" import knex from "../../database/knex" import BaseModel from "./base_model" import QuizAnswerStatusModification from "./quiz_answer_status_modification" +import { ModelObject } from "objection" class SpamFlag extends BaseModel { id!: string @@ -103,4 +104,6 @@ class SpamFlag extends BaseModel { } } +export type SpamFlagType = ModelObject + export default SpamFlag diff --git a/packages/backendv2/src/models/user.ts b/packages/backendv2/src/models/user.ts index fad1fe6cf..301eb95ed 100644 --- a/packages/backendv2/src/models/user.ts +++ b/packages/backendv2/src/models/user.ts @@ -1,9 +1,11 @@ import QuizAnswer from "./quiz_answer" import UserQuizState from "./user_quiz_state" -import UserCourseRole from "./user_course_role" +import UserCourseRole, { UserCourseRoleType } from "./user_course_role" import Knex from "knex" import BaseModel from "./base_model" +import { ModelObject } from "objection" + class User extends BaseModel { id!: number roles!: UserCourseRole[] @@ -47,4 +49,6 @@ class User extends BaseModel { } } +export type UserType = ModelObject + export default User diff --git a/packages/backendv2/src/models/user_course_part_state.ts b/packages/backendv2/src/models/user_course_part_state.ts index 7b15fd2e2..f926c169d 100644 --- a/packages/backendv2/src/models/user_course_part_state.ts +++ b/packages/backendv2/src/models/user_course_part_state.ts @@ -4,6 +4,7 @@ import Course from "./course" import { PointsByGroup } from "../types" import Quiz from "./quiz" import BaseModel from "./base_model" +import { ModelObject } from "objection" class UserCoursePartState extends BaseModel { userId!: number @@ -179,4 +180,6 @@ class UserCoursePartState extends BaseModel { } } +export type UserCoursePartStateType = ModelObject + export default UserCoursePartState diff --git a/packages/backendv2/src/models/user_course_role.ts b/packages/backendv2/src/models/user_course_role.ts index 538b2f536..c9f4e9a17 100644 --- a/packages/backendv2/src/models/user_course_role.ts +++ b/packages/backendv2/src/models/user_course_role.ts @@ -1,3 +1,4 @@ +import { ModelObject } from "objection" import BaseModel from "./base_model" import User from "./user" @@ -27,4 +28,6 @@ class UserCourseRole extends BaseModel { } } +export type UserCourseRoleType = ModelObject + export default UserCourseRole diff --git a/packages/backendv2/src/models/user_quiz_state.ts b/packages/backendv2/src/models/user_quiz_state.ts index 485265ff0..956bb8959 100644 --- a/packages/backendv2/src/models/user_quiz_state.ts +++ b/packages/backendv2/src/models/user_quiz_state.ts @@ -4,6 +4,7 @@ import QuizAnswer from "./quiz_answer" import Knex, { Transaction } from "knex" import BaseModel from "./base_model" import knex from "../../database/knex" +import { ModelObject } from "objection" class UserQuizState extends BaseModel { userId!: number @@ -130,4 +131,6 @@ class UserQuizState extends BaseModel { // } } +export type UserQuizStateType = ModelObject + export default UserQuizState diff --git a/packages/backendv2/tests/quizItemMultipleChoiceGradingPolicyTests.test.ts b/packages/backendv2/tests/quizItemMultipleChoiceGradingPolicyTests.test.ts new file mode 100644 index 000000000..2db3dcbcd --- /dev/null +++ b/packages/backendv2/tests/quizItemMultipleChoiceGradingPolicyTests.test.ts @@ -0,0 +1,327 @@ +import nock from "nock" +import { Model, snakeCaseMappers } from "objection" +import request from "supertest" +import app from "../app" +import redis from "../config/redis" +import knex from "../database/knex" +import { UserInfo } from "../src/types" +import { safeClean, safeSeed } from "./util" + +const allWrongQuizItem1 = { + userId: 1234, + quizId: "d7389c86-7a3a-4593-b810-b2be35319520", + itemAnswers: [ + { + quizItemId: "40f7a704-fbbe-4411-8176-31449a5968fe", + optionAnswers: [ + { + quizOptionId: "6b906647-7956-44b5-ac0f-77157c6c8a74", + }, + { + quizOptionId: "12a56d49-1f21-46ba-bcf7-5e90da61ecd1", + }, + ], + }, + ], +} + +const someCorrectQuizItem1 = { + userId: 1234, + quizId: "d7389c86-7a3a-4593-b810-b2be35319520", + itemAnswers: [ + { + quizItemId: "40f7a704-fbbe-4411-8176-31449a5968fe", + optionAnswers: [ + { + quizOptionId: "6b906647-7956-44b5-ac0f-77157c6c8a74", + }, + { + quizOptionId: "08e648dd-176c-4ba9-a1e8-44231aef221f", + }, + ], + }, + ], +} + +const allCorrectQuizItem1 = { + userId: 1234, + quizId: "d7389c86-7a3a-4593-b810-b2be35319520", + itemAnswers: [ + { + quizItemId: "40f7a704-fbbe-4411-8176-31449a5968fe", + optionAnswers: [ + { + quizOptionId: "08e648dd-176c-4ba9-a1e8-44231aef221f", + }, + { + quizOptionId: "53c77121-daf7-485e-805d-78550b6e435d", + }, + ], + }, + ], +} + +const allWrongQuizItem2 = { + userId: 1234, + quizId: "d7389c86-7a3a-4593-b810-b2be35319520", + itemAnswers: [ + { + quizItemId: "ae7cd864-236d-4294-a6d8-57cf8ab71383", + optionAnswers: [ + { + quizOptionId: "0862ddd4-e9d2-485b-b605-03cdfc94bcc4", + }, + { + quizOptionId: "853e536a-a374-4e80-ba81-23c75f475529", + }, + ], + }, + ], +} + +const someCorrectQuizItem2 = { + userId: 1234, + quizId: "d7389c86-7a3a-4593-b810-b2be35319520", + itemAnswers: [ + { + quizItemId: "ae7cd864-236d-4294-a6d8-57cf8ab71383", + optionAnswers: [ + { + quizOptionId: "853e536a-a374-4e80-ba81-23c75f475529", + }, + { + quizOptionId: "43142a08-2fd6-4356-b5cc-1a4b2d9ea085", + }, + ], + }, + ], +} + +const allCorrectQuizItem2 = { + userId: 1234, + quizId: "d7389c86-7a3a-4593-b810-b2be35319520", + itemAnswers: [ + { + quizItemId: "ae7cd864-236d-4294-a6d8-57cf8ab71383", + optionAnswers: [ + { + quizOptionId: "43142a08-2fd6-4356-b5cc-1a4b2d9ea085", + }, + { + quizOptionId: "2f5ad9fd-59c2-4909-bf85-42931149e47c", + }, + ], + }, + ], +} + +const tooManySelectedQuizItem2 = { + userId: 1234, + quizId: "d7389c86-7a3a-4593-b810-b2be35319520", + itemAnswers: [ + { + quizItemId: "ae7cd864-236d-4294-a6d8-57cf8ab71383", + optionAnswers: [ + { + quizOptionId: "43142a08-2fd6-4356-b5cc-1a4b2d9ea085", + }, + { + quizOptionId: "2f5ad9fd-59c2-4909-bf85-42931149e47c", + }, + { + quizOptionId: "853e536a-a374-4e80-ba81-23c75f475529", + }, + ], + }, + ], +} + +const tooFewSelectedQuizItem2 = { + userId: 1234, + quizId: "d7389c86-7a3a-4593-b810-b2be35319520", + itemAnswers: [ + { + quizItemId: "ae7cd864-236d-4294-a6d8-57cf8ab71383", + optionAnswers: [ + { + quizOptionId: "43142a08-2fd6-4356-b5cc-1a4b2d9ea085", + }, + ], + }, + ], +} + +describe("multiple-choice grading tests", () => { + beforeAll(() => { + Model.knex(knex) + Model.columnNameMappers = snakeCaseMappers() + }) + + afterAll(async () => { + await safeClean() + await knex.destroy() + await redis.client?.quit() + }) + + describe("NeedToSelectAllCorrectOptions correct grading", () => { + beforeEach(async () => { + await safeSeed({ + directory: "./database/seeds", + specific: "multipleChoiceGradingPolicy.ts", + }) + }) + + afterEach(async () => { + await safeClean() + }) + + beforeEach(() => { + nock("https://tmc.mooc.fi") + .get("/api/v8/users/current?show_user_fields=true") + .reply(function() { + const auth = this.req.headers.authorization + if (auth === "Bearer PLEB_TOKEN") { + return [ + 200, + { + id: 1234, + administrator: false, + } as UserInfo, + ] + } + if (auth === "Bearer ADMIN_TOKEN") { + return [ + 200, + { + administrator: true, + } as UserInfo, + ] + } + }) + }) + + it("grades correctly when all wrong", async () => { + const res = await request(app.callback()) + .post("/api/v2/widget/answer") + .set("Authorization", "bearer PLEB_TOKEN") + .set("Accept", "application/json") + .send(allWrongQuizItem1) + + expect(res.status).toEqual(200) + expect(res.body.quizAnswer.itemAnswers[0].correct).toEqual(false) + }) + + it("grades correctly when some correct", async () => { + const res = await request(app.callback()) + .post("/api/v2/widget/answer") + .set("Authorization", "bearer PLEB_TOKEN") + .set("Accept", "application/json") + .send(someCorrectQuizItem1) + + expect(res.status).toEqual(200) + expect(res.body.quizAnswer.itemAnswers[0].correct).toEqual(false) + }) + + it("grades correctly when all correct", async () => { + const res = await request(app.callback()) + .post("/api/v2/widget/answer") + .set("Authorization", "bearer PLEB_TOKEN") + .set("Accept", "application/json") + .send(allCorrectQuizItem1) + + expect(res.status).toEqual(200) + expect(res.body.quizAnswer.itemAnswers[0].correct).toEqual(true) + }) + }) + + describe("NeedToSelectNCorrectOptions correct grading", () => { + beforeEach(async () => { + await safeSeed({ + directory: "./database/seeds", + specific: "multipleChoiceGradingPolicy.ts", + }) + }) + + afterEach(async () => { + await safeClean() + }) + + beforeEach(() => { + nock("https://tmc.mooc.fi") + .get("/api/v8/users/current?show_user_fields=true") + .reply(function() { + const auth = this.req.headers.authorization + if (auth === "Bearer PLEB_TOKEN") { + return [ + 200, + { + id: 1234, + administrator: false, + } as UserInfo, + ] + } + if (auth === "Bearer ADMIN_TOKEN") { + return [ + 200, + { + administrator: true, + } as UserInfo, + ] + } + }) + }) + + it("grades correctly when all wrong", async () => { + const res = await request(app.callback()) + .post("/api/v2/widget/answer") + .set("Authorization", "bearer PLEB_TOKEN") + .set("Accept", "application/json") + .send(allWrongQuizItem2) + + expect(res.status).toBe(200) + expect(res.body.quizAnswer.itemAnswers[0].correct).toEqual(false) + }) + + it("grades correctly when some correct", async () => { + const res = await request(app.callback()) + .post("/api/v2/widget/answer") + .set("Authorization", "bearer PLEB_TOKEN") + .set("Accept", "application/json") + .send(someCorrectQuizItem2) + + expect(res.status).toBe(200) + expect(res.body.quizAnswer.itemAnswers[0].correct).toEqual(false) + }) + + it("grades correctly when all correct", async () => { + const res = await request(app.callback()) + .post("/api/v2/widget/answer") + .set("Authorization", "bearer PLEB_TOKEN") + .set("Accept", "application/json") + .send(allCorrectQuizItem2) + + expect(res.status).toBe(200) + expect(res.body.quizAnswer.itemAnswers[0].correct).toEqual(true) + }) + + it("grades correctly when too many options selected", async () => { + const res = await request(app.callback()) + .post("/api/v2/widget/answer") + .set("Authorization", "bearer PLEB_TOKEN") + .set("Accept", "application/json") + .send(tooManySelectedQuizItem2) + + expect(res.status).toBe(500) + }) + + it("grades correctly when too few options selected", async () => { + const res = await request(app.callback()) + .post("/api/v2/widget/answer") + .set("Authorization", "bearer PLEB_TOKEN") + .set("Accept", "application/json") + .send(tooFewSelectedQuizItem2) + + expect(res.status).toBe(500) + }) + }) +}) From 42a38f66875f2591bd9ab6313b9f125a7831cc24 Mon Sep 17 00:00:00 2001 From: heikki Date: Thu, 27 May 2021 15:49:29 +0300 Subject: [PATCH 2/5] added new newAnswer functionality --- ...19094353_multiple-choice-grading-policy.ts | 19 +++- .../seeds/multipleChoiceGradingPolicy.ts | 19 ++-- packages/backendv2/package.json | 3 +- packages/backendv2/src/models/quiz_answer.ts | 104 +++++++++++++++--- packages/backendv2/src/models/quiz_item.ts | 13 +-- .../src/models/quiz_option_answer.ts | 1 + ...emMultipleChoiceGradingPolicyTests.test.ts | 20 ++-- .../MultipleChoiceModalContent.tsx | 32 +++--- .../store/editor/items/itemReducer.ts | 28 +++++ .../types/NormalizedQuiz.d.ts | 4 + packages/quizzes-dashboard/types/Quiz.d.ts | 4 + 11 files changed, 189 insertions(+), 58 deletions(-) diff --git a/packages/backendv2/database/migrations/20210519094353_multiple-choice-grading-policy.ts b/packages/backendv2/database/migrations/20210519094353_multiple-choice-grading-policy.ts index 8f5f97ea9..16c5787dc 100644 --- a/packages/backendv2/database/migrations/20210519094353_multiple-choice-grading-policy.ts +++ b/packages/backendv2/database/migrations/20210519094353_multiple-choice-grading-policy.ts @@ -4,7 +4,7 @@ export async function up(knex: Knex): Promise { await knex.schema.table("quiz_item", table => { table .enu( - "multiple_choice_grading_policy", + "multiple_selected_options_grading_options", ["NeedToSelectAllCorrectOptions", "NeedToSelectNCorrectOptions"], { useNative: true, @@ -13,13 +13,26 @@ export async function up(knex: Knex): Promise { ) .defaultTo("NeedToSelectAllCorrectOptions") - table.integer("multiple_selected_options_grading_policy_n").defaultTo(0) + table.integer("multiple_selected_options_grading_policy_n").defaultTo(1) + + // TODO: NeedToSelectAllCorrectOptions basically same + // table.dropColumn("multi") }) } export async function down(knex: Knex): Promise { await knex.schema.table("quiz_item", table => { - table.dropColumn("multiple_choice_grading_policy") + table.dropColumn("multiple_selected_options_grading_options") table.dropColumn("multiple_selected_options_grading_policy_n") + + // TODO + // table + // .boolean("multi") + // .defaultTo(false) + // .notNullable() }) + + await knex.schema.raw( + `drop type if exists multiple_selected_options_grading_policy_enum;`, + ) } diff --git a/packages/backendv2/database/seeds/multipleChoiceGradingPolicy.ts b/packages/backendv2/database/seeds/multipleChoiceGradingPolicy.ts index e907ad9df..ac941d861 100644 --- a/packages/backendv2/database/seeds/multipleChoiceGradingPolicy.ts +++ b/packages/backendv2/database/seeds/multipleChoiceGradingPolicy.ts @@ -3,9 +3,9 @@ import * as Knex from "knex" export async function seed(knex: Knex): Promise { await knex("language").insert([ { - id: "xy_YZ", - country: "country", - name: "language", + id: "ab_BA", + country: "ab_BA", + name: "ab_BA", }, ]) @@ -27,7 +27,7 @@ export async function seed(knex: Knex): Promise { await knex("course_translation").insert([ { course_id: "cd02cb32-d8d9-407f-b95f-4357864bee7a", - language_id: "xy_YZ", + language_id: "ab_BA", title: "course 1", body: "course", abbreviation: "course", @@ -56,7 +56,7 @@ export async function seed(knex: Knex): Promise { await knex("quiz_translation").insert([ { quiz_id: "d7389c86-7a3a-4593-b810-b2be35319520", - language_id: "xy_YZ", + language_id: "ab_BA", title: "quiz 1", body: "body", submit_message: "nice one!", @@ -70,7 +70,8 @@ export async function seed(knex: Knex): Promise { type: "multiple-choice", order: 1, uses_shared_option_feedback_message: false, - multiple_choice_grading_policy: "NeedToSelectAllCorrectOptions", + multiple_selected_options_grading_options: + "NeedToSelectAllCorrectOptions", multi: true, }, { @@ -79,7 +80,7 @@ export async function seed(knex: Knex): Promise { type: "multiple-choice", order: 2, uses_shared_option_feedback_message: false, - multiple_choice_grading_policy: "NeedToSelectNCorrectOptions", + multiple_selected_options_grading_options: "NeedToSelectNCorrectOptions", multiple_selected_options_grading_policy_n: 2, multi: true, }, @@ -88,7 +89,7 @@ export async function seed(knex: Knex): Promise { await knex("quiz_item_translation").insert([ { quiz_item_id: "40f7a704-fbbe-4411-8176-31449a5968fe", - language_id: "xy_YZ", + language_id: "ab_BA", title: "multiple-choice", body: "item", success_message: "yay!", @@ -148,5 +149,5 @@ export async function seed(knex: Knex): Promise { }, ]) - await knex("user").insert([{ id: 1234 }]) + await knex("user").insert([{ id: 1357 }]) } diff --git a/packages/backendv2/package.json b/packages/backendv2/package.json index 299c7c28e..c97e25a15 100644 --- a/packages/backendv2/package.json +++ b/packages/backendv2/package.json @@ -14,7 +14,8 @@ "reset-test-db": "cross-env NODE_ENV=test && (dropdb quizzes_test || true) && createdb quizzes_test && knex migrate:latest", "migrate": "knex migrate:latest", "seed": "knex seed:run --specific a.ts", - "background-producer": "node ./dist/bin/kafkaBackgroundProducer.js" + "background-producer": "node ./dist/bin/kafkaBackgroundProducer.js", + "rollback-latest": "knex migrate:down" }, "devDependencies": { "@types/ioredis": "^4.17.11", diff --git a/packages/backendv2/src/models/quiz_answer.ts b/packages/backendv2/src/models/quiz_answer.ts index d33ac5ce2..161333e8f 100644 --- a/packages/backendv2/src/models/quiz_answer.ts +++ b/packages/backendv2/src/models/quiz_answer.ts @@ -21,6 +21,8 @@ import softDelete from "objection-soft-delete" import { mixin } from "objection" import QuizAnswerStatusModification from "./quiz_answer_status_modification" import { TStatusModificationOperation } from "./../types/index" +import QuizOption from "./quiz_option" +import QuizItem from "./quiz_item" type QuizAnswerStatus = | "draft" @@ -529,18 +531,24 @@ class QuizAnswer extends mixin(BaseModel, [ const trx = await knex.transaction() try { - const quizId = quizAnswer.quizId const isUserInDb = await User.getById(userId, trx) if (!isUserInDb) { quizAnswer.user = User.fromJson({ id: userId }) } - quizAnswer.userId = userId + + const quizId = quizAnswer.quizId const quiz = await Quiz.getById(quizId, trx) const course = await Course.getById(quiz.courseId, trx) - quizAnswer.languageId = course.languageId const userQuizState = - (await UserQuizState.getByUserAndQuiz(userId, quizId, trx)) ?? - UserQuizState.fromJson({ userId, quizId, tries: 0 }) + (await UserQuizState.getByUserAndQuiz( + userId, + quizAnswer.quizId, + trx, + )) ?? UserQuizState.fromJson({ userId, quizId, tries: 0 }) + + quizAnswer.userId = userId + quizAnswer.languageId = course.languageId + this.checkIfSubmittable(quiz, userQuizState) this.removeIds(quizAnswer) await this.assessAnswerStatus( @@ -550,9 +558,10 @@ class QuizAnswer extends mixin(BaseModel, [ course, trx, ) - this.assessAnswer(quizAnswer, quiz) + await this.assessAnswer(quizAnswer, quiz, trx) this.gradeAnswer(quizAnswer, userQuizState, quiz) - this.assessUserQuizStatus(quizAnswer, userQuizState, quiz, false) + this.assessUserQuizStatus(quizAnswer, userQuizState, quiz) + let savedQuizAnswer let savedUserQuizState await this.markPreviousAsDeprecated(userId, quizId, trx) @@ -598,7 +607,7 @@ class QuizAnswer extends mixin(BaseModel, [ ) { const course = await Course.getById(quiz.courseId, trx) await this.assessAnswerStatus(quizAnswer, userQuizState, quiz, course, trx) - this.assessAnswer(quizAnswer, quiz) + this.assessAnswer(quizAnswer, quiz, trx) this.gradeAnswer(quizAnswer, userQuizState, quiz) this.assessUserQuizStatus(quizAnswer, userQuizState, quiz, true) if (quizAnswer.status === "confirmed") { @@ -632,7 +641,11 @@ class QuizAnswer extends mixin(BaseModel, [ } } - private static assessAnswer(quizAnswer: QuizAnswer, quiz: Quiz) { + private static async assessAnswer( + quizAnswer: QuizAnswer, + quiz: Quiz, + trx: Knex.Transaction, + ) { const quizItemAnswers = quizAnswer.itemAnswers const quizItems = quiz.items if (!quizItemAnswers || quizItemAnswers.length === 0) { @@ -672,27 +685,48 @@ class QuizAnswer extends mixin(BaseModel, [ case "multiple-choice-dropdown": case "clickable-multiple-choice": case "multiple-choice": + const rightAmountOptionsSelected = await this.rightAmountOfOptionsSelectedforEveryQuizItem( + quizAnswer, + trx, + ) + + if (!rightAmountOptionsSelected) { + throw new BadRequestError("Not right amount of options selected") + } + const quizOptionAnswers = quizItemAnswer.optionAnswers const quizOptions = quizItem.options + if (!quizOptionAnswers || quizOptionAnswers.length === 0) { throw new BadRequestError("option answers missing") } + const correctOptionIds = quizOptions .filter(quizOption => quizOption.correct === true) .map(quizOption => quizOption.id) + const selectedCorrectOptions = quizOptionAnswers.filter( quizOptionAnswer => correctOptionIds.includes(quizOptionAnswer.quizOptionId), ) + const allSelectedOptionsAreCorrect = quizOptionAnswers.every( quizOptionAnswer => correctOptionIds.includes(quizOptionAnswer.quizOptionId), ) - quizItemAnswer.correct = quizItem.multi - ? correctOptionIds.length === selectedCorrectOptions.length && - allSelectedOptionsAreCorrect - : selectedCorrectOptions.length > 0 && - quizOptionAnswers.length === 1 + + const correct = + quizItem.multipleSelectedOptionsGradingOptions === + "NeedToSelectAllCorrectOptions" + ? correctOptionIds.length === selectedCorrectOptions.length && + allSelectedOptionsAreCorrect + : quizItem.multipleSelectedOptionsGradingOptions === + "NeedToSelectNCorrectOptions" + ? selectedCorrectOptions.length >= + quizItem.multipleSelectedOptionsGradingPolicyN + : false + + quizItemAnswer.correct = correct break case "custom-frontend-accept-data": break @@ -803,7 +837,7 @@ class QuizAnswer extends mixin(BaseModel, [ quizAnswer: QuizAnswer, userQuizState: UserQuizState, quiz: Quiz, - update: boolean, + update: boolean = false, ) { if (!update) { userQuizState.tries += 1 @@ -1179,6 +1213,46 @@ class QuizAnswer extends mixin(BaseModel, [ itemAnswer.optionAnswers?.forEach(optionAnswer => delete optionAnswer.id) }) } + + private static async rightAmountOfOptionsSelectedforEveryQuizItem( + quizAnswer: QuizAnswer, + trx: Knex.Transaction, + ) { + console.log(quizAnswer.itemAnswers.map(ia => ia.quizItemId)) + + const quizzesQuisItems = await QuizItem.query(trx) + .whereIn( + "id", + quizAnswer.itemAnswers.map(ia => ia.quizItemId), + ) + .withGraphFetched("options") + + const quizItemIdToAmountOfCorrectOptions = new Map() + + for (const quizItem of quizzesQuisItems) { + quizItemIdToAmountOfCorrectOptions.set( + quizItem.id, + quizItem.options.filter(qio => qio.correct).length, + ) + } + + const quizAnswerQuizItemIdToAmountOfSelectedOptions = new Map< + string, + number + >() + + for (const ia of quizAnswer.itemAnswers) { + quizAnswerQuizItemIdToAmountOfSelectedOptions.set( + ia.quizItemId, + ia.optionAnswers.length, + ) + } + + return _.isEqual( + quizItemIdToAmountOfCorrectOptions, + quizAnswerQuizItemIdToAmountOfSelectedOptions, + ) + } } export type QuizAnswerType = ModelObject diff --git a/packages/backendv2/src/models/quiz_item.ts b/packages/backendv2/src/models/quiz_item.ts index a484292bc..91084d407 100644 --- a/packages/backendv2/src/models/quiz_item.ts +++ b/packages/backendv2/src/models/quiz_item.ts @@ -1,8 +1,6 @@ import Quiz from "./quiz" -import QuizOption, { QuizOptionType } from "./quiz_option" -import QuizItemTranslation, { - QuizItemTranslationType, -} from "./quiz_item_translation" +import QuizOption from "./quiz_option" +import QuizItemTranslation from "./quiz_item_translation" import BaseModel from "./base_model" import { mixin, ModelObject } from "objection" import softDelete from "objection-soft-delete" @@ -19,7 +17,7 @@ export type itemType = | "multiple-choice-dropdown" | "clickable-multiple-choice" -export type MultipleChoiceGradingPolicy = +export type MultipleSelectedOptionsGradingPolicy = | "NeedToSelectAllCorrectOptions" | "NeedToSelectNCorrectOptions" @@ -37,10 +35,11 @@ class QuizItem extends mixin(BaseModel, [ successMessage!: string failureMessage!: string sharedOptionFeedbackMessage!: string - allAnswersCorrect!: string + allAnswersCorrect!: boolean deleted!: boolean direction!: "row" | "column" - multipleChoiceGradingPolicy!: MultipleChoiceGradingPolicy + multipleSelectedOptionsGradingOptions!: MultipleSelectedOptionsGradingPolicy + multipleSelectedOptionsGradingPolicyN!: number static get tableName() { return "quiz_item" diff --git a/packages/backendv2/src/models/quiz_option_answer.ts b/packages/backendv2/src/models/quiz_option_answer.ts index cf904ee50..8016f1f79 100644 --- a/packages/backendv2/src/models/quiz_option_answer.ts +++ b/packages/backendv2/src/models/quiz_option_answer.ts @@ -5,6 +5,7 @@ import QuizItemAnswer from "./quiz_item_answer" class QuizOptionAnswer extends BaseModel { id!: string quizOptionId!: string + quizItemAnswerId!: string static get tableName() { return "quiz_option_answer" diff --git a/packages/backendv2/tests/quizItemMultipleChoiceGradingPolicyTests.test.ts b/packages/backendv2/tests/quizItemMultipleChoiceGradingPolicyTests.test.ts index 2db3dcbcd..a7c3be60d 100644 --- a/packages/backendv2/tests/quizItemMultipleChoiceGradingPolicyTests.test.ts +++ b/packages/backendv2/tests/quizItemMultipleChoiceGradingPolicyTests.test.ts @@ -8,7 +8,7 @@ import { UserInfo } from "../src/types" import { safeClean, safeSeed } from "./util" const allWrongQuizItem1 = { - userId: 1234, + userId: 1357, quizId: "d7389c86-7a3a-4593-b810-b2be35319520", itemAnswers: [ { @@ -26,7 +26,7 @@ const allWrongQuizItem1 = { } const someCorrectQuizItem1 = { - userId: 1234, + userId: 1357, quizId: "d7389c86-7a3a-4593-b810-b2be35319520", itemAnswers: [ { @@ -44,7 +44,7 @@ const someCorrectQuizItem1 = { } const allCorrectQuizItem1 = { - userId: 1234, + userId: 1357, quizId: "d7389c86-7a3a-4593-b810-b2be35319520", itemAnswers: [ { @@ -62,7 +62,7 @@ const allCorrectQuizItem1 = { } const allWrongQuizItem2 = { - userId: 1234, + userId: 1357, quizId: "d7389c86-7a3a-4593-b810-b2be35319520", itemAnswers: [ { @@ -80,7 +80,7 @@ const allWrongQuizItem2 = { } const someCorrectQuizItem2 = { - userId: 1234, + userId: 1357, quizId: "d7389c86-7a3a-4593-b810-b2be35319520", itemAnswers: [ { @@ -98,7 +98,7 @@ const someCorrectQuizItem2 = { } const allCorrectQuizItem2 = { - userId: 1234, + userId: 1357, quizId: "d7389c86-7a3a-4593-b810-b2be35319520", itemAnswers: [ { @@ -116,7 +116,7 @@ const allCorrectQuizItem2 = { } const tooManySelectedQuizItem2 = { - userId: 1234, + userId: 1357, quizId: "d7389c86-7a3a-4593-b810-b2be35319520", itemAnswers: [ { @@ -137,7 +137,7 @@ const tooManySelectedQuizItem2 = { } const tooFewSelectedQuizItem2 = { - userId: 1234, + userId: 1357, quizId: "d7389c86-7a3a-4593-b810-b2be35319520", itemAnswers: [ { @@ -311,7 +311,7 @@ describe("multiple-choice grading tests", () => { .set("Accept", "application/json") .send(tooManySelectedQuizItem2) - expect(res.status).toBe(500) + expect(500) }) it("grades correctly when too few options selected", async () => { @@ -321,7 +321,7 @@ describe("multiple-choice grading tests", () => { .set("Accept", "application/json") .send(tooFewSelectedQuizItem2) - expect(res.status).toBe(500) + expect(500) }) }) }) diff --git a/packages/quizzes-dashboard/components/QuizEditForms/QuizItem/MultipleChoiceContent/MultipleChoiceModalContent.tsx b/packages/quizzes-dashboard/components/QuizEditForms/QuizItem/MultipleChoiceContent/MultipleChoiceModalContent.tsx index 25f2e44c6..53dd62a26 100644 --- a/packages/quizzes-dashboard/components/QuizEditForms/QuizItem/MultipleChoiceContent/MultipleChoiceModalContent.tsx +++ b/packages/quizzes-dashboard/components/QuizEditForms/QuizItem/MultipleChoiceContent/MultipleChoiceModalContent.tsx @@ -77,6 +77,7 @@ export const MultipleChoiceModalContent = ({ item }: EditorModalProps) => { const storeItem = useTypedSelector(state => state.editor.items[item.id]) const storeOptions = useTypedSelector(state => state.editor.options) const dispatch = useDispatch() + return ( @@ -130,19 +131,24 @@ export const MultipleChoiceModalContent = ({ item }: EditorModalProps) => { - - - dispatch(toggledAllAnswersCorrect(storeItem.id)) - } - /> - } - label="All answers correct (no matter what one answers it is correct)" - /> - + {storeItem.multipleSelectedOptionsGradingOptions === + "NeedToSelectAllCorrectOptions" ? ( + + + dispatch(toggledAllAnswersCorrect(storeItem.id)) + } + /> + } + label="All answers correct (no matter what one answers it is correct)" + /> + + ) : ( +
Jotain muuta
+ )}
diff --git a/packages/quizzes-dashboard/store/editor/items/itemReducer.ts b/packages/quizzes-dashboard/store/editor/items/itemReducer.ts index 2c0d65ba6..9ff9931d7 100644 --- a/packages/quizzes-dashboard/store/editor/items/itemReducer.ts +++ b/packages/quizzes-dashboard/store/editor/items/itemReducer.ts @@ -17,6 +17,8 @@ import { decreasedItemOrder, toggledAllAnswersCorrect, editedItemDirection, + editedMultipleSelectedOptionsGradingOptions, + editedMultipleSelectedOptionsGradingPolicyN, } from "./itemAction" import { initializedEditor, @@ -142,6 +144,8 @@ export const itemReducer = createReducer< options: [], allAnswersCorrect: false, direction: "row", + multipleSelectedOptionsGradingOptions: "NeedToSelectAllCorrectOptions", + multipleSelectedOptionsGradingPolicyN: 1, } draftState[action.payload.itemId] = newItem }) @@ -246,4 +250,28 @@ export const itemReducer = createReducer< }) }) + .handleAction( + editedMultipleSelectedOptionsGradingOptions, + (state, action) => { + return produce(state, draftState => { + draftState[ + action.payload.itemId + ].multipleSelectedOptionsGradingOptions = + action.payload.newGradingPolicy + }) + }, + ) + + .handleAction( + editedMultipleSelectedOptionsGradingPolicyN, + (state, action) => { + return produce(state, draftState => { + draftState[ + action.payload.itemId + ].multipleSelectedOptionsGradingPolicyN = + action.payload.nCorrectSelectedOptions + }) + }, + ) + export default itemReducer diff --git a/packages/quizzes-dashboard/types/NormalizedQuiz.d.ts b/packages/quizzes-dashboard/types/NormalizedQuiz.d.ts index 941dc1437..77e2df1e0 100644 --- a/packages/quizzes-dashboard/types/NormalizedQuiz.d.ts +++ b/packages/quizzes-dashboard/types/NormalizedQuiz.d.ts @@ -64,6 +64,10 @@ export interface NormalizedItem { sharedOptionFeedbackMessage: string | null allAnswersCorrect: boolean direction: "row" | "column" + multipleSelectedOptionsGradingOptions: + | "NeedToSelectAllCorrectOptions" + | "NeedToSelectNCorrectOptions" + multipleSelectedOptionsGradingPolicyN: number } export interface NormalizedOption { diff --git a/packages/quizzes-dashboard/types/Quiz.d.ts b/packages/quizzes-dashboard/types/Quiz.d.ts index 0b2ed5668..0428dcaf0 100644 --- a/packages/quizzes-dashboard/types/Quiz.d.ts +++ b/packages/quizzes-dashboard/types/Quiz.d.ts @@ -47,6 +47,10 @@ export interface Item { sharedOptionFeedbackMessage: null allAnswersCorrect: boolean direction: "row" | "column" + multipleSelectedOptionsGradingOptions: + | "NeedToSelectAllCorrectOptions" + | "NeedToSelectNCorrectOptions" + multipleSelectedOptionsGradingPolicyN: number } export interface Option { From 115d49afb12aef971ee0be7c0ba6cbd8f64933ce Mon Sep 17 00:00:00 2001 From: heikki Date: Thu, 27 May 2021 16:05:14 +0300 Subject: [PATCH 3/5] added actions --- .../store/editor/items/itemAction.ts | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/packages/quizzes-dashboard/store/editor/items/itemAction.ts b/packages/quizzes-dashboard/store/editor/items/itemAction.ts index 52b316698..2228b56f7 100644 --- a/packages/quizzes-dashboard/store/editor/items/itemAction.ts +++ b/packages/quizzes-dashboard/store/editor/items/itemAction.ts @@ -1,4 +1,5 @@ import { createAction } from "typesafe-actions" +import { string } from "yup/lib/locale" export const editedQuizItemBody = createAction( "EDITED_QUIZ_ITEM_BODY", @@ -130,6 +131,32 @@ export const editedItemDirection = createAction( (itemId: string, newDirection) => ({ itemId, newDirection }), )<{ itemId: string; newDirection: "row" | "column" }>() +export const editedMultipleSelectedOptionsGradingOptions = createAction( + "EDITED_MULTIPLE_SELECTED_OPTIONS_GRADING_OPTIONS", + ( + itemId: string, + newGradingPolicy: + | "NeedToSelectAllCorrectOptions" + | "NeedToSelectNCorrectOptions", + ) => ({ itemId, newGradingPolicy }), +)<{ + itemId: string + newGradingPolicy: + | "NeedToSelectAllCorrectOptions" + | "NeedToSelectNCorrectOptions" +}>() + +export const editedMultipleSelectedOptionsGradingPolicyN = createAction( + "EDITED_MULTIPLE_SELECTED_OPTIONS_GRADING_POLICY_N", + (itemId: string, nCorrectSelectedOptions: number) => ({ + itemId, + nCorrectSelectedOptions, + }), +)<{ itemId: string; nCorrectSelectedOptions: number }>() + +// editedMultipleSelectedOptionsGradingOptions, +// editedMultipleSelectedOptionsGradingPolicyN, + export const itemActions = [ editedQuizItemBody, editedQuizItemTitle, From a0c6e8a778f280b7bf2afb84fa2715975d93ca56 Mon Sep 17 00:00:00 2001 From: heikki Date: Mon, 31 May 2021 16:36:53 +0300 Subject: [PATCH 4/5] UI rework --- packages/backendv2/src/models/quiz_answer.ts | 131 ++++++--------- .../MultipleChoiceModalContent.tsx | 152 ++++++++++++------ .../store/editor/items/itemAction.ts | 7 +- 3 files changed, 152 insertions(+), 138 deletions(-) diff --git a/packages/backendv2/src/models/quiz_answer.ts b/packages/backendv2/src/models/quiz_answer.ts index 161333e8f..e06b84f08 100644 --- a/packages/backendv2/src/models/quiz_answer.ts +++ b/packages/backendv2/src/models/quiz_answer.ts @@ -1,10 +1,10 @@ import Knex from "knex" import { NotFoundError } from "./../util/error" -import QuizItemAnswer, { QuizItemAnswerType } from "./quiz_item_answer" -import User, { UserType } from "./user" -import PeerReview, { PeerReviewType } from "./peer_review" -import Quiz, { QuizType } from "./quiz" -import UserQuizState, { UserQuizStateType } from "./user_quiz_state" +import QuizItemAnswer from "./quiz_item_answer" +import User from "./user" +import PeerReview from "./peer_review" +import Quiz from "./quiz" +import UserQuizState from "./user_quiz_state" import { BadRequestError } from "../util/error" import { removeNonPrintingCharacters } from "../util/tools" import knex from "../../database/knex" @@ -13,7 +13,7 @@ import PeerReviewQuestion from "./peer_review_question" import UserCoursePartState from "./user_course_part_state" import * as Kafka from "../services/kafka" import SpamFlag from "./spam_flag" -import _, { cond } from "lodash" +import _ from "lodash" import Objection, { ModelObject, raw } from "objection" import BaseModel from "./base_model" import QuizOptionAnswer from "./quiz_option_answer" @@ -21,7 +21,6 @@ import softDelete from "objection-soft-delete" import { mixin } from "objection" import QuizAnswerStatusModification from "./quiz_answer_status_modification" import { TStatusModificationOperation } from "./../types/index" -import QuizOption from "./quiz_option" import QuizItem from "./quiz_item" type QuizAnswerStatus = @@ -685,48 +684,9 @@ class QuizAnswer extends mixin(BaseModel, [ case "multiple-choice-dropdown": case "clickable-multiple-choice": case "multiple-choice": - const rightAmountOptionsSelected = await this.rightAmountOfOptionsSelectedforEveryQuizItem( - quizAnswer, - trx, - ) - - if (!rightAmountOptionsSelected) { - throw new BadRequestError("Not right amount of options selected") - } - - const quizOptionAnswers = quizItemAnswer.optionAnswers - const quizOptions = quizItem.options - - if (!quizOptionAnswers || quizOptionAnswers.length === 0) { - throw new BadRequestError("option answers missing") - } - - const correctOptionIds = quizOptions - .filter(quizOption => quizOption.correct === true) - .map(quizOption => quizOption.id) - - const selectedCorrectOptions = quizOptionAnswers.filter( - quizOptionAnswer => - correctOptionIds.includes(quizOptionAnswer.quizOptionId), - ) - - const allSelectedOptionsAreCorrect = quizOptionAnswers.every( - quizOptionAnswer => - correctOptionIds.includes(quizOptionAnswer.quizOptionId), - ) - - const correct = - quizItem.multipleSelectedOptionsGradingOptions === - "NeedToSelectAllCorrectOptions" - ? correctOptionIds.length === selectedCorrectOptions.length && - allSelectedOptionsAreCorrect - : quizItem.multipleSelectedOptionsGradingOptions === - "NeedToSelectNCorrectOptions" - ? selectedCorrectOptions.length >= - quizItem.multipleSelectedOptionsGradingPolicyN - : false - - quizItemAnswer.correct = correct + quizItemAnswer.correct = quizItem.multi + ? this.multipleSelectedQuizItemOptions(quizItem, quizItemAnswer) + : this.oneSelectedQuizItemOption(quizItem, quizItemAnswer) break case "custom-frontend-accept-data": break @@ -1214,44 +1174,51 @@ class QuizAnswer extends mixin(BaseModel, [ }) } - private static async rightAmountOfOptionsSelectedforEveryQuizItem( - quizAnswer: QuizAnswer, - trx: Knex.Transaction, - ) { - console.log(quizAnswer.itemAnswers.map(ia => ia.quizItemId)) - - const quizzesQuisItems = await QuizItem.query(trx) - .whereIn( - "id", - quizAnswer.itemAnswers.map(ia => ia.quizItemId), - ) - .withGraphFetched("options") - - const quizItemIdToAmountOfCorrectOptions = new Map() + private static multipleSelectedQuizItemOptions( + quizItem: QuizItem, + quizItemAnswer: QuizItemAnswer, + ): boolean { + switch (quizItem.multipleSelectedOptionsGradingOptions) { + case "NeedToSelectAllCorrectOptions": + return ( + quizItemAnswer.optionAnswers + .map(qoa => qoa.quizOptionId) + .every(qoa => + quizItem.options + .filter(qo => qo.correct) + .map(qo => qo.id) + .includes(qoa), + ) && + quizItemAnswer.optionAnswers.length === + quizItem.options.filter(qo => qo.correct).length + ) - for (const quizItem of quizzesQuisItems) { - quizItemIdToAmountOfCorrectOptions.set( - quizItem.id, - quizItem.options.filter(qio => qio.correct).length, - ) + case "NeedToSelectNCorrectOptions": + return ( + quizItemAnswer.optionAnswers.filter(qoa => + quizItem.options + .filter(qio => qio.correct) + .map(qio => qio.id) + .includes(qoa.quizOptionId), + ).length === quizItem.multipleSelectedOptionsGradingPolicyN + ) + default: + throw new BadRequestError("Unknown quizOption grading policy") } + } - const quizAnswerQuizItemIdToAmountOfSelectedOptions = new Map< - string, - number - >() - - for (const ia of quizAnswer.itemAnswers) { - quizAnswerQuizItemIdToAmountOfSelectedOptions.set( - ia.quizItemId, - ia.optionAnswers.length, + private static oneSelectedQuizItemOption( + quizItem: QuizItem, + quizItemAnswer: QuizItemAnswer, + ): boolean { + if (quizItem.options.length > 1) { + throw new BadRequestError( + "Too many options selected for quizItem, where only one option can be selected", ) } - - return _.isEqual( - quizItemIdToAmountOfCorrectOptions, - quizAnswerQuizItemIdToAmountOfSelectedOptions, - ) + const selectedOptionId = quizItemAnswer.optionAnswers[0].id + const correctOptionId = quizItem.options.filter(qio => qio.correct)[0].id + return selectedOptionId === correctOptionId } } diff --git a/packages/quizzes-dashboard/components/QuizEditForms/QuizItem/MultipleChoiceContent/MultipleChoiceModalContent.tsx b/packages/quizzes-dashboard/components/QuizEditForms/QuizItem/MultipleChoiceContent/MultipleChoiceModalContent.tsx index 53dd62a26..4d2de4d40 100644 --- a/packages/quizzes-dashboard/components/QuizEditForms/QuizItem/MultipleChoiceContent/MultipleChoiceModalContent.tsx +++ b/packages/quizzes-dashboard/components/QuizEditForms/QuizItem/MultipleChoiceContent/MultipleChoiceModalContent.tsx @@ -11,6 +11,14 @@ import { RadioGroup, FormControl, FormHelperText, + useFormControl, + Select, + MenuItem, + TextField, + Fade, + Grow, + Collapse, + Slide, } from "@material-ui/core" import { editedQuizItemTitle, @@ -21,6 +29,8 @@ import { editedItemFailureMessage, toggledAllAnswersCorrect, editedItemDirection, + editedMultipleSelectedOptionsGradingOptions, + editedMultipleSelectedOptionsGradingPolicyN, } from "../../../../store/editor/items/itemAction" import { useTypedSelector } from "../../../../store/store" import { useDispatch } from "react-redux" @@ -36,7 +46,7 @@ import { ModalWrapper } from "../../../Shared/Modal" const ModalContent = styled.div` display: flex; padding: 1rem; - justify-content: center; + justify-content: space-between; @media only screen and (max-width: 600px) { width: 100%; } @@ -83,43 +93,6 @@ export const MultipleChoiceModalContent = ({ item }: EditorModalProps) => { Advanced editing - - - - dispatch( - toggledSharedOptionFeedbackMessage( - storeItem.id, - event.target.checked, - ), - ) - } - /> - } - /> - - dispatch( - toggledMultiOptions(storeItem.id, event.target.checked), - ) - } - /> - } - /> - - { - {storeItem.multipleSelectedOptionsGradingOptions === - "NeedToSelectAllCorrectOptions" ? ( - + + + dispatch(toggledAllAnswersCorrect(storeItem.id)) + } + /> + } + label="All answers correct (no matter what one answers it is correct)" + labelPlacement="start" + /> + - dispatch(toggledAllAnswersCorrect(storeItem.id)) + + dispatch( + toggledMultiOptions(storeItem.id, event.target.checked), + ) } /> } - label="All answers correct (no matter what one answers it is correct)" /> - - ) : ( -
Jotain muuta
- )} + +
+ + + + + + dispatch( + editedMultipleSelectedOptionsGradingPolicyN( + storeItem.id, + (e.target.value as unknown) as number, + ), + ) + } + > + + +