Skip to content

Commit

Permalink
Assessments controller (source-academy#116)
Browse files Browse the repository at this point in the history
* First draft

* Added path for unattempted assessments

* Change to Ecto Query

* WIP

* Add factories and seeds

* Add test helpers

* Update api spec according to changes from Vignesh

* Implement index

* Hotfix tests

* Implement show sans test

* Add tests

* Move stuffs into context

* Fix merge conflict

* Finish GET /assessments functionality

* Meh

* Update swagger schema

* WIP

* Add faker

* Change tests to use faker

* Add more factories

* Add more seeds

* Add more seeds

* Minor changes to factory

* Done with functionality

* Check is_published for assessments/show

* Minor change

* Wups

* Fix wupsies

* Add test seeds

* Add ordering

* HMMM

* Add some tests

* Minor changes

* Move helpers into contexts

* Add tests

* Add one test jesus christ that took forever

* Add one more test

* Add one more test

* Minor refactor of tests

* Minor refactor of tests

* Add open_at time validation

* Fix open_at check

* remove unused case

* Use view helpers in tests

* Fix according to review and add comment

* Minor change to seeds to fix styling

* lol

* Add attempted flag to GET /assessments

* Update swagger schema
  • Loading branch information
tuesmiddt authored and indocomsoft committed Aug 17, 2018
1 parent 02491de commit b95b0f8
Show file tree
Hide file tree
Showing 16 changed files with 875 additions and 46 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -104,5 +104,7 @@ erl_crash.dump
*.tfstate
*.backup

# VSCode Elixir language support
.elixir_ls/

.env
5 changes: 3 additions & 2 deletions lib/cadet/assessments/assessment.ex
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ defmodule Cadet.Assessments.Assessment do

schema "assessments" do
field(:max_xp, :integer, virtual: true)
field(:attempted, :boolean, virtual: true)
field(:title, :string)
field(:is_published, :boolean, default: false)
field(:type, AssessmentType)
Expand All @@ -30,13 +31,13 @@ defmodule Cadet.Assessments.Assessment do
@optional_fields ~w(summary_short summary_long is_published)a
@optional_file_fields ~w(cover_picture mission_pdf)a

def changeset(mission, params) do
def changeset(assessment, params) do
params =
params
|> convert_date(:open_at)
|> convert_date(:close_at)

mission
assessment
|> cast(params, @required_fields ++ @optional_fields)
|> validate_required(@required_fields)
|> cast_attachments(params, @optional_file_fields)
Expand Down
50 changes: 50 additions & 0 deletions lib/cadet/assessments/assessments.ex
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,39 @@ defmodule Cadet.Assessments do
Repo.all(from(a in Assessment, where: a.type == ^assessment_type))
end

def assessment_with_questions_and_answers(id, user = %User{}) when is_ecto_id(id) do
assessment =
Assessment
|> where(id: ^id)
|> where(is_published: true)
|> select([:type, :title, :summary_long, :mission_pdf, :id, :open_at])
|> Repo.one()

if assessment do
if Timex.after?(Timex.now(), assessment.open_at) do
answer_query =
Answer
|> join(:inner, [a], s in assoc(a, :submission))
|> where([_, s], s.student_id == ^user.id)

questions =
Question
|> where(assessment_id: ^id)
|> join(:left, [q], a in subquery(answer_query), q.id == a.question_id)
|> select([q, a], %{q | answer: a})
|> order_by(:display_order)
|> Repo.all()

assessment = Map.put(assessment, :questions, questions)
{:ok, assessment}
else
{:error, {:unauthorized, "Assessment not open"}}
end
else
{:error, {:bad_request, "Assessment not found"}}
end
end

def all_open_assessments(assessment_type) do
now = Timex.now()

Expand All @@ -31,6 +64,23 @@ defmodule Cadet.Assessments do
Enum.filter(assessment_with_type, &(&1.is_published and Timex.before?(&1.open_at, now)))
end

@doc """
Returns a list of assessments with all fields and an indicator showing whether it has been attempted
by the supplied user
"""
def all_published_assessments(user = %User{}) do
assessments =
Query.all_assessments_with_max_xp()
|> subquery()
|> join(:left, [a], s in Submission, a.id == s.assessment_id and s.student_id == ^user.id)
|> select([a, s], %{a | attempted: not is_nil(s.id)})
|> where(is_published: true)
|> order_by(:open_at)
|> Repo.all()

{:ok, assessments}
end

def assessments_due_soon() do
now = Timex.now()
week_after = Timex.add(now, Duration.from_weeks(1))
Expand Down
8 changes: 8 additions & 0 deletions lib/cadet/assessments/query.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,21 @@ defmodule Cadet.Assessments.Query do

alias Cadet.Assessments.{Answer, Assessment, Question, Submission}

@doc """
Returns a query with the following bindings:
[submissions_with_xp, answers]
"""
@spec all_submissions_with_xp :: Submission.t()
def all_submissions_with_xp do
Submission
|> join(:inner, [s], q in subquery(submissions_xp()), s.id == q.submission_id)
|> select([s, q], %Submission{s | xp: q.xp})
end

@doc """
Returns a query with the following bindings:
[assessments_with_xp, questions]
"""
@spec all_assessments_with_max_xp :: Assessment.t()
def all_assessments_with_max_xp do
Assessment
Expand Down
1 change: 1 addition & 0 deletions lib/cadet/assessments/question.ex
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ defmodule Cadet.Assessments.Question do
field(:type, QuestionType)
field(:raw_question, :string, virtual: true)
field(:max_xp, :integer)
field(:answer, :map, virtual: true)
embeds_one(:library, Library)
belongs_to(:assessment, Assessment)
timestamps()
Expand Down
58 changes: 45 additions & 13 deletions lib/cadet/factory.ex
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@ defmodule Cadet.Factory do

def user_factory do
%User{
name: "John Smith",
name: Faker.Name.En.name(),
role: :staff
}
end

def student_factory do
%User{
name: sequence("student"),
name: Faker.Name.En.name(),
role: :student
}
end
Expand All @@ -34,30 +34,30 @@ defmodule Cadet.Factory do

def group_factory do
%Group{
name: sequence("group")
name: Faker.Company.name()
}
end

def announcement_factory do
%Announcement{
title: sequence(:title, &"Announcement #{&1}"),
content: "Some content",
title: sequence(:title, &"Announcement #{&1}") <> Faker.Company.catch_phrase(),
content: Faker.StarWars.quote(),
poster: build(:user)
}
end

def material_folder_factory do
%Material{
name: "Folder",
description: "This is a folder",
name: Faker.Cat.name(),
description: Faker.Cat.breed(),
uploader: build(:user, %{role: :staff})
}
end

def material_file_factory do
%Material{
name: "Folder",
description: "This is a folder",
name: Faker.StarWars.character(),
description: Faker.StarWars.planet(),
file: build(:upload),
parent: build(:material_folder),
uploader: build(:user, %{role: :staff})
Expand All @@ -74,7 +74,9 @@ defmodule Cadet.Factory do

def assessment_factory do
%Assessment{
title: "assessment",
title: Faker.Lorem.Shakespeare.En.hamlet(),
summary_short: Faker.Lorem.Shakespeare.En.king_richard_iii(),
summary_long: Faker.Lorem.Shakespeare.En.romeo_and_juliet(),
type: Enum.random([:mission, :sidequest, :contest, :path]),
open_at: Timex.now(),
close_at: Timex.shift(Timex.now(), days: Enum.random(1..30)),
Expand All @@ -100,9 +102,24 @@ defmodule Cadet.Factory do

def programming_question_factory do
%{
content: sequence("ProgrammingQuestion"),
solution_template: "f => f(f);",
solution: "(f => f(f))(f => f(f));"
content: Faker.Pokemon.name(),
solution_header: Faker.Pokemon.location(),
solution_template: Faker.Lorem.Shakespeare.as_you_like_it(),
solution: Faker.Lorem.Shakespeare.hamlet()
}
end

def mcq_question_factory do
%{
content: Faker.Pokemon.name(),
choices: Enum.map(0..2, &build(:mcq_choice, %{choice_id: &1, is_correct: &1 == 0}))
}
end

def mcq_choice_factory do
%{
content: Faker.Pokemon.name(),
hint: Faker.Pokemon.location()
}
end

Expand All @@ -117,4 +134,19 @@ defmodule Cadet.Factory do
code: sequence(:code, &"alert(#{&1})")
}
end

def mcq_answer_factory do
%{
choice_id: Enum.random(0..2)
}
end

def library_factory do
%{
chapter: Enum.random(1..20),
globals: Faker.Lorem.words(Enum.random(1..3)),
externals: Faker.Lorem.words(Enum.random(1..3)),
files: (&Faker.File.file_name/0) |> Stream.repeatedly() |> Enum.take(5)
}
end
end
44 changes: 34 additions & 10 deletions lib/cadet_web/controllers/assessments_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,24 @@ defmodule CadetWeb.AssessmentsController do

use PhoenixSwagger

alias Cadet.Assessments

def index(conn, _) do
user = conn.assigns[:current_user]
{:ok, assessments} = Assessments.all_published_assessments(user)

render(conn, "index.json", assessments: assessments)
end

def show(conn, %{"id" => assessment_id}) do
user = conn.assigns[:current_user]

case Assessments.assessment_with_questions_and_answers(assessment_id, user) do
{:ok, assessment} -> render(conn, "show.json", assessment: assessment)
{:error, {status, message}} -> send_resp(conn, status, message)
end
end

swagger_path :index do
get("/assessments")

Expand Down Expand Up @@ -48,17 +66,23 @@ defmodule CadetWeb.AssessmentsController do
id(:integer, "The assessment id", required: true)
title(:string, "The title of the assessment", required: true)
type(:string, "Either mission/sidequest/path/contest", required: true)
summary_short(:string, "Short summary", required: true)
open_at(:string, "The opening date", format: "date-time", required: true)
close_at(:string, "The closing date", format: "date-time", required: true)
shortSummary(:string, "Short summary", required: true)
openAt(:string, "The opening date", format: "date-time", required: true)
closeAt(:string, "The closing date", format: "date-time", required: true)

attempted(
:boolean,
"Whether the assessment has been attempted by the current user",
required: true
)

max_xp(
maximumEXP(
:integer,
"The maximum amount of XP to be earned from this assessment",
required: true
)

cover_picture(:string, "The URL to the cover picture", required: true)
coverImage(:string, "The URL to the cover picture", required: true)
end
end,
Assessment:
Expand All @@ -67,8 +91,8 @@ defmodule CadetWeb.AssessmentsController do
id(:integer, "The assessment id", required: true)
title(:string, "The title of the assessment", required: true)
type(:string, "Either mission/sidequest/path/contest", required: true)
summary_long(:string, "Long summary", required: true)
mission_pdf(:string, "The URL to the assessment pdf")
longSummary(:string, "Long summary", required: true)
missionPDF(:string, "The URL to the assessment pdf")

questions(Schema.ref(:Questions), "The list of questions for this assessment")
end
Expand All @@ -82,8 +106,8 @@ defmodule CadetWeb.AssessmentsController do
Question:
swagger_schema do
properties do
questionId(:integer, "The question id", required: true)
questionType(:string, "The question type (mcq/programming)", required: true)
id(:integer, "The question id", required: true)
type(:string, "The question type (mcq/programming)", required: true)
content(:string, "The question content", required: true)

choices(
Expand All @@ -104,7 +128,7 @@ defmodule CadetWeb.AssessmentsController do

library(
Schema.ref(:Library),
"The library used for this question (programming questions only)"
"The library used for this question"
)

solution_template(:string, "Solution template for programming questions")
Expand Down
Loading

0 comments on commit b95b0f8

Please sign in to comment.