Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions lib/cadet/assessments/answer.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
49 changes: 49 additions & 0 deletions lib/cadet/assessments/assessments.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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{}
Expand Down
15 changes: 15 additions & 0 deletions lib/cadet/assessments/query.ex
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions lib/cadet/assessments/submission.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
25 changes: 25 additions & 0 deletions lib/cadet/factory.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down Expand Up @@ -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
37 changes: 35 additions & 2 deletions lib/cadet_web/controllers/grading_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down Expand Up @@ -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:
Expand Down
24 changes: 24 additions & 0 deletions lib/cadet_web/views/grading_view.ex
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion priv/repo/migrations/20180704020027_create_answers.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
10 changes: 5 additions & 5 deletions test/cadet/assessments/answer_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,30 @@ 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"})
}
end

invalid_changesets Answer do
%{
marks: -2,
xp: -2,
answer: %{}
}
end
Expand Down
60 changes: 60 additions & 0 deletions test/cadet_web/controllers/grading_controller_test.exs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
defmodule CadetWeb.GradingControllerTest do
use CadetWeb.ConnCase

import Cadet.Factory

alias CadetWeb.GradingController

test "swagger" do
Expand All @@ -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
9 changes: 8 additions & 1 deletion test/support/conn_case.ex
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ defmodule CadetWeb.ConnCase do

use ExUnit.CaseTemplate

import Plug.Conn

using do
quote do
# Import conveniences for testing with connections
Expand All @@ -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}
Expand Down