diff --git a/lib/cadet/assessments.ex b/lib/cadet/assessments.ex new file mode 100644 index 000000000..c94d62374 --- /dev/null +++ b/lib/cadet/assessments.ex @@ -0,0 +1,31 @@ +defmodule Cadet.Assessments do + @moduledoc """ + Assessments context contains domain logic for assessments management such as + missions, sidequests, paths, etc. + """ + use Cadet, :context + + alias Cadet.Assessments.QuestionTypes.MCQQuestion + alias Cadet.Assessments.QuestionTypes.ProgrammingQuestion + + # To be uncommented when assessments context is merged + # def create_multiple_choice_question(json_attr) when is_binary(json_attr) do + # %MCQQuestion{} + # |> MCQQuestion.changeset(%{raw_mcqquestion: json_attr}) + # end + + # def create_multiple_choice_question(attr = %{}) do + # %MCQQuestion{} + # |> MCQQuestion.changeset(attr) + # end + + # def create_programming_question(json_attr) when is_binary(json_attr) do + # %ProgrammingQuestion{} + # |> ProgrammingQuestion.changeset(%{raw_programmingquestion: json_attr}) + # end + + # def create_programming_question(attr = %{}) do + # %ProgrammingQuestion{} + # |> ProgrammingQuestion.changeset(attr) + # end +end diff --git a/lib/cadet/assessments/question_types/library.ex b/lib/cadet/assessments/question_types/library.ex new file mode 100644 index 000000000..0ff3f9504 --- /dev/null +++ b/lib/cadet/assessments/question_types/library.ex @@ -0,0 +1,24 @@ +defmodule Cadet.Assessments.QuestionTypes.Library do + @moduledoc """ + The library entity represents a library to be used in a programming question. + """ + use Ecto.Schema + + import Ecto.Changeset + + embedded_schema do + field(:version, :integer) + field(:globals, {:array, :string}) + field(:externals, {:array, :string}) + field(:files, {:array, :string}) + end + + @required_fields ~w(version)a + @optional_fields ~w(globals externals files)a + + def changeset(library, params \\ %{}) do + library + |> cast(params, @required_fields ++ @optional_fields) + |> validate_required(@required_fields) + end +end diff --git a/lib/cadet/assessments/question_types/mcq_choice.ex b/lib/cadet/assessments/question_types/mcq_choice.ex new file mode 100644 index 000000000..c23b419f5 --- /dev/null +++ b/lib/cadet/assessments/question_types/mcq_choice.ex @@ -0,0 +1,25 @@ +defmodule Cadet.Assessments.QuestionTypes.MCQChoice do + @moduledoc """ + The Assessments.QuestionTypes.MCQChoice entity represents an MCQ Choice. + """ + use Ecto.Schema + + import Ecto.Changeset + + alias Cadet.Assessments.QuestionTypes.MCQQuestion + + embedded_schema do + field(:content, :string) + field(:hint, :string) + field(:is_correct, :boolean) + end + + @required_fields ~w(content is_correct)a + @optional_fields ~w(is_correct)a + + def changeset(question, params \\ %{}) do + question + |> cast(params, @required_fields ++ @optional_fields) + |> validate_required(@required_fields) + end +end diff --git a/lib/cadet/assessments/question_types/mcq_question.ex b/lib/cadet/assessments/question_types/mcq_question.ex new file mode 100644 index 000000000..cbe069d42 --- /dev/null +++ b/lib/cadet/assessments/question_types/mcq_question.ex @@ -0,0 +1,57 @@ +defmodule Cadet.Assessments.QuestionTypes.MCQQuestion do + @moduledoc """ + The Assessments.QuestionTypes.MCQQuestion entity represents an MCQ Question. + It comprises of content and choices. + """ + use Ecto.Schema + + import Ecto.Changeset + + alias Cadet.Assessments.QuestionTypes.MCQChoice + + embedded_schema do + field(:content, :string) + embeds_many(:choices, MCQChoice) + field(:raw_mcqquestion, :string, virtual: true) + end + + @required_fields ~w(content)a + @optional_fields ~w(raw_mcqquestion)a + + def changeset(question, params \\ %{}) do + question + |> cast(params, @required_fields ++ @optional_fields) + |> put_question() + |> cast_embed(:choices, with: &MCQChoice.changeset/2, required: true) + |> validate_one_correct_answer() + |> validate_required(@required_fields ++ ~w(choices)a) + end + + defp put_question(changeset) do + change = get_change(changeset, :raw_mcqquestion) + + if change do + json = Poison.decode!(change) + + changeset + |> cast(json, @required_fields) + else + changeset + end + end + + defp validate_one_correct_answer(changeset) do + changeset + |> validate_change(:choices, fn :choices, choices -> + no_of_correct_choices = + choices + |> Enum.reduce(0, &if(&1.changes && &1.changes[:is_correct], do: &2 + 1, else: &2)) + + if no_of_correct_choices == 1 do + [] + else + [choices: "Number of correct answer must be one."] + end + end) + end +end diff --git a/lib/cadet/assessments/question_types/programming_question.ex b/lib/cadet/assessments/question_types/programming_question.ex new file mode 100644 index 000000000..a3c81288b --- /dev/null +++ b/lib/cadet/assessments/question_types/programming_question.ex @@ -0,0 +1,43 @@ +defmodule Cadet.Assessments.QuestionTypes.ProgrammingQuestion do + @moduledoc """ + The ProgrammingQuestion entity represents a Programming question. + """ + use Ecto.Schema + + import Ecto.Changeset + + alias Cadet.Assessments.QuestionTypes.Library + + embedded_schema do + field(:content, :string) + field(:solution_template, :string) + field(:solution_header, :string) + field(:solution, :string) + embeds_one(:library, Library) + field(:raw_programmingquestion, :string, virtual: true) + end + + @required_fields ~w(content solution_template solution)a + @optional_fields ~w(solution_header raw_programmingquestion)a + + def changeset(question, params \\ %{}) do + question + |> cast(params, @required_fields ++ @optional_fields) + |> put_programmingquestion() + |> cast_embed(:library, required: true, with: &Library.changeset/2) + |> validate_required(@required_fields) + end + + defp put_programmingquestion(changeset) do + change = get_change(changeset, :raw_programmingquestion) + + if change do + json = Poison.decode!(change) + + changeset + |> cast(json, @required_fields) + else + changeset + end + end +end diff --git a/test/cadet/assessments/question_types/mcq_choice_test.exs b/test/cadet/assessments/question_types/mcq_choice_test.exs new file mode 100644 index 000000000..835ddfd0b --- /dev/null +++ b/test/cadet/assessments/question_types/mcq_choice_test.exs @@ -0,0 +1,18 @@ +defmodule Cadet.Assessments.QuestionTypes.MCQChoiceTest do + use Cadet.ChangesetCase, async: true + + alias Cadet.Assessments.QuestionTypes.MCQChoice + + valid_changesets MCQChoice do + %{content: "asd", is_correct: true} + %{content: "asd", hint: "asd", is_correct: true} + end + + invalid_changesets MCQChoice do + %{content: "asd"} + %{hint: "asd"} + %{is_correct: false} + %{content: "asd", hint: "aaa"} + %{content: 1, is_correct: true} + end +end diff --git a/test/cadet/assessments/question_types/mcq_question_test.exs b/test/cadet/assessments/question_types/mcq_question_test.exs new file mode 100644 index 000000000..067fa4a3e --- /dev/null +++ b/test/cadet/assessments/question_types/mcq_question_test.exs @@ -0,0 +1,19 @@ +defmodule Cadet.Assessments.QuestionTypes.MCQQuestionTest do + use Cadet.ChangesetCase, async: true + + alias Cadet.Assessments.QuestionTypes.MCQQuestion + + valid_changesets MCQQuestion do + %{content: "asd", choices: [%{content: "asd", is_correct: true}]} + + %{ + raw_mcqquestion: + "{\"content\":\"asd\",\"choices\":[{\"is_correct\":true,\"content\":\"asd\"}]}" + } + end + + invalid_changesets MCQQuestion do + %{content: "asd"} + %{content: "asd", choices: [%{content: "asd", is_correct: false}]} + end +end diff --git a/test/cadet/assessments/question_types/programming_question_test.exs b/test/cadet/assessments/question_types/programming_question_test.exs new file mode 100644 index 000000000..de5b8b33b --- /dev/null +++ b/test/cadet/assessments/question_types/programming_question_test.exs @@ -0,0 +1,31 @@ +defmodule Cadet.Assessments.QuestionTypes.ProgrammingQuestionTest do + use Cadet.ChangesetCase, async: true + + alias Cadet.Assessments.QuestionTypes.ProgrammingQuestion + + valid_changesets ProgrammingQuestion do + %{ + content: "asd", + solution_template: "asd", + solution: "asd", + library: %{version: 1} + } + + %{ + raw_programmingquestion: + "{\"solution_template\":\"asd\",\"solution_header\":\"asd\",\"solution\":\"asd\",\"content\":\"asd\",\"library\":{\"version\":1}}" + } + end + + invalid_changesets ProgrammingQuestion do + %{content: "asd"} + + %{ + content: "asd", + solution_template: "asd", + solution_header: "asd", + solution: "asd", + library: %{globals: ["a"]} + } + end +end