diff --git a/lib/cadet/assessments/answer.ex b/lib/cadet/assessments/answer.ex index 2c771c623..0845d2f6b 100644 --- a/lib/cadet/assessments/answer.ex +++ b/lib/cadet/assessments/answer.ex @@ -10,7 +10,7 @@ defmodule Cadet.Assessments.Answer do alias Cadet.Assessments.Question schema "answers" do - field(:marks, :float, default: 0.0) + field(:xp, :integer, default: 0) field(:answer, :map) field(:raw_answer, :string, virtual: true) belongs_to(:submission, Submission) @@ -19,13 +19,13 @@ defmodule Cadet.Assessments.Answer do end @required_fields ~w(answer)a - @optional_fields ~w(marks raw_answer)a + @optional_fields ~w(xp raw_answer)a def changeset(answer, params) do answer |> cast(params, @required_fields ++ @optional_fields) |> validate_required(@required_fields) - |> validate_number(:marks, greater_than_or_equal_to: 0.0) + |> validate_number(:xp, greater_than_or_equal_to: 0.0) |> put_json(:answer, :raw_answer) end end diff --git a/lib/cadet/assessments/assessments.ex b/lib/cadet/assessments/assessments.ex index 4a9b15715..9500ed24b 100644 --- a/lib/cadet/assessments/assessments.ex +++ b/lib/cadet/assessments/assessments.ex @@ -9,8 +9,14 @@ defmodule Cadet.Assessments do alias Timex.Duration + alias Cadet.Accounts.User + alias Cadet.Assessments.Answer alias Cadet.Assessments.Assessment + alias Cadet.Assessments.Query alias Cadet.Assessments.Question + alias Cadet.Assessments.Submission + + @grading_roles ~w(staff)a def all_assessments() do Repo.all(Assessment) @@ -115,6 +121,49 @@ defmodule Cadet.Assessments do Repo.delete(question) end + @spec get_submission_xp(String.t() | integer()) :: number() + def get_submission_xp(id) when is_binary(id) or is_integer(id) do + Answer + |> where(submission_id: ^id) + |> Repo.aggregate(:sum, :marks) + end + + # def all_submissions_by_grader(grader, preload \\ true) + + # @spec all_submissions_by_grader(String.t() | integer(), boolean()) :: Submission.t() + # def all_submissions_by_grader(grader_id, preload) + # when is_binary(grader_id) or is_integer(grader_id) do + # submissions = + # Submission + # |> where(grader_id: ^grader_id) + # |> Repo.all() + + # if preload do + # submissions + # |> Repo.preload([:assessment, :student]) + # |> Enum.map(&Map.put(&1, :xp, get_submission_xp(&1.id))) + # else + # submissions + # end + # end + + @spec all_submissions_by_grader(User.t()) :: Submission.t() + def all_submissions_by_grader(%User{id: id, role: role}) do + if role in @grading_roles do + submissions = + Submission + |> join(:left, [s], a in subquery(Query.submissions_xp()), a.submission_id == s.id) + |> select([s, a], %{s | xp: a.xp}) + |> where(grader_id: ^id) + |> preload([:assessment, :student]) + |> Repo.all() + + {:ok, submissions} + else + {:error, {:unauthorized, "User is not permitted to grade submissions"}} + end + end + # TODO: Decide what to do with these methods # def create_multiple_choice_question(json_attr) when is_binary(json_attr) do # %MCQQuestion{} diff --git a/lib/cadet/assessments/query.ex b/lib/cadet/assessments/query.ex new file mode 100644 index 000000000..163f1313a --- /dev/null +++ b/lib/cadet/assessments/query.ex @@ -0,0 +1,15 @@ +defmodule Cadet.Assessments.Query do + @moduledoc """ + Generate queries related to the Assessments context + """ + + import Ecto.Query + + alias Cadet.Assessments.Answer + + def submissions_xp do + Answer + |> select([a], %{submission_id: a.submission_id, xp: sum(a.xp)}) + |> group_by([a], a.submission_id) + end +end diff --git a/lib/cadet/assessments/submission.ex b/lib/cadet/assessments/submission.ex index 0d64689fe..4e72d1013 100644 --- a/lib/cadet/assessments/submission.ex +++ b/lib/cadet/assessments/submission.ex @@ -11,6 +11,7 @@ defmodule Cadet.Assessments.Submission do field(:status, SubmissionStatus, default: :attempting) field(:submitted_at, Timex.Ecto.DateTime) field(:override_xp, :integer) + field(:xp, :integer, virtual: true) belongs_to(:assessment, Assessment) belongs_to(:student, User) diff --git a/lib/cadet/factory.ex b/lib/cadet/factory.ex index bdeefb1a1..76eb269a5 100644 --- a/lib/cadet/factory.ex +++ b/lib/cadet/factory.ex @@ -12,8 +12,10 @@ defmodule Cadet.Factory do alias Cadet.Course.Point alias Cadet.Course.Group alias Cadet.Course.Material + alias Cadet.Assessments.Answer alias Cadet.Assessments.Assessment alias Cadet.Assessments.Question + alias Cadet.Assessments.Submission def user_factory do %User{ @@ -100,4 +102,27 @@ defmodule Cadet.Factory do assessment: build(:assessment) } end + + def answer_factory do + %Answer{ + xp: Enum.random(1..10) * 100, + answer: %{}, + question: build(:question) + } + end + + def question_factory do + %Question{ + title: "How much wood could a woodchuck chuck?", + weight: 1, + question: %{}, + type: Enum.random([:mission, :sidequest, :path, :contest]) + } + end + + def submission_factory do + %Submission{ + status: Enum.random([:attempting, :submitted, :graded]) + } + end end diff --git a/lib/cadet_web/controllers/grading_controller.ex b/lib/cadet_web/controllers/grading_controller.ex index eb10e796a..4847d2376 100644 --- a/lib/cadet_web/controllers/grading_controller.ex +++ b/lib/cadet_web/controllers/grading_controller.ex @@ -3,6 +3,17 @@ defmodule CadetWeb.GradingController do use PhoenixSwagger + alias Cadet.Assessments + + def index(conn, _) do + user = conn.assigns[:current_user] + + case Assessments.all_submissions_by_grader(user) do + {:ok, submissions} -> render(conn, "index.json", submissions: submissions) + {:error, {status, error}} -> send_resp(conn, status, error) + end + end + swagger_path :index do get("/grading") @@ -72,8 +83,30 @@ defmodule CadetWeb.GradingController do swagger_schema do properties do submissionId(:integer, "submission id", required: true) - missionId(:integer, "mission id", required: true) - studentId(:integer, "student id", required: true) + xp(:integer, "xp given") + graded(:boolean, "whether this submission has been graded", required: true) + assessment(Schema.ref(:AssessmentInfo)) + student(Schema.ref(:StudentInfo)) + end + end, + AssessmentInfo: + swagger_schema do + properties do + id(:integer, "assessment id", required: true) + type(:string, "Either mission/sidequest/path/contest", required: true) + + max_xp( + :integer, + "The max amount of XP to be earned from this assessment", + required: true + ) + end + end, + StudentInfo: + swagger_schema do + properties do + id(:integer, "student id", required: true) + name(:string, "student name", required: true) end end, GradingInfo: diff --git a/lib/cadet_web/views/grading_view.ex b/lib/cadet_web/views/grading_view.ex new file mode 100644 index 000000000..d6b04ea24 --- /dev/null +++ b/lib/cadet_web/views/grading_view.ex @@ -0,0 +1,24 @@ +defmodule CadetWeb.GradingView do + use CadetWeb, :view + + def render("index.json", %{submissions: submissions}) do + render_many(submissions, CadetWeb.GradingView, "submission.json", as: :submission) + end + + def render("submission.json", %{submission: submission}) do + %{ + xp: submission.xp, + submissionId: submission.id, + student: %{ + name: submission.student.name, + id: submission.student.id + }, + graded: submission.status == :graded, + assessment: %{ + type: submission.assessment.category, + max_xp: submission.assessment.max_xp, + id: submission.assessment.id + } + } + end +end diff --git a/priv/repo/migrations/20180704020027_create_answers.exs b/priv/repo/migrations/20180704020027_create_answers.exs index c8c81ef33..cd84b07fc 100644 --- a/priv/repo/migrations/20180704020027_create_answers.exs +++ b/priv/repo/migrations/20180704020027_create_answers.exs @@ -3,7 +3,7 @@ defmodule Cadet.Repo.Migrations.CreateAnswersTable do def change do create table(:answers) do - add(:marks, :float, default: 0.0) + add(:xp, :integer, default: 0) add(:answer, :map, null: false) add(:submission_id, references(:submissions, null: false)) add(:question_id, references(:questions, null: false)) diff --git a/test/cadet/assessments/answer_test.exs b/test/cadet/assessments/answer_test.exs index 0e9c087af..f6eb78333 100644 --- a/test/cadet/assessments/answer_test.exs +++ b/test/cadet/assessments/answer_test.exs @@ -5,22 +5,22 @@ defmodule Cadet.Assessments.AnswerTest do valid_changesets Answer do %{ - marks: 2, + xp: 2, answer: %{} } %{ - marks: 1, + xp: 1, answer: %{} } %{ - marks: 1, + xp: 1, answer: %{} } %{ - marks: 100, + xp: 100, answer: %{}, raw_answer: Poison.encode!(%{answer: "This is a sample json"}) } @@ -28,7 +28,7 @@ defmodule Cadet.Assessments.AnswerTest do invalid_changesets Answer do %{ - marks: -2, + xp: -2, answer: %{} } end diff --git a/test/cadet_web/controllers/grading_controller_test.exs b/test/cadet_web/controllers/grading_controller_test.exs index f588af8ce..aec46a633 100644 --- a/test/cadet_web/controllers/grading_controller_test.exs +++ b/test/cadet_web/controllers/grading_controller_test.exs @@ -1,6 +1,8 @@ defmodule CadetWeb.GradingControllerTest do use CadetWeb.ConnCase + import Cadet.Factory + alias CadetWeb.GradingController test "swagger" do @@ -9,4 +11,62 @@ defmodule CadetWeb.GradingControllerTest do GradingController.swagger_path_show(nil) GradingController.swagger_path_update(nil) end + + # Unauthenticated user + describe "GET /, Unauthenticated" do + test "is disallowed", %{conn: conn} do + conn = get(conn, "/v1/grading") + + assert response(conn, 401) =~ "Unauthorised" + end + end + + @tag authenticate: :student + describe "GET /, :student" do + test "is disallowed", %{conn: conn} do + conn = get(conn, "/v1/grading") + + assert response(conn, 401) =~ "User is not permitted to grade submissions" + end + end + + @tag authenticate: :staff + describe "GET /, :staff" do + test "successful", %{conn: conn} do + user = conn.assigns[:current_user] + student = insert(:user) + assessment = insert(:assessment) + + submission = + insert( + :submission, + grader_id: user.id, + student_id: student.id, + assessment_id: assessment.id + ) + + answers = insert_list(3, :answer, submission_id: submission.id) + xp = answers |> Enum.map(& &1.xp) |> Enum.reduce(&(&1 + &2)) + + conn = get(conn, "/v1/grading") + + body = json_response(conn, 200) + + expected = [ + %{ + "xp" => xp, + "submissionId" => submission.id, + "student" => %{"id" => student.id, "name" => student.name}, + "graded" => submission.status == :graded, + "assessment" => %{ + "type" => "#{assessment.category}", + "max_xp" => assessment.max_xp, + "id" => assessment.id + } + } + ] + + assert ^expected = body + end + end end diff --git a/test/support/conn_case.ex b/test/support/conn_case.ex index aaa043369..32e49efde 100644 --- a/test/support/conn_case.ex +++ b/test/support/conn_case.ex @@ -15,6 +15,8 @@ defmodule CadetWeb.ConnCase do use ExUnit.CaseTemplate + import Plug.Conn + using do quote do # Import conveniences for testing with connections @@ -37,7 +39,12 @@ defmodule CadetWeb.ConnCase do if tags[:authenticate] != nil do user = Cadet.Factory.insert(:user, %{role: tags[:authenticate]}) - conn = Cadet.Auth.Guardian.Plug.sign_in(conn, user) + + conn = + conn + |> Cadet.Auth.Guardian.Plug.sign_in(user) + |> assign(:current_user, user) + {:ok, conn: conn} else {:ok, conn: conn}